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;
}
