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