1 /++ High-level control of the Git version control system.
2 	See: https://github.com/s-ludwig/dlibgit
3  +/
4 module nxt.git;
5 
6 @safe:
7 
8 /// Depth of git clone.
9 struct Depth {
10 	uint value;
11 	alias value this;
12 	static Depth min() pure nothrow @safe @nogc => Depth(1);
13 	static Depth max() pure nothrow @safe @nogc => Depth(value.max);
14 }
15 
16 /++ SHA-1 Digest +/
17 struct SHA1Digest {
18 	ubyte[20] bytes;
19 	alias bytes this;
20 }
21 
22 /++ (Git) Repository URL.
23  +/
24 struct RepoURL {
25 	import nxt.path : URL;
26 	this(string value) pure nothrow @nogc {
27 		this._value = URL(value); // don't enforce ".git" extension in `value` because it's optional
28 	}
29 	URL _value; /+ TODO: replace _value with url +/
30 	alias _value this;
31 }
32 
33 /++ (Git) Repository URL with optional sub-directory relative path.
34  +/
35 struct RepoURLDir {
36 	import nxt.path : DirPath;
37 	this(RepoURL url, DirPath subDir = DirPath.init) pure nothrow @nogc {
38 		this.url = url;
39 		this.subDir = subDir;
40 	}
41 	RepoURL url;
42 	DirPath subDir; ///< Optional sub-directory in `url`. Can be both in absolute and relative.
43 }
44 
45 struct RepositoryAndDir
46 {
47 	import std.exception : enforce;
48 	import nxt.path : DirPath, buildNormalizedPath, exists;
49 	import nxt.cmd : Spawn, spawn, writeFlushed;
50 	import nxt.logging : LogLevel, trace, info, warning, error;
51 	import std.file : exists;
52 
53 	const RepoURL url;
54 	const DirPath lrd;			///< Local checkout root directory.
55 	LogLevel logLevel = LogLevel.warning;
56 
57 	this(const RepoURL url, const DirPath lrd = [], in bool echoOutErr = true)
58 	{
59 		this.url = url;
60 		if (lrd != lrd.init)
61 			this.lrd = lrd;
62 		else
63 		{
64 			import std.path : baseName, stripExtension;
65 			this.lrd = DirPath(this.url._value.str.baseName.stripExtension);
66 		}
67 	}
68 
69 	this(this) @disable;
70 
71 	// Actions:
72 
73 	auto ref setLogLevel(in LogLevel logLevel) scope pure nothrow @nogc {
74 		this.logLevel = logLevel;
75 		return this;
76 	}
77 
78 	Spawn cloneOrPull(in bool recursive = true, string branch = [], in Depth depth = Depth.max)
79 	{
80 		if (lrd.buildNormalizedPath(DirPath(".git")).exists)
81 			return pull(recursive);
82 		else
83 			return clone(recursive, branch, depth);
84 	}
85 
86 	Spawn cloneOrResetHard(in bool recursive = true, string remote = "origin", string branch = [], in Depth depth = Depth.max)
87 	{
88 		if (lrd.buildNormalizedPath(DirPath(".git")).exists)
89 			return resetHard(recursive, remote, branch);
90 		else
91 			return clone(recursive, branch, depth);
92 	}
93 
94 	Spawn clone(in bool recursive = true, string branch = [], in Depth depth = Depth.max) const
95 	{
96 		import std.conv : to;
97 		if (logLevel <= LogLevel.trace) trace("Cloning ", url, " to ", lrd);
98 		return spawn(["git", "clone"]
99 					 ~ [url._value.str, lrd.str]
100 					 ~ (branch.length ? ["-b", branch] : [])
101 					 ~ (recursive ? ["--recurse-submodules"] : [])
102 					 ~ (depth != depth.max ? ["--depth", depth.to!string] : []),
103 					 logLevel);
104 	}
105 
106 	Spawn pull(in bool recursive = true)
107 	{
108 		if (logLevel <= LogLevel.trace) trace("Pulling ", url, " to ", lrd);
109 		return spawn(["git", "-C", lrd.str, "pull"]
110 					 ~ (recursive ? ["--recurse-submodules"] : []),
111 					 logLevel);
112 	}
113 
114 	Spawn resetHard(in bool recursive = true, string remote = "origin", string branch = [])
115 	{
116 		if (logLevel <= LogLevel.trace) trace("Resetting hard ", url, " to ", lrd);
117 		if (branch)
118 			return resetHardTo(remote~"/"~branch, recursive);
119 		else
120 			return resetHard(recursive);
121 	}
122 
123 	Spawn checkout(string branchName)
124 	{
125 		if (logLevel <= LogLevel.trace) trace("Checking out branch ", branchName, " at ", lrd);
126 		return spawn(["git", "-C", lrd.str, "checkout" , branchName], logLevel);
127 	}
128 
129 	Spawn remoteRemove(string name)
130 	{
131 		if (logLevel <= LogLevel.trace) trace("Removing remote ", name, " at ", lrd);
132 		return spawn(["git", "-C", lrd.str, "remote", "remove", name], logLevel);
133 	}
134 	alias removeRemote = remoteRemove;
135 
136 	Spawn remoteAdd(in RepoURL url, string name)
137 	{
138 		if (logLevel <= LogLevel.trace) trace("Adding remote ", url, " at ", lrd, (name ? " as " ~ name : "") ~ " ...");
139 		return spawn(["git", "-C", lrd.str, "remote", "add", name, url._value.str], logLevel);
140 	}
141 	alias addRemote = remoteAdd;
142 
143 	Spawn fetch(string[] names)
144 	{
145 		if (logLevel <= LogLevel.trace) trace("Fetching remotes ", names, " at ", lrd);
146 		if (names)
147 			return spawn(["git", "-C", lrd.str, "fetch", "--multiple"] ~ names, logLevel);
148 		else
149 			return spawn(["git", "-C", lrd.str, "fetch"], logLevel);
150 	}
151 
152 	Spawn fetchAll()
153 	{
154 		if (logLevel <= LogLevel.trace) trace("Fetching all remotes at ", lrd);
155 		return spawn(["git", "-C", lrd.str, "fetch", "--all"], logLevel);
156 	}
157 
158 	Spawn clean()
159 	{
160 		if (logLevel <= LogLevel.trace) trace("Cleaning ", lrd);
161 		return spawn(["git", "-C", lrd.str, "clean", "-ffdx"], logLevel);
162 	}
163 
164 	Spawn resetHard(in bool recursive = true)
165 	{
166 		if (logLevel <= LogLevel.trace) trace("Resetting hard ", lrd);
167 		return spawn(["git", "-C", lrd.str, "reset", "--hard"]
168 					 ~ (recursive ? ["--recurse-submodules"] : []), logLevel);
169 	}
170 
171 	Spawn resetHardTo(string treeish, in bool recursive = true)
172 	{
173 		if (logLevel <= LogLevel.trace) trace("Resetting hard ", lrd);
174 		return spawn(["git", "-C", lrd.str, "reset", "--hard", treeish]
175 					 ~ (recursive ? ["--recurse-submodules"] : []), logLevel);
176 	}
177 
178 	Spawn merge(string[] commits, string[] args = [])
179 	{
180 		if (logLevel <= LogLevel.trace) trace("Merging commits ", commits, " with flags ", args, " at ", lrd);
181 		return spawn(["git", "-C", lrd.str, "merge"] ~ args ~ commits, logLevel);
182 	}
183 
184 	Spawn revParse(string treeish, string[] args = []) const
185 	{
186 		if (logLevel <= LogLevel.trace) trace("Getting rev-parse of ", treeish, " at ", lrd);
187 		return spawn(["git", "rev-parse", treeish] ~ args, logLevel);
188 	}
189 
190 	// Getters:
191 
192 @property:
193 
194 	SHA1Digest commitSHAOf(string treeish) const
195 	{
196 		Spawn spawn = revParse(treeish);
197 		enforce(spawn.wait() == 0);
198 		return typeof(return).init; /+ TODO: use `spawn.output` +/
199 	}
200 }
201 
202 /++ State of repository.
203  +/
204 struct RepoState {
205 	string commitShaHex;		// SHA of commit in hexadecimal form.
206 	string branchOrTag;			// Optional.
207 }