From ff887e06407cdd0f2b53c9c6e46ec146ec84a6b6 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 23 May 2025 02:10:07 +0300 Subject: [PATCH 01/43] =?UTF-8?q?=D0=A1=D1=84=D0=BE=D1=80=D0=BC=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=BC=D0=B5=D1=85=D0=B0=D0=BD=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D1=8F=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=D0=BE=D0=B3=D0=BE=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0.=20?= =?UTF-8?q?=D0=9D=D0=B5=D0=BE=D0=B1=D1=85=D0=BE=D0=B4=D0=B8=D0=BC=D0=BE=20?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D0=B0=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=B1=D1=83=D0=B4=D0=B5=D1=82=20=D1=81=D0=BE=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=B0=D1=82=D1=8C=20=D0=BF=D1=83=D1=82=D0=B8=20=D0=BA=20?= =?UTF-8?q?=D1=80=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D1=8E=20git=20=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D1=83=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=D0=B6=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + snapd.json | 4 ++++ source/app.d | 13 +++++++++++++ source/snapd/config.d | 20 ++++++++++++++++++++ source/snapd/package.d | 1 + 5 files changed, 39 insertions(+) create mode 100644 snapd.json create mode 100644 source/snapd/config.d diff --git a/.gitignore b/.gitignore index 3986dde..07c2a16 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ snapd-test-* *.o *.obj *.lst +bin diff --git a/snapd.json b/snapd.json new file mode 100644 index 0000000..1dc307f --- /dev/null +++ b/snapd.json @@ -0,0 +1,4 @@ +{ + "git": "/var/lib/snapd", + "project": "/" +} diff --git a/source/app.d b/source/app.d index 497c1a8..b452dbf 100644 --- a/source/app.d +++ b/source/app.d @@ -1,5 +1,6 @@ import snapd; import commandr; +import std.file; import core.stdc.stdlib : EXIT_SUCCESS; @@ -8,7 +9,19 @@ private string programName = "snapd"; int main(string[] args) { auto argumets = new Program(programName, snapdVersion) + .add(new Option("c", "config", "Сonfiguration file path") + .optional + .validateEachWith( + opt => opt.exists && opt.isFile, + "A JSON file path must be provided" + ) + ) .parse(args); + string configFile = "snapd.json"; + configFile = argumets.option("config", configFile); + + auto sc = new SnapdConfig(configFile); + return EXIT_SUCCESS; } diff --git a/source/snapd/config.d b/source/snapd/config.d new file mode 100644 index 0000000..d093331 --- /dev/null +++ b/source/snapd/config.d @@ -0,0 +1,20 @@ +module snapd.config; + +import std.json; +import std.file; +import std.stdio : writeln; + +class SnapdConfig { + private string _git; + private string _project; + + this(string configFile) { + string jsonText = readText(configFile); + + auto jsonData = parseJSON(jsonText); + if ("gits" !in jsonData) + writeln("Ключ отсутствует"); + writeln(jsonData["git"].str); + writeln(jsonData["project"].str); + } +} diff --git a/source/snapd/package.d b/source/snapd/package.d index 45456d4..4f5e10f 100644 --- a/source/snapd/package.d +++ b/source/snapd/package.d @@ -1,3 +1,4 @@ module snapd; public import snapd.version_; +public import snapd.config; From c1051c850739b8f7b319aa1bc4c2b483428fcc38 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 23 May 2025 20:45:48 +0300 Subject: [PATCH 02/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20vscode=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BB=D0=B0=D0=B4=D0=BA=D0=B8=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..400e7c1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Используйте IntelliSense, чтобы узнать о возможных атрибутах. + // Наведите указатель мыши, чтобы просмотреть описания существующих атрибутов. + // Для получения дополнительной информации посетите: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "code-d", + "request": "launch", + "dubBuild": true, + "name": "Build & Debug DUB project", + "cwd": "${command:dubWorkingDirectory}", + "program": "bin/${command:dubTarget}" + } + ] +} \ No newline at end of file From 3cb714f5445b02e53c22b9b98750bb0e6e759727 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 23 May 2025 20:46:23 +0300 Subject: [PATCH 03/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=20email?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- snapd.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/snapd.json b/snapd.json index 1dc307f..d8b0536 100644 --- a/snapd.json +++ b/snapd.json @@ -1,4 +1,5 @@ { - "git": "/var/lib/snapd", - "project": "/" + "git": "/var/lib/snapd", + "project": "/", + "email": "user@site.domain" } From 309a47ea3a011dd88e91f1b53d73115e909e042b Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 23 May 2025 20:47:04 +0300 Subject: [PATCH 04/43] =?UTF-8?q?=D0=9C=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=B1=D1=8B=D0=BB=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=20=D0=B2=20=D0=B4=D0=B8=D1=80?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snapd/config.d | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 source/snapd/config.d diff --git a/source/snapd/config.d b/source/snapd/config.d deleted file mode 100644 index d093331..0000000 --- a/source/snapd/config.d +++ /dev/null @@ -1,20 +0,0 @@ -module snapd.config; - -import std.json; -import std.file; -import std.stdio : writeln; - -class SnapdConfig { - private string _git; - private string _project; - - this(string configFile) { - string jsonText = readText(configFile); - - auto jsonData = parseJSON(jsonText); - if ("gits" !in jsonData) - writeln("Ключ отсутствует"); - writeln(jsonData["git"].str); - writeln(jsonData["project"].str); - } -} From f1d1bce20b8250f89d725b60dbedd13d8ce12bae Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 23 May 2025 20:47:53 +0300 Subject: [PATCH 05/43] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8,=20=D0=B2=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=D1=8E?= =?UTF-8?q?=D1=89=D0=B8=D0=B9=20=D0=B8=D1=81=D0=BA=D0=BB=D1=8E=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80=D0=B8=20=D0=B2=D0=BE=D0=B7?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=BD=D0=BE=D0=B2=D0=B5=D0=BD=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snapd/config/config.d | 93 +++++++++++++++++++++++++++++++++ source/snapd/config/exception.d | 14 +++++ source/snapd/config/package.d | 4 ++ 3 files changed, 111 insertions(+) create mode 100644 source/snapd/config/config.d create mode 100644 source/snapd/config/exception.d create mode 100644 source/snapd/config/package.d diff --git a/source/snapd/config/config.d b/source/snapd/config/config.d new file mode 100644 index 0000000..5822758 --- /dev/null +++ b/source/snapd/config/config.d @@ -0,0 +1,93 @@ +module snapd.config.config; + +import std.json; +import std.file; +import std.path; +import std.regex; +import std.string; + +import snapd.config.exception; + +class SnapdConfig { + private string _git; + private string _project; + private string _email; + + private bool isValidEmail(string email) { + auto emailPattern = ctRegex!r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"; + return !matchFirst(email, emailPattern).empty; + } + + this(string configFile) { + string jsonText; + JSONValue jsonData; + + try { + jsonText = readText(configFile); + jsonData = parseJSON(jsonText); + } catch (Exception e) { + throw new SnapdConfigException( + "An error occurred while reading the configuration file:\n\t" + ~ e.msg + ); + } + + if ("git" !in jsonData) + throw new SnapdConfigException( + "The configuration file is missing the \"git\" parameter" + ); + + _git = jsonData["git"].str; + + if (!_git.length) + throw new SnapdConfigException( + "The \"git\" parameter must contain the path to the directory" + ); + + if (!_git.isAbsolute) + throw new SnapdConfigException( + "The \"git\" parameter must be an absolute path to the directory:\n\t" + ~ _git + ); + + if ("project" !in jsonData) + throw new SnapdConfigException( + "The configuration file is missing the \"project\" parameter" + ); + + _project = jsonData["project"].str; + + if (!_project.length) + throw new SnapdConfigException( + "The \"project\" parameter must contain the path to the directory" + ); + + if (!_project.isAbsolute) + throw new SnapdConfigException( + "The \"project\" parameter must be an absolute path to the directory:\n\t" + ~ _project + ); + + if ("email" !in jsonData) + throw new SnapdConfigException( + "The configuration file is missing the \"email\" parameter" + ); + + _email = jsonData["email"].str; + + if (!_email.length) + throw new SnapdConfigException( + "The \"email\" parameter must contain an email address" + ); + + if (!isValidEmail(_email)) + throw new SnapdConfigException( + "Invalid email address provided in the \"email\" parameter:\n\t" + ~ _email + ); + } + + @property string git() const { return _git; } + @property string project() const { return _project; } + @property string email() const { return _email; } +} diff --git a/source/snapd/config/exception.d b/source/snapd/config/exception.d new file mode 100644 index 0000000..1abc0e5 --- /dev/null +++ b/source/snapd/config/exception.d @@ -0,0 +1,14 @@ +module snapd.config.exception; + +import std.exception; +import std.stdio : writeln; + +class SnapdConfigException : Exception { + this(string msg, string file = __FILE__, size_t line = __LINE__) { + super(msg, file, line); + } + + void print() { + writeln(msg); + } +} diff --git a/source/snapd/config/package.d b/source/snapd/config/package.d new file mode 100644 index 0000000..5dd2aed --- /dev/null +++ b/source/snapd/config/package.d @@ -0,0 +1,4 @@ +module snapd.config; + +public import snapd.config.exception; +public import snapd.config.config; From dad3d356c66078ce9bc5046eca0b61bdf3c1a98c Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 23 May 2025 20:48:14 +0300 Subject: [PATCH 06/43] 0.0.2 --- source/app.d | 16 ++++++++++++---- source/snapd/version_.d | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/source/app.d b/source/app.d index b452dbf..3ac5b7a 100644 --- a/source/app.d +++ b/source/app.d @@ -2,7 +2,9 @@ import snapd; import commandr; import std.file; -import core.stdc.stdlib : EXIT_SUCCESS; +import std.stdio : writeln; + +import core.stdc.stdlib : EXIT_SUCCESS, EXIT_FAILURE; private string programName = "snapd"; @@ -18,10 +20,16 @@ int main(string[] args) ) .parse(args); - string configFile = "snapd.json"; - configFile = argumets.option("config", configFile); + string configFile = argumets.option("config", "snapd.json"); - auto sc = new SnapdConfig(configFile); + SnapdConfig config; + + try { + config = new SnapdConfig(configFile); + } catch (SnapdConfigException e) { + e.print(); + return EXIT_FAILURE; + } return EXIT_SUCCESS; } diff --git a/source/snapd/version_.d b/source/snapd/version_.d index e09faec..98d0114 100644 --- a/source/snapd/version_.d +++ b/source/snapd/version_.d @@ -1,3 +1,3 @@ module snapd.version_; -enum snapdVersion = "0.0.1"; +enum snapdVersion = "0.0.2"; From cf85cc3c774c16732b0309dcb5bfb0ab03f8bba8 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sat, 24 May 2025 01:33:42 +0300 Subject: [PATCH 07/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=20user=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=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=B8=20git?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snapd/config/config.d | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/source/snapd/config/config.d b/source/snapd/config/config.d index 5822758..e6d7f8f 100644 --- a/source/snapd/config/config.d +++ b/source/snapd/config/config.d @@ -12,6 +12,7 @@ class SnapdConfig { private string _git; private string _project; private string _email; + private string _user; private bool isValidEmail(string email) { auto emailPattern = ctRegex!r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"; @@ -85,9 +86,22 @@ class SnapdConfig { "Invalid email address provided in the \"email\" parameter:\n\t" ~ _email ); + + if ("user" !in jsonData) + throw new SnapdConfigException( + "The configuration file is missing the \"user\" parameter" + ); + + _user = jsonData["user"].str; + + if (!_user.length) + throw new SnapdConfigException( + "The \"user\" parameter must contain an user name" + ); } @property string git() const { return _git; } @property string project() const { return _project; } @property string email() const { return _email; } + @property string user() const { return _user; } } From ff9dd8757d45522b60128461465186376538ac29 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sat, 24 May 2025 01:34:32 +0300 Subject: [PATCH 08/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D1=80=D1=8B=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=BE=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D0=B0=20-=20user=20-=20=D0=B8=D0=BC=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8F=20git=20=D0=B4=D0=BB=D1=8F=20=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D1=81=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20=D0=9D=D0=B0=20=D0=BF=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D0=BF=D0=B5=D0=BA=D1=82=D0=B8=D0=B2=D1=83:=20-=20presnap=20-?= =?UTF-8?q?=20=D0=BD=D0=B0=D0=B1=D0=BE=D1=80=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2=20(=D0=B8=D0=BB=D0=B8=20=D0=BA=D0=BE=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=D0=B4=3F!)=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=8B=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=BE=20?= =?UTF-8?q?=D0=B0=D1=80=D1=85=D0=B8=D0=B2=D0=B0=D1=86=D0=B8=D0=B8=20(?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82=D0=B0)=20=D0=BE=D1=82?= =?UTF-8?q?=D1=81=D0=BB=D0=B5=D0=B6=D0=B8=D0=B2=D0=B0=D0=B5=D0=BC=D1=8B?= =?UTF-8?q?=D1=85=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20-=20postsnap=20?= =?UTF-8?q?-=20=D0=BD=D0=B0=D0=B1=D0=BE=D1=80=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2=20(=D0=B8=D0=BB=D0=B8=20=D0=BA=D0=BE=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=D0=B4=3F!)=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=8B=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=20=D0=B0=D1=80=D1=85=D0=B8=D0=B2=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20(=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82=D0=B0)=20=D0=BE?= =?UTF-8?q?=D1=82=D1=81=D0=BB=D0=B5=D0=B6=D0=B8=D0=B2=D0=B0=D0=B5=D0=BC?= =?UTF-8?q?=D1=8B=D1=85=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 --- snapd.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/snapd.json b/snapd.json index d8b0536..3fe8034 100644 --- a/snapd.json +++ b/snapd.json @@ -1,5 +1,14 @@ { - "git": "/var/lib/snapd", - "project": "/", - "email": "user@site.domain" + "git": "/tmp/testgit", + "project": "/home/alexander/Programming/new/dlang/snapd/source", + "email": "user@site.domain", + "user": "snapd", + "presnap": [ + "/usr/bin/ls", + "/usr/local/bin/script.sh" + ], + "postsnap": [ + "/usr/bin/ls", + "/usr/local/bin/script.sh" + ] } From 66d64a964200f074c9c88778655a7554f083de06 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sat, 24 May 2025 01:38:52 +0300 Subject: [PATCH 09/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20=D1=8F?= =?UTF-8?q?=D0=B4=D1=80=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=BC=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snapd/core/core.d | 109 ++++++++++++++++++++++++++++++++++++ source/snapd/core/package.d | 3 + source/snapd/package.d | 1 + 3 files changed, 113 insertions(+) create mode 100644 source/snapd/core/core.d create mode 100644 source/snapd/core/package.d diff --git a/source/snapd/core/core.d b/source/snapd/core/core.d new file mode 100644 index 0000000..2d285c4 --- /dev/null +++ b/source/snapd/core/core.d @@ -0,0 +1,109 @@ +module snapd.core.core; + +import snapd.config; +import std.format; +import std.stdio; +import std.array; +import std.exception; +import std.process; +import std.algorithm; +import std.string; + +class SnapdException : Exception { + this(string msg, string file = __FILE__, size_t line = __LINE__) { + super(msg, file, line); + } + + void print() { + writeln(msg); + } +} + +class Snapd { + private string[] _baseCommand; + private SnapdConfig _config; + + this(SnapdConfig config) { + _baseCommand = format( + "git --git-dir=%s --work-tree=%s", + config.git, config.project + ).split(); + _config = config; + } + + void initialize() { + auto result = execute(_baseCommand ~ "init"); + if (result.status) + throw new SnapdException( + "A Git repository initialization error occurred:\n" + ~ result.output + ); + + result = execute( + _baseCommand ~ ["config", "user.email", _config.email] + ); + if (result.status) + throw new SnapdException( + "A Git repository initialization error occurred:\n" + ~ result.output + ); + + result = execute( + _baseCommand ~ ["config", "user.name", _config.user] + ); + if (result.status) + throw new SnapdException( + "A Git repository initialization error occurred:\n" + ~ result.output + ); + + writeln( + "The Git repository has been initialized successfully: " + ~ _config.git + ); + } + + void status() { + auto result = execute( + _baseCommand ~ ["status", "--porcelain"] + ); + if (result.status) + throw new SnapdException( + "An error occurred while checking the file tracking status:\n" + ~ result.output + ); + + 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]); + }); + } + + void create() { + auto result = execute( + _baseCommand ~ ["add", "."] + ); + if (result.status) + throw new SnapdException( + "Failed to prepare files for archiving:\n" + ~ result.output + ); + + result = execute( + _baseCommand ~ ["commit", "-m", "test"] + ); + if (result.status) + throw new SnapdException( + "Failed to create a backup:\n" + ~ result.output + ); + + writeln("Backup was created successfully"); + } +} diff --git a/source/snapd/core/package.d b/source/snapd/core/package.d new file mode 100644 index 0000000..ac6ebaf --- /dev/null +++ b/source/snapd/core/package.d @@ -0,0 +1,3 @@ +module snapd.core; + +public import snapd.core.core; diff --git a/source/snapd/package.d b/source/snapd/package.d index 4f5e10f..4360689 100644 --- a/source/snapd/package.d +++ b/source/snapd/package.d @@ -2,3 +2,4 @@ module snapd; public import snapd.version_; public import snapd.config; +public import snapd.core; From 7714bc498e608ab228d3845bbdd2550ea203d579 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sat, 24 May 2025 01:39:20 +0300 Subject: [PATCH 10/43] 0.0.3 --- source/app.d | 23 +++++++++++++++++++++-- source/snapd/version_.d | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/source/app.d b/source/app.d index 3ac5b7a..72df7c3 100644 --- a/source/app.d +++ b/source/app.d @@ -2,8 +2,6 @@ import snapd; import commandr; import std.file; -import std.stdio : writeln; - import core.stdc.stdlib : EXIT_SUCCESS, EXIT_FAILURE; private string programName = "snapd"; @@ -11,6 +9,9 @@ private string programName = "snapd"; int main(string[] args) { auto argumets = new Program(programName, snapdVersion) + .add(new Command("init", "Initializing the repository for storing snapshots")) + .add(new Command("status", "Checking the status of tracked files")) + .add(new Command("create", "Create a new backup")) .add(new Option("c", "config", "Сonfiguration file path") .optional .validateEachWith( @@ -31,5 +32,23 @@ int main(string[] args) return EXIT_FAILURE; } + auto snapd = new Snapd(config); + + try { + argumets + .on("init", (init) { + snapd.initialize(); + }) + .on("status", (status) { + snapd.status(); + }) + .on("create", (create) { + snapd.create(); + }); + } catch (SnapdException e) { + e.print(); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; } diff --git a/source/snapd/version_.d b/source/snapd/version_.d index 98d0114..bc63b1b 100644 --- a/source/snapd/version_.d +++ b/source/snapd/version_.d @@ -1,3 +1,3 @@ module snapd.version_; -enum snapdVersion = "0.0.2"; +enum snapdVersion = "0.0.3"; From cc1a81261a35fb3a6bcadba298188e0814eab629 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 24 May 2025 03:38:53 +0300 Subject: [PATCH 11/43] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 14 +++++------ README.md | 4 +-- dub.json | 2 +- snapd.json => snag.json | 8 +++--- source/app.d | 24 +++++++++--------- source/{snapd => snag}/config/config.d | 30 +++++++++++------------ source/{snapd => snag}/config/exception.d | 4 +-- source/snag/config/package.d | 4 +++ source/{snapd => snag}/core/core.d | 24 +++++++++--------- source/snag/core/package.d | 3 +++ source/snag/package.d | 5 ++++ source/snag/version_.d | 3 +++ source/snapd/config/package.d | 4 --- source/snapd/core/package.d | 3 --- source/snapd/package.d | 5 ---- source/snapd/version_.d | 3 --- 16 files changed, 70 insertions(+), 70 deletions(-) rename snapd.json => snag.json (58%) rename source/{snapd => snag}/config/config.d (80%) rename source/{snapd => snag}/config/exception.d (72%) create mode 100644 source/snag/config/package.d rename source/{snapd => snag}/core/core.d (87%) create mode 100644 source/snag/core/package.d create mode 100644 source/snag/package.d create mode 100644 source/snag/version_.d delete mode 100644 source/snapd/config/package.d delete mode 100644 source/snapd/core/package.d delete mode 100644 source/snapd/package.d delete mode 100644 source/snapd/version_.d diff --git a/.gitignore b/.gitignore index 07c2a16..ccba2bf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,13 +2,13 @@ docs.json __dummy.html docs/ -/snapd -snapd.so -snapd.dylib -snapd.dll -snapd.a -snapd.lib -snapd-test-* +/snag +snag.so +snag.dylib +snag.dll +snag.a +snag.lib +snag-test-* *.exe *.pdb *.o diff --git a/README.md b/README.md index e91f5bd..868928f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# snapd +# snag -Snapshot D - система резервного копирования на основе фиксации состояния файлов с помощью Git. +Snapshot Git - система резервного копирования на основе фиксации состояния файлов с помощью Git. diff --git a/dub.json b/dub.json index 798e277..f63ef25 100644 --- a/dub.json +++ b/dub.json @@ -5,7 +5,7 @@ "copyright": "Copyright © 2025, Alexander Zhirov", "description": "A backup system based on tracking file states using Git", "license": "GPL-2.0-or-later", - "name": "snapd", + "name": "snag", "targetPath": "bin", "targetType": "executable", "dependencies": { diff --git a/snapd.json b/snag.json similarity index 58% rename from snapd.json rename to snag.json index 3fe8034..f7d3e00 100644 --- a/snapd.json +++ b/snag.json @@ -1,13 +1,13 @@ { "git": "/tmp/testgit", - "project": "/home/alexander/Programming/new/dlang/snapd/source", + "project": "/home/alexander/Programming/new/dlang/snag/source", "email": "user@site.domain", - "user": "snapd", - "presnap": [ + "user": "snag", + "presnag": [ "/usr/bin/ls", "/usr/local/bin/script.sh" ], - "postsnap": [ + "postsnag": [ "/usr/bin/ls", "/usr/local/bin/script.sh" ] diff --git a/source/app.d b/source/app.d index 72df7c3..f3bf551 100644 --- a/source/app.d +++ b/source/app.d @@ -1,14 +1,14 @@ -import snapd; +import snag; import commandr; import std.file; import core.stdc.stdlib : EXIT_SUCCESS, EXIT_FAILURE; -private string programName = "snapd"; +private string programName = "snag"; int main(string[] args) { - auto argumets = new Program(programName, snapdVersion) + auto argumets = new Program(programName, snagVersion) .add(new Command("init", "Initializing the repository for storing snapshots")) .add(new Command("status", "Checking the status of tracked files")) .add(new Command("create", "Create a new backup")) @@ -21,31 +21,31 @@ int main(string[] args) ) .parse(args); - string configFile = argumets.option("config", "snapd.json"); + string configFile = argumets.option("config", "snag.json"); - SnapdConfig config; + SnagConfig config; try { - config = new SnapdConfig(configFile); - } catch (SnapdConfigException e) { + config = new SnagConfig(configFile); + } catch (SnagConfigException e) { e.print(); return EXIT_FAILURE; } - auto snapd = new Snapd(config); + auto snag = new Snag(config); try { argumets .on("init", (init) { - snapd.initialize(); + snag.initialize(); }) .on("status", (status) { - snapd.status(); + snag.status(); }) .on("create", (create) { - snapd.create(); + snag.create(); }); - } catch (SnapdException e) { + } catch (SnagException e) { e.print(); return EXIT_FAILURE; } diff --git a/source/snapd/config/config.d b/source/snag/config/config.d similarity index 80% rename from source/snapd/config/config.d rename to source/snag/config/config.d index e6d7f8f..2d25f51 100644 --- a/source/snapd/config/config.d +++ b/source/snag/config/config.d @@ -1,4 +1,4 @@ -module snapd.config.config; +module snag.config.config; import std.json; import std.file; @@ -6,9 +6,9 @@ import std.path; import std.regex; import std.string; -import snapd.config.exception; +import snag.config.exception; -class SnapdConfig { +class SnagConfig { private string _git; private string _project; private string _email; @@ -27,75 +27,75 @@ class SnapdConfig { jsonText = readText(configFile); jsonData = parseJSON(jsonText); } catch (Exception e) { - throw new SnapdConfigException( + throw new SnagConfigException( "An error occurred while reading the configuration file:\n\t" ~ e.msg ); } if ("git" !in jsonData) - throw new SnapdConfigException( + throw new SnagConfigException( "The configuration file is missing the \"git\" parameter" ); _git = jsonData["git"].str; if (!_git.length) - throw new SnapdConfigException( + throw new SnagConfigException( "The \"git\" parameter must contain the path to the directory" ); if (!_git.isAbsolute) - throw new SnapdConfigException( + throw new SnagConfigException( "The \"git\" parameter must be an absolute path to the directory:\n\t" ~ _git ); if ("project" !in jsonData) - throw new SnapdConfigException( + throw new SnagConfigException( "The configuration file is missing the \"project\" parameter" ); _project = jsonData["project"].str; if (!_project.length) - throw new SnapdConfigException( + throw new SnagConfigException( "The \"project\" parameter must contain the path to the directory" ); if (!_project.isAbsolute) - throw new SnapdConfigException( + throw new SnagConfigException( "The \"project\" parameter must be an absolute path to the directory:\n\t" ~ _project ); if ("email" !in jsonData) - throw new SnapdConfigException( + throw new SnagConfigException( "The configuration file is missing the \"email\" parameter" ); _email = jsonData["email"].str; if (!_email.length) - throw new SnapdConfigException( + throw new SnagConfigException( "The \"email\" parameter must contain an email address" ); if (!isValidEmail(_email)) - throw new SnapdConfigException( + throw new SnagConfigException( "Invalid email address provided in the \"email\" parameter:\n\t" ~ _email ); if ("user" !in jsonData) - throw new SnapdConfigException( + throw new SnagConfigException( "The configuration file is missing the \"user\" parameter" ); _user = jsonData["user"].str; if (!_user.length) - throw new SnapdConfigException( + throw new SnagConfigException( "The \"user\" parameter must contain an user name" ); } diff --git a/source/snapd/config/exception.d b/source/snag/config/exception.d similarity index 72% rename from source/snapd/config/exception.d rename to source/snag/config/exception.d index 1abc0e5..fc21ee1 100644 --- a/source/snapd/config/exception.d +++ b/source/snag/config/exception.d @@ -1,9 +1,9 @@ -module snapd.config.exception; +module snag.config.exception; import std.exception; import std.stdio : writeln; -class SnapdConfigException : Exception { +class SnagConfigException : Exception { this(string msg, string file = __FILE__, size_t line = __LINE__) { super(msg, file, line); } diff --git a/source/snag/config/package.d b/source/snag/config/package.d new file mode 100644 index 0000000..09c7b1d --- /dev/null +++ b/source/snag/config/package.d @@ -0,0 +1,4 @@ +module snag.config; + +public import snag.config.exception; +public import snag.config.config; diff --git a/source/snapd/core/core.d b/source/snag/core/core.d similarity index 87% rename from source/snapd/core/core.d rename to source/snag/core/core.d index 2d285c4..c60d315 100644 --- a/source/snapd/core/core.d +++ b/source/snag/core/core.d @@ -1,6 +1,6 @@ -module snapd.core.core; +module snag.core.core; -import snapd.config; +import snag.config; import std.format; import std.stdio; import std.array; @@ -9,7 +9,7 @@ import std.process; import std.algorithm; import std.string; -class SnapdException : Exception { +class SnagException : Exception { this(string msg, string file = __FILE__, size_t line = __LINE__) { super(msg, file, line); } @@ -19,11 +19,11 @@ class SnapdException : Exception { } } -class Snapd { +class Snag { private string[] _baseCommand; - private SnapdConfig _config; + private SnagConfig _config; - this(SnapdConfig config) { + this(SnagConfig config) { _baseCommand = format( "git --git-dir=%s --work-tree=%s", config.git, config.project @@ -34,7 +34,7 @@ class Snapd { void initialize() { auto result = execute(_baseCommand ~ "init"); if (result.status) - throw new SnapdException( + throw new SnagException( "A Git repository initialization error occurred:\n" ~ result.output ); @@ -43,7 +43,7 @@ class Snapd { _baseCommand ~ ["config", "user.email", _config.email] ); if (result.status) - throw new SnapdException( + throw new SnagException( "A Git repository initialization error occurred:\n" ~ result.output ); @@ -52,7 +52,7 @@ class Snapd { _baseCommand ~ ["config", "user.name", _config.user] ); if (result.status) - throw new SnapdException( + throw new SnagException( "A Git repository initialization error occurred:\n" ~ result.output ); @@ -68,7 +68,7 @@ class Snapd { _baseCommand ~ ["status", "--porcelain"] ); if (result.status) - throw new SnapdException( + throw new SnagException( "An error occurred while checking the file tracking status:\n" ~ result.output ); @@ -90,7 +90,7 @@ class Snapd { _baseCommand ~ ["add", "."] ); if (result.status) - throw new SnapdException( + throw new SnagException( "Failed to prepare files for archiving:\n" ~ result.output ); @@ -99,7 +99,7 @@ class Snapd { _baseCommand ~ ["commit", "-m", "test"] ); if (result.status) - throw new SnapdException( + throw new SnagException( "Failed to create a backup:\n" ~ result.output ); diff --git a/source/snag/core/package.d b/source/snag/core/package.d new file mode 100644 index 0000000..7e8e82a --- /dev/null +++ b/source/snag/core/package.d @@ -0,0 +1,3 @@ +module snag.core; + +public import snag.core.core; diff --git a/source/snag/package.d b/source/snag/package.d new file mode 100644 index 0000000..22b28bf --- /dev/null +++ b/source/snag/package.d @@ -0,0 +1,5 @@ +module snag; + +public import snag.version_; +public import snag.config; +public import snag.core; diff --git a/source/snag/version_.d b/source/snag/version_.d new file mode 100644 index 0000000..2465932 --- /dev/null +++ b/source/snag/version_.d @@ -0,0 +1,3 @@ +module snag.version_; + +enum snagVersion = "0.0.4"; diff --git a/source/snapd/config/package.d b/source/snapd/config/package.d deleted file mode 100644 index 5dd2aed..0000000 --- a/source/snapd/config/package.d +++ /dev/null @@ -1,4 +0,0 @@ -module snapd.config; - -public import snapd.config.exception; -public import snapd.config.config; diff --git a/source/snapd/core/package.d b/source/snapd/core/package.d deleted file mode 100644 index ac6ebaf..0000000 --- a/source/snapd/core/package.d +++ /dev/null @@ -1,3 +0,0 @@ -module snapd.core; - -public import snapd.core.core; diff --git a/source/snapd/package.d b/source/snapd/package.d deleted file mode 100644 index 4360689..0000000 --- a/source/snapd/package.d +++ /dev/null @@ -1,5 +0,0 @@ -module snapd; - -public import snapd.version_; -public import snapd.config; -public import snapd.core; diff --git a/source/snapd/version_.d b/source/snapd/version_.d deleted file mode 100644 index bc63b1b..0000000 --- a/source/snapd/version_.d +++ /dev/null @@ -1,3 +0,0 @@ -module snapd.version_; - -enum snapdVersion = "0.0.3"; From cdd2ec5875dc8d5fb9bc1f91c9e378b444e8e7e5 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sat, 24 May 2025 11:02:01 +0300 Subject: [PATCH 12/43] =?UTF-8?q?=D0=98=D1=81=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20core=20=D0=B2=D1=8B=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D0=B5=D0=BD=D1=8B=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB?= =?UTF-8?q?=D1=8C=20exception?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snag/core/core.d | 11 +---------- source/snag/core/exception.d | 14 ++++++++++++++ source/snag/core/package.d | 1 + 3 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 source/snag/core/exception.d diff --git a/source/snag/core/core.d b/source/snag/core/core.d index c60d315..06aad49 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -4,20 +4,11 @@ import snag.config; import std.format; import std.stdio; import std.array; -import std.exception; import std.process; import std.algorithm; import std.string; -class SnagException : Exception { - this(string msg, string file = __FILE__, size_t line = __LINE__) { - super(msg, file, line); - } - - void print() { - writeln(msg); - } -} +import snag.core.exception; class Snag { private string[] _baseCommand; diff --git a/source/snag/core/exception.d b/source/snag/core/exception.d new file mode 100644 index 0000000..1b79433 --- /dev/null +++ b/source/snag/core/exception.d @@ -0,0 +1,14 @@ +module snag.core.exception; + +import std.exception; +import std.stdio; + +class SnagException : Exception { + this(string msg, string file = __FILE__, size_t line = __LINE__) { + super(msg, file, line); + } + + void print() { + writeln(msg); + } +} diff --git a/source/snag/core/package.d b/source/snag/core/package.d index 7e8e82a..1b88bba 100644 --- a/source/snag/core/package.d +++ b/source/snag/core/package.d @@ -1,3 +1,4 @@ module snag.core; public import snag.core.core; +public import snag.core.exception; From a193f5387199956ddce5949280e81097fcbdefe7 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sat, 24 May 2025 21:59:01 +0300 Subject: [PATCH 13/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=8B:=20-=20list=20-=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B1=D1=8D=D0=BA=D0=B0=D0=BF=D0=BE?= =?UTF-8?q?=D0=B2=20(=D1=81=D0=BD=D0=B8=D0=BC=D0=BA=D0=BE=D0=B2=20=D1=81?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F)=20-=20restore?= =?UTF-8?q?=20-=20=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20=D1=83?= =?UTF-8?q?=D0=BA=D0=B0=D0=B7=D0=B0=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=81?= =?UTF-8?q?=D0=BD=D0=B8=D0=BC=D0=BA=D0=B0=20=D0=98=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE:=20-=20create=20-=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BD=D0=B8=D0=BC=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D1=8F=D0=B5=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BE=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8?= =?UTF-8?q?=20=D0=BC=D0=B5=D1=80=D0=B4=D0=B6=20=D0=BE=D1=82=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D0=B2=D0=BB=D0=B5=D0=BD=D0=BD=D0=BE=D0=B9=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Текущие изменения позволяют создавать репозиторий, делать снимки состояния файлов и восстанавливать конкретное состояние --- source/snag/core/core.d | 180 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 3 deletions(-) diff --git a/source/snag/core/core.d b/source/snag/core/core.d index 06aad49..b09fb89 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -7,6 +7,7 @@ import std.array; import std.process; import std.algorithm; import std.string; +import std.regex; import snag.core.exception; @@ -14,6 +15,11 @@ class Snag { private string[] _baseCommand; private SnagConfig _config; + private bool isValidHash(string hash) { + auto hashPattern = ctRegex!r"^[a-fA-F0-9]{7}$"; + return !matchFirst(hash, hashPattern).empty; + } + this(SnagConfig config) { _baseCommand = format( "git --git-dir=%s --work-tree=%s", @@ -67,9 +73,9 @@ class Snag { 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]); @@ -77,7 +83,45 @@ class Snag { } void create() { + /** + * Проверка файлов на состояние изменения + */ auto result = execute( + _baseCommand ~ ["status", "--porcelain"] + ); + if (result.status) + throw new SnagException( + "An error occurred while checking the file tracking status:\n" + ~ result.output + ); + if (!result.output.length) + throw new SnagException( + "Current file state doesn't need to be archived again" + ); + + result = execute( + _baseCommand ~ [ + "rev-parse", "--short", "HEAD" + ] + ); + if (result.status) + throw new SnagException( + "Failed to retrieve current snapshot information:\n" + ~ result.output + ); + + string currentSnapshot = result.output.strip('\n'); + + result = execute( + _baseCommand ~ [ + "checkout", + "-b", + "mod", + currentSnapshot + ] + ); + + result = execute( _baseCommand ~ ["add", "."] ); if (result.status) @@ -95,6 +139,136 @@ class Snag { ~ result.output ); + result = execute( + _baseCommand ~ ["checkout", "master"] + ); + if (result.status) + throw new SnagException( + "Failed to perform intermediate state switching:\n" + ~ result.output + ); + + result = execute( + _baseCommand ~ [ + "merge", + "-X", + "theirs", + "--squash", + "mod" + ] + ); + if (result.status) + throw new SnagException( + "Failed to retrieve changes from the new intermediate snapshot:\n" + ~ result.output + ); + + result = execute( + _baseCommand ~ ["commit", "-m", "test"] + ); + if (result.status) + throw new SnagException( + "Failed to create backup after acquiring new intermediate snapshot:\n" + ~ result.output + ); + + result = execute( + _baseCommand ~ ["branch", "-D", "mod"] + ); + if (result.status) + throw new SnagException( + "Error while deleting temporary intermediate snapshot:\n" + ~ result.output + ); + writeln("Backup was created successfully"); } + + void list() { + auto result = execute( + _baseCommand ~ [ + "rev-parse", "--short", "HEAD" + ] + ); + if (result.status) + throw new SnagException( + "Failed to retrieve current snapshot information:\n" + ~ result.output + ); + + string current = result.output.strip('\n'); + + result = execute( + _baseCommand ~ [ + "log", + "--all", + "--date=format:%Y.%m.%d %H:%M", + "--pretty=format:%ad\t%h" + ] + ); + if (result.status) + throw new SnagException( + "Failed to retrieve the list of snapshots:\n" + ~ result.output + ); + + result.output.split('\n').each!((e) { + auto eArray = e.split('\t'); + if (current == eArray[1]) + writefln(" >\t%s\t%s", eArray[0], eArray[1]); + else + writefln("\t%s\t%s", eArray[0], eArray[1]); + }); + } + + void restore(string hash) { + if (!isValidHash(hash)) + throw new SnagException( + "Invalid snapshot hash provided" + ); + + auto result = execute( + _baseCommand ~ ["status", "--porcelain"] + ); + if (result.status) + throw new SnagException( + "An error occurred while checking the file tracking status:\n" + ~ result.output + ); + + if (result.output.length) { + result = execute( + _baseCommand ~ ["restore", "."] + ); + if (result.status) + throw new SnagException( + "Failed to reset file changes state:\n" + ~ result.output + ); + } + + result = execute( + _baseCommand ~ [ + "rev-parse", hash + ] + ); + if (result.status) + throw new SnagException( + "This snapshot is not available in the archive:\n" + ~ result.output.split('\n')[0] + ); + + result = execute( + _baseCommand ~ [ + "checkout", hash + ] + ); + if (result.status) + throw new SnagException( + "Failed to restore the snapshot state %s:\n" + .format(hash, result.output) + ); + + writeln("Backup was restored successfully"); + } } From fe55e8680fd9543d3e310f6ca5c9bde4cc4868dc Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sat, 24 May 2025 22:01:59 +0300 Subject: [PATCH 14/43] 0.0.5 --- .vscode/launch.json | 3 ++- snag.json | 2 +- source/app.d | 30 ++++++++++++++++++++---------- source/snag/version_.d | 2 +- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 400e7c1..03d6784 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,8 @@ "dubBuild": true, "name": "Build & Debug DUB project", "cwd": "${command:dubWorkingDirectory}", - "program": "bin/${command:dubTarget}" + "program": "bin/${command:dubTarget}", + "args": ["list"] } ] } \ No newline at end of file diff --git a/snag.json b/snag.json index f7d3e00..42a6ae2 100644 --- a/snag.json +++ b/snag.json @@ -1,6 +1,6 @@ { "git": "/tmp/testgit", - "project": "/home/alexander/Programming/new/dlang/snag/source", + "project": "/tmp/test", "email": "user@site.domain", "user": "snag", "presnag": [ diff --git a/source/app.d b/source/app.d index f3bf551..d0ff61d 100644 --- a/source/app.d +++ b/source/app.d @@ -12,6 +12,10 @@ int main(string[] args) .add(new Command("init", "Initializing the repository for storing snapshots")) .add(new Command("status", "Checking the status of tracked files")) .add(new Command("create", "Create a new backup")) + .add(new Command("list", "List of backups")) + .add(new Command("restore", "Restore to the specified snapshot state") + .add(new Argument("hash", "hash").required) + ) .add(new Option("c", "config", "Сonfiguration file path") .optional .validateEachWith( @@ -33,18 +37,24 @@ int main(string[] args) } auto snag = new Snag(config); - + import std.stdio; try { argumets - .on("init", (init) { - snag.initialize(); - }) - .on("status", (status) { - snag.status(); - }) - .on("create", (create) { - snag.create(); - }); + .on("init", init => + snag.initialize() + ) + .on("status", status => + snag.status() + ) + .on("create", create => + snag.create() + ) + .on("list", list => + snag.list() + ) + .on("restore", restore => + snag.restore(restore.arg("hash")) + ); } catch (SnagException e) { e.print(); return EXIT_FAILURE; diff --git a/source/snag/version_.d b/source/snag/version_.d index 2465932..2d93c85 100644 --- a/source/snag/version_.d +++ b/source/snag/version_.d @@ -1,3 +1,3 @@ module snag.version_; -enum snagVersion = "0.0.4"; +enum snagVersion = "0.0.5"; From dd5d57c75ba291d59a4da52f778932fb4ea22118 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sun, 25 May 2025 01:27:24 +0300 Subject: [PATCH 15/43] =?UTF-8?q?=D0=91=D1=8B=D0=BB=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B8=D0=B7=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=20=D1=80=D0=B5=D1=84?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=BE=D0=B2=20git.=20=D0=9D=D0=B5=D0=BE?= =?UTF-8?q?=D0=B1=D1=85=D0=BE=D0=B4=D0=B8=D0=BC=D0=BE=20=D0=B4=D0=BE=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D1=82=D1=8C=20create,=20=D1=82?= =?UTF-8?q?=D0=B0=D0=BA=20=D0=BA=D0=B0=D0=BA=20=D0=BF=D1=80=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B8=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BC=D0=B8=D1=82=D0=B0=20=D0=B2=20=D0=BE=D0=B4=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B2=D0=B5=D1=82=D0=B2=D0=B8=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B8=D1=85=D0=BE=D0=B4=D0=B8=D1=82=20"=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=BE=D1=81"=20=D0=BD=D0=B5=D0=BD=D1=83=D0=B6?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B2=20=D0=B2=D1=8B=D1=88=D0=B5=D1=81=D1=82=D0=BE=D1=8F=D1=89?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82=D1=8B,=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B5=D0=B4=D1=83=D1=8E=D1=89=D0=B8=D1=85=20=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=80=D0=B8=D0=B8=20=D0=BA=D0=BE=D0=BC=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D0=BE=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snag/core/core.d | 261 ++++++++++++++-------------------------- 1 file changed, 87 insertions(+), 174 deletions(-) diff --git a/source/snag/core/core.d b/source/snag/core/core.d index b09fb89..c283cc8 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -28,32 +28,28 @@ class Snag { _config = config; } + private auto git(string[] command, string message, string separator = ":\n") { + auto result = execute(_baseCommand ~ command); + if (result.status) + throw new SnagException( + message ~ separator ~ result.output + ); + return result; + } + void initialize() { - auto result = execute(_baseCommand ~ "init"); - if (result.status) - throw new SnagException( - "A Git repository initialization error occurred:\n" - ~ result.output - ); - - result = execute( - _baseCommand ~ ["config", "user.email", _config.email] + git( + ["init"], + "A Git repository initialization error occurred" ); - if (result.status) - throw new SnagException( - "A Git repository initialization error occurred:\n" - ~ result.output - ); - - result = execute( - _baseCommand ~ ["config", "user.name", _config.user] + git( + ["config", "user.email", _config.email], + "A Git repository initialization error occurred" + ); + git( + ["config", "user.name", _config.user], + "A Git repository initialization error occurred" ); - if (result.status) - throw new SnagException( - "A Git repository initialization error occurred:\n" - ~ result.output - ); - writeln( "The Git repository has been initialized successfully: " ~ _config.git @@ -61,50 +57,36 @@ class Snag { } void status() { - auto result = execute( - _baseCommand ~ ["status", "--porcelain"] + auto result = git( + ["status", "--porcelain"], + "An error occurred while checking the file tracking status" ); - if (result.status) - throw new SnagException( - "An error occurred while checking the file tracking status:\n" - ~ result.output - ); 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]); }); } void create() { - /** - * Проверка файлов на состояние изменения - */ - auto result = execute( - _baseCommand ~ ["status", "--porcelain"] + auto result = git( + ["status", "--porcelain"], + "An error occurred while checking the file tracking status" ); - if (result.status) - throw new SnagException( - "An error occurred while checking the file tracking status:\n" - ~ result.output - ); + if (!result.output.length) throw new SnagException( "Current file state doesn't need to be archived again" ); - result = execute( - _baseCommand ~ [ - "rev-parse", "--short", "HEAD" - ] - ); - if (result.status) + result = execute(_baseCommand ~ ["rev-parse", "--short", "HEAD"]); + if (result.status == 128) { + git(["add", "."], "Failed to prepare files for archiving"); + git(["commit", "-m", "test"], "Failed to create a backup"); + writeln("Backup was created successfully"); + return; + } else if (result.status != 0) throw new SnagException( "Failed to retrieve current snapshot information:\n" ~ result.output @@ -112,113 +94,67 @@ class Snag { string currentSnapshot = result.output.strip('\n'); - result = execute( - _baseCommand ~ [ - "checkout", - "-b", - "mod", - currentSnapshot - ] + git( + ["checkout", "-b", "mod", currentSnapshot ], + "Failed to create a branch from the current state" + ); + git( + ["add", "."], + "Failed to prepare files for archiving" + ); + git( + ["commit", "-m", "test"], + "Failed to create a backup" + ); + git( + ["checkout", "master"], + "Failed to perform intermediate state switching" + ); + git( + ["rebase", "--onto", "mod", currentSnapshot, "master", "-X", "theirs"], + "Ошибка при rebase" ); - result = execute( - _baseCommand ~ ["add", "."] + result = git( + ["rev-parse", "--short", "mod"], + "An error occurred while checking the file tracking status" ); - if (result.status) - throw new SnagException( - "Failed to prepare files for archiving:\n" - ~ result.output - ); - result = execute( - _baseCommand ~ ["commit", "-m", "test"] - ); - if (result.status) - throw new SnagException( - "Failed to create a backup:\n" - ~ result.output - ); + string newSnapshot = result.output.strip('\n'); - result = execute( - _baseCommand ~ ["checkout", "master"] + git( + ["checkout", newSnapshot], + "Failed to perform intermediate state switching" ); - if (result.status) - throw new SnagException( - "Failed to perform intermediate state switching:\n" - ~ result.output - ); - - result = execute( - _baseCommand ~ [ - "merge", - "-X", - "theirs", - "--squash", - "mod" - ] + git( + ["branch", "-D", "mod"], + "Error while deleting temporary intermediate snapshot" ); - if (result.status) - throw new SnagException( - "Failed to retrieve changes from the new intermediate snapshot:\n" - ~ result.output - ); - - result = execute( - _baseCommand ~ ["commit", "-m", "test"] - ); - if (result.status) - throw new SnagException( - "Failed to create backup after acquiring new intermediate snapshot:\n" - ~ result.output - ); - - result = execute( - _baseCommand ~ ["branch", "-D", "mod"] - ); - if (result.status) - throw new SnagException( - "Error while deleting temporary intermediate snapshot:\n" - ~ result.output - ); - writeln("Backup was created successfully"); } void list() { - auto result = execute( - _baseCommand ~ [ - "rev-parse", "--short", "HEAD" - ] + auto result = git( + ["rev-parse", "--short", "HEAD"], + "Failed to retrieve current snapshot information" ); - if (result.status) - throw new SnagException( - "Failed to retrieve current snapshot information:\n" - ~ result.output - ); string current = result.output.strip('\n'); - result = execute( - _baseCommand ~ [ + result = git( + [ "log", "--all", "--date=format:%Y.%m.%d %H:%M", "--pretty=format:%ad\t%h" - ] + ], + "Failed to retrieve the list of snapshots" ); - if (result.status) - throw new SnagException( - "Failed to retrieve the list of snapshots:\n" - ~ result.output - ); - result.output.split('\n').each!((e) { - auto eArray = e.split('\t'); - if (current == eArray[1]) - writefln(" >\t%s\t%s", eArray[0], eArray[1]); - else - writefln("\t%s\t%s", eArray[0], eArray[1]); - }); + result.output.split('\n').map!(line => line.split('\t')).array + .sort!((a, b) => a[0] > b[0]).each!(e => + writefln("%s\t%s\t%s", current == e[1] ? " >" : "", e[0], e[1]) + ); } void restore(string hash) { @@ -227,48 +163,25 @@ class Snag { "Invalid snapshot hash provided" ); - auto result = execute( - _baseCommand ~ ["status", "--porcelain"] + auto result = git( + ["status", "--porcelain"], + "An error occurred while checking the file tracking status" ); - if (result.status) - throw new SnagException( - "An error occurred while checking the file tracking status:\n" - ~ result.output + + if (result.output.length) + git( + ["restore", "."], + "Failed to reset file changes state" ); - if (result.output.length) { - result = execute( - _baseCommand ~ ["restore", "."] - ); - if (result.status) - throw new SnagException( - "Failed to reset file changes state:\n" - ~ result.output - ); - } - - result = execute( - _baseCommand ~ [ - "rev-parse", hash - ] + git( + ["rev-parse", hash], + "This snapshot is not available in the archive" ); - if (result.status) - throw new SnagException( - "This snapshot is not available in the archive:\n" - ~ result.output.split('\n')[0] - ); - - result = execute( - _baseCommand ~ [ - "checkout", hash - ] + git( + ["checkout", hash], + "Failed to restore the snapshot state " ~ hash ); - if (result.status) - throw new SnagException( - "Failed to restore the snapshot state %s:\n" - .format(hash, result.output) - ); - writeln("Backup was restored successfully"); } } From 44f8a2207d9b24426fc9a87f39aced6a7658597a Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sun, 25 May 2025 03:32:47 +0300 Subject: [PATCH 16/43] =?UTF-8?q?=D0=A2=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82=D1=8B=20=D1=81=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D1=8E=D1=82=D1=81=D1=8F=20=D0=BA=D0=BE=D1=80=D1=80?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=BD=D0=BE,=20=D0=BD=D0=B5=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=BA=D1=80=D1=8B=D0=B2=D0=B0=D1=8E=D1=82=20=D1=81?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=BE=D0=B2=20=D0=B4=D1=80=D1=83=D0=B3=20=D0=B4?= =?UTF-8?q?=D1=80=D1=83=D0=B3=D0=B0.=20=D0=9F=D1=80=D0=B8=20=D1=81=D0=BE?= =?UTF-8?q?=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B8=20=D0=BA=D0=BE=D0=BC=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D0=BE=D0=B2=20=D0=BF=D1=80=D0=BE=D0=B8=D1=81=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=D0=B8=D1=82=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BD=D0=B0=20=D0=BD=D0=B0=D0=BB=D0=B8=D1=87?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=D0=B4=D0=BD=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82=D0=B0=20?= =?UTF-8?q?=D0=B2=20=D0=B2=D0=B5=D1=82=D0=BA=D0=B5=20=D0=B8,=20=D0=B5?= =?UTF-8?q?=D1=81=D0=BB=D0=B8=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D1=8F=D0=B2=D0=BB=D1=8F=D0=B5=D1=82=D1=81=D1=8F?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=D0=B4=D0=BD=D0=B8=D0=BC=20-?= =?UTF-8?q?=20=D1=82=D0=BE=20=D0=BF=D1=80=D0=BE=D0=B8=D1=81=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D1=82=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20"=D1=82=D0=B5=D0=BA=D1=83=D1=89?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=B4=D0=B0=D1=82=D0=B0"=20+=20"=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=D1=83=D1=89=D0=B8=D0=B9=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8?= =?UTF-8?q?=D1=82".=20=D0=9D=D0=B5=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=BE?= =?UTF-8?q?=D0=B9=20=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B3.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snag/core/core.d | 126 +++++++++++++++++++++++++--------------- 1 file changed, 80 insertions(+), 46 deletions(-) diff --git a/source/snag/core/core.d b/source/snag/core/core.d index c283cc8..61f561c 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -2,6 +2,7 @@ module snag.core.core; import snag.config; import std.format; +import std.datetime : Clock; import std.stdio; import std.array; import std.process; @@ -14,6 +15,7 @@ import snag.core.exception; class Snag { private string[] _baseCommand; private SnagConfig _config; + private string _date; private bool isValidHash(string hash) { auto hashPattern = ctRegex!r"^[a-fA-F0-9]{7}$"; @@ -26,6 +28,15 @@ class Snag { config.git, config.project ).split(); _config = config; + + auto currentTime = Clock.currTime(); + + _date = format("%02d%03d%02d%02d%02d", + currentTime.year % 100, + currentTime.dayOfYear, + currentTime.hour, + currentTime.minute, + currentTime.second); } private auto git(string[] command, string message, string separator = ":\n") { @@ -51,8 +62,8 @@ class Snag { "A Git repository initialization error occurred" ); writeln( - "The Git repository has been initialized successfully: " - ~ _config.git + "The Git repository has been initialized successfully: ", + _config.git ); } @@ -62,8 +73,12 @@ class Snag { "An error occurred while checking the file tracking status" ); - writeln("The following list of files requires backup:"); + if (!result.output.length) { + writeln("The current state of the files is up to date as of the latest snapshot"); + 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]); }); @@ -75,6 +90,7 @@ class Snag { "An error occurred while checking the file tracking status" ); + // Если нечего коммитить, то выходим if (!result.output.length) throw new SnagException( "Current file state doesn't need to be archived again" @@ -82,6 +98,7 @@ class Snag { result = execute(_baseCommand ~ ["rev-parse", "--short", "HEAD"]); if (result.status == 128) { + // Если это самый первый коммит после инициализации репозитория git(["add", "."], "Failed to prepare files for archiving"); git(["commit", "-m", "test"], "Failed to create a backup"); writeln("Backup was created successfully"); @@ -92,56 +109,76 @@ class Snag { ~ result.output ); + // Текущий коммит, который был изменен string currentSnapshot = result.output.strip('\n'); - git( - ["checkout", "-b", "mod", currentSnapshot ], - "Failed to create a branch from the current state" - ); - git( - ["add", "."], - "Failed to prepare files for archiving" - ); - git( - ["commit", "-m", "test"], - "Failed to create a backup" - ); - git( - ["checkout", "master"], - "Failed to perform intermediate state switching" - ); - git( - ["rebase", "--onto", "mod", currentSnapshot, "master", "-X", "theirs"], - "Ошибка при rebase" - ); + // Если текущий измененный коммит является последним в ветке - то продолжить коммиты в этой ветке + string currentBranch = git( + ["for-each-ref", "--contains", currentSnapshot, "--format='%(refname:short)'"], + "Error while getting the current branch" + ).output.split('\n')[0].strip('\''); + // Получение списка коммитов между текущим и веткой result = git( - ["rev-parse", "--short", "mod"], - "An error occurred while checking the file tracking status" + ["log", "--oneline", "HEAD.." ~ currentBranch], + "Failed to get the commit list between HEAD and " ~ currentBranch ); - string newSnapshot = result.output.strip('\n'); + string newSnapshot; - git( - ["checkout", newSnapshot], - "Failed to perform intermediate state switching" - ); - git( - ["branch", "-D", "mod"], - "Error while deleting temporary intermediate snapshot" - ); - writeln("Backup was created successfully"); + // Если список существует + if (result.output.length) { + // Если коммит не является последним, то необходимо ответвление + string newBranch = "%s-%s".format(_date, currentSnapshot); + git( + ["checkout", "-b", newBranch, currentSnapshot ], + "Failed to create a branch from the current state" + ); + git( + ["add", "."], + "Failed to prepare files for archiving" + ); + git( + ["commit", "-m", "test"], + "Failed to create a backup" + ); + newSnapshot = git( + ["rev-parse", "--short", "HEAD"], + "Failed to retrieve current snapshot information" + ).output.strip('\n'); + } else { + // Если коммит является посленим в ветке + git( + ["add", "."], + "Failed to prepare files for archiving" + ); + git( + ["commit", "-m", "test"], + "Failed to create a backup" + ); + newSnapshot = git( + ["rev-parse", "--short", "HEAD"], + "Failed to retrieve current snapshot information" + ).output.strip('\n'); + git( + ["checkout", currentBranch], + "Failed to perform intermediate state switching" + ); + git( + ["merge", newSnapshot], + "Issue with including the commit into the branch " ~ currentBranch + ); + } + writeln("Backup was created successfully: ", newSnapshot); } void list() { - auto result = git( + string currentSnapshot = git( ["rev-parse", "--short", "HEAD"], "Failed to retrieve current snapshot information" - ); + ).output.strip('\n'); - string current = result.output.strip('\n'); - - result = git( + git( [ "log", "--all", @@ -149,11 +186,8 @@ class Snag { "--pretty=format:%ad\t%h" ], "Failed to retrieve the list of snapshots" - ); - - result.output.split('\n').map!(line => line.split('\t')).array - .sort!((a, b) => a[0] > b[0]).each!(e => - writefln("%s\t%s\t%s", current == e[1] ? " >" : "", e[0], e[1]) + ).output.split('\n').map!(line => line.split('\t')).array.each!(e => + writefln("%s\t%s\t%s", currentSnapshot == e[1] ? " >" : "", e[0], e[1]) ); } @@ -182,6 +216,6 @@ class Snag { ["checkout", hash], "Failed to restore the snapshot state " ~ hash ); - writeln("Backup was restored successfully"); + writeln("Backup was restored successfully: ", hash); } } From 3dedf89b09352589d4b37befaba2d2226b906a58 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sun, 25 May 2025 03:49:40 +0300 Subject: [PATCH 17/43] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=BE=D0=B1=20=D0=B8=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/README.md b/README.md index 868928f..e5473f0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,68 @@ # snag Snapshot Git - система резервного копирования на основе фиксации состояния файлов с помощью Git. + +## Использование + +### Описание файла конфигурации + +--- + +```json +{ + "git": "/tmp/testgit", + "project": "/tmp/test", + "email": "user@site.domain", + "user": "snag" +} +``` + +1. **`"git"`** + - *Тип:* Строка (путь) + - *Назначение:* Указывает расположение Git-репозитория + +2. **`"project"`** + - *Тип:* Строка (путь) + - *Назначение:* Корневая директория для отслеживания состояния файлов + +3. **`"email"`** + - *Тип:* Строка (email) + - *Назначение:* Email автора коммитов в Git + +4. **`"user"`** + - *Тип:* Строка + - *Назначение:* Имя пользователя для Git-операций + +### Описание программы `snag` + +--- + +#### **Общий синтаксис** + +```bash +snag [ОПЦИИ] [ГЛАВНАЯ_КОМАНДА] [ОПЦИИ] [АРГУМЕНТЫ] +``` + +--- + +#### **Основные команды** + +| Команда | Описание | +|------------|--------------------------------------------------------------------------| +| `restore` | Восстановить проект до состояния указанного снимка | +| `init` | Инициализировать репозиторий для хранения снимков | +| `list` | Показать список доступных снимков | +| `status` | Проверить состояние отслеживаемых файлов (изменения с последнего снимка) | +| `create` | Создать новый снимок состояния проекта | + +--- + +#### **Флаги и опции** + +| Опция | Описание | +|---------------------|--------------------------------------------------------------------------| +| `-h`, `--help` | Показать справку по команде | +| `--version` | Показать версию программы | +| `-c`, `--config` | Указать путь к файлу конфигурации (необязательно) | + +--- From 11eecbf1282cd7c5ae73319a3b3506782b705fe0 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sun, 25 May 2025 03:49:50 +0300 Subject: [PATCH 18/43] 0.0.6 --- source/snag/version_.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/snag/version_.d b/source/snag/version_.d index 2d93c85..b137cfa 100644 --- a/source/snag/version_.d +++ b/source/snag/version_.d @@ -1,3 +1,3 @@ module snag.version_; -enum snagVersion = "0.0.5"; +enum snagVersion = "0.0.6"; From 9835924defca81a24d199cd8542ead28c167b43f Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sun, 25 May 2025 13:21:52 +0300 Subject: [PATCH 19/43] =?UTF-8?q?=D0=9E=D1=82=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B0=D1=82=D1=8C=20=D0=B2=20=D0=B2=D1=8B=D0=B2=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20=D1=81?= =?UTF-8?q?=D0=BD=D0=B8=D0=BC=D0=BA=D0=BE=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B9,=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F,?= =?UTF-8?q?=20=D1=8D=D0=BB=D0=B5=D0=BA=D1=82=D1=80=D0=BE=D0=BD=D0=BD=D1=83?= =?UTF-8?q?=D1=8E=20=D0=BF=D0=BE=D1=87=D1=82=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snag/core/core.d | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/source/snag/core/core.d b/source/snag/core/core.d index 61f561c..5aae3ec 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -172,22 +172,31 @@ class Snag { writeln("Backup was created successfully: ", newSnapshot); } - void list() { + void list(bool comment, bool user, bool email) { string currentSnapshot = git( ["rev-parse", "--short", "HEAD"], "Failed to retrieve current snapshot information" ).output.strip('\n'); + string format = "format:%h\t%ad"; + + comment && (format ~= "\t%s"); + user && (format ~= "\t%an"); + email && (format ~= "\t%ae"); + git( [ "log", "--all", "--date=format:%Y.%m.%d %H:%M", - "--pretty=format:%ad\t%h" + "--pretty=" ~ format ], "Failed to retrieve the list of snapshots" ).output.split('\n').map!(line => line.split('\t')).array.each!(e => - writefln("%s\t%s\t%s", currentSnapshot == e[1] ? " >" : "", e[0], e[1]) + writefln("%s\t%s", + currentSnapshot == e[0] ? " >" : "", + e.join("\t") + ) ); } From eff4fa2fe6b01272f5c0def107724e87147769bf Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sun, 25 May 2025 13:22:11 +0300 Subject: [PATCH 20/43] 0.0.7 --- source/app.d | 21 +++++++++++++++++++-- source/snag/version_.d | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/source/app.d b/source/app.d index d0ff61d..97c24ba 100644 --- a/source/app.d +++ b/source/app.d @@ -12,7 +12,20 @@ int main(string[] args) .add(new Command("init", "Initializing the repository for storing snapshots")) .add(new Command("status", "Checking the status of tracked files")) .add(new Command("create", "Create a new backup")) - .add(new Command("list", "List of backups")) + .add(new Command("list", "List of backups") + .add(new Flag("c", "comment", "Show comment") + .name("comment") + .optional + ) + .add(new Flag("u", "user", "Show user") + .name("user") + .optional + ) + .add(new Flag("e", "email", "Show email") + .name("email") + .optional + ) + ) .add(new Command("restore", "Restore to the specified snapshot state") .add(new Argument("hash", "hash").required) ) @@ -50,7 +63,11 @@ int main(string[] args) snag.create() ) .on("list", list => - snag.list() + snag.list( + list.flag("comment"), + list.flag("user"), + list.flag("email") + ) ) .on("restore", restore => snag.restore(restore.arg("hash")) diff --git a/source/snag/version_.d b/source/snag/version_.d index b137cfa..2bdebb8 100644 --- a/source/snag/version_.d +++ b/source/snag/version_.d @@ -1,3 +1,3 @@ module snag.version_; -enum snagVersion = "0.0.6"; +enum snagVersion = "0.0.7"; From 4301c27ca9e3ce4ecf52fe9e3a42f69e851abf0a Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sun, 25 May 2025 18:34:52 +0300 Subject: [PATCH 21/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D0=B0=20diff=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BF=D1=80=D0=BE=D1=81=D0=BC=D0=BE=D1=82=D1=80=D0=B0?= =?UTF-8?q?=20=D0=B2=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=82=D0=B5=D0=BA=D1=83=D1=89=D0=B8=D0=B9=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=BC=D0=B5=D0=BD=D1=82.=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE?= =?UTF-8?q?=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=83=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D1=8F,=20=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B0,=20=D1=8D?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=82=D1=80=D0=BE=D0=BD=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=87=D1=82=D1=8B=20=D0=BF=D1=80=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B8=20=D1=81=D0=BD=D0=B8?= =?UTF-8?q?=D0=BC=D0=BA=D0=B0.=20=D0=A4=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BE=D0=BA=20?= =?UTF-8?q?=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D1=80=D0=B5=D0=B3=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D1=80=D0=BD=D1=8B=D0=B5=20=D0=B2=D1=8B=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D0=B5=D0=BD=D1=8B=20=D0=B2=20=D0=BD=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20lib.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- snag.json | 2 +- source/app.d | 44 +++++++++++++++++++++++++++++++------ source/snag/config/config.d | 21 +++++++----------- source/snag/core/core.d | 40 +++++++++++++++++++++------------ source/snag/lib/lib.d | 13 +++++++++++ source/snag/lib/package.d | 3 +++ source/snag/package.d | 1 + source/snag/version_.d | 2 +- 8 files changed, 90 insertions(+), 36 deletions(-) create mode 100644 source/snag/lib/lib.d create mode 100644 source/snag/lib/package.d diff --git a/snag.json b/snag.json index 42a6ae2..d8f176b 100644 --- a/snag.json +++ b/snag.json @@ -2,7 +2,7 @@ "git": "/tmp/testgit", "project": "/tmp/test", "email": "user@site.domain", - "user": "snag", + "author": "snag", "presnag": [ "/usr/bin/ls", "/usr/local/bin/script.sh" diff --git a/source/app.d b/source/app.d index 97c24ba..7592ee2 100644 --- a/source/app.d +++ b/source/app.d @@ -11,14 +11,37 @@ int main(string[] args) auto argumets = new Program(programName, snagVersion) .add(new Command("init", "Initializing the repository for storing snapshots")) .add(new Command("status", "Checking the status of tracked files")) - .add(new Command("create", "Create a new backup")) - .add(new Command("list", "List of backups") + .add(new Command("diff", "Show changed data")) + .add(new Command("create", "Create a new snapshot") + .add(new Option("c", "comment", "Specify comment") + .optional + .validateEachWith( + opt => opt.length > 0, + "cannot be empty" + ) + ) + .add(new Option("a", "author", "Specify author") + .optional + .validateEachWith( + opt => opt.length > 0, + "cannot be empty" + ) + ) + .add(new Option("e", "email", "Specify email") + .optional + .validateEachWith( + opt => opt.isValidEmail, + "must contain an email address" + ) + ) + ) + .add(new Command("list", "List of snapshots") .add(new Flag("c", "comment", "Show comment") .name("comment") .optional ) - .add(new Flag("u", "user", "Show user") - .name("user") + .add(new Flag("a", "author", "Show author") + .name("author") .optional ) .add(new Flag("e", "email", "Show email") @@ -33,7 +56,7 @@ int main(string[] args) .optional .validateEachWith( opt => opt.exists && opt.isFile, - "A JSON file path must be provided" + "must specify the path to the JSON file" ) ) .parse(args); @@ -56,16 +79,23 @@ int main(string[] args) .on("init", init => snag.initialize() ) + .on("diff", diff => + snag.diff() + ) .on("status", status => snag.status() ) .on("create", create => - snag.create() + snag.create( + create.option("comment", ""), + create.option("author", ""), + create.option("email", "") + ) ) .on("list", list => snag.list( list.flag("comment"), - list.flag("user"), + list.flag("author"), list.flag("email") ) ) diff --git a/source/snag/config/config.d b/source/snag/config/config.d index 2d25f51..75de340 100644 --- a/source/snag/config/config.d +++ b/source/snag/config/config.d @@ -3,8 +3,8 @@ module snag.config.config; import std.json; import std.file; import std.path; -import std.regex; import std.string; +import snag.lib; import snag.config.exception; @@ -12,12 +12,7 @@ class SnagConfig { private string _git; private string _project; private string _email; - private string _user; - - private bool isValidEmail(string email) { - auto emailPattern = ctRegex!r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"; - return !matchFirst(email, emailPattern).empty; - } + private string _author; this(string configFile) { string jsonText; @@ -87,21 +82,21 @@ class SnagConfig { ~ _email ); - if ("user" !in jsonData) + if ("author" !in jsonData) throw new SnagConfigException( - "The configuration file is missing the \"user\" parameter" + "The configuration file is missing the \"author\" parameter" ); - _user = jsonData["user"].str; + _author = jsonData["author"].str; - if (!_user.length) + if (!_author.length) throw new SnagConfigException( - "The \"user\" parameter must contain an user name" + "The \"author\" parameter must contain an author name" ); } @property string git() const { return _git; } @property string project() const { return _project; } @property string email() const { return _email; } - @property string user() const { return _user; } + @property string author() const { return _author; } } diff --git a/source/snag/core/core.d b/source/snag/core/core.d index 5aae3ec..828563b 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -8,8 +8,8 @@ import std.array; import std.process; import std.algorithm; import std.string; -import std.regex; +import snag.lib; import snag.core.exception; class Snag { @@ -17,11 +17,6 @@ class Snag { private SnagConfig _config; private string _date; - private bool isValidHash(string hash) { - auto hashPattern = ctRegex!r"^[a-fA-F0-9]{7}$"; - return !matchFirst(hash, hashPattern).empty; - } - this(SnagConfig config) { _baseCommand = format( "git --git-dir=%s --work-tree=%s", @@ -58,7 +53,7 @@ class Snag { "A Git repository initialization error occurred" ); git( - ["config", "user.name", _config.user], + ["config", "user.name", _config.author], "A Git repository initialization error occurred" ); writeln( @@ -84,7 +79,7 @@ class Snag { }); } - void create() { + void create(string comment, string author, string email) { auto result = git( ["status", "--porcelain"], "An error occurred while checking the file tracking status" @@ -96,11 +91,16 @@ class Snag { "Current file state doesn't need to be archived again" ); + author.length && (environment["GIT_AUTHOR_NAME"] = author); + email.length && (environment["GIT_AUTHOR_EMAIL"] = email); + + string message = comment.length ? comment : "Standard snapshot creation"; + result = execute(_baseCommand ~ ["rev-parse", "--short", "HEAD"]); if (result.status == 128) { // Если это самый первый коммит после инициализации репозитория git(["add", "."], "Failed to prepare files for archiving"); - git(["commit", "-m", "test"], "Failed to create a backup"); + git(["commit", "-m"] ~ message, "Failed to create a backup"); writeln("Backup was created successfully"); return; } else if (result.status != 0) @@ -139,7 +139,7 @@ class Snag { "Failed to prepare files for archiving" ); git( - ["commit", "-m", "test"], + ["commit", "-m"] ~ message, "Failed to create a backup" ); newSnapshot = git( @@ -153,7 +153,7 @@ class Snag { "Failed to prepare files for archiving" ); git( - ["commit", "-m", "test"], + ["commit", "-m"] ~ message, "Failed to create a backup" ); newSnapshot = git( @@ -172,7 +172,7 @@ class Snag { writeln("Backup was created successfully: ", newSnapshot); } - void list(bool comment, bool user, bool email) { + void list(bool comment, bool author, bool email) { string currentSnapshot = git( ["rev-parse", "--short", "HEAD"], "Failed to retrieve current snapshot information" @@ -181,7 +181,7 @@ class Snag { string format = "format:%h\t%ad"; comment && (format ~= "\t%s"); - user && (format ~= "\t%an"); + author && (format ~= "\t%an"); email && (format ~= "\t%ae"); git( @@ -193,7 +193,7 @@ class Snag { ], "Failed to retrieve the list of snapshots" ).output.split('\n').map!(line => line.split('\t')).array.each!(e => - writefln("%s\t%s", + writefln("%s\t%s", currentSnapshot == e[0] ? " >" : "", e.join("\t") ) @@ -227,4 +227,16 @@ class Snag { ); writeln("Backup was restored successfully: ", hash); } + + void diff() { + auto result = git( + ["diff"], + "Failed to retrieve changes" + ); + if (result.output.length) { + result.output.write; + return; + } + writeln("No changes at the moment"); + } } diff --git a/source/snag/lib/lib.d b/source/snag/lib/lib.d new file mode 100644 index 0000000..3277d0e --- /dev/null +++ b/source/snag/lib/lib.d @@ -0,0 +1,13 @@ +module snag.lib.lib; + +import std.regex; + +bool isValidHash(string hash) { + auto hashPattern = ctRegex!r"^[a-fA-F0-9]{7}$"; + return !matchFirst(hash, hashPattern).empty; +} + +bool isValidEmail(string email) { + auto emailPattern = ctRegex!r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"; + return !matchFirst(email, emailPattern).empty; +} diff --git a/source/snag/lib/package.d b/source/snag/lib/package.d new file mode 100644 index 0000000..46fa21c --- /dev/null +++ b/source/snag/lib/package.d @@ -0,0 +1,3 @@ +module snag.lib; + +public import snag.lib.lib; diff --git a/source/snag/package.d b/source/snag/package.d index 22b28bf..ae8b15d 100644 --- a/source/snag/package.d +++ b/source/snag/package.d @@ -1,5 +1,6 @@ module snag; public import snag.version_; +public import snag.lib; public import snag.config; public import snag.core; diff --git a/source/snag/version_.d b/source/snag/version_.d index 2bdebb8..0c40657 100644 --- a/source/snag/version_.d +++ b/source/snag/version_.d @@ -1,3 +1,3 @@ module snag.version_; -enum snagVersion = "0.0.7"; +enum snagVersion = "0.0.8"; From 4590ee5fbcefd182d20c8b50a03e6e8d64ab22f2 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Mon, 26 May 2025 02:14:18 +0300 Subject: [PATCH 22/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D0=B0?= =?UTF-8?q?=20export=20=D0=B4=D0=BB=D1=8F=20=D1=8D=D0=BA=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=D0=B0=20=D0=B2=20=D0=B0=D1=80=D1=85=D0=B8=D0=B2=20?= =?UTF-8?q?tar.gz=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20=D1=83=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D0=B0=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=81=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F=20=D1=81=D0=BD=D0=B8?= =?UTF-8?q?=D0=BC=D0=BA=D0=B0,=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB?= =?UTF-8?q?=D1=87=D0=B0=D0=BD=D0=B8=D1=8E=20=D1=8D=D0=BA=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=D0=B8=D1=80=D1=83=D0=B5=D1=82=D1=81=D1=8F=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D1=83=D1=89=D0=B5=D0=B5=20=D1=81=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D1=8F=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BD=D0=B8=D0=BC=D0=BA?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app.d | 20 ++++++++++++-- source/snag/core/core.d | 61 +++++++++++++++++++++++++++++++++++------ source/snag/version_.d | 2 +- 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/source/app.d b/source/app.d index 7592ee2..26110bb 100644 --- a/source/app.d +++ b/source/app.d @@ -12,6 +12,16 @@ int main(string[] args) .add(new Command("init", "Initializing the repository for storing snapshots")) .add(new Command("status", "Checking the status of tracked files")) .add(new Command("diff", "Show changed data")) + .add(new Command("export", "Export snapshot to a tar.gz archive") + .add(new Argument("path", "Output directory path for the archive").required) + .add(new Option("s", "snapshot", "Specify snapshot hash") + .optional + .validateEachWith( + opt => opt.isValidHash, + "must contain snapshot hash provided" + ) + ) + ) .add(new Command("create", "Create a new snapshot") .add(new Option("c", "comment", "Specify comment") .optional @@ -50,7 +60,7 @@ int main(string[] args) ) ) .add(new Command("restore", "Restore to the specified snapshot state") - .add(new Argument("hash", "hash").required) + .add(new Argument("snapshot", "Specify snapshot hash").required) ) .add(new Option("c", "config", "Сonfiguration file path") .optional @@ -100,7 +110,13 @@ int main(string[] args) ) ) .on("restore", restore => - snag.restore(restore.arg("hash")) + snag.restore(restore.arg("snapshot")) + ) + .on("export", e => + snag.exportSnapshot( + e.arg("path"), + e.option("snapshot", ""), + ) ); } catch (SnagException e) { e.print(); diff --git a/source/snag/core/core.d b/source/snag/core/core.d index 828563b..63f51d8 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -8,6 +8,8 @@ import std.array; import std.process; import std.algorithm; import std.string; +import std.file; +import std.path; import snag.lib; import snag.core.exception; @@ -22,10 +24,9 @@ class Snag { "git --git-dir=%s --work-tree=%s", config.git, config.project ).split(); + _config = config; - auto currentTime = Clock.currTime(); - _date = format("%02d%03d%02d%02d%02d", currentTime.year % 100, currentTime.dayOfYear, @@ -67,12 +68,10 @@ class Snag { ["status", "--porcelain"], "An error occurred while checking the file tracking status" ); - if (!result.output.length) { writeln("The current state of the files is up to date as of the latest snapshot"); 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]); @@ -205,18 +204,20 @@ class Snag { throw new SnagException( "Invalid snapshot hash provided" ); - auto result = git( ["status", "--porcelain"], "An error occurred while checking the file tracking status" ); - - if (result.output.length) + if (result.output.length) { git( ["restore", "."], "Failed to reset file changes state" ); - + git( + ["clean", "-fd"], + "Failed to clean untracked files" + ); + } git( ["rev-parse", hash], "This snapshot is not available in the archive" @@ -239,4 +240,48 @@ class Snag { } writeln("No changes at the moment"); } + + void exportSnapshot(string path, string hash) { + try { + !path.isDir && + throw new SnagException( + "Path is not a directory" + ); + } catch (Exception e) { + throw new SnagException( + "Failed to verify the archive output directory:\n\t" + ~ e.msg + ); + } + if (hash.length) { + !isValidHash(hash) && + throw new SnagException( + "Invalid snapshot hash provided" + ); + git( + ["rev-parse", hash], + "This snapshot is not available in the archive" + ); + } else { + hash = git( + ["rev-parse", "--short", "HEAD"], + "Failed to retrieve current snapshot information" + ).output.strip('\n'); + } + string file = buildPath( + path.absolutePath.buildNormalizedPath, + "%s-%s.tar.gz".format(_date, hash) + ); + git( + [ + "archive", + "--format=tar.gz", + hash, + "-o", + file + ], + "Failed to export snapshot to archive" + ); + writeln("Export to archive completed successfully: ", file); + } } diff --git a/source/snag/version_.d b/source/snag/version_.d index 0c40657..fad1eb1 100644 --- a/source/snag/version_.d +++ b/source/snag/version_.d @@ -1,3 +1,3 @@ module snag.version_; -enum snagVersion = "0.0.8"; +enum snagVersion = "0.0.9"; From 38b37eea70a58a7f9e334f9f85dbc4e4ad3d373e Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Mon, 26 May 2025 02:19:53 +0300 Subject: [PATCH 23/43] =?UTF-8?q?=D0=92=D1=8B=D0=B2=D0=BE=D0=B4=20=D1=82?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE?= =?UTF-8?q?=D0=B9=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B8=20git=20=D1=81?= =?UTF-8?q?=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snag/core/core.d | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/snag/core/core.d b/source/snag/core/core.d index 63f51d8..e528ab1 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -35,11 +35,11 @@ class Snag { currentTime.second); } - private auto git(string[] command, string message, string separator = ":\n") { + 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 + message ~ separator ~ result.output.split('\n')[0] ); return result; } From a7676d5886a036f2fe5b0a577aea64ca6ce91cf1 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Mon, 26 May 2025 02:41:55 +0300 Subject: [PATCH 24/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B0=20=D0=B8=D0=BD=D0=B8=D1=86=D0=B8=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8E=20=D1=80=D0=B5=D0=BF?= =?UTF-8?q?=D0=BE=D0=B7=D0=B8=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 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/source/snag/core/core.d b/source/snag/core/core.d index e528ab1..b114dd7 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -45,6 +45,12 @@ class Snag { } void initialize() { + auto result = execute(_baseCommand ~ ["rev-parse", "--git-dir"]); + !result.status && + throw new SnagException( + "The Git repository has already been initialized: " + ~ result.output.strip('\n') + ); git( ["init"], "A Git repository initialization error occurred" From cc85cdec78976ceca5b0bbe0e2510b65df8c32ce Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Mon, 26 May 2025 03:09:15 +0300 Subject: [PATCH 25/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B0=20=D0=BD=D0=B0=D0=BB=D0=B8=D1=87=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20git=20=D0=B2=20=D1=81=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=B5=D0=BC=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app.d | 8 +++++++- source/snag/lib/lib.d | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/source/app.d b/source/app.d index 26110bb..55f9e4a 100644 --- a/source/app.d +++ b/source/app.d @@ -1,6 +1,7 @@ import snag; import commandr; import std.file; +import std.stdio : writeln; import core.stdc.stdlib : EXIT_SUCCESS, EXIT_FAILURE; @@ -8,6 +9,11 @@ private string programName = "snag"; int main(string[] args) { + if (!checkGit()) { + "Using snag requires Git to be installed".writeln; + return EXIT_FAILURE; + } + auto argumets = new Program(programName, snagVersion) .add(new Command("init", "Initializing the repository for storing snapshots")) .add(new Command("status", "Checking the status of tracked files")) @@ -83,7 +89,7 @@ int main(string[] args) } auto snag = new Snag(config); - import std.stdio; + try { argumets .on("init", init => diff --git a/source/snag/lib/lib.d b/source/snag/lib/lib.d index 3277d0e..902691d 100644 --- a/source/snag/lib/lib.d +++ b/source/snag/lib/lib.d @@ -1,6 +1,7 @@ module snag.lib.lib; import std.regex; +import std.process; bool isValidHash(string hash) { auto hashPattern = ctRegex!r"^[a-fA-F0-9]{7}$"; @@ -11,3 +12,8 @@ bool isValidEmail(string email) { auto emailPattern = ctRegex!r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"; return !matchFirst(email, emailPattern).empty; } + +bool checkGit() { + auto result = execute(["which", "git"]); + return !result.status; +} From 9c4c2c9d057fd8fd24e0ea482db25859f8ca160f Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Mon, 26 May 2025 23:13:54 +0300 Subject: [PATCH 26/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D0=B0?= =?UTF-8?q?=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D0=B0=20import=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=B0=D1=80=D1=85=D0=B8=D0=B2=D0=B0=20tar.gz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app.d | 32 ++++++++++++++ source/snag/core/core.d | 95 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/source/app.d b/source/app.d index 55f9e4a..6633a1e 100644 --- a/source/app.d +++ b/source/app.d @@ -18,6 +18,30 @@ int main(string[] args) .add(new Command("init", "Initializing the repository for storing snapshots")) .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") + .add(new Argument("archive", "The path to the tar.gz archive file").required) + .add(new Option("c", "comment", "Specify comment") + .optional + .validateEachWith( + opt => opt.length > 0, + "cannot be empty" + ) + ) + .add(new Option("a", "author", "Specify author") + .optional + .validateEachWith( + opt => opt.length > 0, + "cannot be empty" + ) + ) + .add(new Option("e", "email", "Specify email") + .optional + .validateEachWith( + opt => opt.isValidEmail, + "must contain an email address" + ) + ) + ) .add(new Command("export", "Export snapshot to a tar.gz archive") .add(new Argument("path", "Output directory path for the archive").required) .add(new Option("s", "snapshot", "Specify snapshot hash") @@ -123,6 +147,14 @@ int main(string[] args) e.arg("path"), e.option("snapshot", ""), ) + ) + .on("import", i => + snag.importSnapshot( + i.arg("archive"), + i.option("comment", ""), + i.option("author", ""), + i.option("email", "") + ) ); } catch (SnagException e) { e.print(); diff --git a/source/snag/core/core.d b/source/snag/core/core.d index b114dd7..fe75fb1 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -290,4 +290,99 @@ class Snag { ); writeln("Export to archive completed successfully: ", file); } + + void importSnapshot(string path, string comment, string author, string email) { + try { + (!path.isFile || !path.endsWith("tar.gz")) && + throw new SnagException( + "Path is not an archive file" + ); + } catch (Exception e) { + throw new SnagException( + "Failed to verify the archive file:\n\t" + ~ e.msg + ); + } + + string newBranch = _date ~ "-import"; + string tempDirectory = tempDir().buildPath(newBranch); + + mkdir(tempDirectory); + + scope(exit) tempDirectory.exists + && tempDirectory.isDir + && tempDirectory.rmdirRecurse; + + auto result = execute([ + "tar", "xf", path, "-C", tempDirectory + ]); + result.status && + throw new SnagException( + "The error occurred during decompression (or unpacking) of the archive:\n\t" + ~ result.output.split('\n')[0] + ); + + // Выполнение git команд относительно распакованного архива + string[] customCommand = format( + "git --git-dir=%s --work-tree=%s", + _config.git, tempDirectory + ).split(); + + // Необходимо проверить, что текущее состояние файлов не идентично файлам распакованного архива + result = execute(customCommand ~ ["status", "--porcelain"]); + result.status && + throw new SnagException( + "An error occurred while checking the file tracking status:\n" + ~ result.output.split('\n')[0] + ); + + // Если текущее состояние файлов идентично файлам распакованного архива + !result.output.length && + throw new SnagException( + "Import aborted:\n\t" + ~ "The new state of the files is up to date as of the current snapshot" + ); + + git( + ["checkout", "--orphan", newBranch ], + "Failed to create a branch from the new state" + ); + + // Создание нового снимка на основе состояния файлов из распакованного архива + result = execute(customCommand ~ ["add", "."]); + result.status && + throw new SnagException( + "Failed to prepare files for archiving:\n" + ~ result.output.split('\n')[0] + ); + + author.length && (environment["GIT_AUTHOR_NAME"] = author); + email.length && (environment["GIT_AUTHOR_EMAIL"] = email); + + string message = comment.length ? comment : "Creating a snapshot from import"; + + result = execute(customCommand ~ ["commit", "-m"] ~ message); + result.status && + throw new SnagException( + "Failed to create a snapshot:\n" + ~ result.output.split('\n')[0] + ); + + // Сброс состояния файлов + git( + ["restore", "."], + "Failed to reset file changes state" + ); + git( + ["clean", "-fd"], + "Failed to clean untracked files" + ); + + string newSnapshot = git( + ["rev-parse", "--short", "HEAD"], + "Failed to retrieve current snapshot information" + ).output.strip('\n'); + + writeln("Import completed successfully: ", newSnapshot); + } } From 3638d0c48d4b1acfcd3071c0912ae9bfedef3d5d Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Mon, 26 May 2025 23:27:36 +0300 Subject: [PATCH 27/43] =?UTF-8?q?=D0=95=D1=81=D0=BB=D0=B8=20=D0=BD=D0=B0?= =?UTF-8?q?=D0=B9=D0=B4=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BC=D0=B8=D1=82=20=D1=81=D1=83=D1=89=D0=B5=D1=81=D1=82=D0=B2?= =?UTF-8?q?=D1=83=D0=B5=D1=82=20=D0=B2=20=D0=BD=D0=B5=D1=81=D0=BA=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=BA=D0=B8=D1=85=20=D0=B2=D0=B5=D1=82=D0=B2=D1=8F?= =?UTF-8?q?=D1=85,=20=D1=82=D0=BE=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D1=83=D0=B5=D1=82=D1=81=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=B2=D0=B5=D1=82=D0=B2=D1=8C,=20=D0=B2=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D0=BE=D0=B9=20=D1=8D=D1=82=D0=BE?= =?UTF-8?q?=D1=82=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=8F=D0=B2=D0=B8=D0=BB=D1=81=D1=8F=20-=20=D0=B8=D0=B7=20?= =?UTF-8?q?=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20=D1=81=D0=B0=D0=BC=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=D0=B4=D0=BD=D0=B8=D0=B9?= =?UTF-8?q?=20=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5=D0=BD=D1=82.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snag/core/core.d | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/snag/core/core.d b/source/snag/core/core.d index fe75fb1..284c87b 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -118,10 +118,11 @@ class Snag { string currentSnapshot = result.output.strip('\n'); // Если текущий измененный коммит является последним в ветке - то продолжить коммиты в этой ветке + // При разбивке по '\n' присутствует последний пустой элемент, поэтому нужный элемент 2-ой с конца string currentBranch = git( - ["for-each-ref", "--contains", currentSnapshot, "--format='%(refname:short)'"], + ["for-each-ref", "--format='%(refname:short)'", "--contains", currentSnapshot], "Error while getting the current branch" - ).output.split('\n')[0].strip('\''); + ).output.split('\n')[$-2].strip('\''); // Получение списка коммитов между текущим и веткой result = git( From e6f9a7a0ffaf405e1a972ebf0889c54fabab11cd Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Mon, 26 May 2025 23:42:12 +0300 Subject: [PATCH 28/43] =?UTF-8?q?=D0=98=D0=BD=D0=B8=D1=86=D0=B8=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=80=D0=B5=D0=BF?= =?UTF-8?q?=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D1=8F=20=D1=81=20def?= =?UTF-8?q?ault=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B5=D0=BC=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D0=B2=D0=B8=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB?= =?UTF-8?q?=D1=87=D0=B0=D0=BD=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snag/core/core.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/snag/core/core.d b/source/snag/core/core.d index 284c87b..34e75ae 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -52,7 +52,7 @@ class Snag { ~ result.output.strip('\n') ); git( - ["init"], + ["init", "--initial-branch=default"], "A Git repository initialization error occurred" ); git( From 35a7b26a4ab11f7c40939a964566f33ed1142633 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Mon, 26 May 2025 23:42:39 +0300 Subject: [PATCH 29/43] 0.0.10 --- source/snag/version_.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/snag/version_.d b/source/snag/version_.d index fad1eb1..82e9fc2 100644 --- a/source/snag/version_.d +++ b/source/snag/version_.d @@ -1,3 +1,3 @@ module snag.version_; -enum snagVersion = "0.0.9"; +enum snagVersion = "0.0.10"; From db9a6be9f439ee4f41096ba02cbe6d5238e3c7d1 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Tue, 27 May 2025 00:12:43 +0300 Subject: [PATCH 30/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=B8=D0=BD=D0=B8=D1=86=D0=B8=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D1=80=D0=B5=D0=BF?= =?UTF-8?q?=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D1=8F=20=D1=81=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D1=8C?= =?UTF-8?q?=D1=8E=20=D1=81=D1=83=D1=89=D0=B5=D1=81=D1=82=D0=B2=D1=83=D1=8E?= =?UTF-8?q?=D1=89=D0=B5=D0=B3=D0=BE=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20?= =?UTF-8?q?=D1=84=D0=BB=D0=B0=D0=B3=20force?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app.d | 9 +++++++-- source/snag/core/core.d | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/source/app.d b/source/app.d index 6633a1e..4b40f32 100644 --- a/source/app.d +++ b/source/app.d @@ -15,7 +15,12 @@ int main(string[] args) } auto argumets = new Program(programName, snagVersion) - .add(new Command("init", "Initializing the repository for storing snapshots")) + .add(new Command("init", "Initializing the repository for storing snapshots") + .add(new Flag("f", "force", "Initializing a repository with overwriting the existing one") + .name("force") + .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") @@ -117,7 +122,7 @@ int main(string[] args) try { argumets .on("init", init => - snag.initialize() + snag.initialize(init.flag("force")) ) .on("diff", diff => snag.diff() diff --git a/source/snag/core/core.d b/source/snag/core/core.d index 34e75ae..e70ce62 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -44,13 +44,18 @@ class Snag { return result; } - void initialize() { + void initialize(bool force) { auto result = execute(_baseCommand ~ ["rev-parse", "--git-dir"]); - !result.status && + !force && !result.status && throw new SnagException( "The Git repository has already been initialized: " ~ result.output.strip('\n') ); + + force && _config.git.exists + && _config.git.isDir + && _config.git.rmdirRecurse; + git( ["init", "--initial-branch=default"], "A Git repository initialization error occurred" From 5797e83f07e3ec6cecee38aabc7c99b8f9d4bfc2 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Wed, 28 May 2025 00:17:42 +0300 Subject: [PATCH 31/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D1=83=D0=BB=D1=8C=20rules=20=D0=B4=D0=BB=D1=8F=20=D1=83?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=D0=BC=D0=B8=20=D0=BE=D1=82?= =?UTF-8?q?=D1=81=D0=BB=D0=B5=D0=B6=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20=D0=B2=20=D0=B2=D0=B8?= =?UTF-8?q?=D0=B4=D0=B5=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20gitignore=20-=20create:=20=D1=81=D0=BE?= =?UTF-8?q?=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B8=D0=B7=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8?= =?UTF-8?q?=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=BE=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0:=20=20=20=20=20-?= =?UTF-8?q?=20tracking=20-=20=D0=BE=D1=82=D1=81=D0=BB=D0=B5=D0=B6=D0=B8?= =?UTF-8?q?=D0=B2=D0=B0=D0=B5=D0=BC=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D1=8B/=D0=BF=D1=83=D1=82=D0=B8=20=20=20=20=20-=20ignore=20-=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=20gitignore=20-=20up?= =?UTF-8?q?date:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=81=D1=83=D1=89=D0=B5=D1=81=D1=82=D0=B2=D1=83=D1=8E?= =?UTF-8?q?=D1=89=D0=B8=D1=85=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=B8=D0=B7=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=BE=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D0=B0=20-=20reset:=20=D1=81=D0=B1=D1=80?= =?UTF-8?q?=D0=BE=D1=81=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=B2=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0?= =?UTF-8?q?=D1=85=20-=20=D0=BE=D1=82=D0=BA=D0=B0=D1=82=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=BC=D0=B5=D0=BD=D1=82=20=D0=B4=D0=BE=20=D0=B2?= =?UTF-8?q?=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=BE?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB=20-=20clear?= =?UTF-8?q?:=20=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=B8=D1=82=D1=8C=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=20=D1=81=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=D0=B0=D0=BC=D0=B8=20-=20show:=20=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D0=BC=D0=BE=D1=82=D1=80=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20-=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 32/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B8=D0=B7=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=BE=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D0=B0=20=D0=BD=D0=B0=D0=B1=D0=BE=D1=80=D0=B0?= =?UTF-8?q?=20=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 33/43] =?UTF-8?q?=D0=98=D0=BD=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D1=82=D0=B8=D0=B2=D0=BD=D1=8B=D0=B9=20=D0=B2=D1=8B=D0=B2?= =?UTF-8?q?=D0=BE=D0=B4=20=D0=BF=D1=80=D0=B8=20=D0=B2=D1=8B=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=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 34/43] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D0=B8=D0=BD=D0=B8=D1=86=D0=B8=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D1=80=D0=B5=D0=BF=D0=BE=D0=B7?= =?UTF-8?q?=D0=B8=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 35/43] 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"; From 9f8759238b75948e9cd86ca73a3aa023fc872e3d Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Wed, 28 May 2025 02:26:55 +0300 Subject: [PATCH 36/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B8=D1=81=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=BF=D1=83=D1=81=D1=82=D1=8B=D1=85=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=BA=20=D0=B2=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=D0=B5=20=D0=BE=D1=82=20git-=D0=B2=D1=8B=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0,=20=D1=82=D0=B0=D0=BA=20=D0=BA=D0=B0=D0=BA=20=D0=BE?= =?UTF-8?q?=D1=82=D0=B2=D0=B5=D1=82=20=D0=BC=D0=BE=D0=B6=D0=B5=D1=82=20?= =?UTF-8?q?=D0=B1=D1=8B=D1=82=D1=8C=20=D0=BF=D1=83=D1=81=D1=82=D1=8B=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snag/core/rules.d | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/snag/core/rules.d b/source/snag/core/rules.d index ef585bd..d8a2c9c 100644 --- a/source/snag/core/rules.d +++ b/source/snag/core/rules.d @@ -86,7 +86,7 @@ class SnagRules { git( ["diff", "--cached", "--name-only"], "Failed to retrieve the list of files removed from tracking" - ).output.split('\n')[0..$-1].each!( + ).output.split('\n').filter!(e => !e.strip.empty).each!( file => git( ["add", "-f", file], "Failed to restore the file to tracking" @@ -98,7 +98,7 @@ class SnagRules { git( ["ls-files", "-i", "-c", "--exclude-standard"], "Failed to get the list of files to remove from tracking" - ).output.split('\n')[0..$-1].each!( + ).output.split('\n').filter!(e => !e.strip.empty).each!( file => git( ["rm", "--cached", file], "Failed to remove file from tracking" From 5f746c33b71d8413111084e82f5aec1df47bd033 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Wed, 28 May 2025 02:27:59 +0300 Subject: [PATCH 37/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20pre=20=D0=B8=20post?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=20=D0=BF=D1=80=D0=B8=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D0=B8=20?= =?UTF-8?q?snag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- snag.json | 7 +++---- source/snag/config/config.d | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/snag.json b/snag.json index 0cdee67..518738f 100644 --- a/snag.json +++ b/snag.json @@ -4,12 +4,11 @@ "email": "user@site.domain", "author": "snag", "presnag": [ - "/usr/bin/ls", - "/usr/local/bin/script.sh" + "echo $(which ls)", + "pwd" ], "postsnag": [ - "/usr/bin/ls", - "/usr/local/bin/script.sh" + "/usr/bin/ls" ], "rules": { "tracking": [ diff --git a/source/snag/config/config.d b/source/snag/config/config.d index 635cc6d..e9e0ca2 100644 --- a/source/snag/config/config.d +++ b/source/snag/config/config.d @@ -17,6 +17,8 @@ class SnagConfig { private string _author; private string[] _tracking; private string[] _ignore; + private string[] _presnag; + private string[] _postsnag; this(string configFile) { string jsonText; @@ -119,6 +121,22 @@ class SnagConfig { _ignore = rules["ignore"].array.map!(item => item.str).array; } } + + if ("presnag" in jsonData) { + if (jsonData["presnag"].type != JSONType.array) + throw new SnagConfigException( + "The \"presnag\" parameter must be an array containing a set of commands" + ); + _presnag = jsonData["presnag"].array.map!(item => item.str).array; + } + + if ("postsnag" in jsonData) { + if (jsonData["postsnag"].type != JSONType.array) + throw new SnagConfigException( + "The \"postsnag\" parameter must be an array containing a set of commands" + ); + _postsnag = jsonData["postsnag"].array.map!(item => item.str).array; + } } @property string git() const { return _git; } @@ -127,4 +145,6 @@ class SnagConfig { @property string author() const { return _author; } @property const(string[]) tracking() const { return _tracking; } @property const(string[]) ignore() const { return _ignore; } + @property const(string[]) presnag() const { return _presnag; } + @property const(string[]) postsnag() const { return _postsnag; } } From a2131ec5742e68abc4b3aedbb1bbf7463ce30011 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Wed, 28 May 2025 02:28:33 +0300 Subject: [PATCH 38/43] =?UTF-8?q?=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20pre=20=D0=B8=20post=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=B0=D0=BD=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snag/core/core.d | 43 ++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/source/snag/core/core.d b/source/snag/core/core.d index ef51792..7b05b61 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -61,6 +61,32 @@ class Snag { return formatted && fullStatus.length < 8 ? fullStatus ~ "\t" : fullStatus; } + void executePreSnag() { + _config.presnag().each!((command) { + auto result = executeShell(command); + result.status && throw new SnagException( + "%s:\n\t%s\n\n%s".format( + "An error occurred during presnag-command execution", + command, + result.output + ) + ); + }); + } + + void executePostSnag() { + _config.postsnag().each!((command) { + auto result = executeShell(command); + result.status && throw new SnagException( + "%s:\n\t%s\n\n%s".format( + "An error occurred during postsnag-command execution", + command, + result.output + ) + ); + }); + } + void initialize(bool force) { auto result = execute(_baseCommand ~ ["rev-parse", "--git-dir"]); !force && !result.status && @@ -127,13 +153,18 @@ class Snag { email.length && (environment["GIT_AUTHOR_EMAIL"] = email); string message = comment.length ? comment : "Standard snapshot creation"; + string newSnapshot; result = execute(_baseCommand ~ ["rev-parse", "--short", "HEAD"]); if (result.status == 128) { // Если это самый первый коммит после инициализации репозитория git(["add", "."], "Failed to prepare files for archiving"); - git(["commit", "-m"] ~ message, "Failed to create a backup"); - writeln("Backup was created successfully"); + git(["commit", "-m"] ~ message, "Failed to create a snapshot"); + newSnapshot = git( + ["rev-parse", "--short", "HEAD"], + "Failed to retrieve current snapshot information" + ).output.strip('\n'); + writeln("Snapshot was created successfully: ", newSnapshot); return; } else if (result.status != 0) throw new SnagException( @@ -157,8 +188,6 @@ class Snag { "Failed to get the commit list between HEAD and " ~ currentBranch ); - string newSnapshot; - // Если список существует if (result.output.length) { // Если коммит не является последним, то необходимо ответвление @@ -173,7 +202,7 @@ class Snag { ); git( ["commit", "-m"] ~ message, - "Failed to create a backup" + "Failed to create a snapshot" ); newSnapshot = git( ["rev-parse", "--short", "HEAD"], @@ -187,7 +216,7 @@ class Snag { ); git( ["commit", "-m"] ~ message, - "Failed to create a backup" + "Failed to create a snapshot" ); newSnapshot = git( ["rev-parse", "--short", "HEAD"], @@ -202,7 +231,7 @@ class Snag { "Issue with including the commit into the branch " ~ currentBranch ); } - writeln("Backup was created successfully: ", newSnapshot); + writeln("Snapshot was created successfully: ", newSnapshot); } void list(bool comment, bool author, bool email) { From f924800387ccddb685a11b280bee225f39555a0f Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Wed, 28 May 2025 02:28:45 +0300 Subject: [PATCH 39/43] 0.0.12 --- source/app.d | 48 ++++++++++++++++++++++++++++++++++-------- source/snag/version_.d | 2 +- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/source/app.d b/source/app.d index b15d19c..ef30132 100644 --- a/source/app.d +++ b/source/app.d @@ -63,6 +63,14 @@ int main(string[] args) "must contain an email address" ) ) + .add(new Flag(null, "no-presnag", "Execution without running pre-execution commands") + .name("nopre") + .optional + ) + .add(new Flag(null, "no-postsnag", "Execution without running post-execution commands") + .name("nopost") + .optional + ) ) .add(new Command("export", "Export snapshot to a tar.gz archive") .add(new Argument("path", "Output directory path for the archive").required) @@ -96,6 +104,14 @@ int main(string[] args) "must contain an email address" ) ) + .add(new Flag(null, "no-presnag", "Execution without running pre-execution commands") + .name("nopre") + .optional + ) + .add(new Flag(null, "no-postsnag", "Execution without running post-execution commands") + .name("nopost") + .optional + ) ) .add(new Command("list", "List of snapshots") .add(new Flag("c", "comment", "Show comment") @@ -113,6 +129,14 @@ int main(string[] args) ) .add(new Command("restore", "Restore to the specified snapshot state") .add(new Argument("snapshot", "Specify snapshot hash").required) + .add(new Flag(null, "no-presnag", "Execution without running pre-execution commands") + .name("nopre") + .optional + ) + .add(new Flag(null, "no-postsnag", "Execution without running post-execution commands") + .name("nopost") + .optional + ) ) .add(new Option("c", "config", "Сonfiguration file path") .optional @@ -147,13 +171,15 @@ int main(string[] args) .on("status", status => snag.status() ) - .on("create", create => + .on("create", (create) { + create.flag("nopre") || snag.executePreSnag(); snag.create( create.option("comment", ""), create.option("author", ""), create.option("email", "") - ) - ) + ); + create.flag("nopost") || snag.executePostSnag(); + }) .on("list", list => snag.list( list.flag("comment"), @@ -161,23 +187,27 @@ int main(string[] args) list.flag("email") ) ) - .on("restore", restore => - snag.restore(restore.arg("snapshot")) - ) + .on("restore", (restore) { + restore.flag("nopre") || snag.executePreSnag(); + snag.restore(restore.arg("snapshot")); + restore.flag("nopost") || snag.executePostSnag(); + }) .on("export", e => snag.exportSnapshot( e.arg("path"), e.option("snapshot", ""), ) ) - .on("import", i => + .on("import", (i) { + i.flag("nopre") || snag.executePreSnag(); snag.importSnapshot( i.arg("archive"), i.option("comment", ""), i.option("author", ""), i.option("email", "") - ) - ) + ); + i.flag("nopost") || snag.executePostSnag(); + }) .on("rules", (r) { auto rules = new SnagRules(config); r diff --git a/source/snag/version_.d b/source/snag/version_.d index 2c89ac7..b3c91c4 100644 --- a/source/snag/version_.d +++ b/source/snag/version_.d @@ -1,3 +1,3 @@ module snag.version_; -enum snagVersion = "0.0.11"; +enum snagVersion = "0.0.12"; From 31211516cbb924d5798e0cf28f30f5825471a54a Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Wed, 28 May 2025 15:07:28 +0300 Subject: [PATCH 40/43] =?UTF-8?q?=D0=91=D0=BE=D0=BB=D0=B5=D0=B5=20=D0=BB?= =?UTF-8?q?=D0=B0=D0=BA=D0=BE=D0=BD=D0=B8=D1=87=D0=BD=D0=BE=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D1=84=D0=BB=D0=B0=D0=B3=D0=BE=D0=B2:=20no-presnag=20->=20no?= =?UTF-8?q?-pre,=20no-postsnag=20->=20no-post?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app.d | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/app.d b/source/app.d index ef30132..6332a02 100644 --- a/source/app.d +++ b/source/app.d @@ -63,11 +63,11 @@ int main(string[] args) "must contain an email address" ) ) - .add(new Flag(null, "no-presnag", "Execution without running pre-execution commands") + .add(new Flag(null, "no-pre", "Execution without running pre-execution commands") .name("nopre") .optional ) - .add(new Flag(null, "no-postsnag", "Execution without running post-execution commands") + .add(new Flag(null, "no-post", "Execution without running post-execution commands") .name("nopost") .optional ) @@ -104,11 +104,11 @@ int main(string[] args) "must contain an email address" ) ) - .add(new Flag(null, "no-presnag", "Execution without running pre-execution commands") + .add(new Flag(null, "no-pre", "Execution without running pre-execution commands") .name("nopre") .optional ) - .add(new Flag(null, "no-postsnag", "Execution without running post-execution commands") + .add(new Flag(null, "no-post", "Execution without running post-execution commands") .name("nopost") .optional ) @@ -129,11 +129,11 @@ int main(string[] args) ) .add(new Command("restore", "Restore to the specified snapshot state") .add(new Argument("snapshot", "Specify snapshot hash").required) - .add(new Flag(null, "no-presnag", "Execution without running pre-execution commands") + .add(new Flag(null, "no-pre", "Execution without running pre-execution commands") .name("nopre") .optional ) - .add(new Flag(null, "no-postsnag", "Execution without running post-execution commands") + .add(new Flag(null, "no-post", "Execution without running post-execution commands") .name("nopost") .optional ) From 795fb0bdc470e490e0c02237d6bdea522e396b97 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Thu, 29 May 2025 16:23:10 +0300 Subject: [PATCH 41/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B8=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=80=D0=B0=20?= =?UTF-8?q?=D1=80=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=81=D0=BE=20=D1=81=D0=BD=D0=B8=D0=BC=D0=BA=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/snag/core/core.d | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/source/snag/core/core.d b/source/snag/core/core.d index 7b05b61..9917240 100644 --- a/source/snag/core/core.d +++ b/source/snag/core/core.d @@ -442,4 +442,21 @@ class Snag { writeln("Import completed successfully: ", newSnapshot); } + + void size() { + try { + "Total size of the snapshots is: %.2f MB".writefln( + dirEntries(_config.git, SpanMode.depth) + .filter!(path => path.isFile) + .array + .map!(path => path.getSize) + .sum / (1024.0 * 1024.0) + ); + } catch (Exception e) { + throw new SnagException( + "Error while checking the snapshots size:\n\t" + ~ e.msg + ); + } + } } From 60139a1874ee183d7d8a691268c8d67fb4801c32 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Thu, 29 May 2025 16:23:47 +0300 Subject: [PATCH 42/43] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D0=B0=20size=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D0=B8?= =?UTF-8?q?=20=D0=B8=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=80=D0=B5=20=D1=80?= =?UTF-8?q?=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=20=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D0=BC=D1=8B=D0=BC=D0=B8?= =?UTF-8?q?=20=D1=81=D0=BD=D0=B8=D0=BC=D0=BA=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app.d | 12 +++++++++++- source/snag/version_.d | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/source/app.d b/source/app.d index 6332a02..a79b3f8 100644 --- a/source/app.d +++ b/source/app.d @@ -40,6 +40,7 @@ int main(string[] args) ) .add(new Command("status", "Checking the status of tracked files")) .add(new Command("diff", "Show changed data")) + .add(new Command("size", "Size of snapshots")) .add(new Command("import", "Import snapshot from a tar.gz archive") .add(new Argument("archive", "The path to the tar.gz archive file").required) .add(new Option("c", "comment", "Specify comment") @@ -138,7 +139,7 @@ int main(string[] args) .optional ) ) - .add(new Option("c", "config", "Сonfiguration file path") + .add(new Option("c", "config", "Configuration file path") .optional .validateEachWith( opt => opt.exists && opt.isFile, @@ -168,6 +169,9 @@ int main(string[] args) .on("diff", diff => snag.diff() ) + .on("size", size => + snag.size() + ) .on("status", status => snag.status() ) @@ -231,6 +235,12 @@ int main(string[] args) } catch (SnagException e) { e.print(); return EXIT_FAILURE; + } catch (Exception e) { + writeln( + "AN UNEXPECTED ERROR HAS OCCURRED! PLEASE REPORT IT TO THE AUTHOR OF THE SNAG!\n\n", + e + ); + return EXIT_FAILURE; } return EXIT_SUCCESS; diff --git a/source/snag/version_.d b/source/snag/version_.d index b3c91c4..b4f3fd3 100644 --- a/source/snag/version_.d +++ b/source/snag/version_.d @@ -1,3 +1,3 @@ module snag.version_; -enum snagVersion = "0.0.12"; +enum snagVersion = "0.0.13"; From b2a0a9eeae8f701a82cc412c3f435b9912200d80 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Thu, 29 May 2025 20:08:06 +0300 Subject: [PATCH 43/43] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BB=D0=B8=D1=86=D0=B5=D0=BD=D0=B7=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 338 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 279 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 575 insertions(+), 42 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9efa6fb --- /dev/null +++ b/LICENSE @@ -0,0 +1,338 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Moe Ghoul, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md index e5473f0..fc0c46a 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,263 @@ # snag -Snapshot Git - система резервного копирования на основе фиксации состояния файлов с помощью Git. +`snag` (snapshot git) — это утилита командной строки для создания, управления и восстановления снимков данных. Она позволяет импортировать, экспортировать, создавать и восстанавливать снимки, а также управлять правилами отслеживания файлов. + +## Сборка + +Для сборки необходим предустановленный комплятор языка D (dmd/ldc/gdc), для удобства использовать пакетный менеджер dub: + +```sh +dub build --build=release +``` ## Использование -### Описание файла конфигурации +Основной формат команды: ---- +```bash +snag [флаги] [опции] команда +``` + +### Флаги +- `-h, --help` — Выводит справку. +- `--version` — Выводит версию утилиты. + +### Опции +- `-c, --config <путь>` — Указывает путь к файлу конфигурации. + +### Команды +- `init` — Инициализация репозитория для хранения снимков. +- `create` — Создание нового снимка. +- `import` — Импорт снимка из архива tar.gz. +- `export` — Экспорт снимка в архив tar.gz. +- `restore` — Восстановление состояния из указанного снимка. +- `list` — Вывод списка снимков. +- `diff` — Показ изменённых данных. +- `status` — Проверка статуса отслеживаемых файлов. +- `rules` — Управление правилами отслеживания. +- `size` — Отображение размера снимков. + +## Подробное описание команд + +### `snag init` +Инициализирует репозиторий для хранения снимков. + +```bash +snag init [-h] [-f] +``` + +- `-f, --force` — Перезаписывает существующий репозиторий. + +### `snag create` +Создаёт новый снимок. + +```bash +snag create [-h] [--no-pre] [--no-post] [-c <комментарий>] [-a <автор>] [-e ] +``` + +- `--no-pre` — Выполнение без предкоманд. +- `--no-post` — Выполнение без посткоманд. +- `-c, --comment <значение>` — Указывает комментарий к снимку. +- `-a, --author <значение>` — Указывает автора снимка. +- `-e, --email <значение>` — Указывает email автора. + +### `snag import` +Импортирует снимок из архива tar.gz. + +```bash +snag import [-h] [--no-pre] [--no-post] [-c <комментарий>] [-a <автор>] [-e ] <путь_к_архиву> +``` + +- `--no-pre` — Без выполнения предкоманд. +- `--no-post` — Без выполнения посткоманд. +- `-c, --comment <значение>` — Комментарий к снимку. +- `-a, --author <значение>` — Автор снимка. +- `-e, --email <значение>` — Email автора. +- `<путь_к_архиву>` — Путь к файлу tar.gz. + +### `snag export` +Экспортирует снимок в архив tar.gz. + +```bash +snag export [-h] [-s <хэш_снимка>] <путь_к_папке> +``` + +- `-s, --snapshot <хэш>` — Указывает хэш снимка. +- `<путь_к_папке>` — Путь к папке для сохранения архива. + +### `snag restore` +Восстанавливает состояние из указанного снимка. + +```bash +snag restore [-h] [--no-pre] [--no-post] <хэш_снимка> +``` + +- `--no-pre` — Без выполнения предкоманд. +- `--no-post` — Без выполнения посткоманд. +- `<хэш_снимка>` — Хэш восстанавливаемого снимка. + +### `snag list` +Выводит список снимков. + +```bash +snag list [-h] [-c] [-a] [-e] +``` + +- `-c, --comment` — Показать комментарии к снимкам. +- `-a, --author` — Показать авторов снимков. +- `-e, --email` — Показать email авторов. + +### `snag diff` +Показывает изменённые данные. + +```bash +snag diff +``` + +### `snag status` +Проверяет статус отслеживаемых файлов. + +```bash +snag status +``` + +### `snag size` +Отображает размер снимков. + +```bash +snag size +``` + +### `snag rules` +Управление правилами отслеживания. + +```bash +snag rules [-h] save|show|update|reset|clear +``` + +- `save` — Сохраняет правила. +- `show` — Показывает правила. + ```bash + snag rules show [-h] [-c] + ``` + - `-c, --config` — Показать правила из файла конфигурации. +- `update` — Обновляет правила. + ```bash + snag rules update [-h] [-r] + ``` + - `-r, --remove` — Удаляет игнорируемые файлы из отслеживания (Требуется повышенная осторожность!). +- `reset` — Сбрасывает правила до состояния внесенных изменений. +- `clear` — Очищает правила. + +## Примеры использования + +1. **Инициализация репозитория:** + ```bash + snag init + ``` + +2. **Создание снимка с комментарием:** + ```bash + snag create -c "Начальный снимок" -a "Иван Иванов" -e "ivan@example.com" + ``` + +3. **Импорт снимка из архива:** + ```bash + snag import archive.tar.gz + ``` + +4. **Экспорт снимка:** + ```bash + snag export -s abc123 /path/to/output + ``` + +5. **Восстановление снимка:** + ```bash + snag restore abc123 + ``` + +6. **Просмотр списка снимков с комментариями:** + ```bash + snag list -c + ``` + +7. **Обновление правил отслеживания:** + ```bash + snag rules update + ``` + +## Конфигурация + +Утилита `snag` поддерживает настройку через конфигурационный файл в формате JSON, который задаётся с помощью опции `-c` или `--config`. Этот файл определяет параметры работы утилиты, включая пути, автора, команды для выполнения до и после создания снимков, а также правила отслеживания файлов. + +Пример использования конфигурационного файла: + +```bash +snag -c /path/to/config.json create +``` + +### Пример конфигурационного файла ```json { - "git": "/tmp/testgit", - "project": "/tmp/test", + "git": "/path/to/git/repository/dir", + "project": "/path/to/project", "email": "user@site.domain", - "user": "snag" + "author": "snag", + "presnag": [ + "systemctl stop my.service" + ], + "postsnag": [ + "systemctl start my.service" + ], + "rules": { + "tracking": [ + "/first_dir/", + "/second_dir/*.conf", + "/second_dir/more/" + ], + "ignore": [ + "/second_dir/more/*.so" + ] + } } ``` -1. **`"git"`** - - *Тип:* Строка (путь) - - *Назначение:* Указывает расположение Git-репозитория +### Описание полей конфигурационного файла -2. **`"project"`** - - *Тип:* Строка (путь) - - *Назначение:* Корневая директория для отслеживания состояния файлов +- **`git`** (`string`): Путь к репозиторию для хранения снимков. В примере: `/path/to/git/repository/dir`. Этот путь используется при инициализации репозитория (`snag init`) для определения места хранения метаданных и снимков. -3. **`"email"`** - - *Тип:* Строка (email) - - *Назначение:* Email автора коммитов в Git +- **`project`** (`string`): Путь к проекту или директории, файлы которой отслеживаются утилитой. В примере: `/path/to/project`. Указывает корневую папку, в которой `snag` будет создавать, импортировать или восстанавливать снимки. -4. **`"user"`** - - *Тип:* Строка - - *Назначение:* Имя пользователя для Git-операций +- **`email`** (`string`): Email автора снимков. Используется для метаданных снимков при выполнении команд, таких как `snag create` или `snag import`. В примере: `user@site.domain`. Может переопределяться опцией `-e` или `--email`. -### Описание программы `snag` +- **`author`** (`string`): Имя автора снимков. Используется для метаданных снимков. В примере: `snag`. Может переопределяться опцией `-a` или `--author`. ---- +- **`presnag`** (`array of strings`): Список команд, выполняемых перед созданием или импортом снимка (если не указан флаг `--no-pre`). В примере: + - `systemctl stop my.service` — останавливает сервис `my.service` перед выполнением операции. Это может быть полезно для временной остановки сервиса, чтобы обеспечить целостность данных во время создания снимка. -#### **Общий синтаксис** +- **`postsnag`** (`array of strings`): Список команд, выполняемых после создания или импорта снимка (если не указан флаг `--no-post`). В примере: + - `systemctl start my.service` — запускает сервис `my.service` после завершения операции. Это может использоваться для восстановления работы сервиса после создания или импорта снимка. -```bash -snag [ОПЦИИ] [ГЛАВНАЯ_КОМАНДА] [ОПЦИИ] [АРГУМЕНТЫ] -``` +- **`rules`** (`object`): Определяет правила отслеживания файлов. + - **`tracking`** (`array of strings`): Список шаблонов файлов или директорий, которые отслеживаются утилитой. В примере: + - `/first_dir/` — отслеживается вся директория `/first_dir` и её содержимое. + - `/second_dir/*.conf` — отслеживаются все файлы с расширением `.conf` в директории `/second_dir`. + - `/second_dir/more/` — отслеживается вся директория `/second_dir/more` и её содержимое. + - **`ignore`** (`array of strings`): Список файлов или директорий, которые исключаются из отслеживания. В примере: + - `/second_dir/more/*.so` — игнорируются все файлы с расширением `.so` в директории `/second_dir/more`. ---- +### Примечания -#### **Основные команды** +- Правила в разделе `rules` можно обновлять с помощью команды `snag rules update` или просматривать с помощью `snag rules show -c`. +- Если параметры, такие как `email` или `author`, указаны в командной строке (например, через `-e` или `-a`), они имеют приоритет над значениями из конфигурационного файла. +- Убедитесь, что пути, указанные в `git` и `project`, существуют и доступны для записи/чтения перед выполнением операций. +- Команды в `presnag` и `postsnag` должны быть корректными и доступными в системе, иначе выполнение операции может завершиться ошибкой. -| Команда | Описание | -|------------|--------------------------------------------------------------------------| -| `restore` | Восстановить проект до состояния указанного снимка | -| `init` | Инициализировать репозиторий для хранения снимков | -| `list` | Показать список доступных снимков | -| `status` | Проверить состояние отслеживаемых файлов (изменения с последнего снимка) | -| `create` | Создать новый снимок состояния проекта | +## Лицензия ---- +Лицензия GPL-2.0. Подробности см. в файле `LICENSE`. -#### **Флаги и опции** +## Контакты -| Опция | Описание | -|---------------------|--------------------------------------------------------------------------| -| `-h`, `--help` | Показать справку по команде | -| `--version` | Показать версию программы | -| `-c`, `--config` | Указать путь к файлу конфигурации (необязательно) | - ---- +Для вопросов и предложений: alexander@zhirov.kz