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 }