From 5797e83f07e3ec6cecee38aabc7c99b8f9d4bfc2 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Wed, 28 May 2025 00:17:42 +0300 Subject: [PATCH] =?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"); + } +}