| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright 2013 the V8 project authors. All rights reserved. | |
| 3 # Redistribution and use in source and binary forms, with or without | |
| 4 # modification, are permitted provided that the following conditions are | |
| 5 # met: | |
| 6 # | |
| 7 # * Redistributions of source code must retain the above copyright | |
| 8 # notice, this list of conditions and the following disclaimer. | |
| 9 # * Redistributions in binary form must reproduce the above | |
| 10 # copyright notice, this list of conditions and the following | |
| 11 # disclaimer in the documentation and/or other materials provided | |
| 12 # with the distribution. | |
| 13 # * Neither the name of Google Inc. nor the names of its | |
| 14 # contributors may be used to endorse or promote products derived | |
| 15 # from this software without specific prior written permission. | |
| 16 # | |
| 17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 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. | |
| 28 | |
| 29 import argparse | |
| 30 import datetime | |
| 31 import httplib | |
| 32 import glob | |
| 33 import imp | |
| 34 import json | |
| 35 import os | |
| 36 import re | |
| 37 import shutil | |
| 38 import subprocess | |
| 39 import sys | |
| 40 import textwrap | |
| 41 import time | |
| 42 import urllib | |
| 43 import urllib2 | |
| 44 | |
| 45 from git_recipes import GitRecipesMixin | |
| 46 from git_recipes import GitFailedException | |
| 47 | |
| 48 CHANGELOG_FILE = "ChangeLog" | |
| 49 VERSION_FILE = os.path.join("src", "version.cc") | |
| 50 | |
| 51 # V8 base directory. | |
| 52 V8_BASE = os.path.dirname( | |
| 53 os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| 54 | |
| 55 | |
| 56 def TextToFile(text, file_name): | |
| 57 with open(file_name, "w") as f: | |
| 58 f.write(text) | |
| 59 | |
| 60 | |
| 61 def AppendToFile(text, file_name): | |
| 62 with open(file_name, "a") as f: | |
| 63 f.write(text) | |
| 64 | |
| 65 | |
| 66 def LinesInFile(file_name): | |
| 67 with open(file_name) as f: | |
| 68 for line in f: | |
| 69 yield line | |
| 70 | |
| 71 | |
| 72 def FileToText(file_name): | |
| 73 with open(file_name) as f: | |
| 74 return f.read() | |
| 75 | |
| 76 | |
| 77 def MSub(rexp, replacement, text): | |
| 78 return re.sub(rexp, replacement, text, flags=re.MULTILINE) | |
| 79 | |
| 80 | |
| 81 def Fill80(line): | |
| 82 # Replace tabs and remove surrounding space. | |
| 83 line = re.sub(r"\t", r" ", line.strip()) | |
| 84 | |
| 85 # Format with 8 characters indentation and line width 80. | |
| 86 return textwrap.fill(line, width=80, initial_indent=" ", | |
| 87 subsequent_indent=" ") | |
| 88 | |
| 89 | |
| 90 def MakeComment(text): | |
| 91 return MSub(r"^( ?)", "#", text) | |
| 92 | |
| 93 | |
| 94 def StripComments(text): | |
| 95 # Use split not splitlines to keep terminal newlines. | |
| 96 return "\n".join(filter(lambda x: not x.startswith("#"), text.split("\n"))) | |
| 97 | |
| 98 | |
| 99 def MakeChangeLogBody(commit_messages, auto_format=False): | |
| 100 result = "" | |
| 101 added_titles = set() | |
| 102 for (title, body, author) in commit_messages: | |
| 103 # TODO(machenbach): Better check for reverts. A revert should remove the | |
| 104 # original CL from the actual log entry. | |
| 105 title = title.strip() | |
| 106 if auto_format: | |
| 107 # Only add commits that set the LOG flag correctly. | |
| 108 log_exp = r"^[ \t]*LOG[ \t]*=[ \t]*(?:(?:Y(?:ES)?)|TRUE)" | |
| 109 if not re.search(log_exp, body, flags=re.I | re.M): | |
| 110 continue | |
| 111 # Never include reverts. | |
| 112 if title.startswith("Revert "): | |
| 113 continue | |
| 114 # Don't include duplicates. | |
| 115 if title in added_titles: | |
| 116 continue | |
| 117 | |
| 118 # Add and format the commit's title and bug reference. Move dot to the end. | |
| 119 added_titles.add(title) | |
| 120 raw_title = re.sub(r"(\.|\?|!)$", "", title) | |
| 121 bug_reference = MakeChangeLogBugReference(body) | |
| 122 space = " " if bug_reference else "" | |
| 123 result += "%s\n" % Fill80("%s%s%s." % (raw_title, space, bug_reference)) | |
| 124 | |
| 125 # Append the commit's author for reference if not in auto-format mode. | |
| 126 if not auto_format: | |
| 127 result += "%s\n" % Fill80("(%s)" % author.strip()) | |
| 128 | |
| 129 result += "\n" | |
| 130 return result | |
| 131 | |
| 132 | |
| 133 def MakeChangeLogBugReference(body): | |
| 134 """Grep for "BUG=xxxx" lines in the commit message and convert them to | |
| 135 "(issue xxxx)". | |
| 136 """ | |
| 137 crbugs = [] | |
| 138 v8bugs = [] | |
| 139 | |
| 140 def AddIssues(text): | |
| 141 ref = re.match(r"^BUG[ \t]*=[ \t]*(.+)$", text.strip()) | |
| 142 if not ref: | |
| 143 return | |
| 144 for bug in ref.group(1).split(","): | |
| 145 bug = bug.strip() | |
| 146 match = re.match(r"^v8:(\d+)$", bug) | |
| 147 if match: v8bugs.append(int(match.group(1))) | |
| 148 else: | |
| 149 match = re.match(r"^(?:chromium:)?(\d+)$", bug) | |
| 150 if match: crbugs.append(int(match.group(1))) | |
| 151 | |
| 152 # Add issues to crbugs and v8bugs. | |
| 153 map(AddIssues, body.splitlines()) | |
| 154 | |
| 155 # Filter duplicates, sort, stringify. | |
| 156 crbugs = map(str, sorted(set(crbugs))) | |
| 157 v8bugs = map(str, sorted(set(v8bugs))) | |
| 158 | |
| 159 bug_groups = [] | |
| 160 def FormatIssues(prefix, bugs): | |
| 161 if len(bugs) > 0: | |
| 162 plural = "s" if len(bugs) > 1 else "" | |
| 163 bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs))) | |
| 164 | |
| 165 FormatIssues("", v8bugs) | |
| 166 FormatIssues("Chromium ", crbugs) | |
| 167 | |
| 168 if len(bug_groups) > 0: | |
| 169 return "(%s)" % ", ".join(bug_groups) | |
| 170 else: | |
| 171 return "" | |
| 172 | |
| 173 | |
| 174 def SortingKey(version): | |
| 175 """Key for sorting version number strings: '3.11' > '3.2.1.1'""" | |
| 176 version_keys = map(int, version.split(".")) | |
| 177 # Fill up to full version numbers to normalize comparison. | |
| 178 while len(version_keys) < 4: # pragma: no cover | |
| 179 version_keys.append(0) | |
| 180 # Fill digits. | |
| 181 return ".".join(map("{0:04d}".format, version_keys)) | |
| 182 | |
| 183 | |
| 184 # Some commands don't like the pipe, e.g. calling vi from within the script or | |
| 185 # from subscripts like git cl upload. | |
| 186 def Command(cmd, args="", prefix="", pipe=True, cwd=None): | |
| 187 cwd = cwd or os.getcwd() | |
| 188 # TODO(machenbach): Use timeout. | |
| 189 cmd_line = "%s %s %s" % (prefix, cmd, args) | |
| 190 print "Command: %s" % cmd_line | |
| 191 print "in %s" % cwd | |
| 192 sys.stdout.flush() | |
| 193 try: | |
| 194 if pipe: | |
| 195 return subprocess.check_output(cmd_line, shell=True, cwd=cwd) | |
| 196 else: | |
| 197 return subprocess.check_call(cmd_line, shell=True, cwd=cwd) | |
| 198 except subprocess.CalledProcessError: | |
| 199 return None | |
| 200 finally: | |
| 201 sys.stdout.flush() | |
| 202 sys.stderr.flush() | |
| 203 | |
| 204 | |
| 205 # Wrapper for side effects. | |
| 206 class SideEffectHandler(object): # pragma: no cover | |
| 207 def Call(self, fun, *args, **kwargs): | |
| 208 return fun(*args, **kwargs) | |
| 209 | |
| 210 def Command(self, cmd, args="", prefix="", pipe=True, cwd=None): | |
| 211 return Command(cmd, args, prefix, pipe, cwd=cwd) | |
| 212 | |
| 213 def ReadLine(self): | |
| 214 return sys.stdin.readline().strip() | |
| 215 | |
| 216 def ReadURL(self, url, params=None): | |
| 217 # pylint: disable=E1121 | |
| 218 url_fh = urllib2.urlopen(url, params, 60) | |
| 219 try: | |
| 220 return url_fh.read() | |
| 221 finally: | |
| 222 url_fh.close() | |
| 223 | |
| 224 def ReadClusterFuzzAPI(self, api_key, **params): | |
| 225 params["api_key"] = api_key.strip() | |
| 226 params = urllib.urlencode(params) | |
| 227 | |
| 228 headers = {"Content-type": "application/x-www-form-urlencoded"} | |
| 229 | |
| 230 conn = httplib.HTTPSConnection("backend-dot-cluster-fuzz.appspot.com") | |
| 231 conn.request("POST", "/_api/", params, headers) | |
| 232 | |
| 233 response = conn.getresponse() | |
| 234 data = response.read() | |
| 235 | |
| 236 try: | |
| 237 return json.loads(data) | |
| 238 except: | |
| 239 print data | |
| 240 print "ERROR: Could not read response. Is your key valid?" | |
| 241 raise | |
| 242 | |
| 243 def Sleep(self, seconds): | |
| 244 time.sleep(seconds) | |
| 245 | |
| 246 def GetDate(self): | |
| 247 return datetime.date.today().strftime("%Y-%m-%d") | |
| 248 | |
| 249 def GetUTCStamp(self): | |
| 250 return time.mktime(datetime.datetime.utcnow().timetuple()) | |
| 251 | |
| 252 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler() | |
| 253 | |
| 254 | |
| 255 class NoRetryException(Exception): | |
| 256 pass | |
| 257 | |
| 258 | |
| 259 class VCInterface(object): | |
| 260 def InjectStep(self, step): | |
| 261 self.step=step | |
| 262 | |
| 263 def Pull(self): | |
| 264 raise NotImplementedError() | |
| 265 | |
| 266 def Fetch(self): | |
| 267 raise NotImplementedError() | |
| 268 | |
| 269 def GetTags(self): | |
| 270 raise NotImplementedError() | |
| 271 | |
| 272 def GetBranches(self): | |
| 273 raise NotImplementedError() | |
| 274 | |
| 275 def MasterBranch(self): | |
| 276 raise NotImplementedError() | |
| 277 | |
| 278 def CandidateBranch(self): | |
| 279 raise NotImplementedError() | |
| 280 | |
| 281 def RemoteMasterBranch(self): | |
| 282 raise NotImplementedError() | |
| 283 | |
| 284 def RemoteCandidateBranch(self): | |
| 285 raise NotImplementedError() | |
| 286 | |
| 287 def RemoteBranch(self, name): | |
| 288 raise NotImplementedError() | |
| 289 | |
| 290 def CLLand(self): | |
| 291 raise NotImplementedError() | |
| 292 | |
| 293 def Tag(self, tag, remote, message): | |
| 294 """Sets a tag for the current commit. | |
| 295 | |
| 296 Assumptions: The commit already landed and the commit message is unique. | |
| 297 """ | |
| 298 raise NotImplementedError() | |
| 299 | |
| 300 | |
| 301 class GitInterface(VCInterface): | |
| 302 def Pull(self): | |
| 303 self.step.GitPull() | |
| 304 | |
| 305 def Fetch(self): | |
| 306 self.step.Git("fetch") | |
| 307 | |
| 308 def GetTags(self): | |
| 309 return self.step.Git("tag").strip().splitlines() | |
| 310 | |
| 311 def GetBranches(self): | |
| 312 # Get relevant remote branches, e.g. "branch-heads/3.25". | |
| 313 branches = filter( | |
| 314 lambda s: re.match(r"^branch\-heads/\d+\.\d+$", s), | |
| 315 self.step.GitRemotes()) | |
| 316 # Remove 'branch-heads/' prefix. | |
| 317 return map(lambda s: s[13:], branches) | |
| 318 | |
| 319 def MasterBranch(self): | |
| 320 return "master" | |
| 321 | |
| 322 def CandidateBranch(self): | |
| 323 return "candidates" | |
| 324 | |
| 325 def RemoteMasterBranch(self): | |
| 326 return "origin/master" | |
| 327 | |
| 328 def RemoteCandidateBranch(self): | |
| 329 return "origin/candidates" | |
| 330 | |
| 331 def RemoteBranch(self, name): | |
| 332 if name in ["candidates", "master"]: | |
| 333 return "origin/%s" % name | |
| 334 return "branch-heads/%s" % name | |
| 335 | |
| 336 def Tag(self, tag, remote, message): | |
| 337 # Wait for the commit to appear. Assumes unique commit message titles (this | |
| 338 # is the case for all automated merge and push commits - also no title is | |
| 339 # the prefix of another title). | |
| 340 commit = None | |
| 341 for wait_interval in [3, 7, 15, 35, 45, 60]: | |
| 342 self.step.Git("fetch") | |
| 343 commit = self.step.GitLog(n=1, format="%H", grep=message, branch=remote) | |
| 344 if commit: | |
| 345 break | |
| 346 print("The commit has not replicated to git. Waiting for %s seconds." % | |
| 347 wait_interval) | |
| 348 self.step._side_effect_handler.Sleep(wait_interval) | |
| 349 else: | |
| 350 self.step.Die("Couldn't determine commit for setting the tag. Maybe the " | |
| 351 "git updater is lagging behind?") | |
| 352 | |
| 353 self.step.Git("tag %s %s" % (tag, commit)) | |
| 354 self.step.Git("push origin %s" % tag) | |
| 355 | |
| 356 def CLLand(self): | |
| 357 self.step.GitCLLand() | |
| 358 | |
| 359 | |
| 360 class Step(GitRecipesMixin): | |
| 361 def __init__(self, text, number, config, state, options, handler): | |
| 362 self._text = text | |
| 363 self._number = number | |
| 364 self._config = config | |
| 365 self._state = state | |
| 366 self._options = options | |
| 367 self._side_effect_handler = handler | |
| 368 self.vc = GitInterface() | |
| 369 self.vc.InjectStep(self) | |
| 370 | |
| 371 # The testing configuration might set a different default cwd. | |
| 372 self.default_cwd = (self._config.get("DEFAULT_CWD") or | |
| 373 os.path.join(self._options.work_dir, "v8")) | |
| 374 | |
| 375 assert self._number >= 0 | |
| 376 assert self._config is not None | |
| 377 assert self._state is not None | |
| 378 assert self._side_effect_handler is not None | |
| 379 | |
| 380 def __getitem__(self, key): | |
| 381 # Convenience method to allow direct [] access on step classes for | |
| 382 # manipulating the backed state dict. | |
| 383 return self._state[key] | |
| 384 | |
| 385 def __setitem__(self, key, value): | |
| 386 # Convenience method to allow direct [] access on step classes for | |
| 387 # manipulating the backed state dict. | |
| 388 self._state[key] = value | |
| 389 | |
| 390 def Config(self, key): | |
| 391 return self._config[key] | |
| 392 | |
| 393 def Run(self): | |
| 394 # Restore state. | |
| 395 state_file = "%s-state.json" % self._config["PERSISTFILE_BASENAME"] | |
| 396 if not self._state and os.path.exists(state_file): | |
| 397 self._state.update(json.loads(FileToText(state_file))) | |
| 398 | |
| 399 print ">>> Step %d: %s" % (self._number, self._text) | |
| 400 try: | |
| 401 return self.RunStep() | |
| 402 finally: | |
| 403 # Persist state. | |
| 404 TextToFile(json.dumps(self._state), state_file) | |
| 405 | |
| 406 def RunStep(self): # pragma: no cover | |
| 407 raise NotImplementedError | |
| 408 | |
| 409 def Retry(self, cb, retry_on=None, wait_plan=None): | |
| 410 """ Retry a function. | |
| 411 Params: | |
| 412 cb: The function to retry. | |
| 413 retry_on: A callback that takes the result of the function and returns | |
| 414 True if the function should be retried. A function throwing an | |
| 415 exception is always retried. | |
| 416 wait_plan: A list of waiting delays between retries in seconds. The | |
| 417 maximum number of retries is len(wait_plan). | |
| 418 """ | |
| 419 retry_on = retry_on or (lambda x: False) | |
| 420 wait_plan = list(wait_plan or []) | |
| 421 wait_plan.reverse() | |
| 422 while True: | |
| 423 got_exception = False | |
| 424 try: | |
| 425 result = cb() | |
| 426 except NoRetryException as e: | |
| 427 raise e | |
| 428 except Exception as e: | |
| 429 got_exception = e | |
| 430 if got_exception or retry_on(result): | |
| 431 if not wait_plan: # pragma: no cover | |
| 432 raise Exception("Retried too often. Giving up. Reason: %s" % | |
| 433 str(got_exception)) | |
| 434 wait_time = wait_plan.pop() | |
| 435 print "Waiting for %f seconds." % wait_time | |
| 436 self._side_effect_handler.Sleep(wait_time) | |
| 437 print "Retrying..." | |
| 438 else: | |
| 439 return result | |
| 440 | |
| 441 def ReadLine(self, default=None): | |
| 442 # Don't prompt in forced mode. | |
| 443 if self._options.force_readline_defaults and default is not None: | |
| 444 print "%s (forced)" % default | |
| 445 return default | |
| 446 else: | |
| 447 return self._side_effect_handler.ReadLine() | |
| 448 | |
| 449 def Command(self, name, args, cwd=None): | |
| 450 cmd = lambda: self._side_effect_handler.Command( | |
| 451 name, args, "", True, cwd=cwd or self.default_cwd) | |
| 452 return self.Retry(cmd, None, [5]) | |
| 453 | |
| 454 def Git(self, args="", prefix="", pipe=True, retry_on=None, cwd=None): | |
| 455 cmd = lambda: self._side_effect_handler.Command( | |
| 456 "git", args, prefix, pipe, cwd=cwd or self.default_cwd) | |
| 457 result = self.Retry(cmd, retry_on, [5, 30]) | |
| 458 if result is None: | |
| 459 raise GitFailedException("'git %s' failed." % args) | |
| 460 return result | |
| 461 | |
| 462 def Editor(self, args): | |
| 463 if self._options.requires_editor: | |
| 464 return self._side_effect_handler.Command( | |
| 465 os.environ["EDITOR"], | |
| 466 args, | |
| 467 pipe=False, | |
| 468 cwd=self.default_cwd) | |
| 469 | |
| 470 def ReadURL(self, url, params=None, retry_on=None, wait_plan=None): | |
| 471 wait_plan = wait_plan or [3, 60, 600] | |
| 472 cmd = lambda: self._side_effect_handler.ReadURL(url, params) | |
| 473 return self.Retry(cmd, retry_on, wait_plan) | |
| 474 | |
| 475 def GetDate(self): | |
| 476 return self._side_effect_handler.GetDate() | |
| 477 | |
| 478 def Die(self, msg=""): | |
| 479 if msg != "": | |
| 480 print "Error: %s" % msg | |
| 481 print "Exiting" | |
| 482 raise Exception(msg) | |
| 483 | |
| 484 def DieNoManualMode(self, msg=""): | |
| 485 if not self._options.manual: # pragma: no cover | |
| 486 msg = msg or "Only available in manual mode." | |
| 487 self.Die(msg) | |
| 488 | |
| 489 def Confirm(self, msg): | |
| 490 print "%s [Y/n] " % msg, | |
| 491 answer = self.ReadLine(default="Y") | |
| 492 return answer == "" or answer == "Y" or answer == "y" | |
| 493 | |
| 494 def DeleteBranch(self, name): | |
| 495 for line in self.GitBranch().splitlines(): | |
| 496 if re.match(r"\*?\s*%s$" % re.escape(name), line): | |
| 497 msg = "Branch %s exists, do you want to delete it?" % name | |
| 498 if self.Confirm(msg): | |
| 499 self.GitDeleteBranch(name) | |
| 500 print "Branch %s deleted." % name | |
| 501 else: | |
| 502 msg = "Can't continue. Please delete branch %s and try again." % name | |
| 503 self.Die(msg) | |
| 504 | |
| 505 def InitialEnvironmentChecks(self, cwd): | |
| 506 # Cancel if this is not a git checkout. | |
| 507 if not os.path.exists(os.path.join(cwd, ".git")): # pragma: no cover | |
| 508 self.Die("This is not a git checkout, this script won't work for you.") | |
| 509 | |
| 510 # Cancel if EDITOR is unset or not executable. | |
| 511 if (self._options.requires_editor and (not os.environ.get("EDITOR") or | |
| 512 self.Command( | |
| 513 "which", os.environ["EDITOR"]) is None)): # pragma: no cover | |
| 514 self.Die("Please set your EDITOR environment variable, you'll need it.") | |
| 515 | |
| 516 def CommonPrepare(self): | |
| 517 # Check for a clean workdir. | |
| 518 if not self.GitIsWorkdirClean(): # pragma: no cover | |
| 519 self.Die("Workspace is not clean. Please commit or undo your changes.") | |
| 520 | |
| 521 # Persist current branch. | |
| 522 self["current_branch"] = self.GitCurrentBranch() | |
| 523 | |
| 524 # Fetch unfetched revisions. | |
| 525 self.vc.Fetch() | |
| 526 | |
| 527 def PrepareBranch(self): | |
| 528 # Delete the branch that will be created later if it exists already. | |
| 529 self.DeleteBranch(self._config["BRANCHNAME"]) | |
| 530 | |
| 531 def CommonCleanup(self): | |
| 532 if ' ' in self["current_branch"]: | |
| 533 self.GitCheckout('master') | |
| 534 else: | |
| 535 self.GitCheckout(self["current_branch"]) | |
| 536 if self._config["BRANCHNAME"] != self["current_branch"]: | |
| 537 self.GitDeleteBranch(self._config["BRANCHNAME"]) | |
| 538 | |
| 539 # Clean up all temporary files. | |
| 540 for f in glob.iglob("%s*" % self._config["PERSISTFILE_BASENAME"]): | |
| 541 if os.path.isfile(f): | |
| 542 os.remove(f) | |
| 543 if os.path.isdir(f): | |
| 544 shutil.rmtree(f) | |
| 545 | |
| 546 def ReadAndPersistVersion(self, prefix=""): | |
| 547 def ReadAndPersist(var_name, def_name): | |
| 548 match = re.match(r"^#define %s\s+(\d*)" % def_name, line) | |
| 549 if match: | |
| 550 value = match.group(1) | |
| 551 self["%s%s" % (prefix, var_name)] = value | |
| 552 for line in LinesInFile(os.path.join(self.default_cwd, VERSION_FILE)): | |
| 553 for (var_name, def_name) in [("major", "MAJOR_VERSION"), | |
| 554 ("minor", "MINOR_VERSION"), | |
| 555 ("build", "BUILD_NUMBER"), | |
| 556 ("patch", "PATCH_LEVEL")]: | |
| 557 ReadAndPersist(var_name, def_name) | |
| 558 | |
| 559 def WaitForLGTM(self): | |
| 560 print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit " | |
| 561 "your change. (If you need to iterate on the patch or double check " | |
| 562 "that it's sane, do so in another shell, but remember to not " | |
| 563 "change the headline of the uploaded CL.") | |
| 564 answer = "" | |
| 565 while answer != "LGTM": | |
| 566 print "> ", | |
| 567 answer = self.ReadLine(None if self._options.wait_for_lgtm else "LGTM") | |
| 568 if answer != "LGTM": | |
| 569 print "That was not 'LGTM'." | |
| 570 | |
| 571 def WaitForResolvingConflicts(self, patch_file): | |
| 572 print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", " | |
| 573 "or resolve the conflicts, stage *all* touched files with " | |
| 574 "'git add', and type \"RESOLVED<Return>\"") | |
| 575 self.DieNoManualMode() | |
| 576 answer = "" | |
| 577 while answer != "RESOLVED": | |
| 578 if answer == "ABORT": | |
| 579 self.Die("Applying the patch failed.") | |
| 580 if answer != "": | |
| 581 print "That was not 'RESOLVED' or 'ABORT'." | |
| 582 print "> ", | |
| 583 answer = self.ReadLine() | |
| 584 | |
| 585 # Takes a file containing the patch to apply as first argument. | |
| 586 def ApplyPatch(self, patch_file, revert=False): | |
| 587 try: | |
| 588 self.GitApplyPatch(patch_file, revert) | |
| 589 except GitFailedException: | |
| 590 self.WaitForResolvingConflicts(patch_file) | |
| 591 | |
| 592 def FindLastCandidatesPush( | |
| 593 self, parent_hash="", branch="", include_patches=False): | |
| 594 push_pattern = "^Version [[:digit:]]*\.[[:digit:]]*\.[[:digit:]]*" | |
| 595 if not include_patches: | |
| 596 # Non-patched versions only have three numbers followed by the "(based | |
| 597 # on...) comment." | |
| 598 push_pattern += " (based" | |
| 599 branch = "" if parent_hash else branch or self.vc.RemoteCandidateBranch() | |
| 600 return self.GitLog(n=1, format="%H", grep=push_pattern, | |
| 601 parent_hash=parent_hash, branch=branch) | |
| 602 | |
| 603 def ArrayToVersion(self, prefix): | |
| 604 return ".".join([self[prefix + "major"], | |
| 605 self[prefix + "minor"], | |
| 606 self[prefix + "build"], | |
| 607 self[prefix + "patch"]]) | |
| 608 | |
| 609 def StoreVersion(self, version, prefix): | |
| 610 version_parts = version.split(".") | |
| 611 if len(version_parts) == 3: | |
| 612 version_parts.append("0") | |
| 613 major, minor, build, patch = version_parts | |
| 614 self[prefix + "major"] = major | |
| 615 self[prefix + "minor"] = minor | |
| 616 self[prefix + "build"] = build | |
| 617 self[prefix + "patch"] = patch | |
| 618 | |
| 619 def SetVersion(self, version_file, prefix): | |
| 620 output = "" | |
| 621 for line in FileToText(version_file).splitlines(): | |
| 622 if line.startswith("#define MAJOR_VERSION"): | |
| 623 line = re.sub("\d+$", self[prefix + "major"], line) | |
| 624 elif line.startswith("#define MINOR_VERSION"): | |
| 625 line = re.sub("\d+$", self[prefix + "minor"], line) | |
| 626 elif line.startswith("#define BUILD_NUMBER"): | |
| 627 line = re.sub("\d+$", self[prefix + "build"], line) | |
| 628 elif line.startswith("#define PATCH_LEVEL"): | |
| 629 line = re.sub("\d+$", self[prefix + "patch"], line) | |
| 630 output += "%s\n" % line | |
| 631 TextToFile(output, version_file) | |
| 632 | |
| 633 | |
| 634 class BootstrapStep(Step): | |
| 635 MESSAGE = "Bootstapping v8 checkout." | |
| 636 | |
| 637 def RunStep(self): | |
| 638 if os.path.realpath(self.default_cwd) == os.path.realpath(V8_BASE): | |
| 639 self.Die("Can't use v8 checkout with calling script as work checkout.") | |
| 640 # Directory containing the working v8 checkout. | |
| 641 if not os.path.exists(self._options.work_dir): | |
| 642 os.makedirs(self._options.work_dir) | |
| 643 if not os.path.exists(self.default_cwd): | |
| 644 self.Command("fetch", "v8", cwd=self._options.work_dir) | |
| 645 | |
| 646 | |
| 647 class UploadStep(Step): | |
| 648 MESSAGE = "Upload for code review." | |
| 649 | |
| 650 def RunStep(self): | |
| 651 if self._options.reviewer: | |
| 652 print "Using account %s for review." % self._options.reviewer | |
| 653 reviewer = self._options.reviewer | |
| 654 else: | |
| 655 print "Please enter the email address of a V8 reviewer for your patch: ", | |
| 656 self.DieNoManualMode("A reviewer must be specified in forced mode.") | |
| 657 reviewer = self.ReadLine() | |
| 658 self.GitUpload(reviewer, self._options.author, self._options.force_upload, | |
| 659 bypass_hooks=self._options.bypass_upload_hooks, | |
| 660 cc=self._options.cc) | |
| 661 | |
| 662 | |
| 663 class DetermineV8Sheriff(Step): | |
| 664 MESSAGE = "Determine the V8 sheriff for code review." | |
| 665 | |
| 666 def RunStep(self): | |
| 667 self["sheriff"] = None | |
| 668 if not self._options.sheriff: # pragma: no cover | |
| 669 return | |
| 670 | |
| 671 try: | |
| 672 # The googlers mapping maps @google.com accounts to @chromium.org | |
| 673 # accounts. | |
| 674 googlers = imp.load_source('googlers_mapping', | |
| 675 self._options.googlers_mapping) | |
| 676 googlers = googlers.list_to_dict(googlers.get_list()) | |
| 677 except: # pragma: no cover | |
| 678 print "Skip determining sheriff without googler mapping." | |
| 679 return | |
| 680 | |
| 681 # The sheriff determined by the rotation on the waterfall has a | |
| 682 # @google.com account. | |
| 683 url = "https://chromium-build.appspot.com/p/chromium/sheriff_v8.js" | |
| 684 match = re.match(r"document\.write\('(\w+)'\)", self.ReadURL(url)) | |
| 685 | |
| 686 # If "channel is sheriff", we can't match an account. | |
| 687 if match: | |
| 688 g_name = match.group(1) | |
| 689 self["sheriff"] = googlers.get(g_name + "@google.com", | |
| 690 g_name + "@chromium.org") | |
| 691 self._options.reviewer = self["sheriff"] | |
| 692 print "Found active sheriff: %s" % self["sheriff"] | |
| 693 else: | |
| 694 print "No active sheriff found." | |
| 695 | |
| 696 | |
| 697 def MakeStep(step_class=Step, number=0, state=None, config=None, | |
| 698 options=None, side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER): | |
| 699 # Allow to pass in empty dictionaries. | |
| 700 state = state if state is not None else {} | |
| 701 config = config if config is not None else {} | |
| 702 | |
| 703 try: | |
| 704 message = step_class.MESSAGE | |
| 705 except AttributeError: | |
| 706 message = step_class.__name__ | |
| 707 | |
| 708 return step_class(message, number=number, config=config, | |
| 709 state=state, options=options, | |
| 710 handler=side_effect_handler) | |
| 711 | |
| 712 | |
| 713 class ScriptsBase(object): | |
| 714 def __init__(self, | |
| 715 config=None, | |
| 716 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER, | |
| 717 state=None): | |
| 718 self._config = config or self._Config() | |
| 719 self._side_effect_handler = side_effect_handler | |
| 720 self._state = state if state is not None else {} | |
| 721 | |
| 722 def _Description(self): | |
| 723 return None | |
| 724 | |
| 725 def _PrepareOptions(self, parser): | |
| 726 pass | |
| 727 | |
| 728 def _ProcessOptions(self, options): | |
| 729 return True | |
| 730 | |
| 731 def _Steps(self): # pragma: no cover | |
| 732 raise Exception("Not implemented.") | |
| 733 | |
| 734 def _Config(self): | |
| 735 return {} | |
| 736 | |
| 737 def MakeOptions(self, args=None): | |
| 738 parser = argparse.ArgumentParser(description=self._Description()) | |
| 739 parser.add_argument("-a", "--author", default="", | |
| 740 help="The author email used for rietveld.") | |
| 741 parser.add_argument("--dry-run", default=False, action="store_true", | |
| 742 help="Perform only read-only actions.") | |
| 743 parser.add_argument("-g", "--googlers-mapping", | |
| 744 help="Path to the script mapping google accounts.") | |
| 745 parser.add_argument("-r", "--reviewer", default="", | |
| 746 help="The account name to be used for reviews.") | |
| 747 parser.add_argument("--sheriff", default=False, action="store_true", | |
| 748 help=("Determine current sheriff to review CLs. On " | |
| 749 "success, this will overwrite the reviewer " | |
| 750 "option.")) | |
| 751 parser.add_argument("-s", "--step", | |
| 752 help="Specify the step where to start work. Default: 0.", | |
| 753 default=0, type=int) | |
| 754 parser.add_argument("--work-dir", | |
| 755 help=("Location where to bootstrap a working v8 " | |
| 756 "checkout.")) | |
| 757 self._PrepareOptions(parser) | |
| 758 | |
| 759 if args is None: # pragma: no cover | |
| 760 options = parser.parse_args() | |
| 761 else: | |
| 762 options = parser.parse_args(args) | |
| 763 | |
| 764 # Process common options. | |
| 765 if options.step < 0: # pragma: no cover | |
| 766 print "Bad step number %d" % options.step | |
| 767 parser.print_help() | |
| 768 return None | |
| 769 if options.sheriff and not options.googlers_mapping: # pragma: no cover | |
| 770 print "To determine the current sheriff, requires the googler mapping" | |
| 771 parser.print_help() | |
| 772 return None | |
| 773 | |
| 774 # Defaults for options, common to all scripts. | |
| 775 options.manual = getattr(options, "manual", True) | |
| 776 options.force = getattr(options, "force", False) | |
| 777 options.bypass_upload_hooks = False | |
| 778 | |
| 779 # Derived options. | |
| 780 options.requires_editor = not options.force | |
| 781 options.wait_for_lgtm = not options.force | |
| 782 options.force_readline_defaults = not options.manual | |
| 783 options.force_upload = not options.manual | |
| 784 | |
| 785 # Process script specific options. | |
| 786 if not self._ProcessOptions(options): | |
| 787 parser.print_help() | |
| 788 return None | |
| 789 | |
| 790 if not options.work_dir: | |
| 791 options.work_dir = "/tmp/v8-release-scripts-work-dir" | |
| 792 return options | |
| 793 | |
| 794 def RunSteps(self, step_classes, args=None): | |
| 795 options = self.MakeOptions(args) | |
| 796 if not options: | |
| 797 return 1 | |
| 798 | |
| 799 state_file = "%s-state.json" % self._config["PERSISTFILE_BASENAME"] | |
| 800 if options.step == 0 and os.path.exists(state_file): | |
| 801 os.remove(state_file) | |
| 802 | |
| 803 steps = [] | |
| 804 for (number, step_class) in enumerate([BootstrapStep] + step_classes): | |
| 805 steps.append(MakeStep(step_class, number, self._state, self._config, | |
| 806 options, self._side_effect_handler)) | |
| 807 for step in steps[options.step:]: | |
| 808 if step.Run(): | |
| 809 return 0 | |
| 810 return 0 | |
| 811 | |
| 812 def Run(self, args=None): | |
| 813 return self.RunSteps(self._Steps(), args) | |
| OLD | NEW |