1 /** 2 * Wrappers for the git command-line tools. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.sys.git; 15 16 import core.stdc.time : time_t; 17 18 import std.algorithm; 19 import std.array; 20 import std.conv; 21 import std.exception; 22 import std.file; 23 import std.format; 24 import std.path; 25 import std.process; 26 import std.string; 27 import std.typecons : RefCounted; 28 import std.utf; 29 30 import ae.sys.cmd; 31 import ae.sys.file; 32 import ae.utils.aa; 33 import ae.utils.array; 34 import ae.utils.meta; 35 import ae.utils.text; 36 37 /// Represents an object which allows manipulating a Git repository. 38 struct Git 39 { 40 /// Create an object which allows manipulating a Git repository. 41 /// Because the location of $GIT_DIR (the .git directory) is queried at construction, 42 /// the repository must exist. 43 this(string path) 44 { 45 path = path.absolutePath(); 46 enforce(path.exists, "Repository path does not exist: " ~ path); 47 gitDir = path.buildPath(".git"); 48 if (gitDir.exists && gitDir.isFile) 49 gitDir = path.buildNormalizedPath(gitDir.readText().strip()[8..$]); 50 //path = path.replace(`\`, `/`); 51 this.path = path; 52 this.commandPrefix = ["git", 53 "-c", "core.autocrlf=false", 54 "-c", "gc.autoDetach=false", 55 "-C", path 56 ] ~ globalOptions; 57 version (Windows) {} else 58 this.environment["GIT_CONFIG_NOSYSTEM"] = "1"; 59 this.environment["HOME"] = gitDir; 60 this.environment["XDG_CONFIG_HOME"] = gitDir; 61 } 62 63 /// The path to the repository's work tree. 64 string path; 65 66 /// The path to the repository's .git directory. 67 string gitDir; 68 69 /// The environment that commands will be executed with. 70 /// This field and `commandPrefix` are populated at construction, 71 /// but may be modified afterwards, before any operations. 72 string[string] environment; 73 74 /// The prefix to apply to all executed commands. 75 /// Includes the "git" program, and all of its top-level options. 76 string[] commandPrefix; 77 78 /// Global options to add to `commandPrefix` during construction. 79 static string[] globalOptions; // per-thread 80 81 invariant() 82 { 83 assert(environment !is null, "Not initialized"); 84 } 85 86 // Have just some primitives here. 87 // Higher-level functionality can be added using UFCS. 88 89 /// Run a command. Throw if it fails. 90 void run (string[] args...) const { return .run (commandPrefix ~ args, environment, path); } 91 /// Run a command, and return its output, sans trailing newline. 92 string query(string[] args...) const { return .query(commandPrefix ~ args, environment, path).chomp(); } 93 /// Run a command, and return true if it succeeds. 94 bool check(string[] args...) const { return spawnProcess(commandPrefix ~ args, environment, Config.none, path).wait() == 0; } 95 /// Run a command with pipe redirections. Return the pipes. 96 auto pipe (string[] args, Redirect redirect) 97 const { return pipeProcess(commandPrefix ~ args, redirect, environment, Config.none, path); } 98 auto pipe (string[] args...) const { return pipe(args, Redirect.stdin | Redirect.stdout); } /// ditto 99 100 /// A parsed Git author/committer line. 101 struct Authorship 102 { 103 /// Format string which can be used with 104 /// ae.utils.time to parse or format Git dates. 105 enum dateFormat = "U O"; 106 107 string name; /// Name (without email). 108 string email; /// Email address (without the < / > delimiters). 109 string date; /// Raw date. Use `dateFormat` with ae.utils.time to parse. 110 111 /// Parse from a raw author/committer line. 112 this(string authorship) 113 { 114 auto parts1 = authorship.findSplit(" <"); 115 auto parts2 = parts1[2].findSplit("> "); 116 this.name = parts1[0]; 117 this.email = parts2[0]; 118 this.date = parts2[2]; 119 } 120 121 /// Construct from fields. 122 this(string name, string email, string date) 123 { 124 this.name = name; 125 this.email = email; 126 this.date = date; 127 } 128 129 /// Convert to a raw author/committer line. 130 string toString() const { return name ~ " <" ~ email ~ "> " ~ date; } 131 } 132 133 /// A convenience function which loads the entire Git history into a graph. 134 struct History 135 { 136 /// An entry corresponding to a Git commit in this `History` object. 137 struct Commit 138 { 139 size_t index; /// Index in this `History` instance. 140 CommitID oid; /// The commit hash. 141 time_t time; /// UNIX time. 142 string author; /// Raw author/committer lines. Use Authorship to parse. 143 string committer; /// ditto 144 string[] message; /// Commit message lines. 145 Commit*[] parents; /// Edges to neighboring commits. Children order is unspecified. 146 Commit*[] children; /// ditto 147 148 deprecated alias hash = oid; 149 deprecated alias id = index; 150 151 /// Get or set author/committer lines as parsed object. 152 @property Authorship parsedAuthor() { return Authorship(author); } 153 @property Authorship parsedCommitter() { return Authorship(committer); } /// ditto 154 @property void parsedAuthor(Authorship authorship) { author = authorship.toString(); } /// ditto 155 @property void parsedCommitter(Authorship authorship) { committer = authorship.toString(); } /// ditto 156 } 157 158 Commit*[CommitID] commits; /// All commits in this `History` object. 159 size_t numCommits = 0; /// Number of commits in `commits`. 160 CommitID[string] refs; /// A map of full Git refs (e.g. "refs/heads/master") to their commit IDs. 161 } 162 163 /// ditto 164 History getHistory(string[] extraArgs = null) const 165 { 166 History history; 167 168 History.Commit* getCommit(CommitID oid) 169 { 170 auto pcommit = oid in history.commits; 171 return pcommit ? *pcommit : (history.commits[oid] = new History.Commit(history.numCommits++, oid)); 172 } 173 174 History.Commit* commit; 175 string currentBlock; 176 177 foreach (line; query([`log`, `--all`, `--pretty=raw`] ~ extraArgs).split('\n')) 178 { 179 if (!line.length) 180 { 181 if (currentBlock) 182 currentBlock = null; 183 continue; 184 } 185 186 if (currentBlock) 187 { 188 enforce(line.startsWith(" "), "Expected " ~ currentBlock ~ " line in git log"); 189 continue; 190 } 191 192 if (line.startsWith("commit ")) 193 { 194 auto hash = CommitID(line[7..$]); 195 commit = getCommit(hash); 196 } 197 else 198 if (line.startsWith("tree ")) 199 continue; 200 else 201 if (line.startsWith("parent ")) 202 { 203 auto hash = CommitID(line[7..$]); 204 auto parent = getCommit(hash); 205 commit.parents ~= parent; 206 parent.children ~= commit; 207 } 208 else 209 if (line.startsWith("author ")) 210 commit.author = line[7..$]; 211 else 212 if (line.startsWith("committer ")) 213 { 214 commit.committer = line[10..$]; 215 commit.time = line.split(" ")[$-2].to!int(); 216 } 217 else 218 if (line.startsWith(" ")) 219 commit.message ~= line[4..$]; 220 else 221 if (line.startsWith("gpgsig ")) 222 currentBlock = "GPG signature"; 223 else 224 if (line.startsWith("mergetag ")) 225 currentBlock = "Tag merge"; 226 else 227 enforce(false, "Unknown line in git log: " ~ line); 228 } 229 230 if (!history.commits) 231 return history; // show-ref will fail if there are no refs 232 233 foreach (line; query([`show-ref`, `--dereference`]).splitLines()) 234 { 235 auto h = CommitID(line[0..40]); 236 enforce(h in history.commits, "Ref commit not in log: " ~ line); 237 history.refs[line[41..$]] = h; 238 } 239 240 return history; 241 } 242 243 // Low-level pipes 244 245 /// Git object identifier (identifies blobs, trees, commits, etc.) 246 struct OID 247 { 248 /// Watch me: new hash algorithms may be supported in the future. 249 ubyte[20] sha1; 250 251 deprecated alias sha1 this; 252 253 /// Construct from an ASCII string. 254 this(in char[] sha1) 255 { 256 enforce(sha1.length == 40, "Bad SHA-1 length: " ~ sha1); 257 foreach (i, ref b; this.sha1) 258 b = to!ubyte(sha1[i*2..i*2+2], 16); 259 } 260 261 /// Convert to the ASCII representation. 262 string toString() pure const 263 { 264 char[40] buf = sha1.toLowerHex(); 265 return buf[].idup; 266 } 267 268 debug(ae_unittest) unittest 269 { 270 OID oid; 271 oid.sha1 = [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67]; 272 assert(oid.toString() == "0123456789abcdef0123456789abcdef01234567"); 273 } 274 } 275 276 private mixin template TypedObjectID(string type_) 277 { 278 /// As in git-hash-object's -t parameter. 279 enum type = type_; 280 281 OID oid; /// The generic object identifier. 282 alias oid this; 283 284 this(OID oid) { this.oid = oid; } /// Construct from a generic identifier. 285 this(in char[] hash) { oid = OID(hash); } /// Construct from an ASCII string. 286 string toString() pure const { return oid.toString(); } /// Convert to the ASCII representation. 287 288 // Disable implicit conversion directly between different kinds of OIDs. 289 static if (!is(typeof(this) == CommitID)) @disable this(CommitID); 290 static if (!is(typeof(this) == TreeID)) @disable this(TreeID); 291 static if (!is(typeof(this) == BlobID)) @disable this(BlobID); 292 } 293 /// Strong typed OIDs to distinguish which kind of object they identify. 294 struct CommitID { mixin TypedObjectID!"commit"; } 295 struct TreeID { mixin TypedObjectID!"tree" ; } /// ditto 296 struct BlobID { mixin TypedObjectID!"blob" ; } /// ditto 297 298 /// The parsed representation of a raw Git object. 299 struct Object 300 { 301 /// Object identifier. Will be OID.init initially. 302 OID oid; 303 304 /// Object type, as in git-hash-object's -t parameter. 305 string type; 306 307 /// The raw data contained in this object. 308 immutable(ubyte)[] data; 309 310 deprecated alias hash = oid; 311 312 /// Create a blob object (i.e., a file) from raw bytes. 313 static Object createBlob(immutable(ubyte)[] data) 314 { 315 return Object(OID.init, "blob", data); 316 } 317 318 /// "Parse" this raw Git object as a blob. 319 immutable(ubyte)[] parseBlob() 320 { 321 enforce(type == "blob", "Wrong object type"); 322 return data; 323 } 324 325 /// Represents a parsed Git commit object. 326 struct ParsedCommit 327 { 328 /// The Git OID of this commit's tree. 329 TreeID tree; 330 331 /// This commit's parents. 332 CommitID[] parents; 333 334 /// Raw author/committer lines. 335 string author, committer; 336 337 /// Commit message lines. 338 string[] message; 339 340 /// GPG signature certifying this commit, if any. 341 string[][] gpgsig; 342 343 /// "mergetag" signatures (on commits that merge a tag), if any. 344 string[][] mergetag; 345 346 /// Get or set author/committer lines as parsed object. 347 @property Authorship parsedAuthor() { return Authorship(author); } 348 @property Authorship parsedCommitter() { return Authorship(committer); } /// ditto 349 @property void parsedAuthor(Authorship authorship) { author = authorship.toString(); } /// ditto 350 @property void parsedCommitter(Authorship authorship) { committer = authorship.toString(); } /// ditto 351 } 352 353 /// Parse this raw Git object as a commit. 354 ParsedCommit parseCommit() 355 { 356 enforce(type == "commit", "Wrong object type"); 357 ParsedCommit result; 358 auto lines = (cast(string)data).split('\n'); 359 while (lines.length) 360 { 361 auto line = lines.shift(); 362 if (line == "") 363 { 364 result.message = lines; 365 break; // commit message begins 366 } 367 auto parts = line.findSplit(" "); 368 auto field = parts[0]; 369 line = parts[2]; 370 switch (field) 371 { 372 case "tree": 373 result.tree = TreeID(line); 374 break; 375 case "parent": 376 result.parents ~= CommitID(line); 377 break; 378 case "author": 379 result.author = line; 380 break; 381 case "committer": 382 result.committer = line; 383 break; 384 case "gpgsig": 385 { 386 auto p = lines.countUntil!(line => !line.startsWith(" ")); 387 if (p < 0) 388 p = lines.length; 389 result.gpgsig ~= [line] ~ lines[0 .. p].apply!(each!((ref line) => line.skipOver(" ").enforce("gpgsig line without leading space"))); 390 lines = lines[p .. $]; 391 break; 392 } 393 case "mergetag": 394 { 395 auto p = lines.countUntil!(line => !line.startsWith(" ")); 396 if (p < 0) 397 p = lines.length; 398 result.mergetag ~= [line] ~ lines[0 .. p].apply!(each!((ref line) => line.skipOver(" ").enforce("mergetag line without leading space"))); 399 lines = lines[p .. $]; 400 break; 401 } 402 default: 403 throw new Exception("Unknown commit field: " ~ field ~ "\n" ~ cast(string)data); 404 } 405 } 406 return result; 407 } 408 409 /// Format a Git commit into a raw Git object. 410 static Object createCommit(in ParsedCommit commit) 411 { 412 auto s = "tree %s\n%-(parent %s\n%|%)author %s\ncommitter %s\n\n%-(%s\n%)".format( 413 commit.tree.toString(), 414 commit.parents.map!((ref const CommitID oid) => oid.toString()), 415 commit.author, 416 commit.committer, 417 commit.message, 418 ); 419 return Object(OID.init, "commit", cast(immutable(ubyte)[])s); 420 } 421 422 /// Represents an entry in a parsed Git commit object. 423 struct TreeEntry 424 { 425 uint mode; /// POSIX mode. E.g., will be 100644 or 100755 for files. 426 string name; /// Name within this subtree. 427 OID hash; /// Object identifier of the entry's contents. Could be a tree or blob ID. 428 429 /// Sort key to be used when constructing a tree object. 430 @property string sortName() const { return (mode & octal!40000) ? name ~ "/" : name; } 431 432 /// Implements comparison using `sortName`. 433 int opCmp(ref const TreeEntry b) const 434 { 435 return cmp(sortName, b.sortName); 436 } 437 } 438 439 /// Parse this raw Git object as a tree. 440 TreeEntry[] parseTree() 441 { 442 enforce(type == "tree", "Wrong object type"); 443 TreeEntry[] result; 444 auto rem = data; 445 while (rem.length) 446 { 447 auto si = rem.countUntil(' '); 448 auto zi = rem.countUntil(0); 449 auto ei = zi + 1 + OID.sha1.length; 450 auto str = cast(string)rem[0..zi]; 451 enforce(0 < si && si < zi && ei <= rem.length, "Malformed tree entry:\n" ~ hexDump(rem)); 452 OID oid; 453 oid.sha1 = rem[zi+1..ei][0..OID.sha1.length]; 454 result ~= TreeEntry(str[0..si].to!uint(8), str[si+1..zi], oid); // https://issues.dlang.org/show_bug.cgi?id=13112 455 rem = rem[ei..$]; 456 } 457 return result; 458 } 459 460 /// Format a Git tree into a raw Git object. 461 /// Tree entries must be sorted (TreeEntry implements an appropriate opCmp). 462 static Object createTree(in TreeEntry[] entries) 463 { 464 auto buf = appender!(immutable(ubyte)[]); 465 foreach (entry; entries) 466 { 467 buf.formattedWrite("%o %s\0", entry.mode, entry.name); 468 buf.put(entry.hash[]); 469 } 470 return Object(OID.init, "tree", buf.data); 471 } 472 } 473 474 /// Spawn a cat-file process which can read git objects by demand. 475 struct ObjectReaderImpl 476 { 477 private ProcessPipes pipes; 478 479 /// Read an object by its identifier. 480 Object read(string name) 481 { 482 pipes.stdin.writeln(name); 483 pipes.stdin.flush(); 484 485 auto headerLine = pipes.stdout.safeReadln().strip(); 486 auto header = headerLine.split(" "); 487 enforce(header.length == 3, "Malformed header during cat-file: " ~ headerLine); 488 auto oid = OID(header[0]); 489 490 Object obj; 491 obj.oid = oid; 492 obj.type = header[1]; 493 auto size = to!size_t(header[2]); 494 if (size) 495 { 496 auto data = new ubyte[size]; 497 auto read = pipes.stdout.rawRead(data); 498 enforce(read.length == size, "Unexpected EOF during cat-file"); 499 obj.data = data.assumeUnique(); 500 } 501 502 char[1] lf; 503 pipes.stdout.rawRead(lf[]); 504 enforce(lf[0] == '\n', "Terminating newline expected"); 505 506 return obj; 507 } 508 509 /// ditto 510 Object read(OID oid) 511 { 512 auto obj = read(oid.toString()); 513 enforce(obj.oid == oid, "Unexpected object during cat-file"); 514 return obj; 515 } 516 517 ~this() 518 { 519 pipes.stdin.close(); 520 enforce(pipes.pid.wait() == 0, "git cat-file exited with failure"); 521 } 522 } 523 alias ObjectReader = RefCounted!ObjectReaderImpl; /// ditto 524 525 /// ditto 526 ObjectReader createObjectReader() 527 { 528 auto pipes = this.pipe(`cat-file`, `--batch`); 529 return ObjectReader(pipes); 530 } 531 532 /// Run a batch cat-file query. 533 Object[] getObjects(OID[] hashes) 534 { 535 Object[] result; 536 result.reserve(hashes.length); 537 auto reader = createObjectReader(); 538 539 foreach (hash; hashes) 540 result ~= reader.read(hash); 541 542 return result; 543 } 544 545 /// Spawn a hash-object process which can hash and write git objects on the fly. 546 struct ObjectWriterImpl 547 { 548 private bool initialized; 549 private ProcessPipes pipes; 550 551 /*private*/ this(ProcessPipes pipes) 552 { 553 this.pipes = pipes; 554 initialized = true; 555 } 556 557 /// Write a raw Git object of this writer's type, and return the OID. 558 OID write(in ubyte[] data) 559 { 560 import std.random : uniform; 561 auto p = NamedPipe("ae-sys-git-writeObjects-%d".format(uniform!ulong)); 562 pipes.stdin.writeln(p.fileName); 563 pipes.stdin.flush(); 564 565 auto f = p.connect(); 566 f.rawWrite(data); 567 f.flush(); 568 f.close(); 569 570 return OID(pipes.stdout.safeReadln().strip()); 571 } 572 573 deprecated OID write(const(void)[] data) { return write(cast(const(ubyte)[]) data); } 574 575 ~this() 576 { 577 if (initialized) 578 { 579 pipes.stdin.close(); 580 enforce(pipes.pid.wait() == 0, "git hash-object exited with failure"); 581 initialized = false; 582 } 583 } 584 } 585 alias ObjectWriter = RefCounted!ObjectWriterImpl; /// ditto 586 587 ObjectWriter createObjectWriter(string type) 588 { 589 auto pipes = this.pipe(`hash-object`, `-t`, type, `-w`, `--stdin-paths`); 590 return ObjectWriter(pipes); 591 } /// ditto 592 593 struct ObjectMultiWriterImpl 594 { 595 private Git repo; 596 597 /// The ObjectWriter instances for each individual type. 598 ObjectWriter treeWriter, blobWriter, commitWriter; 599 600 /// Write a Git object, and return the OID. 601 OID write(in Object obj) 602 { 603 ObjectWriter* pwriter; 604 switch (obj.type) // https://issues.dlang.org/show_bug.cgi?id=14595 605 { 606 case "tree" : pwriter = &treeWriter ; break; 607 case "blob" : pwriter = &blobWriter ; break; 608 case "commit": pwriter = &commitWriter; break; 609 default: throw new Exception("Unknown object type: " ~ obj.type); 610 } 611 if (!pwriter.initialized) 612 *pwriter = ObjectWriter(repo.pipe(`hash-object`, `-t`, obj.type, `-w`, `--stdin-paths`)); 613 return pwriter.write(obj.data); 614 } 615 616 /// Format and write a Git object, and return the OID. 617 CommitID write(in Object.ParsedCommit commit) { return CommitID(write(Object.createCommit(commit))); } 618 TreeID write(in Object.TreeEntry[] entries) { return TreeID (write(Object.createTree(entries))); } /// ditto 619 BlobID write(immutable(ubyte)[] bytes ) { return BlobID (write(Object.createBlob(bytes))); } /// ditto 620 } /// ditto 621 alias ObjectMultiWriter = RefCounted!ObjectMultiWriterImpl; /// ditto 622 623 /// ditto 624 ObjectMultiWriter createObjectWriter() 625 { 626 return ObjectMultiWriter(this); 627 } 628 629 /// Batch-write the given objects to the database. 630 /// The hashes are saved to the "hash" fields of the passed objects. 631 void writeObjects(Git.Object[] objects) 632 { 633 string[] allTypes = objects.map!(obj => obj.type).toSet().keys; 634 foreach (type; allTypes) 635 { 636 auto writer = createObjectWriter(type); 637 foreach (ref obj; objects) 638 if (obj.type == type) 639 obj.oid = writer.write(obj.data); 640 } 641 } 642 643 /// Extract a commit's tree to a given directory 644 void exportCommit(string commit, string path, ObjectReader reader, bool delegate(string) pathFilter = null) 645 { 646 exportTree(reader.read(commit).parseCommit().tree, path, reader, pathFilter); 647 } 648 649 /// Extract a tree to a given directory 650 void exportTree(TreeID treeHash, string path, ObjectReader reader, bool delegate(string) pathFilter = null) 651 { 652 void exportSubTree(OID treeHash, string[] subPath) 653 { 654 auto tree = reader.read(treeHash).parseTree(); 655 foreach (entry; tree) 656 { 657 auto entrySubPath = subPath ~ entry.name; 658 if (pathFilter && !pathFilter(entrySubPath.join("/"))) 659 continue; 660 auto entryPath = buildPath([path] ~ entrySubPath); 661 switch (entry.mode) 662 { 663 case octal!100644: // file 664 case octal!100755: // executable file 665 std.file.write(entryPath, reader.read(entry.hash).data); 666 version (Posix) 667 { 668 // Make executable 669 if (entry.mode == octal!100755) 670 entryPath.setAttributes(entryPath.getAttributes | ((entryPath.getAttributes & octal!444) >> 2)); 671 } 672 break; 673 case octal! 40000: // tree 674 mkdirRecurse(entryPath); 675 exportSubTree(entry.hash, entrySubPath); 676 break; 677 case octal!160000: // submodule 678 mkdirRecurse(entryPath); 679 break; 680 default: 681 throw new Exception("Unknown git file mode: %o".format(entry.mode)); 682 } 683 } 684 } 685 exportSubTree(treeHash, null); 686 } 687 688 /// Import a directory tree into the object store, and return the new tree object's hash. 689 TreeID importTree(string path, ObjectMultiWriter writer, bool delegate(string) pathFilter = null) 690 { 691 static // Error: variable ae.sys.git.Repository.importTree.writer has scoped destruction, cannot build closure 692 TreeID importSubTree(string path, string subPath, ref ObjectMultiWriter writer, bool delegate(string) pathFilter) 693 { 694 auto entries = subPath 695 .dirEntries(SpanMode.shallow) 696 .filter!(de => !pathFilter || pathFilter(de.relativePath(path))) 697 .map!(de => 698 de.isDir 699 ? Object.TreeEntry( 700 octal!40000, 701 de.baseName, 702 importSubTree(path, buildPath(subPath, de.baseName), writer, pathFilter) 703 ) 704 : Object.TreeEntry( 705 isVersion!`Posix` && (de.attributes & octal!111) ? octal!100755 : octal!100644, 706 de.baseName, 707 writer.write(Git.Object(OID.init, "blob", cast(immutable(ubyte)[])read(de.name))) 708 ) 709 ) 710 .array 711 .sort!((a, b) => a.sortName < b.sortName).release 712 ; 713 return TreeID(writer.write(Object.createTree(entries))); 714 } 715 return importSubTree(path, path, writer, pathFilter); 716 } 717 718 /// Spawn a update-ref process which can update git refs on the fly. 719 struct RefWriterImpl 720 { 721 private bool initialized; 722 private ProcessPipes pipes; 723 724 /*private*/ this(ProcessPipes pipes) 725 { 726 this.pipes = pipes; 727 initialized = true; 728 } 729 730 private void op(string op) 731 { 732 pipes.stdin.write(op, '\0'); 733 pipes.stdin.flush(); 734 } 735 736 private void op(string op, bool noDeref, string refName, CommitID*[] hashes...) 737 { 738 if (noDeref) 739 pipes.stdin.write("option no-deref\0"); 740 pipes.stdin.write(op, " ", refName, '\0'); 741 foreach (hash; hashes) 742 { 743 if (hash) 744 pipes.stdin.write((*hash).toString()); 745 pipes.stdin.write('\0'); 746 } 747 pipes.stdin.flush(); 748 } 749 750 /// Send update-ref operations (as specified in its man page). 751 void update (string refName, CommitID newValue , bool noDeref = false) { op("update", noDeref, refName, &newValue, null ); } 752 void update (string refName, CommitID newValue, CommitID oldValue, bool noDeref = false) { op("update", noDeref, refName, &newValue, &oldValue); } /// ditto 753 void create (string refName, CommitID newValue , bool noDeref = false) { op("create", noDeref, refName, &newValue ); } /// ditto 754 void deleteRef(string refName , bool noDeref = false) { op("delete", noDeref, refName, null ); } /// ditto 755 void deleteRef(string refName, CommitID oldValue, bool noDeref = false) { op("delete", noDeref, refName, &oldValue); } /// ditto 756 void verify (string refName , bool noDeref = false) { op("verify", noDeref, refName, null ); } /// ditto 757 void verify (string refName, CommitID oldValue, bool noDeref = false) { op("verify", noDeref, refName, &oldValue); } /// ditto 758 void start ( ) { op("start" ); } /// ditto 759 void prepare ( ) { op("prepare" ); } /// ditto 760 void commit ( ) { op("commit" ); } /// ditto 761 void abort ( ) { op("abort" ); } /// ditto 762 763 deprecated void update (string refName, OID newValue , bool noDeref = false) { op("update", noDeref, refName, cast(CommitID*)&newValue, null ); } 764 deprecated void update (string refName, OID newValue, OID oldValue, bool noDeref = false) { op("update", noDeref, refName, cast(CommitID*)&newValue, cast(CommitID*)&oldValue); } 765 deprecated void create (string refName, OID newValue , bool noDeref = false) { op("create", noDeref, refName, cast(CommitID*)&newValue ); } 766 deprecated void deleteRef(string refName, OID oldValue, bool noDeref = false) { op("delete", noDeref, refName, cast(CommitID*)&oldValue); } 767 deprecated void verify (string refName, OID oldValue, bool noDeref = false) { op("verify", noDeref, refName, cast(CommitID*)&oldValue); } 768 769 ~this() 770 { 771 if (initialized) 772 { 773 pipes.stdin.close(); 774 enforce(pipes.pid.wait() == 0, "git update-ref exited with failure"); 775 initialized = false; 776 } 777 } 778 } 779 alias RefWriter = RefCounted!RefWriterImpl; /// ditto 780 781 /// ditto 782 RefWriter createRefWriter() 783 { 784 auto pipes = this.pipe(`update-ref`, `-z`, `--stdin`); 785 return RefWriter(pipes); 786 } 787 788 /// Tries to match the default destination of `git clone`. 789 static string repositoryNameFromURL(string url) 790 { 791 return url 792 .split(":")[$-1] 793 .split("/")[$-1] 794 .chomp(".git"); 795 } 796 797 debug(ae_unittest) unittest 798 { 799 assert(repositoryNameFromURL("https://github.com/CyberShadow/ae.git") == "ae"); 800 assert(repositoryNameFromURL("git@example.com:ae.git") == "ae"); 801 } 802 } 803 804 deprecated alias Repository = Git; 805 deprecated alias History = Git.History; 806 deprecated alias Commit = Git.History.Commit; 807 deprecated alias GitObject = Git.Object; 808 deprecated alias Hash = Git.OID; 809 deprecated Git.Authorship parseAuthorship(string authorship) { return Git.Authorship(authorship); } 810 811 deprecated Git.CommitID toCommitHash(in char[] hash) { return Git.CommitID(Git.OID(hash)); } 812 813 debug(ae_unittest) deprecated unittest 814 { 815 assert(toCommitHash("0123456789abcdef0123456789ABCDEF01234567").oid.sha1 == [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67]); 816 } 817 818 deprecated string toString(ref const Git.OID oid) { return oid.toString(); } 819 820 deprecated alias repositoryNameFromURL = Git.repositoryNameFromURL;