- module dub.internal.git;
-
- import dub.internal.vibecompat.core.file;
- import dub.internal.logging;
- import std.file;
- import std.string;
-
- version (Windows)
- {
- import dub.internal.vibecompat.data.json;
-
- string determineVersionWithGit(NativePath path)
- {
- // On Windows, which is slow at running external processes,
- // cache the version numbers that are determined using
- // git to speed up the initialization phase.
- import dub.internal.utils : jsonFromFile;
-
- // quickly determine head commit without invoking git
- string head_commit;
- auto hpath = (path ~ ".git/HEAD").toNativeString();
- if (exists(hpath)) {
- auto head_ref = readText(hpath).strip();
- if (head_ref.startsWith("ref: ")) {
- auto rpath = (path ~ (".git/"~head_ref[5 .. $])).toNativeString();
- if (exists(rpath))
- head_commit = readText(rpath).strip();
- }
- }
-
- // return the last determined version for that commit
- // not that this is not always correct, most notably when
- // a tag gets added/removed/changed and changes the outcome
- // of the full version detection computation
- auto vcachepath = path ~ ".dub/version.json";
- if (existsFile(vcachepath)) {
- auto ver = jsonFromFile(vcachepath);
- if (head_commit == ver["commit"].opt!string)
- return ver["version"].get!string;
- }
-
- // if no cache file or the HEAD commit changed, perform full detection
- auto ret = determineVersionWithGitTool(path);
-
- // update version cache file
- if (head_commit.length) {
- import dub.internal.utils : atomicWriteJsonFile;
-
- ensureDirectory(path ~ ".dub");
- atomicWriteJsonFile(vcachepath, Json(["commit": Json(head_commit), "version": Json(ret)]));
- }
-
- return ret;
- }
- }
- else
- {
- string determineVersionWithGit(NativePath path)
- {
- return determineVersionWithGitTool(path);
- }
- }
-
- // determines the version of a package that is stored in a Git working copy
- // by invoking the "git" executable
- private string determineVersionWithGitTool(NativePath path)
- {
- import std.process;
-
- auto git_dir = path ~ ".git";
- if (!existsFile(git_dir) || !isDir(git_dir.toNativeString)) return null;
- auto git_dir_param = "--git-dir=" ~ git_dir.toNativeString();
-
- static string exec(scope string[] params...) {
- auto ret = execute(params);
- if (ret.status == 0) return ret.output.strip;
- logDebug("'%s' failed with exit code %s: %s", params.join(" "), ret.status, ret.output.strip);
- return null;
- }
-
- if (const describeOutput = exec("git", git_dir_param, "describe", "--long", "--tags")) {
- if (const ver = determineVersionFromGitDescribe(describeOutput))
- return ver;
- }
-
- auto branch = exec("git", git_dir_param, "rev-parse", "--abbrev-ref", "HEAD");
- if (branch !is null) {
- if (branch != "HEAD") return "~" ~ branch;
- }
-
- return null;
- }
-
- private string determineVersionFromGitDescribe(string describeOutput)
- {
- import dub.semver : isValidVersion;
- import std.conv : to;
-
- const parts = describeOutput.split("-");
- const commit = parts[$-1];
- const num = parts[$-2].to!int;
- const tag = parts[0 .. $-2].join("-");
- if (tag.startsWith("v") && isValidVersion(tag[1 .. $])) {
- if (num == 0) return tag[1 .. $];
- const i = tag.indexOf('+');
- return format("%s%scommit.%s.%s", tag[1 .. $], i >= 0 ? '.' : '+', num, commit);
- }
- return null;
- }
-
- unittest {
- // tag v1.0.0
- assert(determineVersionFromGitDescribe("v1.0.0-0-deadbeef") == "1.0.0");
- // 1 commit after v1.0.0
- assert(determineVersionFromGitDescribe("v1.0.0-1-deadbeef") == "1.0.0+commit.1.deadbeef");
- // tag v1.0.0+2.0.0
- assert(determineVersionFromGitDescribe("v1.0.0+2.0.0-0-deadbeef") == "1.0.0+2.0.0");
- // 12 commits after tag v1.0.0+2.0.0
- assert(determineVersionFromGitDescribe("v1.0.0+2.0.0-12-deadbeef") == "1.0.0+2.0.0.commit.12.deadbeef");
- // tag v1.0.0-beta.1
- assert(determineVersionFromGitDescribe("v1.0.0-beta.1-0-deadbeef") == "1.0.0-beta.1");
- // 2 commits after tag v1.0.0-beta.1
- assert(determineVersionFromGitDescribe("v1.0.0-beta.1-2-deadbeef") == "1.0.0-beta.1+commit.2.deadbeef");
- // tag v1.0.0-beta.2+2.0.0
- assert(determineVersionFromGitDescribe("v1.0.0-beta.2+2.0.0-0-deadbeef") == "1.0.0-beta.2+2.0.0");
- // 3 commits after tag v1.0.0-beta.2+2.0.0
- assert(determineVersionFromGitDescribe("v1.0.0-beta.2+2.0.0-3-deadbeef") == "1.0.0-beta.2+2.0.0.commit.3.deadbeef");
-
- // invalid tags
- assert(determineVersionFromGitDescribe("1.0.0-0-deadbeef") is null);
- assert(determineVersionFromGitDescribe("v1.0-0-deadbeef") is null);
- }
-
- /** Clones a repository into a new directory.
-
- Params:
- remote = The (possibly remote) repository to clone from
- reference = The branch to check out after cloning
- destination = Repository destination directory
-
- Returns:
- Whether the cloning succeeded.
- */
- bool cloneRepository(string remote, string reference, string destination)
- {
- import std.process : Pid, spawnProcess, wait;
-
- Pid command;
-
- if (!exists(destination)) {
- string[] args = ["git", "clone", "--no-checkout"];
- if (getLogLevel > LogLevel.diagnostic) args ~= "-q";
-
- command = spawnProcess(args~[remote, destination]);
- if (wait(command) != 0) {
- return false;
- }
- }
-
- string[] args = ["git", "-C", destination, "checkout", "--detach"];
- if (getLogLevel > LogLevel.diagnostic) args ~= "-q";
- command = spawnProcess(args~[reference]);
-
- if (wait(command) != 0) {
- rmdirRecurse(destination);
- return false;
- }
-
- return true;
- }