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