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;