1 module dubproxy.git; 2 3 import std.array : empty; 4 import std.algorithm.iteration : filter, splitter; 5 import std.algorithm.searching : startsWith; 6 import std.exception : enforce; 7 import std.experimental.logger; 8 import std.file : exists, isDir, mkdirRecurse, rmdirRecurse, getcwd, chdir, 9 readText; 10 import std.path : absolutePath, expandTilde, asNormalizedPath, 11 buildNormalizedPath; 12 import std.stdio : File; 13 import std.format : format; 14 import std.process : executeShell; 15 import std.typecons : Flag; 16 import std..string : split, strip; 17 18 import url; 19 20 import dubproxy.options; 21 22 @safe: 23 24 struct TagReturn { 25 string hash; 26 string tag; 27 28 string getVersion() const { 29 import std..string : lastIndexOf; 30 31 enforce(!this.tag.empty, "Can not compute version of empty tag"); 32 const idx = this.tag.lastIndexOf('/'); 33 if(idx == -1) { 34 return this.tag; 35 } 36 return this.tag[idx + 1 .. $]; 37 } 38 } 39 40 string getHashFromVersion(const(TagReturn[]) tags, string ver) pure { 41 import std.algorithm.searching : endsWith; 42 foreach(it; tags) { 43 if(it.tag.endsWith(ver)) { 44 return it.hash; 45 } 46 } 47 return ""; 48 } 49 50 enum TagKind { 51 branch, 52 pull, 53 tags, 54 all 55 } 56 57 TagReturn[] getTags(string path, const(TagKind) tk, 58 ref const(DubProxyOptions) options) 59 { 60 URL u; 61 if(exists(path) && isDir(path)) { 62 return getTagsLocal(path, tk, options); 63 } else if(tryParseURL(path, u)) { 64 return getTagsRemote(path, tk, options); 65 } else { 66 assert(false, format!"Path '%s' could be resolved to get git tags" 67 (path)); 68 } 69 } 70 71 string getTagDataLocal(const string path, const(TagKind) tk, 72 ref const(DubProxyOptions) options) 73 { 74 auto oldCwd = getcwd(); 75 chdir(path); 76 scope(exit) { 77 chdir(oldCwd); 78 } 79 80 const toExe = tk == TagKind.tags 81 ? format!`%s%s ls-remote --tags --sort="-version:refname" .`( 82 options.noUserInteraction ? "GIT_TERMINAL_PROMPT=0 " : "", 83 options.pathToGit) 84 : format!`%s%s ls-remote .`( 85 options.noUserInteraction ? "GIT_TERMINAL_PROMPT=0 " : "", 86 options.pathToGit); 87 88 auto rslt = executeShell(toExe); 89 enforce(rslt.status == 0, format! 90 "'%s' returned with '%d' 0 was expected output '%s'"( 91 toExe, rslt.status, rslt.output)); 92 return rslt.output; 93 } 94 95 TagReturn[] getTagsLocal(string path, const(TagKind) tk, 96 ref const(DubProxyOptions) options) 97 { 98 string data = getTagDataLocal(path, tk, options); 99 return processTagData(data, tk); 100 } 101 102 string getTagDataRemote(string path, const(TagKind) tk, 103 ref const(DubProxyOptions) options) 104 { 105 const toExe = tk == TagKind.tags 106 ? format!`%s%s ls-remote --tags --sort="-version:refname" %s`( 107 options.noUserInteraction ? "GIT_TERMINAL_PROMPT=0 " : "", 108 options.pathToGit, path 109 ) 110 : format!`%s%s ls-remote %s`( 111 options.noUserInteraction ? "GIT_TERMINAL_PROMPT=0 " : "", 112 options.pathToGit, path); 113 114 auto rslt = executeShell(toExe); 115 enforce(rslt.status == 0, format! 116 "'%s' returned with '%d' 0 was expected output '%s' cwd '%s'"( 117 toExe, rslt.status, rslt.output, getcwd())); 118 return rslt.output; 119 } 120 121 TagReturn[] getTagsRemote(string path, const(TagKind) tk, 122 ref const(DubProxyOptions) options) 123 { 124 string data = getTagDataRemote(path, tk, options); 125 return processTagData(data, tk); 126 } 127 128 private TagReturn[] processTagData(string data, const(TagKind) tk) { 129 import std.algorithm.searching : canFind, count; 130 131 const kindFilter = tk == TagKind.branch ? "heads" 132 : tk == TagKind.pull ? "pull" 133 : tk == TagKind.tags ? "tags" : ""; 134 135 TagReturn[] ret; 136 foreach(line; data.splitter("\n") 137 .filter!(line => !line.empty) 138 .filter!(line => !canFind(line, "^{}")) 139 .filter!(line => count(line, "/") == 2) 140 .filter!(line => kindFilter.empty || line.canFind(kindFilter))) 141 { 142 string[] lineSplit = line.split('\t'); 143 enforce(lineSplit.length == 2, format! 144 "Line '%s' split incorrectly in '%s'"(line, lineSplit)); 145 146 ret ~= TagReturn(lineSplit[0].strip(" \t\n\r"), 147 lineSplit[1].strip(" \t\n\r")); 148 } 149 return ret; 150 } 151 152 alias LocalGit = Flag!"LocalGit"; 153 154 void cloneBare(string path, const LocalGit lg, string destDir, 155 ref const(DubProxyOptions) options) 156 { 157 void clone() { 158 const toExe = format!`%s%s clone --bare%s %s %s`( 159 options.noUserInteraction ? "GIT_TERMINAL_PROMPT=0 " : "", 160 options.pathToGit, 161 lg == LocalGit.yes ? " -l" : "", path, destDir); 162 auto rslt = executeShell(toExe); 163 enforce(rslt.status == 0, format! 164 "'%s' returned with '%d' 0 was expected output '%s'"( 165 toExe, rslt.status, rslt.output)); 166 } 167 168 const string absDestDir = destDir.expandTilde() 169 .absolutePath() 170 .buildNormalizedPath(); 171 172 const bool e = exists(absDestDir); 173 174 if(e && options.ovrGF == OverrideGitFolder.yes) { 175 () @trusted { rmdirRecurse(absDestDir); }(); 176 clone(); 177 } else if(!e) { 178 clone(); 179 } else { 180 auto oldCwd = getcwd(); 181 chdir(absDestDir); 182 scope(exit) { 183 chdir(oldCwd); 184 } 185 const toExe = format!`%s%s fetch --all`( 186 options.noUserInteraction ? "GIT_TERMINAL_PROMPT=0 " : "", 187 options.pathToGit); 188 auto rslt = executeShell(toExe); 189 enforce(rslt.status == 0, format! 190 "'%s' returned with '%d' 0 was expected output '%s'"( 191 toExe, rslt.status, rslt.output)); 192 } 193 } 194 195 void createWorkingTree(string clonedGitPath, const(TagReturn) tag, 196 string packageName, string destDir, ref const(DubProxyOptions) options) 197 { 198 const ver = tag.getVersion(); 199 const verTag = ver.startsWith("v") ? ver[1 .. $] : ver; 200 const absGitPath = buildNormalizedPath(absolutePath(expandTilde(clonedGitPath))); 201 const absDestDir = buildNormalizedPath(absolutePath(expandTilde(destDir))); 202 const rsltPath = format!"%s/%s-%s/%s"(absDestDir, packageName, verTag, 203 packageName); 204 tracef("rsltPath %s, absGitPath %s, absDestDir %s, packageName %s, verTag %s", 205 rsltPath, absGitPath, absDestDir, packageName, verTag); 206 207 const bool e = exists(rsltPath); 208 enforce(!e || options.ovrWTF == OverrideWorkTreeFolder.yes, format!( 209 "Path '%s' exist and override flag was not passed")(rsltPath)); 210 211 if(e) { 212 () @trusted { rmdirRecurse(rsltPath); }(); 213 } else { 214 mkdirRecurse(rsltPath); 215 } 216 217 const string cwd = getcwd(); 218 scope(exit) { 219 chdir(cwd); 220 enforce(getcwd() == cwd, format! 221 "Failed to change paths to '%s' cwd '%s'"(cwd, getcwd())); 222 } 223 224 tracef("chdir '%s'", absGitPath); 225 chdir(absGitPath); 226 enforce(getcwd() == absGitPath, 227 format!"Failed change to paths to '%s'"(absGitPath)); 228 229 const toExe = format!"%s worktree add -f %s %s"(options.pathToGit, rsltPath, 230 ver); 231 tracef("toExe '%s'", toExe); 232 auto rslt = executeShell(toExe); 233 enforce(rslt.status == 0, format! 234 "'%s' returned with '%d' 0 was expected output '%s'"( 235 toExe, rslt.status, rslt.output)); 236 237 insertVersionIntoDubFile(rsltPath, ver, options); 238 } 239 240 void insertVersionIntoDubFile(string packageDir, string ver, 241 ref const(DubProxyOptions) options) 242 { 243 const js = format!"%s/dub.json"(packageDir); 244 const jsE = exists(js); 245 const pkg = format!"%s/package.json"(packageDir); 246 const pkgE = exists(pkg); 247 const sdl = format!"%s/dub.sdl"(packageDir); 248 const sdlE = exists(sdl); 249 const cVer = ver.startsWith("v") 250 ? ver[1 .. $] 251 : ver.startsWith("~") ? ver : "~" ~ ver; 252 253 if(jsE) { 254 insertVersionIntoDubJsonFile(js, cVer); 255 } else if(sdlE) { 256 insertVersionIntoDubSDLFile(sdl, cVer, options); 257 } else if(pkgE) { 258 insertVersionIntoDubJsonFile(pkg, cVer); 259 } else { 260 enforce(false, format!"could not find a dub.{json,sdl} file in '%s'" 261 (packageDir)); 262 } 263 } 264 265 void insertVersionIntoDubJsonFile(string fileName, string ver) { 266 import std.json : JSONValue, parseJSON; 267 fileName = buildNormalizedPath(absolutePath(expandTilde(fileName))); 268 JSONValue j = parseJSON(readText(fileName)); 269 j["version"] = ver; 270 271 auto f = File(fileName, "w"); 272 f.write(j.toPrettyString()); 273 f.writeln(); 274 } 275 276 void insertVersionIntoDubSDLFile(string fileName, string ver, 277 ref const(DubProxyOptions) options) 278 { 279 import std.path : dirName; 280 const pth = dirName(buildNormalizedPath(absolutePath(expandTilde(fileName)))); 281 const string cwd = getcwd(); 282 scope(exit) { 283 chdir(cwd); 284 } 285 chdir(pth); 286 const toExe = format!`%s convert --format=json`(options.pathToDub); 287 288 auto rslt = executeShell(toExe); 289 enforce(rslt.status == 0, format!( 290 "dub failed with code '%s' and output '%s' to convert dub.sdl to " 291 ~ "dub.json with cmd '%s'")(rslt.status, rslt.output, toExe)); 292 293 const jsFn = pth ~ "/dub.json"; 294 insertVersionIntoDubJsonFile(jsFn, ver); 295 } 296 297 enum PathKind { 298 remoteGit, 299 localGit, 300 folder 301 } 302 303 PathKind getPathKind(string path) { 304 if(exists(path) && isDir(path) && exists(path ~ "/.git")) { 305 return PathKind.localGit; 306 } else if(exists(path) && isDir(path) && !exists(path ~ "/.git")) { 307 return PathKind.folder; 308 } 309 310 URL u; 311 if(tryParseURL(path, u)) { 312 return PathKind.remoteGit; 313 } 314 315 throw new Exception(format!"Couldn't determine PathKind for '%s'"(path)); 316 }