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 }