OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2013 the V8 project authors. All rights reserved. | 2 # Copyright 2013 the V8 project authors. All rights reserved. |
3 # Redistribution and use in source and binary forms, with or without | 3 # Redistribution and use in source and binary forms, with or without |
4 # modification, are permitted provided that the following conditions are | 4 # modification, are permitted provided that the following conditions are |
5 # met: | 5 # met: |
6 # | 6 # |
7 # * Redistributions of source code must retain the above copyright | 7 # * Redistributions of source code must retain the above copyright |
8 # notice, this list of conditions and the following disclaimer. | 8 # notice, this list of conditions and the following disclaimer. |
9 # * Redistributions in binary form must reproduce the above | 9 # * Redistributions in binary form must reproduce the above |
10 # copyright notice, this list of conditions and the following | 10 # copyright notice, this list of conditions and the following |
(...skipping 12 matching lines...) Expand all Loading... |
23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | 25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
28 | 28 |
29 import os | 29 import os |
30 import re | 30 import re |
31 import subprocess | 31 import subprocess |
32 import sys | 32 import sys |
| 33 import textwrap |
33 | 34 |
34 PERSISTFILE_BASENAME = "PERSISTFILE_BASENAME" | 35 PERSISTFILE_BASENAME = "PERSISTFILE_BASENAME" |
35 TEMP_BRANCH = "TEMP_BRANCH" | 36 TEMP_BRANCH = "TEMP_BRANCH" |
36 BRANCHNAME = "BRANCHNAME" | 37 BRANCHNAME = "BRANCHNAME" |
37 DOT_GIT_LOCATION = "DOT_GIT_LOCATION" | 38 DOT_GIT_LOCATION = "DOT_GIT_LOCATION" |
38 VERSION_FILE = "VERSION_FILE" | 39 VERSION_FILE = "VERSION_FILE" |
39 CHANGELOG_FILE = "CHANGELOG_FILE" | 40 CHANGELOG_FILE = "CHANGELOG_FILE" |
40 CHANGELOG_ENTRY_FILE = "CHANGELOG_ENTRY_FILE" | 41 CHANGELOG_ENTRY_FILE = "CHANGELOG_ENTRY_FILE" |
41 COMMITMSG_FILE = "COMMITMSG_FILE" | 42 COMMITMSG_FILE = "COMMITMSG_FILE" |
42 PATCH_FILE = "PATCH_FILE" | 43 PATCH_FILE = "PATCH_FILE" |
(...skipping 17 matching lines...) Expand all Loading... |
60 | 61 |
61 def FileToText(file_name): | 62 def FileToText(file_name): |
62 with open(file_name) as f: | 63 with open(file_name) as f: |
63 return f.read() | 64 return f.read() |
64 | 65 |
65 | 66 |
66 def MSub(rexp, replacement, text): | 67 def MSub(rexp, replacement, text): |
67 return re.sub(rexp, replacement, text, flags=re.MULTILINE) | 68 return re.sub(rexp, replacement, text, flags=re.MULTILINE) |
68 | 69 |
69 | 70 |
| 71 def Fill80(line): |
| 72 return textwrap.fill(line, width=80, initial_indent=" ", |
| 73 subsequent_indent=" ") |
| 74 |
| 75 |
70 def GetLastChangeLogEntries(change_log_file): | 76 def GetLastChangeLogEntries(change_log_file): |
71 result = [] | 77 result = [] |
72 for line in LinesInFile(change_log_file): | 78 for line in LinesInFile(change_log_file): |
73 if re.search(r"^\d{4}-\d{2}-\d{2}:", line) and result: break | 79 if re.search(r"^\d{4}-\d{2}-\d{2}:", line) and result: break |
74 result.append(line) | 80 result.append(line) |
75 return "".join(result) | 81 return "".join(result) |
76 | 82 |
77 | 83 |
78 def MakeChangeLogBody(commit_generator): | 84 def MakeChangeLogBody(commit_generator): |
79 result = "" | 85 result = "" |
80 for (title, body, author) in commit_generator(): | 86 for (title, body, author) in commit_generator(): |
81 # Add the commit's title line. | 87 # Add the commit's title line. |
82 result += "%s\n" % title.rstrip() | 88 result += "%s\n" % title.rstrip() |
83 | 89 |
84 # Grep for "BUG=xxxx" lines in the commit message and convert them to | 90 # Add bug references. |
85 # "(issue xxxx)". | 91 result += MakeChangeLogBugReference(body) |
86 out = body.splitlines() | |
87 out = filter(lambda x: re.search(r"^BUG=", x), out) | |
88 out = filter(lambda x: not re.search(r"BUG=$", x), out) | |
89 out = filter(lambda x: not re.search(r"BUG=none$", x), out) | |
90 | |
91 # TODO(machenbach): Handle multiple entries (e.g. BUG=123, 234). | |
92 def FormatIssue(text): | |
93 text = re.sub(r"BUG=v8:(.*)$", r"(issue \1)", text) | |
94 text = re.sub(r"BUG=chromium:(.*)$", r"(Chromium issue \1)", text) | |
95 text = re.sub(r"BUG=(.*)$", r"(Chromium issue \1)", text) | |
96 return " %s\n" % text | |
97 | |
98 for line in map(FormatIssue, out): | |
99 result += line | |
100 | 92 |
101 # Append the commit's author for reference. | 93 # Append the commit's author for reference. |
102 result += "%s\n\n" % author.rstrip() | 94 result += "%s\n\n" % author.rstrip() |
103 return result | 95 return result |
104 | 96 |
105 | 97 |
| 98 def MakeChangeLogBugReference(body): |
| 99 # Grep for "BUG=xxxx" lines in the commit message and convert them to |
| 100 # "(issue xxxx)". |
| 101 out = body.splitlines() |
| 102 out = filter(lambda x: re.search(r"^[ \t]*BUG[ \t]*=", x), out) |
| 103 out = filter(lambda x: not re.search(r"BUG[ \t]*=[ \t]*$", x), out) |
| 104 out = filter(lambda x: not re.search(r"BUG[ \t]*=[ \t]*none[ \t]*$", x), out) |
| 105 |
| 106 crbugs = [] |
| 107 v8bugs = [] |
| 108 |
| 109 def AddSafe(bugs, bug): |
| 110 try: |
| 111 bugs.append(int(bug)) |
| 112 except ValueError: |
| 113 pass |
| 114 |
| 115 def AddIssues(text): |
| 116 ref = re.match(r"^[ \t]*BUG[ \t]*=[ \t]*(.*?)[ \t]*$", text) |
| 117 if not ref: |
| 118 return |
| 119 for bug in ref.group(1).split(","): |
| 120 bug = bug.strip() |
| 121 match = re.match(r"^v8[ \t]*:[ \t]*(.*)$", bug) |
| 122 if match: AddSafe(v8bugs, match.group(1)) |
| 123 else: |
| 124 match = re.match(r"^(?:chromium[ \t]*:)?[ \t]*(.*)$", bug) |
| 125 if match: AddSafe(crbugs, match.group(1)) |
| 126 |
| 127 # Add issues to crbugs and v8bugs. |
| 128 map(AddIssues, out) |
| 129 |
| 130 # Filter duplicates, sort, stringify. |
| 131 crbugs = map(str, sorted(set(crbugs))) |
| 132 v8bugs = map(str, sorted(set(v8bugs))) |
| 133 |
| 134 bug_groups = [] |
| 135 def FormatIssues(prefix, bugs): |
| 136 if len(bugs) > 0: |
| 137 plural = "s" if len(bugs) > 1 else "" |
| 138 bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs))) |
| 139 |
| 140 FormatIssues("Chromium ", crbugs) |
| 141 FormatIssues("", v8bugs) |
| 142 |
| 143 if len(bug_groups) > 0: |
| 144 # Format with 8 characters indentation and max 80 character lines. |
| 145 return "%s\n" % Fill80("(%s)" % ", ".join(bug_groups)) |
| 146 else: |
| 147 return "" |
| 148 |
| 149 |
106 # Some commands don't like the pipe, e.g. calling vi from within the script or | 150 # Some commands don't like the pipe, e.g. calling vi from within the script or |
107 # from subscripts like git cl upload. | 151 # from subscripts like git cl upload. |
108 def Command(cmd, args="", prefix="", pipe=True): | 152 def Command(cmd, args="", prefix="", pipe=True): |
109 cmd_line = "%s %s %s" % (prefix, cmd, args) | 153 cmd_line = "%s %s %s" % (prefix, cmd, args) |
110 print "Command: %s" % cmd_line | 154 print "Command: %s" % cmd_line |
111 try: | 155 try: |
112 if pipe: | 156 if pipe: |
113 return subprocess.check_output(cmd_line, shell=True) | 157 return subprocess.check_output(cmd_line, shell=True) |
114 else: | 158 else: |
115 return subprocess.check_call(cmd_line, shell=True) | 159 return subprocess.check_call(cmd_line, shell=True) |
116 except subprocess.CalledProcessError: | 160 except subprocess.CalledProcessError: |
117 return None | 161 return None |
118 | 162 |
119 | 163 |
120 # Wrapper for side effects. | 164 # Wrapper for side effects. |
121 class SideEffectHandler(object): | 165 class SideEffectHandler(object): |
122 def Command(self, cmd, args="", prefix="", pipe=True): | 166 def Command(self, cmd, args="", prefix="", pipe=True): |
123 return Command(cmd, args, prefix, pipe) | 167 return Command(cmd, args, prefix, pipe) |
124 | 168 |
125 def ReadLine(self): | 169 def ReadLine(self): |
126 return sys.stdin.readline().strip() | 170 return sys.stdin.readline().strip() |
127 | 171 |
128 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler() | 172 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler() |
129 | 173 |
130 | 174 |
131 class Step(object): | 175 class Step(object): |
132 def __init__(self, text="", requires=None): | 176 def __init__(self, text="", requires=None): |
133 self._text = text | 177 self._text = text |
134 self._number = -1 | 178 self._number = -1 |
| 179 self._options = None |
135 self._requires = requires | 180 self._requires = requires |
136 self._side_effect_handler = DEFAULT_SIDE_EFFECT_HANDLER | 181 self._side_effect_handler = DEFAULT_SIDE_EFFECT_HANDLER |
137 | 182 |
138 def SetNumber(self, number): | 183 def SetNumber(self, number): |
139 self._number = number | 184 self._number = number |
140 | 185 |
141 def SetConfig(self, config): | 186 def SetConfig(self, config): |
142 self._config = config | 187 self._config = config |
143 | 188 |
144 def SetState(self, state): | 189 def SetState(self, state): |
(...skipping 16 matching lines...) Expand all Loading... |
161 if self._requires: | 206 if self._requires: |
162 self.RestoreIfUnset(self._requires) | 207 self.RestoreIfUnset(self._requires) |
163 if not self._state[self._requires]: | 208 if not self._state[self._requires]: |
164 return | 209 return |
165 print ">>> Step %d: %s" % (self._number, self._text) | 210 print ">>> Step %d: %s" % (self._number, self._text) |
166 self.RunStep() | 211 self.RunStep() |
167 | 212 |
168 def RunStep(self): | 213 def RunStep(self): |
169 raise NotImplementedError | 214 raise NotImplementedError |
170 | 215 |
171 def ReadLine(self): | 216 def ReadLine(self, default=None): |
172 return self._side_effect_handler.ReadLine() | 217 # Don't prompt in forced mode. |
| 218 if self._options and self._options.f and default is not None: |
| 219 print "%s (forced)" % default |
| 220 return default |
| 221 else: |
| 222 return self._side_effect_handler.ReadLine() |
173 | 223 |
174 def Git(self, args="", prefix="", pipe=True): | 224 def Git(self, args="", prefix="", pipe=True): |
175 return self._side_effect_handler.Command("git", args, prefix, pipe) | 225 return self._side_effect_handler.Command("git", args, prefix, pipe) |
176 | 226 |
177 def Editor(self, args): | 227 def Editor(self, args): |
178 return self._side_effect_handler.Command(os.environ["EDITOR"], args, | 228 return self._side_effect_handler.Command(os.environ["EDITOR"], args, |
179 pipe=False) | 229 pipe=False) |
180 | 230 |
181 def Die(self, msg=""): | 231 def Die(self, msg=""): |
182 if msg != "": | 232 if msg != "": |
183 print "Error: %s" % msg | 233 print "Error: %s" % msg |
184 print "Exiting" | 234 print "Exiting" |
185 raise Exception(msg) | 235 raise Exception(msg) |
186 | 236 |
| 237 def DieInForcedMode(self, msg=""): |
| 238 if self._options and self._options.f: |
| 239 msg = msg or "Not implemented in forced mode." |
| 240 self.Die(msg) |
| 241 |
187 def Confirm(self, msg): | 242 def Confirm(self, msg): |
188 print "%s [Y/n] " % msg, | 243 print "%s [Y/n] " % msg, |
189 answer = self.ReadLine() | 244 answer = self.ReadLine(default="Y") |
190 return answer == "" or answer == "Y" or answer == "y" | 245 return answer == "" or answer == "Y" or answer == "y" |
191 | 246 |
192 def DeleteBranch(self, name): | 247 def DeleteBranch(self, name): |
193 git_result = self.Git("branch").strip() | 248 git_result = self.Git("branch").strip() |
194 for line in git_result.splitlines(): | 249 for line in git_result.splitlines(): |
195 if re.match(r".*\s+%s$" % name, line): | 250 if re.match(r".*\s+%s$" % name, line): |
196 msg = "Branch %s exists, do you want to delete it?" % name | 251 msg = "Branch %s exists, do you want to delete it?" % name |
197 if self.Confirm(msg): | 252 if self.Confirm(msg): |
198 if self.Git("branch -D %s" % name) is None: | 253 if self.Git("branch -D %s" % name) is None: |
199 self.Die("Deleting branch '%s' failed." % name) | 254 self.Die("Deleting branch '%s' failed." % name) |
(...skipping 13 matching lines...) Expand all Loading... |
213 | 268 |
214 def RestoreIfUnset(self, var_name): | 269 def RestoreIfUnset(self, var_name): |
215 if self._state.get(var_name) is None: | 270 if self._state.get(var_name) is None: |
216 self._state[var_name] = self.Restore(var_name) | 271 self._state[var_name] = self.Restore(var_name) |
217 | 272 |
218 def InitialEnvironmentChecks(self): | 273 def InitialEnvironmentChecks(self): |
219 # Cancel if this is not a git checkout. | 274 # Cancel if this is not a git checkout. |
220 if not os.path.exists(self._config[DOT_GIT_LOCATION]): | 275 if not os.path.exists(self._config[DOT_GIT_LOCATION]): |
221 self.Die("This is not a git checkout, this script won't work for you.") | 276 self.Die("This is not a git checkout, this script won't work for you.") |
222 | 277 |
| 278 # TODO(machenbach): Don't use EDITOR in forced mode as soon as script is |
| 279 # well tested. |
223 # Cancel if EDITOR is unset or not executable. | 280 # Cancel if EDITOR is unset or not executable. |
224 if (not os.environ.get("EDITOR") or | 281 if (not os.environ.get("EDITOR") or |
225 Command("which", os.environ["EDITOR"]) is None): | 282 Command("which", os.environ["EDITOR"]) is None): |
226 self.Die("Please set your EDITOR environment variable, you'll need it.") | 283 self.Die("Please set your EDITOR environment variable, you'll need it.") |
227 | 284 |
228 def CommonPrepare(self): | 285 def CommonPrepare(self): |
229 # Check for a clean workdir. | 286 # Check for a clean workdir. |
230 if self.Git("status -s -uno").strip() != "": | 287 if self.Git("status -s -uno").strip() != "": |
231 self.Die("Workspace is not clean. Please commit or undo your changes.") | 288 self.Die("Workspace is not clean. Please commit or undo your changes.") |
232 | 289 |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
284 self.RestoreIfUnset("%s%s" % (prefix, v)) | 341 self.RestoreIfUnset("%s%s" % (prefix, v)) |
285 | 342 |
286 def WaitForLGTM(self): | 343 def WaitForLGTM(self): |
287 print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit " | 344 print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit " |
288 "your change. (If you need to iterate on the patch or double check " | 345 "your change. (If you need to iterate on the patch or double check " |
289 "that it's sane, do so in another shell, but remember to not " | 346 "that it's sane, do so in another shell, but remember to not " |
290 "change the headline of the uploaded CL.") | 347 "change the headline of the uploaded CL.") |
291 answer = "" | 348 answer = "" |
292 while answer != "LGTM": | 349 while answer != "LGTM": |
293 print "> ", | 350 print "> ", |
| 351 # TODO(machenbach): Add default="LGTM" to avoid prompt when script is |
| 352 # well tested and when prepare push cl has TBR flag. |
294 answer = self.ReadLine() | 353 answer = self.ReadLine() |
295 if answer != "LGTM": | 354 if answer != "LGTM": |
296 print "That was not 'LGTM'." | 355 print "That was not 'LGTM'." |
297 | 356 |
298 def WaitForResolvingConflicts(self, patch_file): | 357 def WaitForResolvingConflicts(self, patch_file): |
299 print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", " | 358 print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", " |
300 "or resolve the conflicts, stage *all* touched files with " | 359 "or resolve the conflicts, stage *all* touched files with " |
301 "'git add', and type \"RESOLVED<Return>\"") | 360 "'git add', and type \"RESOLVED<Return>\"") |
| 361 self.DieInForcedMode() |
302 answer = "" | 362 answer = "" |
303 while answer != "RESOLVED": | 363 while answer != "RESOLVED": |
304 if answer == "ABORT": | 364 if answer == "ABORT": |
305 self.Die("Applying the patch failed.") | 365 self.Die("Applying the patch failed.") |
306 if answer != "": | 366 if answer != "": |
307 print "That was not 'RESOLVED' or 'ABORT'." | 367 print "That was not 'RESOLVED' or 'ABORT'." |
308 print "> ", | 368 print "> ", |
309 answer = self.ReadLine() | 369 answer = self.ReadLine() |
310 | 370 |
311 # Takes a file containing the patch to apply as first argument. | 371 # Takes a file containing the patch to apply as first argument. |
312 def ApplyPatch(self, patch_file, reverse_patch=""): | 372 def ApplyPatch(self, patch_file, reverse_patch=""): |
313 args = "apply --index --reject %s \"%s\"" % (reverse_patch, patch_file) | 373 args = "apply --index --reject %s \"%s\"" % (reverse_patch, patch_file) |
314 if self.Git(args) is None: | 374 if self.Git(args) is None: |
315 self.WaitForResolvingConflicts(patch_file) | 375 self.WaitForResolvingConflicts(patch_file) |
316 | 376 |
317 | 377 |
318 class UploadStep(Step): | 378 class UploadStep(Step): |
319 def __init__(self): | 379 def __init__(self): |
320 Step.__init__(self, "Upload for code review.") | 380 Step.__init__(self, "Upload for code review.") |
321 | 381 |
322 def RunStep(self): | 382 def RunStep(self): |
323 print "Please enter the email address of a V8 reviewer for your patch: ", | 383 if self._options and self._options.r: |
324 reviewer = self.ReadLine() | 384 print "Using account %s for review." % self._options.r |
| 385 reviewer = self._options.r |
| 386 else: |
| 387 print "Please enter the email address of a V8 reviewer for your patch: ", |
| 388 self.DieInForcedMode("A reviewer must be specified in forced mode.") |
| 389 reviewer = self.ReadLine() |
325 args = "cl upload -r \"%s\" --send-mail" % reviewer | 390 args = "cl upload -r \"%s\" --send-mail" % reviewer |
326 if self.Git(args,pipe=False) is None: | 391 if self.Git(args,pipe=False) is None: |
327 self.Die("'git cl upload' failed, please try again.") | 392 self.Die("'git cl upload' failed, please try again.") |
328 | 393 |
329 | 394 |
330 def RunScript(step_classes, | 395 def RunScript(step_classes, |
331 config, | 396 config, |
332 options, | 397 options, |
333 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER): | 398 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER): |
334 state = {} | 399 state = {} |
335 steps = [] | 400 steps = [] |
336 number = 0 | 401 number = 0 |
337 | 402 |
338 for step_class in step_classes: | 403 for step_class in step_classes: |
339 # TODO(machenbach): Factory methods. | 404 # TODO(machenbach): Factory methods. |
340 step = step_class() | 405 step = step_class() |
341 step.SetNumber(number) | 406 step.SetNumber(number) |
342 step.SetConfig(config) | 407 step.SetConfig(config) |
343 step.SetOptions(options) | 408 step.SetOptions(options) |
344 step.SetState(state) | 409 step.SetState(state) |
345 step.SetSideEffectHandler(side_effect_handler) | 410 step.SetSideEffectHandler(side_effect_handler) |
346 steps.append(step) | 411 steps.append(step) |
347 number += 1 | 412 number += 1 |
348 | 413 |
349 for step in steps[options.s:]: | 414 for step in steps[options.s:]: |
350 step.Run() | 415 step.Run() |
OLD | NEW |