| 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 13 matching lines...) Expand all Loading... |
| 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 import textwrap |
| 34 import urllib2 |
| 34 | 35 |
| 35 PERSISTFILE_BASENAME = "PERSISTFILE_BASENAME" | 36 PERSISTFILE_BASENAME = "PERSISTFILE_BASENAME" |
| 36 TEMP_BRANCH = "TEMP_BRANCH" | 37 TEMP_BRANCH = "TEMP_BRANCH" |
| 37 BRANCHNAME = "BRANCHNAME" | 38 BRANCHNAME = "BRANCHNAME" |
| 38 DOT_GIT_LOCATION = "DOT_GIT_LOCATION" | 39 DOT_GIT_LOCATION = "DOT_GIT_LOCATION" |
| 39 VERSION_FILE = "VERSION_FILE" | 40 VERSION_FILE = "VERSION_FILE" |
| 40 CHANGELOG_FILE = "CHANGELOG_FILE" | 41 CHANGELOG_FILE = "CHANGELOG_FILE" |
| 41 CHANGELOG_ENTRY_FILE = "CHANGELOG_ENTRY_FILE" | 42 CHANGELOG_ENTRY_FILE = "CHANGELOG_ENTRY_FILE" |
| 42 COMMITMSG_FILE = "COMMITMSG_FILE" | 43 COMMITMSG_FILE = "COMMITMSG_FILE" |
| 43 PATCH_FILE = "PATCH_FILE" | 44 PATCH_FILE = "PATCH_FILE" |
| (...skipping 18 matching lines...) Expand all Loading... |
| 62 def FileToText(file_name): | 63 def FileToText(file_name): |
| 63 with open(file_name) as f: | 64 with open(file_name) as f: |
| 64 return f.read() | 65 return f.read() |
| 65 | 66 |
| 66 | 67 |
| 67 def MSub(rexp, replacement, text): | 68 def MSub(rexp, replacement, text): |
| 68 return re.sub(rexp, replacement, text, flags=re.MULTILINE) | 69 return re.sub(rexp, replacement, text, flags=re.MULTILINE) |
| 69 | 70 |
| 70 | 71 |
| 71 def Fill80(line): | 72 def Fill80(line): |
| 73 # Replace tabs and remove surrounding space. |
| 74 line = re.sub(r"\t", r" ", line.strip()) |
| 75 |
| 76 # Format with 8 characters indentation and line width 80. |
| 72 return textwrap.fill(line, width=80, initial_indent=" ", | 77 return textwrap.fill(line, width=80, initial_indent=" ", |
| 73 subsequent_indent=" ") | 78 subsequent_indent=" ") |
| 74 | 79 |
| 75 | 80 |
| 76 def GetLastChangeLogEntries(change_log_file): | 81 def GetLastChangeLogEntries(change_log_file): |
| 77 result = [] | 82 result = [] |
| 78 for line in LinesInFile(change_log_file): | 83 for line in LinesInFile(change_log_file): |
| 79 if re.search(r"^\d{4}-\d{2}-\d{2}:", line) and result: break | 84 if re.search(r"^\d{4}-\d{2}-\d{2}:", line) and result: break |
| 80 result.append(line) | 85 result.append(line) |
| 81 return "".join(result) | 86 return "".join(result) |
| 82 | 87 |
| 83 | 88 |
| 84 def MakeChangeLogBody(commit_generator): | 89 def MakeComment(text): |
| 90 return MSub(r"^( ?)", "#", text) |
| 91 |
| 92 |
| 93 def StripComments(text): |
| 94 # Use split not splitlines to keep terminal newlines. |
| 95 return "\n".join(filter(lambda x: not x.startswith("#"), text.split("\n"))) |
| 96 |
| 97 |
| 98 def MakeChangeLogBody(commit_messages, auto_format=False): |
| 85 result = "" | 99 result = "" |
| 86 for (title, body, author) in commit_generator(): | 100 added_titles = set() |
| 87 # Add the commit's title line. | 101 for (title, body, author) in commit_messages: |
| 88 result += "%s\n" % title.rstrip() | 102 # TODO(machenbach): Better check for reverts. A revert should remove the |
| 103 # original CL from the actual log entry. |
| 104 title = title.strip() |
| 105 if auto_format: |
| 106 # Only add commits that set the LOG flag correctly. |
| 107 log_exp = r"^[ \t]*LOG[ \t]*=[ \t]*(?:Y(?:ES)?)|TRUE" |
| 108 if not re.search(log_exp, body, flags=re.I | re.M): |
| 109 continue |
| 110 # Never include reverts. |
| 111 if title.startswith("Revert "): |
| 112 continue |
| 113 # Don't include duplicates. |
| 114 if title in added_titles: |
| 115 continue |
| 89 | 116 |
| 90 # Add bug references. | 117 # Add and format the commit's title and bug reference. Move dot to the end. |
| 91 result += MakeChangeLogBugReference(body) | 118 added_titles.add(title) |
| 119 raw_title = re.sub(r"(\.|\?|!)$", "", title) |
| 120 bug_reference = MakeChangeLogBugReference(body) |
| 121 space = " " if bug_reference else "" |
| 122 result += "%s\n" % Fill80("%s%s%s." % (raw_title, space, bug_reference)) |
| 92 | 123 |
| 93 # Append the commit's author for reference. | 124 # Append the commit's author for reference if not in auto-format mode. |
| 94 result += "%s\n\n" % author.rstrip() | 125 if not auto_format: |
| 126 result += "%s\n" % Fill80("(%s)" % author.strip()) |
| 127 |
| 128 result += "\n" |
| 95 return result | 129 return result |
| 96 | 130 |
| 97 | 131 |
| 98 def MakeChangeLogBugReference(body): | 132 def MakeChangeLogBugReference(body): |
| 99 """Grep for "BUG=xxxx" lines in the commit message and convert them to | 133 """Grep for "BUG=xxxx" lines in the commit message and convert them to |
| 100 "(issue xxxx)". | 134 "(issue xxxx)". |
| 101 """ | 135 """ |
| 102 crbugs = [] | 136 crbugs = [] |
| 103 v8bugs = [] | 137 v8bugs = [] |
| 104 | 138 |
| (...skipping 19 matching lines...) Expand all Loading... |
| 124 bug_groups = [] | 158 bug_groups = [] |
| 125 def FormatIssues(prefix, bugs): | 159 def FormatIssues(prefix, bugs): |
| 126 if len(bugs) > 0: | 160 if len(bugs) > 0: |
| 127 plural = "s" if len(bugs) > 1 else "" | 161 plural = "s" if len(bugs) > 1 else "" |
| 128 bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs))) | 162 bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs))) |
| 129 | 163 |
| 130 FormatIssues("", v8bugs) | 164 FormatIssues("", v8bugs) |
| 131 FormatIssues("Chromium ", crbugs) | 165 FormatIssues("Chromium ", crbugs) |
| 132 | 166 |
| 133 if len(bug_groups) > 0: | 167 if len(bug_groups) > 0: |
| 134 # Format with 8 characters indentation and max 80 character lines. | 168 return "(%s)" % ", ".join(bug_groups) |
| 135 return "%s\n" % Fill80("(%s)" % ", ".join(bug_groups)) | |
| 136 else: | 169 else: |
| 137 return "" | 170 return "" |
| 138 | 171 |
| 139 | 172 |
| 140 # Some commands don't like the pipe, e.g. calling vi from within the script or | 173 # Some commands don't like the pipe, e.g. calling vi from within the script or |
| 141 # from subscripts like git cl upload. | 174 # from subscripts like git cl upload. |
| 142 def Command(cmd, args="", prefix="", pipe=True): | 175 def Command(cmd, args="", prefix="", pipe=True): |
| 143 cmd_line = "%s %s %s" % (prefix, cmd, args) | 176 cmd_line = "%s %s %s" % (prefix, cmd, args) |
| 144 print "Command: %s" % cmd_line | 177 print "Command: %s" % cmd_line |
| 145 try: | 178 try: |
| 146 if pipe: | 179 if pipe: |
| 147 return subprocess.check_output(cmd_line, shell=True) | 180 return subprocess.check_output(cmd_line, shell=True) |
| 148 else: | 181 else: |
| 149 return subprocess.check_call(cmd_line, shell=True) | 182 return subprocess.check_call(cmd_line, shell=True) |
| 150 except subprocess.CalledProcessError: | 183 except subprocess.CalledProcessError: |
| 151 return None | 184 return None |
| 152 | 185 |
| 153 | 186 |
| 154 # Wrapper for side effects. | 187 # Wrapper for side effects. |
| 155 class SideEffectHandler(object): | 188 class SideEffectHandler(object): |
| 156 def Command(self, cmd, args="", prefix="", pipe=True): | 189 def Command(self, cmd, args="", prefix="", pipe=True): |
| 157 return Command(cmd, args, prefix, pipe) | 190 return Command(cmd, args, prefix, pipe) |
| 158 | 191 |
| 159 def ReadLine(self): | 192 def ReadLine(self): |
| 160 return sys.stdin.readline().strip() | 193 return sys.stdin.readline().strip() |
| 161 | 194 |
| 195 def ReadURL(self, url): |
| 196 # pylint: disable=E1121 |
| 197 url_fh = urllib2.urlopen(url, None, 60) |
| 198 try: |
| 199 return url_fh.read() |
| 200 finally: |
| 201 url_fh.close() |
| 202 |
| 162 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler() | 203 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler() |
| 163 | 204 |
| 164 | 205 |
| 165 class Step(object): | 206 class Step(object): |
| 166 def __init__(self, text="", requires=None): | 207 def __init__(self, text, requires, number, config, state, options, handler): |
| 167 self._text = text | 208 self._text = text |
| 168 self._number = -1 | |
| 169 self._options = None | |
| 170 self._requires = requires | 209 self._requires = requires |
| 171 self._side_effect_handler = DEFAULT_SIDE_EFFECT_HANDLER | |
| 172 | |
| 173 def SetNumber(self, number): | |
| 174 self._number = number | 210 self._number = number |
| 175 | |
| 176 def SetConfig(self, config): | |
| 177 self._config = config | 211 self._config = config |
| 178 | |
| 179 def SetState(self, state): | |
| 180 self._state = state | 212 self._state = state |
| 181 | |
| 182 def SetOptions(self, options): | |
| 183 self._options = options | 213 self._options = options |
| 184 | |
| 185 def SetSideEffectHandler(self, handler): | |
| 186 self._side_effect_handler = handler | 214 self._side_effect_handler = handler |
| 215 assert self._number >= 0 |
| 216 assert self._config is not None |
| 217 assert self._state is not None |
| 218 assert self._side_effect_handler is not None |
| 187 | 219 |
| 188 def Config(self, key): | 220 def Config(self, key): |
| 189 return self._config[key] | 221 return self._config[key] |
| 190 | 222 |
| 191 def Run(self): | 223 def Run(self): |
| 192 assert self._number >= 0 | |
| 193 assert self._config is not None | |
| 194 assert self._state is not None | |
| 195 assert self._side_effect_handler is not None | |
| 196 if self._requires: | 224 if self._requires: |
| 197 self.RestoreIfUnset(self._requires) | 225 self.RestoreIfUnset(self._requires) |
| 198 if not self._state[self._requires]: | 226 if not self._state[self._requires]: |
| 199 return | 227 return |
| 200 print ">>> Step %d: %s" % (self._number, self._text) | 228 print ">>> Step %d: %s" % (self._number, self._text) |
| 201 self.RunStep() | 229 self.RunStep() |
| 202 | 230 |
| 203 def RunStep(self): | 231 def RunStep(self): |
| 204 raise NotImplementedError | 232 raise NotImplementedError |
| 205 | 233 |
| 206 def ReadLine(self, default=None): | 234 def ReadLine(self, default=None): |
| 207 # Don't prompt in forced mode. | 235 # Don't prompt in forced mode. |
| 208 if self._options and self._options.f and default is not None: | 236 if self._options and self._options.f and default is not None: |
| 209 print "%s (forced)" % default | 237 print "%s (forced)" % default |
| 210 return default | 238 return default |
| 211 else: | 239 else: |
| 212 return self._side_effect_handler.ReadLine() | 240 return self._side_effect_handler.ReadLine() |
| 213 | 241 |
| 214 def Git(self, args="", prefix="", pipe=True): | 242 def Git(self, args="", prefix="", pipe=True): |
| 215 return self._side_effect_handler.Command("git", args, prefix, pipe) | 243 return self._side_effect_handler.Command("git", args, prefix, pipe) |
| 216 | 244 |
| 217 def Editor(self, args): | 245 def Editor(self, args): |
| 218 return self._side_effect_handler.Command(os.environ["EDITOR"], args, | 246 return self._side_effect_handler.Command(os.environ["EDITOR"], args, |
| 219 pipe=False) | 247 pipe=False) |
| 220 | 248 |
| 249 def ReadURL(self, url): |
| 250 return self._side_effect_handler.ReadURL(url) |
| 251 |
| 221 def Die(self, msg=""): | 252 def Die(self, msg=""): |
| 222 if msg != "": | 253 if msg != "": |
| 223 print "Error: %s" % msg | 254 print "Error: %s" % msg |
| 224 print "Exiting" | 255 print "Exiting" |
| 225 raise Exception(msg) | 256 raise Exception(msg) |
| 226 | 257 |
| 227 def DieInForcedMode(self, msg=""): | 258 def DieInForcedMode(self, msg=""): |
| 228 if self._options and self._options.f: | 259 if self._options and self._options.f: |
| 229 msg = msg or "Not implemented in forced mode." | 260 msg = msg or "Not implemented in forced mode." |
| 230 self.Die(msg) | 261 self.Die(msg) |
| (...skipping 128 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 359 answer = self.ReadLine() | 390 answer = self.ReadLine() |
| 360 | 391 |
| 361 # Takes a file containing the patch to apply as first argument. | 392 # Takes a file containing the patch to apply as first argument. |
| 362 def ApplyPatch(self, patch_file, reverse_patch=""): | 393 def ApplyPatch(self, patch_file, reverse_patch=""): |
| 363 args = "apply --index --reject %s \"%s\"" % (reverse_patch, patch_file) | 394 args = "apply --index --reject %s \"%s\"" % (reverse_patch, patch_file) |
| 364 if self.Git(args) is None: | 395 if self.Git(args) is None: |
| 365 self.WaitForResolvingConflicts(patch_file) | 396 self.WaitForResolvingConflicts(patch_file) |
| 366 | 397 |
| 367 | 398 |
| 368 class UploadStep(Step): | 399 class UploadStep(Step): |
| 369 def __init__(self): | 400 MESSAGE = "Upload for code review." |
| 370 Step.__init__(self, "Upload for code review.") | |
| 371 | 401 |
| 372 def RunStep(self): | 402 def RunStep(self): |
| 373 if self._options and self._options.r: | 403 if self._options.r: |
| 374 print "Using account %s for review." % self._options.r | 404 print "Using account %s for review." % self._options.r |
| 375 reviewer = self._options.r | 405 reviewer = self._options.r |
| 376 else: | 406 else: |
| 377 print "Please enter the email address of a V8 reviewer for your patch: ", | 407 print "Please enter the email address of a V8 reviewer for your patch: ", |
| 378 self.DieInForcedMode("A reviewer must be specified in forced mode.") | 408 self.DieInForcedMode("A reviewer must be specified in forced mode.") |
| 379 reviewer = self.ReadLine() | 409 reviewer = self.ReadLine() |
| 380 args = "cl upload -r \"%s\" --send-mail" % reviewer | 410 force_flag = " -f" if self._options.f else "" |
| 381 if self.Git(args,pipe=False) is None: | 411 args = "cl upload -r \"%s\" --send-mail%s" % (reviewer, force_flag) |
| 412 # TODO(machenbach): Check output in forced mode. Verify that all required |
| 413 # base files were uploaded, if not retry. |
| 414 if self.Git(args, pipe=False) is None: |
| 382 self.Die("'git cl upload' failed, please try again.") | 415 self.Die("'git cl upload' failed, please try again.") |
| 383 | 416 |
| 384 | 417 |
| 418 def MakeStep(step_class=Step, number=0, state=None, config=None, |
| 419 options=None, side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER): |
| 420 # Allow to pass in empty dictionaries. |
| 421 state = state if state is not None else {} |
| 422 config = config if config is not None else {} |
| 423 |
| 424 try: |
| 425 message = step_class.MESSAGE |
| 426 except AttributeError: |
| 427 message = step_class.__name__ |
| 428 try: |
| 429 requires = step_class.REQUIRES |
| 430 except AttributeError: |
| 431 requires = None |
| 432 |
| 433 return step_class(message, requires, number=number, config=config, |
| 434 state=state, options=options, |
| 435 handler=side_effect_handler) |
| 436 |
| 437 |
| 385 def RunScript(step_classes, | 438 def RunScript(step_classes, |
| 386 config, | 439 config, |
| 387 options, | 440 options, |
| 388 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER): | 441 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER): |
| 389 state = {} | 442 state = {} |
| 390 steps = [] | 443 steps = [] |
| 391 number = 0 | 444 for (number, step_class) in enumerate(step_classes): |
| 392 | 445 steps.append(MakeStep(step_class, number, state, config, |
| 393 for step_class in step_classes: | 446 options, side_effect_handler)) |
| 394 # TODO(machenbach): Factory methods. | |
| 395 step = step_class() | |
| 396 step.SetNumber(number) | |
| 397 step.SetConfig(config) | |
| 398 step.SetOptions(options) | |
| 399 step.SetState(state) | |
| 400 step.SetSideEffectHandler(side_effect_handler) | |
| 401 steps.append(step) | |
| 402 number += 1 | |
| 403 | 447 |
| 404 for step in steps[options.s:]: | 448 for step in steps[options.s:]: |
| 405 step.Run() | 449 step.Run() |
| OLD | NEW |