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 }