1 /** Wrappers around commands such `git`, `patch`, etc.
2  *
3  * Original version https://gist.github.com/PetarKirov/b4c8b64e7fc9bb7391901bcb541ddf3a
4  *
5  * See_Also: https://github.com/CyberShadow/ae/blob/master/sys/git.d
6  * See_Also: https://forum.dlang.org/post/ziqgqpkdjolplyfztulp@forum.dlang.org
7  *
8  * TODO: integrate @CyberShadow’s ae.sys.git at ae/sys/git.d
9  */
10 module nxt.cmd;
11 
12 import std.exception : enforce;
13 import std.format : format;
14 import std.file : exists, isDir, isFile;
15 import std.path : absolutePath, buildNormalizedPath, dirName, relativePath;
16 import std.stdio : write, writeln, writefln, File, stdin;
17 import std.algorithm.mutation : move;
18 import nxt.fs;
19 
20 @safe:
21 
22 /** Spawn logging configuration.
23  */
24 struct Log
25 {
26 	import std.stdio : stdout, stderr;
27 
28     enum Fmt
29     {
30         tree,                   ///< Tree-like. Default.
31         trad,                   ///< Traditional.
32     }
33 
34     bool opCast(T : bool)() const scope @safe pure nothrow @nogc => fmt != Fmt.none;
35 	File getOut() const scope @trusted => echoOut ? stdout : File("/dev/null", "wb");
36 	File getErr() const scope @trusted => echoErr ? stderr : File("/dev/null", "wb");
37 
38 private:
39     Fmt fmt;
40     bool echoDesc = true;       ///< Echo description.
41     bool echoSpawn = false;		///< Echo spawn arguments.
42     bool echoWait = false;		///< Echo wait.
43     bool echoOut = false; ///< Echo standard output upon completion of execution.
44     bool echoErr = false; ///< Echo standard error upon completion of execution.
45     bool colored = true;
46 }
47 
48 ExitStatus exitMessage(ExitStatus code) => typeof(return)(code);
49 
50 /** Process exit status (code).
51  *
52  * See: https://en.wikipedia.org/wiki/Exit_status
53  */
54 struct ExitStatus
55 {
56     void toString(Sink)(ref scope Sink sink, in bool colored = true) const scope
57     {
58         import nxt.ansi_escape : putWithSGRs, SGR;
59         if (colored)
60             putWithSGRs(sink, value == 0 ? "SUCCESS" : "FAILURE", SGR.whiteForegroundColor);
61     }
62     int value;
63     alias value this;
64 }
65 
66 /** Process spawn state.
67  */
68 @safe struct Spawn
69 {
70     import std.process : Pid;
71     Pid pid;
72     File _out;
73     File _err;
74     Log log;
75 
76     this(Pid pid, File out_, File err_, Log log)
77 		in(out_.isOpen)
78 		in(err_.isOpen)
79     {
80         this.pid = pid;
81         this._out = move(out_);
82         this._err = move(err_);
83         this.log = log;
84     }
85 
86     @disable this(this);        // avoid copying `File`s for now
87 
88     ExitStatus wait()
89     {
90         if (log.echoWait)
91             writeIng("Waiting");
92 
93         import std.process : wait;
94         auto result = typeof(return)(pid.wait());
95 
96         writeln(" (", result, ")");
97 
98         static void echo(ref File src, ref File dst, string name)
99 		    in(src.isOpen)
100 		    in(dst.isOpen)
101         {
102             writeln("  - ", name, ":");
103             src.flush();
104             import std.algorithm.mutation: copy;
105 			version(none)		// TODO: enable
106 				() @trusted { src.byLine().copy(dst.lockingBinaryWriter); } (); // TODO: writeIndented
107         }
108 
109         import std.stdio : stdout, stderr;
110         if (result != 0 ||        // always show stdout upon failure
111             log.echoOut)
112         {
113             () @trusted { echo(_out, stdout, "OUT"); } ();
114         }
115         if (result != 0 ||        // always show stderr upon failure
116             log.echoErr)
117         {
118             () @trusted { echo(_err, stderr, "ERR"); } ();
119         }
120 
121         return result;
122     }
123 }
124 
125 Spawn spawn(scope ref Log log, scope const(char[])[] args, File stdin_ = stdin)
126 {
127     import std.stdio : stdout, stderr;
128     import std.process : spawnProcess;
129     if (log.echoSpawn)
130         writeIng("Spawning ", args);
131 	auto pid = spawnProcess(args, stdin_, log.getOut, log.getErr);
132     return typeof(return)(pid, log.getOut, log.getErr, log);
133 }
134 
135 /** Patch file handling.
136  */
137 struct Patch
138 {
139     Path file;
140     uint level;
141     Log log;
142 
143     this(Path file, in uint level, in bool echoOutErr)
144     {
145         this.file = file;
146         this.level = level;
147         this.log = Log(Log.Fmt.init, echoOutErr);
148     }
149 
150     Spawn applyIn(in DirPath dir)
151     {
152         import std.conv : to;
153         import std.stdio : File;
154         if (log.echoDesc)
155             final switch (log.fmt)
156             {
157             case Log.Fmt.tree: writeIng(dir, ": Applying patch ", file.absolutePath); break;
158             case Log.Fmt.trad: writeIng("Applying patch ", file.absolutePath, " at ", dir); break;
159             }
160         return spawn(log,
161                      ["patch", // TODO: can we use execute shell instead?
162                           "-d", dir[].to!string,
163                           "-p"~level.to!string],
164                          File(file[].absolutePath.to!string));
165     }
166 }
167 
168 struct Repository
169 {
170     const URL url;
171     const DirPath lrd;			///< Local checkout root directory.
172     Log log;
173 
174     this(const URL url, const DirPath lrd = DirPath.init, in bool echoOutErr = true)
175     {
176         this.url = url;
177 		if (lrd != lrd.init)
178 			this.lrd = lrd;
179 		else
180 		{
181 			import std.path : baseName, stripExtension;
182 			this.lrd = DirPath(this.url.baseName.stripExtension);
183 		}
184         this.log = Log(Log.Fmt.init, echoOutErr);
185     }
186 
187     @disable this(this);
188 
189     Spawn cloneOrPull(in bool recursive = true, string branch = [])
190     {
191         if (lrd.buildNormalizedPath(".git").exists)
192             return pull(recursive);
193         else
194             return clone(recursive, branch);
195     }
196 
197     Spawn cloneOrRefresh(in bool recursive = true, string remote = "origin", string branch = [])
198     {
199         if (lrd.buildNormalizedPath(".git").exists)
200             return refresh(recursive, remote, branch);
201         else
202             return clone(recursive, branch);
203     }
204 
205     Spawn clone(in bool recursive = true, string branch = [])
206     {
207         final switch (log.fmt)
208         {
209         case Log.Fmt.tree: writeIng(url, ": Cloning to ", lrd); break;
210         case Log.Fmt.trad: writeIng("Cloning ", url, " to ", lrd); break;
211         }
212         return spawn(log,
213                      ["git", "clone"]
214                      ~ [url, lrd]
215                      ~ (branch.length ? ["-b", branch] : [])
216                      ~ (recursive ? ["--recurse-submodules"] : []));
217     }
218 
219     Spawn pull(in bool recursive = true)
220     {
221         if (log.echoDesc)
222             writeIng("Pulling ", url, " to ", lrd);
223         return spawn(log,
224                      ["git", "-C", lrd, "pull"]
225                      ~ (recursive ? ["--recurse-submodules"] : []));
226     }
227 
228     Spawn refresh(in bool recursive = true, string remote = "origin", string branch = [])
229     {
230         if (log.echoDesc)
231             writeIng("Refreshing ", url, " to ", lrd);
232         fetch([remote]);
233         if (branch)
234             return resetHardTo(remote~"/"~branch, recursive);
235         else
236             return resetHard(recursive);
237     }
238 
239     Spawn checkout(string branchName)
240     {
241         if (log.echoDesc)
242             writeIng("Checking out branch ", branchName, " at ", lrd);
243         return spawn(log, ["git", "-C", lrd, "checkout" , branchName]);
244     }
245 
246     Spawn remoteRemove(string name)
247     {
248         if (log.echoDesc)
249             writeIng("Removing remote ", name, " at ", lrd);
250         return spawn(log, ["git", "-C", lrd, "remote", "remove", name]);
251     }
252     alias removeRemote = remoteRemove;
253 
254     Spawn remoteAdd(in URL url, string name)
255     {
256         if (log.echoDesc)
257             writeIng("Adding remote ", url, " at ", lrd, (name ? " as " ~ name : "") ~ " ...");
258         return spawn(log, ["git", "-C", lrd, "remote", "add", name, url]);
259     }
260     alias addRemote = remoteAdd;
261 
262     Spawn fetch(string[] names)
263     {
264         if (log.echoDesc)
265             writeIng("Fetching remotes ", names, " at ", lrd);
266         if (names)
267             return spawn(log, ["git", "-C", lrd, "fetch", "--multiple"] ~ names);
268         else
269             return spawn(log, ["git", "-C", lrd, "fetch"]);
270     }
271 
272     Spawn fetchAll()
273     {
274         if (log.echoDesc)
275             writeIng("Fetching all remotes at ", lrd);
276         return spawn(log, ["git", "-C", lrd, "fetch", "--all"]);
277     }
278 
279     Spawn clean()
280     {
281         if (log.echoDesc)
282             writeIng("Cleaning ", lrd);
283         return spawn(log, ["git", "-C", lrd, "clean", "-ffdx"]);
284     }
285 
286     Spawn resetHard(in bool recursive = true)
287     {
288         if (log.echoDesc)
289             writeIng("Resetting hard ", lrd);
290         return spawn(log, ["git", "-C", lrd, "reset", "--hard"]
291                      ~ (recursive ? ["--recurse-submodules"] : []));
292     }
293 
294     Spawn resetHardTo(string treeish, in bool recursive = true)
295     {
296         if (log.echoDesc)
297             writeIng("Resetting hard ", lrd);
298         return spawn(log, ["git", "-C", lrd, "reset", "--hard", treeish]
299                      ~ (recursive ? ["--recurse-submodules"] : []));
300     }
301 
302     Spawn merge(string[] commits, string[] args = [])
303     {
304         if (log.echoDesc)
305             writeIng("Merging commits ", commits, " with flags ", args, " at ", lrd);
306         return spawn(log, ["git", "-C", lrd, "merge"]
307                      ~ args ~ commits);
308     }
309 }
310 
311 void writeIng(S...)(scope S args)
312 {
313     import std.stdio : stdout;
314 	() @trusted { write(args, " ... "); stdout.flush(); } ();
315 }