From 573f1d7d519235850034903b07c5545a2547af9b Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Tue, 27 May 2025 03:53:59 +0300 Subject: [PATCH 1/7] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=BC=D0=B5=D0=B6=D1=83?= =?UTF-8?q?=D1=82=D0=BE=D1=87=D0=BD=D1=8B=D0=B9=20=D0=BD=D0=B0=D0=B1=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D0=BE=D0=BA=20=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 2 +- snag.json | 12 ++++- source/app.d | 4 ++ source/snag/config/config.d | 28 ++++++++++++ source/snag/core/package.d | 1 + source/snag/core/rules.d | 90 +++++++++++++++++++++++++++++++++++++ 6 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 source/snag/core/rules.d diff --git a/.vscode/launch.json b/.vscode/launch.json index 03d6784..3e2eea1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "name": "Build & Debug DUB project", "cwd": "${command:dubWorkingDirectory}", "program": "bin/${command:dubTarget}", - "args": ["list"] + "args": ["create"] } ] } \ No newline at end of file diff --git a/snag.json b/snag.json index d8f176b..397d691 100644 --- a/snag.json +++ b/snag.json @@ -10,5 +10,15 @@ "postsnag": [ "/usr/bin/ls", "/usr/local/bin/script.sh" - ] + ], + "rules": { + "tracking": [ + "/etc/systemd/*.conf", + "/usr/exit" + ], + "ignore": [ + "/usr/exit/.gitignore", + "/usr/exit/dd" + ] + } } diff --git a/source/app.d b/source/app.d index 4b40f32..69cb047 100644 --- a/source/app.d +++ b/source/app.d @@ -21,6 +21,7 @@ int main(string[] args) .optional ) ) + .add(new Command("retracking", "Tracking rules update")) .add(new Command("status", "Checking the status of tracked files")) .add(new Command("diff", "Show changed data")) .add(new Command("import", "Import snapshot from a tar.gz archive") @@ -160,6 +161,9 @@ int main(string[] args) i.option("author", ""), i.option("email", "") ) + ) + .on("retracking", e => + (new SnagRules(config)).create() ); } catch (SnagException e) { e.print(); diff --git a/source/snag/config/config.d b/source/snag/config/config.d index 75de340..635cc6d 100644 --- a/source/snag/config/config.d +++ b/source/snag/config/config.d @@ -5,6 +5,8 @@ import std.file; import std.path; import std.string; import snag.lib; +import std.algorithm; +import std.array; import snag.config.exception; @@ -13,6 +15,8 @@ class SnagConfig { private string _project; private string _email; private string _author; + private string[] _tracking; + private string[] _ignore; this(string configFile) { string jsonText; @@ -93,10 +97,34 @@ class SnagConfig { throw new SnagConfigException( "The \"author\" parameter must contain an author name" ); + + if ("rules" in jsonData) { + if (jsonData["rules"].type != JSONType.object) + throw new SnagConfigException( + "The \"rules\" parameter must be an object" + ); + auto rules = jsonData["rules"]; + if ("tracking" in rules) { + if (rules["tracking"].type != JSONType.array) + throw new SnagConfigException( + "The \"tracking\" parameter must be an array containing a set of paths to tracked files" + ); + _tracking = rules["tracking"].array.map!(item => item.str).array; + } + if ("ignore" in rules) { + if (rules["ignore"].type != JSONType.array) + throw new SnagConfigException( + "The \"ignore\" parameter must contain a gitignore rule" + ); + _ignore = rules["ignore"].array.map!(item => item.str).array; + } + } } @property string git() const { return _git; } @property string project() const { return _project; } @property string email() const { return _email; } @property string author() const { return _author; } + @property const(string[]) tracking() const { return _tracking; } + @property const(string[]) ignore() const { return _ignore; } } diff --git a/source/snag/core/package.d b/source/snag/core/package.d index 1b88bba..6ae1164 100644 --- a/source/snag/core/package.d +++ b/source/snag/core/package.d @@ -2,3 +2,4 @@ module snag.core; public import snag.core.core; public import snag.core.exception; +public import snag.core.rules; diff --git a/source/snag/core/rules.d b/source/snag/core/rules.d new file mode 100644 index 0000000..5853d7e --- /dev/null +++ b/source/snag/core/rules.d @@ -0,0 +1,90 @@ +module snag.core.rules; + +import snag.config; +import snag.core.exception; +import std.algorithm; +import std.array; +import std.path; +import std.stdio; +import std.string; +import std.conv; +import std.container; +import std.process; + +class SnagRules { + private string[] _rules; + private SnagConfig _config; + private string[] _baseCommand; + + private string[] generateGitignoreRules(string finalPath) { + string[] rules; + string[] parts = finalPath.split("/").filter!(p => !p.empty).array; + + if (parts.length == 0) return rules; + + rules ~= "/*"; + rules ~= "!/" ~ parts[0]; + rules ~= "/" ~ parts[0] ~ "/*"; + + if (parts.length > 1) { + string currentPath = "/" ~ parts[0]; + foreach (i; 1 .. parts.length) { + currentPath ~= "/" ~ parts[i]; + rules ~= "!" ~ currentPath; + if (i < parts.length.to!int - 1) { + rules ~= currentPath ~ "/*"; + } + } + } + + return rules; + } + + private void generate() { + string[] rules; + auto tempRules = new RedBlackTree!string; + + _config.tracking.each!( + track => rules ~= generateGitignoreRules(track) + ); + + rules.each!((rule) { + if (rule in tempRules) return; + tempRules.insert(rule); + _rules ~= rule; + }); + + _rules ~= _config.ignore; + } + + this(SnagConfig config) { + _config = config; + + _baseCommand = format( + "git --git-dir=%s --work-tree=%s", + config.git, config.project + ).split(); + } + + void create() { + auto result = execute(_baseCommand ~ ["rev-parse", "--git-dir"]); + result.status && + throw new SnagException( + "A problem occurred while checking the repository: " + ~ result.output.strip('\n') + ); + + generate(); + + string gitignore = _config.git.buildPath("info/exclude"); + + File file = File(gitignore, "w"); + + _rules.each!(rule => file.writeln(rule)); + } + + // git ls-files -i -c --exclude-standard -z | xargs -0 git rm --cached + + // git ls-files -i -c --exclude-standard + // git rm --cached +} From bb58bfe0304eb203b00b2703e61c0690d8338587 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Wed, 28 May 2025 00:05:00 +0300 Subject: [PATCH 2/7] =?UTF-8?q?=D0=94=D0=BE=D0=BF=D0=B8=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- snag.json | 6 +-- source/app.d | 39 ++++++++++++-- source/snag/core/core.d | 28 +++++++++-- source/snag/core/rules.d | 106 +++++++++++++++++++++++++++++++++------ 4 files changed, 154 insertions(+), 25 deletions(-) diff --git a/snag.json b/snag.json index 397d691..0cdee67 100644 --- a/snag.json +++ b/snag.json @@ -13,12 +13,12 @@ ], "rules": { "tracking": [ - "/etc/systemd/*.conf", - "/usr/exit" + "/etc/*.conf" ], "ignore": [ "/usr/exit/.gitignore", - "/usr/exit/dd" + "/usr/exit/dd", + "/file1" ] } } diff --git a/source/app.d b/source/app.d index 69cb047..b15d19c 100644 --- a/source/app.d +++ b/source/app.d @@ -21,7 +21,23 @@ int main(string[] args) .optional ) ) - .add(new Command("retracking", "Tracking rules update")) + .add(new Command("rules", "Tracking rules") + .add(new Command("update", "Update rules") + .add(new Flag("r", "remove", "Removing from tracking the found ignored files") + .name("remove") + .optional + ) + ) + .add(new Command("reset", "Reset rules (restores rules to pre-change state)")) + .add(new Command("clear", "Clear rules")) + .add(new Command("save", "Save rules")) + .add(new Command("show", "Show rules") + .add(new Flag("c", "config", "Show rules from the configuration file") + .name("config") + .optional + ) + ) + ) .add(new Command("status", "Checking the status of tracked files")) .add(new Command("diff", "Show changed data")) .add(new Command("import", "Import snapshot from a tar.gz archive") @@ -162,8 +178,25 @@ int main(string[] args) i.option("email", "") ) ) - .on("retracking", e => - (new SnagRules(config)).create() + .on("rules", (r) { + auto rules = new SnagRules(config); + r + .on("update", update => + rules.update(update.flag("remove")) + ) + .on("reset", reset => + rules.reset() + ) + .on("clear", clear => + rules.clear() + ) + .on("save", clear => + rules.save() + ) + .on("show", show => + rules.show(show.flag("config")) + ); + } ); } catch (SnagException e) { e.print(); diff --git a/source/snag/core/core.d b/source/snag/core/core.d index e70ce62..ef51792 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -13,6 +13,7 @@ import std.path; import snag.lib; import snag.core.exception; +import snag.core.rules; class Snag { private string[] _baseCommand; @@ -44,6 +45,22 @@ class Snag { return result; } + private string gitStatus(string shortStatus, bool formatted = false) { + immutable string[string] statusMap = [ + "??": "Untracked", + "A": "Added", + "M": "Modified", + "D": "Deleted", + "R": "Renamed", + "C": "Copied", + "U": "Unmerged", + "T": "Type changed", + "!": "Ignored" + ]; + string fullStatus = statusMap.get(shortStatus, shortStatus); + return formatted && fullStatus.length < 8 ? fullStatus ~ "\t" : fullStatus; + } + void initialize(bool force) { auto result = execute(_baseCommand ~ ["rev-parse", "--git-dir"]); !force && !result.status && @@ -68,6 +85,9 @@ class Snag { ["config", "user.name", _config.author], "A Git repository initialization error occurred" ); + + (new SnagRules(_config)).create(); + writeln( "The Git repository has been initialized successfully: ", _config.git @@ -84,9 +104,11 @@ class Snag { return; } writeln("The following list of files requires backup:"); - result.output.split('\n').filter!(e => !e.strip.empty).each!((e) { - writefln("\t/%s", e.strip.split[1]); - }); + result.output.split('\n')[0..$-1].map!(e => + e.strip.split + ).each!(e => + writefln("\t%s\t/%s", gitStatus(e[0], true), e[1]) + ); } void create(string comment, string author, string email) { diff --git a/source/snag/core/rules.d b/source/snag/core/rules.d index 5853d7e..ef585bd 100644 --- a/source/snag/core/rules.d +++ b/source/snag/core/rules.d @@ -10,11 +10,23 @@ import std.string; import std.conv; import std.container; import std.process; +import std.file; class SnagRules { private string[] _rules; private SnagConfig _config; private string[] _baseCommand; + private string _gitignore; + private string _gitignoreBak; + + private auto git(string[] command, string message, string separator = ":\n\t") { + auto result = execute(_baseCommand ~ command); + if (result.status) + throw new SnagException( + message ~ separator ~ result.output.split('\n')[0] + ); + return result; + } private string[] generateGitignoreRules(string finalPath) { string[] rules; @@ -59,32 +71,94 @@ class SnagRules { this(SnagConfig config) { _config = config; + _gitignore = config.git.buildPath("info/exclude"); + _gitignoreBak = _gitignore ~ ".bak"; _baseCommand = format( "git --git-dir=%s --work-tree=%s", config.git, config.project ).split(); + + generate(); + } + + private void restoreUntracked() { + git( + ["diff", "--cached", "--name-only"], + "Failed to retrieve the list of files removed from tracking" + ).output.split('\n')[0..$-1].each!( + file => git( + ["add", "-f", file], + "Failed to restore the file to tracking" + ) + ); + } + + private void removeUntracked() { + git( + ["ls-files", "-i", "-c", "--exclude-standard"], + "Failed to get the list of files to remove from tracking" + ).output.split('\n')[0..$-1].each!( + file => git( + ["rm", "--cached", file], + "Failed to remove file from tracking" + ) + ); } void create() { - auto result = execute(_baseCommand ~ ["rev-parse", "--git-dir"]); - result.status && - throw new SnagException( - "A problem occurred while checking the repository: " - ~ result.output.strip('\n') - ); - - generate(); - - string gitignore = _config.git.buildPath("info/exclude"); - - File file = File(gitignore, "w"); - + auto file = File(_gitignore, "w"); _rules.each!(rule => file.writeln(rule)); + file.close(); } - // git ls-files -i -c --exclude-standard -z | xargs -0 git rm --cached + void update(bool remove) { + if (!_gitignoreBak.exists) { + readText(_gitignore).splitLines().equal(_rules) && + throw new SnagException( + "Rule configuration is up-to-date and doesn't require updating" + ); + copy(_gitignore, _gitignoreBak); + } - // git ls-files -i -c --exclude-standard - // git rm --cached + restoreUntracked(); + create(); + + remove && removeUntracked(); + } + + void reset() { + !_gitignoreBak.exists && + throw new SnagException( + "No rule changes to reset" + ); + restoreUntracked(); + rename(_gitignoreBak, _gitignore); + } + + void clear() { + !readText(_gitignore).splitLines().length && + throw new SnagException( + "The configuration has no rules" + ); + !_gitignoreBak.exists && copy(_gitignore, _gitignoreBak); + restoreUntracked(); + File(_gitignore, "w").close(); + } + + void show(bool config) { + if (config) + _rules.join('\n').writeln; + else + readText(_gitignore).write; + } + + void save() { + if (!_gitignoreBak.exists) { + writeln("The rules are up to date"); + return; + } + remove(_gitignoreBak); + writeln("The rules have been saved"); + } } From 5797e83f07e3ec6cecee38aabc7c99b8f9d4bfc2 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Wed, 28 May 2025 00:17:42 +0300 Subject: [PATCH 3/7] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D1=83=D0=BB=D1=8C=20rules=20=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=D0=BC=D0=B8=20=D0=BE=D1=82=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=D0=B6=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20=D0=B2=20=D0=B2=D0=B8=D0=B4?= =?UTF-8?q?=D0=B5=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B5=D0=B9=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20gitignore=20-=20create:=20=D1=81=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B8=D0=B7=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3?= =?UTF-8?q?=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=BE=D0=BD=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0:=20=20=20=20=20-=20trac?= =?UTF-8?q?king=20-=20=D0=BE=D1=82=D1=81=D0=BB=D0=B5=D0=B6=D0=B8=D0=B2?= =?UTF-8?q?=D0=B0=D0=B5=D0=BC=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B?= =?UTF-8?q?/=D0=BF=D1=83=D1=82=D0=B8=20=20=20=20=20-=20ignore=20-=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=20gitignore=20-=20update:?= =?UTF-8?q?=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D1=81=D1=83=D1=89=D0=B5=D1=81=D1=82=D0=B2=D1=83=D1=8E=D1=89?= =?UTF-8?q?=D0=B8=D1=85=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=BE=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=B0=20-=20reset:=20=D1=81=D0=B1=D1=80=D0=BE?= =?UTF-8?q?=D1=81=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=B2=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=D1=85=20?= =?UTF-8?q?-=20=D0=BE=D1=82=D0=BA=D0=B0=D1=82=20=D0=BD=D0=B0=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=BC=D0=B5=D0=BD=D1=82=20=D0=B4=D0=BE=20=D0=B2=D0=BD?= =?UTF-8?q?=D0=B5=D1=81=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=BE=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB=20-=20clear:=20?= =?UTF-8?q?=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=B8=D1=82=D1=8C=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=20=D1=81=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8=20-=20show:=20=D0=BF=D1=80=D0=BE=D1=81=D0=BC?= =?UTF-8?q?=D0=BE=D1=82=D1=80=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20-?= =?UTF-8?q?=20save:=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=20=D0=B1?= =?UTF-8?q?=D0=B5=D0=B7=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D1=81=D0=B1=D1=80=D0=BE=D1=81=D0=B0=20(re?= =?UTF-8?q?set)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snag/core/package.d | 1 + source/snag/core/rules.d | 164 +++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 source/snag/core/rules.d diff --git a/source/snag/core/package.d b/source/snag/core/package.d index 1b88bba..6ae1164 100644 --- a/source/snag/core/package.d +++ b/source/snag/core/package.d @@ -2,3 +2,4 @@ module snag.core; public import snag.core.core; public import snag.core.exception; +public import snag.core.rules; diff --git a/source/snag/core/rules.d b/source/snag/core/rules.d new file mode 100644 index 0000000..ef585bd --- /dev/null +++ b/source/snag/core/rules.d @@ -0,0 +1,164 @@ +module snag.core.rules; + +import snag.config; +import snag.core.exception; +import std.algorithm; +import std.array; +import std.path; +import std.stdio; +import std.string; +import std.conv; +import std.container; +import std.process; +import std.file; + +class SnagRules { + private string[] _rules; + private SnagConfig _config; + private string[] _baseCommand; + private string _gitignore; + private string _gitignoreBak; + + private auto git(string[] command, string message, string separator = ":\n\t") { + auto result = execute(_baseCommand ~ command); + if (result.status) + throw new SnagException( + message ~ separator ~ result.output.split('\n')[0] + ); + return result; + } + + private string[] generateGitignoreRules(string finalPath) { + string[] rules; + string[] parts = finalPath.split("/").filter!(p => !p.empty).array; + + if (parts.length == 0) return rules; + + rules ~= "/*"; + rules ~= "!/" ~ parts[0]; + rules ~= "/" ~ parts[0] ~ "/*"; + + if (parts.length > 1) { + string currentPath = "/" ~ parts[0]; + foreach (i; 1 .. parts.length) { + currentPath ~= "/" ~ parts[i]; + rules ~= "!" ~ currentPath; + if (i < parts.length.to!int - 1) { + rules ~= currentPath ~ "/*"; + } + } + } + + return rules; + } + + private void generate() { + string[] rules; + auto tempRules = new RedBlackTree!string; + + _config.tracking.each!( + track => rules ~= generateGitignoreRules(track) + ); + + rules.each!((rule) { + if (rule in tempRules) return; + tempRules.insert(rule); + _rules ~= rule; + }); + + _rules ~= _config.ignore; + } + + this(SnagConfig config) { + _config = config; + _gitignore = config.git.buildPath("info/exclude"); + _gitignoreBak = _gitignore ~ ".bak"; + + _baseCommand = format( + "git --git-dir=%s --work-tree=%s", + config.git, config.project + ).split(); + + generate(); + } + + private void restoreUntracked() { + git( + ["diff", "--cached", "--name-only"], + "Failed to retrieve the list of files removed from tracking" + ).output.split('\n')[0..$-1].each!( + file => git( + ["add", "-f", file], + "Failed to restore the file to tracking" + ) + ); + } + + private void removeUntracked() { + git( + ["ls-files", "-i", "-c", "--exclude-standard"], + "Failed to get the list of files to remove from tracking" + ).output.split('\n')[0..$-1].each!( + file => git( + ["rm", "--cached", file], + "Failed to remove file from tracking" + ) + ); + } + + void create() { + auto file = File(_gitignore, "w"); + _rules.each!(rule => file.writeln(rule)); + file.close(); + } + + void update(bool remove) { + if (!_gitignoreBak.exists) { + readText(_gitignore).splitLines().equal(_rules) && + throw new SnagException( + "Rule configuration is up-to-date and doesn't require updating" + ); + copy(_gitignore, _gitignoreBak); + } + + restoreUntracked(); + create(); + + remove && removeUntracked(); + } + + void reset() { + !_gitignoreBak.exists && + throw new SnagException( + "No rule changes to reset" + ); + restoreUntracked(); + rename(_gitignoreBak, _gitignore); + } + + void clear() { + !readText(_gitignore).splitLines().length && + throw new SnagException( + "The configuration has no rules" + ); + !_gitignoreBak.exists && copy(_gitignore, _gitignoreBak); + restoreUntracked(); + File(_gitignore, "w").close(); + } + + void show(bool config) { + if (config) + _rules.join('\n').writeln; + else + readText(_gitignore).write; + } + + void save() { + if (!_gitignoreBak.exists) { + writeln("The rules are up to date"); + return; + } + remove(_gitignoreBak); + writeln("The rules have been saved"); + } +} From aa4260cdb92670842c2bb2a07dec1d9fa1cd98de Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Wed, 28 May 2025 00:18:55 +0300 Subject: [PATCH 4/7] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=BE=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=B0=20=D0=BD=D0=B0=D0=B1=D0=BE=D1=80=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BE=D1=82=D1=81=D0=BB=D0=B5=D0=B6=D0=B8=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- snag.json | 12 +++++++++++- source/snag/config/config.d | 28 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/snag.json b/snag.json index d8f176b..0cdee67 100644 --- a/snag.json +++ b/snag.json @@ -10,5 +10,15 @@ "postsnag": [ "/usr/bin/ls", "/usr/local/bin/script.sh" - ] + ], + "rules": { + "tracking": [ + "/etc/*.conf" + ], + "ignore": [ + "/usr/exit/.gitignore", + "/usr/exit/dd", + "/file1" + ] + } } diff --git a/source/snag/config/config.d b/source/snag/config/config.d index 75de340..635cc6d 100644 --- a/source/snag/config/config.d +++ b/source/snag/config/config.d @@ -5,6 +5,8 @@ import std.file; import std.path; import std.string; import snag.lib; +import std.algorithm; +import std.array; import snag.config.exception; @@ -13,6 +15,8 @@ class SnagConfig { private string _project; private string _email; private string _author; + private string[] _tracking; + private string[] _ignore; this(string configFile) { string jsonText; @@ -93,10 +97,34 @@ class SnagConfig { throw new SnagConfigException( "The \"author\" parameter must contain an author name" ); + + if ("rules" in jsonData) { + if (jsonData["rules"].type != JSONType.object) + throw new SnagConfigException( + "The \"rules\" parameter must be an object" + ); + auto rules = jsonData["rules"]; + if ("tracking" in rules) { + if (rules["tracking"].type != JSONType.array) + throw new SnagConfigException( + "The \"tracking\" parameter must be an array containing a set of paths to tracked files" + ); + _tracking = rules["tracking"].array.map!(item => item.str).array; + } + if ("ignore" in rules) { + if (rules["ignore"].type != JSONType.array) + throw new SnagConfigException( + "The \"ignore\" parameter must contain a gitignore rule" + ); + _ignore = rules["ignore"].array.map!(item => item.str).array; + } + } } @property string git() const { return _git; } @property string project() const { return _project; } @property string email() const { return _email; } @property string author() const { return _author; } + @property const(string[]) tracking() const { return _tracking; } + @property const(string[]) ignore() const { return _ignore; } } From c388d01a2fd9314b83b596b982753be59d942b9b Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Wed, 28 May 2025 00:20:27 +0300 Subject: [PATCH 5/7] =?UTF-8?q?=D0=98=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D0=B2=D0=BD=D1=8B=D0=B9=20=D0=B2=D1=8B=D0=B2=D0=BE?= =?UTF-8?q?=D0=B4=20=D0=BF=D1=80=D0=B8=20=D0=B2=D1=8B=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snag/core/core.d | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/source/snag/core/core.d b/source/snag/core/core.d index e70ce62..cf1f65c 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -44,6 +44,22 @@ class Snag { return result; } + private string gitStatus(string shortStatus, bool formatted = false) { + immutable string[string] statusMap = [ + "??": "Untracked", + "A": "Added", + "M": "Modified", + "D": "Deleted", + "R": "Renamed", + "C": "Copied", + "U": "Unmerged", + "T": "Type changed", + "!": "Ignored" + ]; + string fullStatus = statusMap.get(shortStatus, shortStatus); + return formatted && fullStatus.length < 8 ? fullStatus ~ "\t" : fullStatus; + } + void initialize(bool force) { auto result = execute(_baseCommand ~ ["rev-parse", "--git-dir"]); !force && !result.status && @@ -84,9 +100,11 @@ class Snag { return; } writeln("The following list of files requires backup:"); - result.output.split('\n').filter!(e => !e.strip.empty).each!((e) { - writefln("\t/%s", e.strip.split[1]); - }); + result.output.split('\n')[0..$-1].map!(e => + e.strip.split + ).each!(e => + writefln("\t%s\t/%s", gitStatus(e[0], true), e[1]) + ); } void create(string comment, string author, string email) { From c65b18a91542eb5e9c4bcf543a7b5da07eb0008c Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Wed, 28 May 2025 00:20:55 +0300 Subject: [PATCH 6/7] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D0=B8=D0=BD=D0=B8=D1=86=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D1=80=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snag/core/core.d | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/snag/core/core.d b/source/snag/core/core.d index cf1f65c..ef51792 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -13,6 +13,7 @@ import std.path; import snag.lib; import snag.core.exception; +import snag.core.rules; class Snag { private string[] _baseCommand; @@ -84,6 +85,9 @@ class Snag { ["config", "user.name", _config.author], "A Git repository initialization error occurred" ); + + (new SnagRules(_config)).create(); + writeln( "The Git repository has been initialized successfully: ", _config.git From 016455d8f2ed5d906c0f8d330c103df6f5deaad0 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Wed, 28 May 2025 00:23:27 +0300 Subject: [PATCH 7/7] 0.0.11 --- source/app.d | 37 +++++++++++++++++++++++++++++++++++++ source/snag/version_.d | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/source/app.d b/source/app.d index 4b40f32..b15d19c 100644 --- a/source/app.d +++ b/source/app.d @@ -21,6 +21,23 @@ int main(string[] args) .optional ) ) + .add(new Command("rules", "Tracking rules") + .add(new Command("update", "Update rules") + .add(new Flag("r", "remove", "Removing from tracking the found ignored files") + .name("remove") + .optional + ) + ) + .add(new Command("reset", "Reset rules (restores rules to pre-change state)")) + .add(new Command("clear", "Clear rules")) + .add(new Command("save", "Save rules")) + .add(new Command("show", "Show rules") + .add(new Flag("c", "config", "Show rules from the configuration file") + .name("config") + .optional + ) + ) + ) .add(new Command("status", "Checking the status of tracked files")) .add(new Command("diff", "Show changed data")) .add(new Command("import", "Import snapshot from a tar.gz archive") @@ -160,6 +177,26 @@ int main(string[] args) i.option("author", ""), i.option("email", "") ) + ) + .on("rules", (r) { + auto rules = new SnagRules(config); + r + .on("update", update => + rules.update(update.flag("remove")) + ) + .on("reset", reset => + rules.reset() + ) + .on("clear", clear => + rules.clear() + ) + .on("save", clear => + rules.save() + ) + .on("show", show => + rules.show(show.flag("config")) + ); + } ); } catch (SnagException e) { e.print(); diff --git a/source/snag/version_.d b/source/snag/version_.d index 82e9fc2..2c89ac7 100644 --- a/source/snag/version_.d +++ b/source/snag/version_.d @@ -1,3 +1,3 @@ module snag.version_; -enum snagVersion = "0.0.10"; +enum snagVersion = "0.0.11";