1 /**
2  * Code to manage a customized D checkout.
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 <vladimir@thecybershadow.net>
12  */
13 
14 module ae.sys.d.customizer;
15 
16 import std.algorithm;
17 import std.exception;
18 import std.file;
19 import std.path;
20 import std.process : environment, escapeShellFileName;
21 import std.regex;
22 import std.string;
23 
24 import ae.sys.d.manager;
25 import ae.utils.regex;
26 
27 /// Class which manages a customized D checkout and its dependencies.
28 class DCustomizer
29 {
30 	DManager d;
31 
32 	this(DManager manager) { this.d = manager; }
33 
34 	/// Initialize the repository and prerequisites.
35 	void initialize(bool update = true)
36 	{
37 		d.initialize(update);
38 
39 		log("Preparing component repositories...");
40 		foreach (component; d.listComponents().parallel)
41 		{
42 			auto crepo = d.componentRepo(component);
43 
44 			if (update)
45 			{
46 				log(component ~ ": Fetching pull requests...");
47 				crepo.run("fetch", "origin", "+refs/pull/*/head:refs/remotes/origin/pr/*");
48 			}
49 		}
50 	}
51 
52 	/// Begin customization, starting at the specified revision
53 	/// (master by default).
54 	void begin(string rev = null)
55 	{
56 		d.reset();
57 		d.checkout(rev);
58 
59 		foreach (component; d.listComponents())
60 		{
61 			auto crepo = d.componentRepo(component);
62 
63 			log(component ~ ": Creating work branch...");
64 			crepo.run("checkout", "-B", "custom");
65 		}
66 	}
67 
68 	private enum mergeMessagePrefix = "ae-custom-merge-";
69 	private enum pullMessageTemplate = mergeMessagePrefix ~ "pr-%s";
70 	private enum remoteMessageTemplate = mergeMessagePrefix ~ "remote-%s-%s";
71 
72 	void setupGitEnv()
73 	{
74 		string[string] mergeEnv;
75 		foreach (person; ["AUTHOR", "COMMITTER"])
76 		{
77 			mergeEnv["GIT_%s_DATE".format(person)] = "Thu, 01 Jan 1970 00:00:00 +0000";
78 			mergeEnv["GIT_%s_NAME".format(person)] = "ae.sys.d.customizer";
79 			mergeEnv["GIT_%s_EMAIL".format(person)] = "ae.sys.d.customizer@thecybershadow.net";
80 		}
81 		foreach (k, v; mergeEnv)
82 			environment[k] = v;
83 		// TODO: restore environment
84 	}
85 
86 	void mergeRef(string component, string refName, string mergeCommitMessage)
87 	{
88 		auto crepo = d.componentRepo(component);
89 
90 		scope(failure)
91 		{
92 			log("Aborting merge...");
93 			crepo.run("merge", "--abort");
94 		}
95 
96 		void doMerge()
97 		{
98 			setupGitEnv();
99 			crepo.run("merge", "--no-ff", "-m", mergeCommitMessage, refName);
100 		}
101 
102 		if (component == "dmd")
103 		{
104 			try
105 				doMerge();
106 			catch (Exception)
107 			{
108 				log("Merge failed. Attempting conflict resolution...");
109 				crepo.run("checkout", "--theirs", "test");
110 				crepo.run("add", "test");
111 				crepo.run("-c", "rerere.enabled=false", "commit", "-m", mergeCommitMessage);
112 			}
113 		}
114 		else
115 			doMerge();
116 
117 		log("Merge successful.");
118 	}
119 
120 	void unmergeRef(string component, string mergeCommitMessage)
121 	{
122 		auto crepo = d.componentRepo(component);
123 
124 		// "sed -i \"s#.*" ~ mergeCommitMessage.escapeRE() ~ ".*##g\"";
125 		setupGitEnv();
126 		environment["GIT_EDITOR"] = "%s %s %s"
127 			.format(getCallbackCommand(), unmergeRebaseEditAction, mergeCommitMessage);
128 		scope(exit) environment.remove("GIT_EDITOR");
129 
130 		crepo.run("rebase", "--interactive", "--preserve-merges", "origin/master");
131 
132 		log("Unmerge successful.");
133 	}
134 
135 	/// Merge in the specified pull request.
136 	void mergePull(string component, string pull)
137 	{
138 		enforce(component.match(re!`^[a-z]+$`), "Bad component");
139 		enforce(pull.match(re!`^\d+$`), "Bad pull number");
140 
141 		log("Merging %s pull request %s...".format(component, pull));
142 
143 		mergeRef(component, "origin/pr/" ~ pull, pullMessageTemplate.format(pull));
144 	}
145 
146 	/// Unmerge the specified pull request.
147 	/// Requires additional set-up - see callback below.
148 	void unmergePull(string component, string pull)
149 	{
150 		enforce(component.match(re!`^[a-z]+$`), "Bad component");
151 		enforce(pull.match(re!`^\d+$`), "Bad pull number");
152 
153 		log("Rebasing to unmerge %s pull request %s...".format(component, pull));
154 
155 		unmergeRef(component, pullMessageTemplate.format(pull));
156 	}
157 
158 	/// Merge in a branch from the given remote.
159 	void mergeRemoteBranch(string component, string remoteName, string repoUrl, string branch)
160 	{
161 		enforce(component.match(re!`^[a-z]+$`), "Bad component");
162 		enforce(remoteName.match(re!`^\w[\w\-]*$`), "Bad remote name");
163 		enforce(repoUrl.match(re!`^\w[\w\-]*:[\w/\-\.]+$`), "Bad remote URL");
164 		enforce(branch.match(re!`^\w[\w\-\.]*$`), "Bad branch name");
165 
166 		auto crepo = d.componentRepo(component);
167 
168 		void rm()
169 		{
170 			try
171 				crepo.run("remote", "rm", remoteName);
172 			catch (Exception e) {}
173 		}
174 		rm();
175 		scope(exit) rm();
176 		crepo.run("remote", "add", "-f", remoteName, repoUrl);
177 
178 		mergeRef(component,
179 			"%s/%s".format(remoteName, branch),
180 			remoteMessageTemplate.format(remoteName, branch));
181 	}
182 
183 	/// Undo a mergeRemoteBranch call.
184 	void unmergeRemoteBranch(string component, string remoteName, string branch)
185 	{
186 		enforce(component.match(re!`^[a-z]+$`), "Bad component");
187 		enforce(remoteName.match(re!`^\w[\w\-]*$`), "Bad remote name");
188 		enforce(branch.match(re!`^\w[\w\-\.]*$`), "Bad branch name");
189 
190 		unmergeRef(component, remoteMessageTemplate.format(remoteName, branch));
191 	}
192 
193 	void mergeFork(string user, string repo, string branch)
194 	{
195 		mergeRemoteBranch(repo, user, "https://github.com/%s/%s".format(user, repo), branch);
196 	}
197 
198 	void unmergeFork(string user, string repo, string branch)
199 	{
200 		unmergeRemoteBranch(repo, user, branch);
201 	}
202 
203 	/// Override this method with one which returns a command,
204 	/// which will invoke the unmergeRebaseEdit function below,
205 	/// passing to it any additional parameters.
206 	abstract string getCallbackCommand();
207 
208 	private enum unmergeRebaseEditAction = "unmerge-rebase-edit";
209 
210 	/// This function must be invoked when the command line
211 	/// returned by getUnmergeEditorCommand() is ran.
212 	void callback(string[] args)
213 	{
214 		enforce(args.length, "No callback parameters");
215 		switch (args[0])
216 		{
217 			case unmergeRebaseEditAction:
218 				enforce(args.length == 3, "Invalid argument count");
219 				unmergeRebaseEdit(args[1], args[2]);
220 				break;
221 			default:
222 				throw new Exception("Unknown callback");
223 		}
224 	}
225 
226 	private void unmergeRebaseEdit(string mergeCommitMessage, string fileName)
227 	{
228 		auto lines = fileName.readText().splitLines();
229 
230 		bool removing, remaining;
231 		foreach_reverse (ref line; lines)
232 			if (line.startsWith("pick "))
233 			{
234 				if (line.match(re!(`^pick [0-9a-f]+ ` ~ escapeRE(mergeMessagePrefix))))
235 					removing = line.canFind(mergeCommitMessage);
236 				if (removing)
237 					line = "# " ~ line;
238 				else
239 					remaining = true;
240 			}
241 		if (!remaining)
242 			lines = ["noop"];
243 
244 		std.file.write(fileName, lines.join("\n"));
245 	}
246 
247 	void log(string s)
248 	{
249 		d.log(s);
250 	}
251 }