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 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 /// Get or set author/committer lines as parsed object. 344 @property Authorship parsedAuthor() { return Authorship(author); } 345 @property Authorship parsedCommitter() { return Authorship(committer); } /// ditto 346 @property void parsedAuthor(Authorship authorship) { author = authorship.toString(); } /// ditto 347 @property void parsedCommitter(Authorship authorship) { committer = authorship.toString(); } /// ditto 348 } 349 350 /// Parse this raw Git object as a commit. 351 ParsedCommit parseCommit() 352 { 353 enforce(type == "commit", "Wrong object type"); 354 ParsedCommit result; 355 auto lines = (cast(string)data).split('\n'); 356 while (lines.length) 357 { 358 auto line = lines.shift(); 359 if (line == "") 360 { 361 result.message = lines; 362 break; // commit message begins 363 } 364 auto parts = line.findSplit(" "); 365 auto field = parts[0]; 366 line = parts[2]; 367 switch (field) 368 { 369 case "tree": 370 result.tree = TreeID(line); 371 break; 372 case "parent": 373 result.parents ~= CommitID(line); 374 break; 375 case "author": 376 result.author = line; 377 break; 378 case "committer": 379 result.committer = line; 380 break; 381 case "gpgsig": 382 { 383 auto p = lines.countUntil!(line => !line.startsWith(" ")); 384 if (p < 0) 385 p = lines.length; 386 result.gpgsig = [line] ~ lines[0 .. p].apply!(each!((ref line) => line.skipOver(" ").enforce("gpgsig line without leading space"))); 387 lines = lines[p .. $]; 388 break; 389 } 390 default: 391 throw new Exception("Unknown commit field: " ~ field ~ "\n" ~ cast(string)data); 392 } 393 } 394 return result; 395 } 396 397 /// Format a Git commit into a raw Git object. 398 static Object createCommit(in ParsedCommit commit) 399 { 400 auto s = "tree %s\n%-(parent %s\n%|%)author %s\ncommitter %s\n\n%-(%s\n%)".format( 401 commit.tree.toString(), 402 commit.parents.map!((ref const CommitID oid) => oid.toString()), 403 commit.author, 404 commit.committer, 405 commit.message, 406 ); 407 return Object(OID.init, "commit", cast(immutable(ubyte)[])s); 408 } 409 410 /// Represents an entry in a parsed Git commit object. 411 struct TreeEntry 412 { 413 uint mode; /// POSIX mode. E.g., will be 100644 or 100755 for files. 414 string name; /// Name within this subtree. 415 OID hash; /// Object identifier of the entry's contents. Could be a tree or blob ID. 416 417 /// Sort key to be used when constructing a tree object. 418 @property string sortName() const { return (mode & octal!40000) ? name ~ "/" : name; } 419 420 /// Implements comparison using `sortName`. 421 int opCmp(ref const TreeEntry b) const 422 { 423 return cmp(sortName, b.sortName); 424 } 425 } 426 427 /// Parse this raw Git object as a tree. 428 TreeEntry[] parseTree() 429 { 430 enforce(type == "tree", "Wrong object type"); 431 TreeEntry[] result; 432 auto rem = data; 433 while (rem.length) 434 { 435 auto si = rem.countUntil(' '); 436 auto zi = rem.countUntil(0); 437 auto ei = zi + 1 + OID.sha1.length; 438 auto str = cast(string)rem[0..zi]; 439 enforce(0 < si && si < zi && ei <= rem.length, "Malformed tree entry:\n" ~ hexDump(rem)); 440 OID oid; 441 oid.sha1 = rem[zi+1..ei][0..OID.sha1.length]; 442 result ~= TreeEntry(str[0..si].to!uint(8), str[si+1..zi], oid); // https://issues.dlang.org/show_bug.cgi?id=13112 443 rem = rem[ei..$]; 444 } 445 return result; 446 } 447 448 /// Format a Git tree into a raw Git object. 449 /// Tree entries must be sorted (TreeEntry implements an appropriate opCmp). 450 static Object createTree(in TreeEntry[] entries) 451 { 452 auto buf = appender!(immutable(ubyte)[]); 453 foreach (entry; entries) 454 { 455 buf.formattedWrite("%o %s\0", entry.mode, entry.name); 456 buf.put(entry.hash[]); 457 } 458 return Object(OID.init, "tree", buf.data); 459 } 460 } 461 462 /// Spawn a cat-file process which can read git objects by demand. 463 struct ObjectReaderImpl 464 { 465 private ProcessPipes pipes; 466 467 /// Read an object by its identifier. 468 Object read(string name) 469 { 470 pipes.stdin.writeln(name); 471 pipes.stdin.flush(); 472 473 auto headerLine = pipes.stdout.safeReadln().strip(); 474 auto header = headerLine.split(" "); 475 enforce(header.length == 3, "Malformed header during cat-file: " ~ headerLine); 476 auto oid = OID(header[0]); 477 478 Object obj; 479 obj.oid = oid; 480 obj.type = header[1]; 481 auto size = to!size_t(header[2]); 482 if (size) 483 { 484 auto data = new ubyte[size]; 485 auto read = pipes.stdout.rawRead(data); 486 enforce(read.length == size, "Unexpected EOF during cat-file"); 487 obj.data = data.assumeUnique(); 488 } 489 490 char[1] lf; 491 pipes.stdout.rawRead(lf[]); 492 enforce(lf[0] == '\n', "Terminating newline expected"); 493 494 return obj; 495 } 496 497 /// ditto 498 Object read(OID oid) 499 { 500 auto obj = read(oid.toString()); 501 enforce(obj.oid == oid, "Unexpected object during cat-file"); 502 return obj; 503 } 504 505 ~this() 506 { 507 pipes.stdin.close(); 508 enforce(pipes.pid.wait() == 0, "git cat-file exited with failure"); 509 } 510 } 511 alias ObjectReader = RefCounted!ObjectReaderImpl; /// ditto 512 513 /// ditto 514 ObjectReader createObjectReader() 515 { 516 auto pipes = this.pipe(`cat-file`, `--batch`); 517 return ObjectReader(pipes); 518 } 519 520 /// Run a batch cat-file query. 521 Object[] getObjects(OID[] hashes) 522 { 523 Object[] result; 524 result.reserve(hashes.length); 525 auto reader = createObjectReader(); 526 527 foreach (hash; hashes) 528 result ~= reader.read(hash); 529 530 return result; 531 } 532 533 /// Spawn a hash-object process which can hash and write git objects on the fly. 534 struct ObjectWriterImpl 535 { 536 private bool initialized; 537 private ProcessPipes pipes; 538 539 /*private*/ this(ProcessPipes pipes) 540 { 541 this.pipes = pipes; 542 initialized = true; 543 } 544 545 /// Write a raw Git object of this writer's type, and return the OID. 546 OID write(in ubyte[] data) 547 { 548 import std.random : uniform; 549 auto p = NamedPipe("ae-sys-git-writeObjects-%d".format(uniform!ulong)); 550 pipes.stdin.writeln(p.fileName); 551 pipes.stdin.flush(); 552 553 auto f = p.connect(); 554 f.rawWrite(data); 555 f.flush(); 556 f.close(); 557 558 return OID(pipes.stdout.safeReadln().strip()); 559 } 560 561 deprecated OID write(const(void)[] data) { return write(cast(const(ubyte)[]) data); } 562 563 ~this() 564 { 565 if (initialized) 566 { 567 pipes.stdin.close(); 568 enforce(pipes.pid.wait() == 0, "git hash-object exited with failure"); 569 initialized = false; 570 } 571 } 572 } 573 alias ObjectWriter = RefCounted!ObjectWriterImpl; /// ditto 574 575 ObjectWriter createObjectWriter(string type) 576 { 577 auto pipes = this.pipe(`hash-object`, `-t`, type, `-w`, `--stdin-paths`); 578 return ObjectWriter(pipes); 579 } /// ditto 580 581 struct ObjectMultiWriterImpl 582 { 583 private Git repo; 584 585 /// The ObjectWriter instances for each individual type. 586 ObjectWriter treeWriter, blobWriter, commitWriter; 587 588 /// Write a Git object, and return the OID. 589 OID write(in Object obj) 590 { 591 ObjectWriter* pwriter; 592 switch (obj.type) // https://issues.dlang.org/show_bug.cgi?id=14595 593 { 594 case "tree" : pwriter = &treeWriter ; break; 595 case "blob" : pwriter = &blobWriter ; break; 596 case "commit": pwriter = &commitWriter; break; 597 default: throw new Exception("Unknown object type: " ~ obj.type); 598 } 599 if (!pwriter.initialized) 600 *pwriter = ObjectWriter(repo.pipe(`hash-object`, `-t`, obj.type, `-w`, `--stdin-paths`)); 601 return pwriter.write(obj.data); 602 } 603 604 /// Format and write a Git object, and return the OID. 605 CommitID write(in Object.ParsedCommit commit) { return CommitID(write(Object.createCommit(commit))); } 606 TreeID write(in Object.TreeEntry[] entries) { return TreeID (write(Object.createTree(entries))); } /// ditto 607 BlobID write(immutable(ubyte)[] bytes ) { return BlobID (write(Object.createBlob(bytes))); } /// ditto 608 } /// ditto 609 alias ObjectMultiWriter = RefCounted!ObjectMultiWriterImpl; /// ditto 610 611 /// ditto 612 ObjectMultiWriter createObjectWriter() 613 { 614 return ObjectMultiWriter(this); 615 } 616 617 /// Batch-write the given objects to the database. 618 /// The hashes are saved to the "hash" fields of the passed objects. 619 void writeObjects(Git.Object[] objects) 620 { 621 string[] allTypes = objects.map!(obj => obj.type).toSet().keys; 622 foreach (type; allTypes) 623 { 624 auto writer = createObjectWriter(type); 625 foreach (ref obj; objects) 626 if (obj.type == type) 627 obj.oid = writer.write(obj.data); 628 } 629 } 630 631 /// Extract a commit's tree to a given directory 632 void exportCommit(string commit, string path, ObjectReader reader, bool delegate(string) pathFilter = null) 633 { 634 exportTree(reader.read(commit).parseCommit().tree, path, reader, pathFilter); 635 } 636 637 /// Extract a tree to a given directory 638 void exportTree(TreeID treeHash, string path, ObjectReader reader, bool delegate(string) pathFilter = null) 639 { 640 void exportSubTree(OID treeHash, string[] subPath) 641 { 642 auto tree = reader.read(treeHash).parseTree(); 643 foreach (entry; tree) 644 { 645 auto entrySubPath = subPath ~ entry.name; 646 if (pathFilter && !pathFilter(entrySubPath.join("/"))) 647 continue; 648 auto entryPath = buildPath([path] ~ entrySubPath); 649 switch (entry.mode) 650 { 651 case octal!100644: // file 652 case octal!100755: // executable file 653 std.file.write(entryPath, reader.read(entry.hash).data); 654 version (Posix) 655 { 656 // Make executable 657 if (entry.mode == octal!100755) 658 entryPath.setAttributes(entryPath.getAttributes | ((entryPath.getAttributes & octal!444) >> 2)); 659 } 660 break; 661 case octal! 40000: // tree 662 mkdirRecurse(entryPath); 663 exportSubTree(entry.hash, entrySubPath); 664 break; 665 case octal!160000: // submodule 666 mkdirRecurse(entryPath); 667 break; 668 default: 669 throw new Exception("Unknown git file mode: %o".format(entry.mode)); 670 } 671 } 672 } 673 exportSubTree(treeHash, null); 674 } 675 676 /// Import a directory tree into the object store, and return the new tree object's hash. 677 TreeID importTree(string path, ObjectMultiWriter writer, bool delegate(string) pathFilter = null) 678 { 679 static // Error: variable ae.sys.git.Repository.importTree.writer has scoped destruction, cannot build closure 680 TreeID importSubTree(string path, string subPath, ref ObjectMultiWriter writer, bool delegate(string) pathFilter) 681 { 682 auto entries = subPath 683 .dirEntries(SpanMode.shallow) 684 .filter!(de => !pathFilter || pathFilter(de.relativePath(path))) 685 .map!(de => 686 de.isDir 687 ? Object.TreeEntry( 688 octal!40000, 689 de.baseName, 690 importSubTree(path, buildPath(subPath, de.baseName), writer, pathFilter) 691 ) 692 : Object.TreeEntry( 693 isVersion!`Posix` && (de.attributes & octal!111) ? octal!100755 : octal!100644, 694 de.baseName, 695 writer.write(Git.Object(OID.init, "blob", cast(immutable(ubyte)[])read(de.name))) 696 ) 697 ) 698 .array 699 .sort!((a, b) => a.sortName < b.sortName).release 700 ; 701 return TreeID(writer.write(Object.createTree(entries))); 702 } 703 return importSubTree(path, path, writer, pathFilter); 704 } 705 706 /// Spawn a update-ref process which can update git refs on the fly. 707 struct RefWriterImpl 708 { 709 private bool initialized; 710 private ProcessPipes pipes; 711 712 /*private*/ this(ProcessPipes pipes) 713 { 714 this.pipes = pipes; 715 initialized = true; 716 } 717 718 private void op(string op) 719 { 720 pipes.stdin.write(op, '\0'); 721 pipes.stdin.flush(); 722 } 723 724 private void op(string op, bool noDeref, string refName, CommitID*[] hashes...) 725 { 726 if (noDeref) 727 pipes.stdin.write("option no-deref\0"); 728 pipes.stdin.write(op, " ", refName, '\0'); 729 foreach (hash; hashes) 730 { 731 if (hash) 732 pipes.stdin.write((*hash).toString()); 733 pipes.stdin.write('\0'); 734 } 735 pipes.stdin.flush(); 736 } 737 738 /// Send update-ref operations (as specified in its man page). 739 void update (string refName, CommitID newValue , bool noDeref = false) { op("update", noDeref, refName, &newValue, null ); } 740 void update (string refName, CommitID newValue, CommitID oldValue, bool noDeref = false) { op("update", noDeref, refName, &newValue, &oldValue); } /// ditto 741 void create (string refName, CommitID newValue , bool noDeref = false) { op("create", noDeref, refName, &newValue ); } /// ditto 742 void deleteRef(string refName , bool noDeref = false) { op("delete", noDeref, refName, null ); } /// ditto 743 void deleteRef(string refName, CommitID oldValue, bool noDeref = false) { op("delete", noDeref, refName, &oldValue); } /// ditto 744 void verify (string refName , bool noDeref = false) { op("verify", noDeref, refName, null ); } /// ditto 745 void verify (string refName, CommitID oldValue, bool noDeref = false) { op("verify", noDeref, refName, &oldValue); } /// ditto 746 void start ( ) { op("start" ); } /// ditto 747 void prepare ( ) { op("prepare" ); } /// ditto 748 void commit ( ) { op("commit" ); } /// ditto 749 void abort ( ) { op("abort" ); } /// ditto 750 751 deprecated void update (string refName, OID newValue , bool noDeref = false) { op("update", noDeref, refName, cast(CommitID*)&newValue, null ); } 752 deprecated void update (string refName, OID newValue, OID oldValue, bool noDeref = false) { op("update", noDeref, refName, cast(CommitID*)&newValue, cast(CommitID*)&oldValue); } 753 deprecated void create (string refName, OID newValue , bool noDeref = false) { op("create", noDeref, refName, cast(CommitID*)&newValue ); } 754 deprecated void deleteRef(string refName, OID oldValue, bool noDeref = false) { op("delete", noDeref, refName, cast(CommitID*)&oldValue); } 755 deprecated void verify (string refName, OID oldValue, bool noDeref = false) { op("verify", noDeref, refName, cast(CommitID*)&oldValue); } 756 757 ~this() 758 { 759 if (initialized) 760 { 761 pipes.stdin.close(); 762 enforce(pipes.pid.wait() == 0, "git update-ref exited with failure"); 763 initialized = false; 764 } 765 } 766 } 767 alias RefWriter = RefCounted!RefWriterImpl; /// ditto 768 769 /// ditto 770 RefWriter createRefWriter() 771 { 772 auto pipes = this.pipe(`update-ref`, `-z`, `--stdin`); 773 return RefWriter(pipes); 774 } 775 776 /// Tries to match the default destination of `git clone`. 777 static string repositoryNameFromURL(string url) 778 { 779 return url 780 .split(":")[$-1] 781 .split("/")[$-1] 782 .chomp(".git"); 783 } 784 785 unittest 786 { 787 assert(repositoryNameFromURL("https://github.com/CyberShadow/ae.git") == "ae"); 788 assert(repositoryNameFromURL("git@example.com:ae.git") == "ae"); 789 } 790 } 791 792 deprecated alias Repository = Git; 793 deprecated alias History = Git.History; 794 deprecated alias Commit = Git.History.Commit; 795 deprecated alias GitObject = Git.Object; 796 deprecated alias Hash = Git.OID; 797 deprecated Git.Authorship parseAuthorship(string authorship) { return Git.Authorship(authorship); } 798 799 deprecated Git.CommitID toCommitHash(in char[] hash) { return Git.CommitID(Git.OID(hash)); } 800 801 deprecated unittest 802 { 803 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]); 804 } 805 806 deprecated string toString(ref const Git.OID oid) { return oid.toString(); } 807 808 deprecated alias repositoryNameFromURL = Git.repositoryNameFromURL;