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 }