Compare commits

...

5 commits

Author SHA1 Message Date
016455d8f2
0.0.11 2025-05-28 00:23:27 +03:00
c65b18a915
Создание правил при инициализации репозитория 2025-05-28 00:20:55 +03:00
c388d01a2f
Информативный вывод при вызове status 2025-05-28 00:20:27 +03:00
aa4260cdb9
Добавлено чтение из конфигурационного файла набора правил для отслеживания файлов 2025-05-28 00:18:55 +03:00
5797e83f07
Добавлен новый модуль rules для управления правилами отслеживания файлов в виде записей для gitignore
- create: создание правил из конфигурационного файла:
    - tracking - отслеживаемые файлы/пути
    - ignore - правила gitignore
- update: обновление существующих правил из конфигурационного файла
- reset: сброс изменений в правилах - откат на момент до внесения изменений в конфигурационный файл
- clear: очистить файл с правилами
- show: просмотр правил
- save: сохранить правила без возможности сброса (reset)
2025-05-28 00:17:42 +03:00
7 changed files with 267 additions and 5 deletions

View file

@ -10,5 +10,15 @@
"postsnag": [ "postsnag": [
"/usr/bin/ls", "/usr/bin/ls",
"/usr/local/bin/script.sh" "/usr/local/bin/script.sh"
] ],
"rules": {
"tracking": [
"/etc/*.conf"
],
"ignore": [
"/usr/exit/.gitignore",
"/usr/exit/dd",
"/file1"
]
}
} }

View file

@ -21,6 +21,23 @@ int main(string[] args)
.optional .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("status", "Checking the status of tracked files"))
.add(new Command("diff", "Show changed data")) .add(new Command("diff", "Show changed data"))
.add(new Command("import", "Import snapshot from a tar.gz archive") .add(new Command("import", "Import snapshot from a tar.gz archive")
@ -160,6 +177,26 @@ int main(string[] args)
i.option("author", ""), i.option("author", ""),
i.option("email", "") 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) { } catch (SnagException e) {
e.print(); e.print();

View file

@ -5,6 +5,8 @@ import std.file;
import std.path; import std.path;
import std.string; import std.string;
import snag.lib; import snag.lib;
import std.algorithm;
import std.array;
import snag.config.exception; import snag.config.exception;
@ -13,6 +15,8 @@ class SnagConfig {
private string _project; private string _project;
private string _email; private string _email;
private string _author; private string _author;
private string[] _tracking;
private string[] _ignore;
this(string configFile) { this(string configFile) {
string jsonText; string jsonText;
@ -93,10 +97,34 @@ class SnagConfig {
throw new SnagConfigException( throw new SnagConfigException(
"The \"author\" parameter must contain an author name" "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 git() const { return _git; }
@property string project() const { return _project; } @property string project() const { return _project; }
@property string email() const { return _email; } @property string email() const { return _email; }
@property string author() const { return _author; } @property string author() const { return _author; }
@property const(string[]) tracking() const { return _tracking; }
@property const(string[]) ignore() const { return _ignore; }
} }

View file

@ -13,6 +13,7 @@ import std.path;
import snag.lib; import snag.lib;
import snag.core.exception; import snag.core.exception;
import snag.core.rules;
class Snag { class Snag {
private string[] _baseCommand; private string[] _baseCommand;
@ -44,6 +45,22 @@ class Snag {
return result; 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) { void initialize(bool force) {
auto result = execute(_baseCommand ~ ["rev-parse", "--git-dir"]); auto result = execute(_baseCommand ~ ["rev-parse", "--git-dir"]);
!force && !result.status && !force && !result.status &&
@ -68,6 +85,9 @@ class Snag {
["config", "user.name", _config.author], ["config", "user.name", _config.author],
"A Git repository initialization error occurred" "A Git repository initialization error occurred"
); );
(new SnagRules(_config)).create();
writeln( writeln(
"The Git repository has been initialized successfully: ", "The Git repository has been initialized successfully: ",
_config.git _config.git
@ -84,9 +104,11 @@ class Snag {
return; return;
} }
writeln("The following list of files requires backup:"); writeln("The following list of files requires backup:");
result.output.split('\n').filter!(e => !e.strip.empty).each!((e) { result.output.split('\n')[0..$-1].map!(e =>
writefln("\t/%s", e.strip.split[1]); e.strip.split
}); ).each!(e =>
writefln("\t%s\t/%s", gitStatus(e[0], true), e[1])
);
} }
void create(string comment, string author, string email) { void create(string comment, string author, string email) {

View file

@ -2,3 +2,4 @@ module snag.core;
public import snag.core.core; public import snag.core.core;
public import snag.core.exception; public import snag.core.exception;
public import snag.core.rules;

164
source/snag/core/rules.d Normal file
View file

@ -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");
}
}

View file

@ -1,3 +1,3 @@
module snag.version_; module snag.version_;
enum snagVersion = "0.0.10"; enum snagVersion = "0.0.11";