snag/source/snag/core/core.d

462 lines
12 KiB
D
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

module snag.core.core;
import snag.config;
import std.format;
import std.datetime : Clock;
import std.stdio;
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;
import snag.core.rules;
class Snag {
private string[] _baseCommand;
private SnagConfig _config;
private string _date;
this(SnagConfig config) {
_baseCommand = format(
"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,
currentTime.hour,
currentTime.minute,
currentTime.second);
}
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 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 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 &&
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"
);
git(
["config", "user.email", _config.email],
"A Git repository initialization error occurred"
);
git(
["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
);
}
void status() {
auto result = git(
["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')[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) {
auto result = git(
["status", "--porcelain"],
"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"
);
author.length && (environment["GIT_AUTHOR_NAME"] = author);
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 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(
"Failed to retrieve current snapshot information:\n"
~ result.output
);
// Текущий коммит, который был изменен
string currentSnapshot = result.output.strip('\n');
// Если текущий измененный коммит является последним в ветке - то продолжить коммиты в этой ветке
// При разбивке по '\n' присутствует последний пустой элемент, поэтому нужный элемент 2-ой с конца
string currentBranch = git(
["for-each-ref", "--format='%(refname:short)'", "--contains", currentSnapshot],
"Error while getting the current branch"
).output.split('\n')[$-2].strip('\'');
// Получение списка коммитов между текущим и веткой
result = git(
["log", "--oneline", "HEAD.." ~ currentBranch],
"Failed to get the commit list between HEAD and " ~ currentBranch
);
// Если список существует
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"] ~ message,
"Failed to create a snapshot"
);
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"] ~ message,
"Failed to create a snapshot"
);
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("Snapshot was created successfully: ", newSnapshot);
}
void list(bool comment, bool author, 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");
author && (format ~= "\t%an");
email && (format ~= "\t%ae");
git(
[
"log",
"--all",
"--date=format:%Y.%m.%d %H:%M",
"--pretty=" ~ format
],
"Failed to retrieve the list of snapshots"
).output.split('\n').map!(line => line.split('\t')).array.each!(e =>
writefln("%s\t%s",
currentSnapshot == e[0] ? " >" : "",
e.join("\t")
)
);
}
void restore(string hash) {
if (!isValidHash(hash))
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) {
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"
);
git(
["checkout", hash],
"Failed to restore the snapshot state " ~ hash
);
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");
}
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);
}
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);
}
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
);
}
}
}