Newer
Older
dub_jkp / source / dub / internal / git.d
@Vladimir Panteleev Vladimir Panteleev on 22 Jul 2023 5 KB Don't wastefully execute everything via a shell
  1. module dub.internal.git;
  2.  
  3. import dub.internal.vibecompat.core.file;
  4. import dub.internal.logging;
  5. import std.file;
  6. import std.string;
  7.  
  8. version (Windows)
  9. {
  10. import dub.internal.vibecompat.data.json;
  11.  
  12. string determineVersionWithGit(NativePath path)
  13. {
  14. // On Windows, which is slow at running external processes,
  15. // cache the version numbers that are determined using
  16. // git to speed up the initialization phase.
  17. import dub.internal.utils : jsonFromFile;
  18.  
  19. // quickly determine head commit without invoking git
  20. string head_commit;
  21. auto hpath = (path ~ ".git/HEAD").toNativeString();
  22. if (exists(hpath)) {
  23. auto head_ref = readText(hpath).strip();
  24. if (head_ref.startsWith("ref: ")) {
  25. auto rpath = (path ~ (".git/"~head_ref[5 .. $])).toNativeString();
  26. if (exists(rpath))
  27. head_commit = readText(rpath).strip();
  28. }
  29. }
  30.  
  31. // return the last determined version for that commit
  32. // not that this is not always correct, most notably when
  33. // a tag gets added/removed/changed and changes the outcome
  34. // of the full version detection computation
  35. auto vcachepath = path ~ ".dub/version.json";
  36. if (existsFile(vcachepath)) {
  37. auto ver = jsonFromFile(vcachepath);
  38. if (head_commit == ver["commit"].opt!string)
  39. return ver["version"].get!string;
  40. }
  41.  
  42. // if no cache file or the HEAD commit changed, perform full detection
  43. auto ret = determineVersionWithGitTool(path);
  44.  
  45. // update version cache file
  46. if (head_commit.length) {
  47. import dub.internal.utils : atomicWriteJsonFile;
  48.  
  49. ensureDirectory(path ~ ".dub");
  50. atomicWriteJsonFile(vcachepath, Json(["commit": Json(head_commit), "version": Json(ret)]));
  51. }
  52.  
  53. return ret;
  54. }
  55. }
  56. else
  57. {
  58. string determineVersionWithGit(NativePath path)
  59. {
  60. return determineVersionWithGitTool(path);
  61. }
  62. }
  63.  
  64. // determines the version of a package that is stored in a Git working copy
  65. // by invoking the "git" executable
  66. private string determineVersionWithGitTool(NativePath path)
  67. {
  68. import std.process;
  69.  
  70. auto git_dir = path ~ ".git";
  71. if (!existsFile(git_dir) || !isDir(git_dir.toNativeString)) return null;
  72. auto git_dir_param = "--git-dir=" ~ git_dir.toNativeString();
  73.  
  74. static string exec(scope string[] params...) {
  75. auto ret = execute(params);
  76. if (ret.status == 0) return ret.output.strip;
  77. logDebug("'%s' failed with exit code %s: %s", params.join(" "), ret.status, ret.output.strip);
  78. return null;
  79. }
  80.  
  81. if (const describeOutput = exec("git", git_dir_param, "describe", "--long", "--tags")) {
  82. if (const ver = determineVersionFromGitDescribe(describeOutput))
  83. return ver;
  84. }
  85.  
  86. auto branch = exec("git", git_dir_param, "rev-parse", "--abbrev-ref", "HEAD");
  87. if (branch !is null) {
  88. if (branch != "HEAD") return "~" ~ branch;
  89. }
  90.  
  91. return null;
  92. }
  93.  
  94. private string determineVersionFromGitDescribe(string describeOutput)
  95. {
  96. import dub.semver : isValidVersion;
  97. import std.conv : to;
  98.  
  99. const parts = describeOutput.split("-");
  100. const commit = parts[$-1];
  101. const num = parts[$-2].to!int;
  102. const tag = parts[0 .. $-2].join("-");
  103. if (tag.startsWith("v") && isValidVersion(tag[1 .. $])) {
  104. if (num == 0) return tag[1 .. $];
  105. const i = tag.indexOf('+');
  106. return format("%s%scommit.%s.%s", tag[1 .. $], i >= 0 ? '.' : '+', num, commit);
  107. }
  108. return null;
  109. }
  110.  
  111. unittest {
  112. // tag v1.0.0
  113. assert(determineVersionFromGitDescribe("v1.0.0-0-deadbeef") == "1.0.0");
  114. // 1 commit after v1.0.0
  115. assert(determineVersionFromGitDescribe("v1.0.0-1-deadbeef") == "1.0.0+commit.1.deadbeef");
  116. // tag v1.0.0+2.0.0
  117. assert(determineVersionFromGitDescribe("v1.0.0+2.0.0-0-deadbeef") == "1.0.0+2.0.0");
  118. // 12 commits after tag v1.0.0+2.0.0
  119. assert(determineVersionFromGitDescribe("v1.0.0+2.0.0-12-deadbeef") == "1.0.0+2.0.0.commit.12.deadbeef");
  120. // tag v1.0.0-beta.1
  121. assert(determineVersionFromGitDescribe("v1.0.0-beta.1-0-deadbeef") == "1.0.0-beta.1");
  122. // 2 commits after tag v1.0.0-beta.1
  123. assert(determineVersionFromGitDescribe("v1.0.0-beta.1-2-deadbeef") == "1.0.0-beta.1+commit.2.deadbeef");
  124. // tag v1.0.0-beta.2+2.0.0
  125. assert(determineVersionFromGitDescribe("v1.0.0-beta.2+2.0.0-0-deadbeef") == "1.0.0-beta.2+2.0.0");
  126. // 3 commits after tag v1.0.0-beta.2+2.0.0
  127. assert(determineVersionFromGitDescribe("v1.0.0-beta.2+2.0.0-3-deadbeef") == "1.0.0-beta.2+2.0.0.commit.3.deadbeef");
  128.  
  129. // invalid tags
  130. assert(determineVersionFromGitDescribe("1.0.0-0-deadbeef") is null);
  131. assert(determineVersionFromGitDescribe("v1.0-0-deadbeef") is null);
  132. }
  133.  
  134. /** Clones a repository into a new directory.
  135.  
  136. Params:
  137. remote = The (possibly remote) repository to clone from
  138. reference = The branch to check out after cloning
  139. destination = Repository destination directory
  140.  
  141. Returns:
  142. Whether the cloning succeeded.
  143. */
  144. bool cloneRepository(string remote, string reference, string destination)
  145. {
  146. import std.process : Pid, spawnProcess, wait;
  147.  
  148. Pid command;
  149.  
  150. if (!exists(destination)) {
  151. string[] args = ["git", "clone", "--no-checkout"];
  152. if (getLogLevel > LogLevel.diagnostic) args ~= "-q";
  153.  
  154. command = spawnProcess(args~[remote, destination]);
  155. if (wait(command) != 0) {
  156. return false;
  157. }
  158. }
  159.  
  160. string[] args = ["git", "-C", destination, "checkout", "--detach"];
  161. if (getLogLevel > LogLevel.diagnostic) args ~= "-q";
  162. command = spawnProcess(args~[reference]);
  163.  
  164. if (wait(command) != 0) {
  165. rmdirRecurse(destination);
  166. return false;
  167. }
  168.  
  169. return true;
  170. }