Newer
Older
dub_jkp / source / dub / internal / git.d
module dub.internal.git;

import dub.internal.vibecompat.core.file;
import dub.internal.vibecompat.core.log;
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;

			if (!existsFile(path ~".dub")) createDirectory(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 dub.semver;
	import std.algorithm : canFind;
	import std.conv : to;
	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 = executeShell(escapeShellCommand(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;
	}

	auto tag = exec("git", git_dir_param, "describe", "--long", "--tags");
	if (tag !is null) {
		auto parts = tag.split("-");
		auto commit = parts[$-1];
		auto num = parts[$-2].to!int;
		tag = parts[0 .. $-2].join("-");
		if (tag.startsWith("v") && isValidVersion(tag[1 .. $])) {
			if (num == 0) return tag[1 .. $];
			else if (tag.canFind("+")) return format("%s.commit.%s.%s", tag[1 .. $], num, commit);
			else return format("%s+commit.%s.%s", tag[1 .. $], num, commit);
		}
	}

	auto branch = exec("git", git_dir_param, "rev-parse", "--abbrev-ref", "HEAD");
	if (branch !is null) {
		if (branch != "HEAD") return "~" ~ branch;
	}

	return null;
}