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
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.
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.
45 from git_recipes import GitRecipesMixin
46 from git_recipes import GitFailedException
48 CHANGELOG_FILE = "ChangeLog"
49 DAY_IN_SECONDS = 24 * 60 * 60
50 PUSH_MSG_GIT_RE = re.compile(r".* \(based on (?P<git_rev>[a-fA-F0-9]+)\)$")
51 PUSH_MSG_NEW_RE = re.compile(r"^Version \d+\.\d+\.\d+$")
52 VERSION_FILE = os.path.join("include", "v8-version.h")
53 VERSION_RE = re.compile(r"^\d+\.\d+\.\d+(?:\.\d+)?$")
56 V8_BASE = os.path.dirname(
57 os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
60 def TextToFile(text, file_name):
61 with open(file_name, "w") as f:
65 def AppendToFile(text, file_name):
66 with open(file_name, "a") as f:
70 def LinesInFile(file_name):
71 with open(file_name) as f:
76 def FileToText(file_name):
77 with open(file_name) as f:
81 def MSub(rexp, replacement, text):
82 return re.sub(rexp, replacement, text, flags=re.MULTILINE)
86 # Replace tabs and remove surrounding space.
87 line = re.sub(r"\t", r" ", line.strip())
89 # Format with 8 characters indentation and line width 80.
90 return textwrap.fill(line, width=80, initial_indent=" ",
91 subsequent_indent=" ")
94 def MakeComment(text):
95 return MSub(r"^( ?)", "#", text)
98 def StripComments(text):
99 # Use split not splitlines to keep terminal newlines.
100 return "\n".join(filter(lambda x: not x.startswith("#"), text.split("\n")))
103 def MakeChangeLogBody(commit_messages, auto_format=False):
106 for (title, body, author) in commit_messages:
107 # TODO(machenbach): Better check for reverts. A revert should remove the
108 # original CL from the actual log entry.
109 title = title.strip()
111 # Only add commits that set the LOG flag correctly.
112 log_exp = r"^[ \t]*LOG[ \t]*=[ \t]*(?:(?:Y(?:ES)?)|TRUE)"
113 if not re.search(log_exp, body, flags=re.I | re.M):
115 # Never include reverts.
116 if title.startswith("Revert "):
118 # Don't include duplicates.
119 if title in added_titles:
122 # Add and format the commit's title and bug reference. Move dot to the end.
123 added_titles.add(title)
124 raw_title = re.sub(r"(\.|\?|!)$", "", title)
125 bug_reference = MakeChangeLogBugReference(body)
126 space = " " if bug_reference else ""
127 result += "%s\n" % Fill80("%s%s%s." % (raw_title, space, bug_reference))
129 # Append the commit's author for reference if not in auto-format mode.
131 result += "%s\n" % Fill80("(%s)" % author.strip())
137 def MakeChangeLogBugReference(body):
138 """Grep for "BUG=xxxx" lines in the commit message and convert them to
145 ref = re.match(r"^BUG[ \t]*=[ \t]*(.+)$", text.strip())
148 for bug in ref.group(1).split(","):
150 match = re.match(r"^v8:(\d+)$", bug)
151 if match: v8bugs.append(int(match.group(1)))
153 match = re.match(r"^(?:chromium:)?(\d+)$", bug)
154 if match: crbugs.append(int(match.group(1)))
156 # Add issues to crbugs and v8bugs.
157 map(AddIssues, body.splitlines())
159 # Filter duplicates, sort, stringify.
160 crbugs = map(str, sorted(set(crbugs)))
161 v8bugs = map(str, sorted(set(v8bugs)))
164 def FormatIssues(prefix, bugs):
166 plural = "s" if len(bugs) > 1 else ""
167 bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs)))
169 FormatIssues("", v8bugs)
170 FormatIssues("Chromium ", crbugs)
172 if len(bug_groups) > 0:
173 return "(%s)" % ", ".join(bug_groups)
178 def SortingKey(version):
179 """Key for sorting version number strings: '3.11' > '3.2.1.1'"""
180 version_keys = map(int, version.split("."))
181 # Fill up to full version numbers to normalize comparison.
182 while len(version_keys) < 4: # pragma: no cover
183 version_keys.append(0)
185 return ".".join(map("{0:04d}".format, version_keys))
188 # Some commands don't like the pipe, e.g. calling vi from within the script or
189 # from subscripts like git cl upload.
190 def Command(cmd, args="", prefix="", pipe=True, cwd=None):
191 cwd = cwd or os.getcwd()
192 # TODO(machenbach): Use timeout.
193 cmd_line = "%s %s %s" % (prefix, cmd, args)
194 print "Command: %s" % cmd_line
199 return subprocess.check_output(cmd_line, shell=True, cwd=cwd)
201 return subprocess.check_call(cmd_line, shell=True, cwd=cwd)
202 except subprocess.CalledProcessError:
209 # Wrapper for side effects.
210 class SideEffectHandler(object): # pragma: no cover
211 def Call(self, fun, *args, **kwargs):
212 return fun(*args, **kwargs)
214 def Command(self, cmd, args="", prefix="", pipe=True, cwd=None):
215 return Command(cmd, args, prefix, pipe, cwd=cwd)
218 return sys.stdin.readline().strip()
220 def ReadURL(self, url, params=None):
221 # pylint: disable=E1121
222 url_fh = urllib2.urlopen(url, params, 60)
228 def ReadClusterFuzzAPI(self, api_key, **params):
229 params["api_key"] = api_key.strip()
230 params = urllib.urlencode(params)
232 headers = {"Content-type": "application/x-www-form-urlencoded"}
234 conn = httplib.HTTPSConnection("backend-dot-cluster-fuzz.appspot.com")
235 conn.request("POST", "/_api/", params, headers)
237 response = conn.getresponse()
238 data = response.read()
241 return json.loads(data)
244 print "ERROR: Could not read response. Is your key valid?"
247 def Sleep(self, seconds):
251 return datetime.date.today().strftime("%Y-%m-%d")
253 def GetUTCStamp(self):
254 return time.mktime(datetime.datetime.utcnow().timetuple())
256 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler()
259 class NoRetryException(Exception):
263 class VCInterface(object):
264 def InjectStep(self, step):
268 raise NotImplementedError()
271 raise NotImplementedError()
274 raise NotImplementedError()
276 def GetBranches(self):
277 raise NotImplementedError()
279 def MasterBranch(self):
280 raise NotImplementedError()
282 def CandidateBranch(self):
283 raise NotImplementedError()
285 def RemoteMasterBranch(self):
286 raise NotImplementedError()
288 def RemoteCandidateBranch(self):
289 raise NotImplementedError()
291 def RemoteBranch(self, name):
292 raise NotImplementedError()
295 raise NotImplementedError()
297 def Tag(self, tag, remote, message):
298 """Sets a tag for the current commit.
300 Assumptions: The commit already landed and the commit message is unique.
302 raise NotImplementedError()
305 class GitInterface(VCInterface):
310 self.step.Git("fetch")
313 return self.step.Git("tag").strip().splitlines()
315 def GetBranches(self):
316 # Get relevant remote branches, e.g. "branch-heads/3.25".
318 lambda s: re.match(r"^branch\-heads/\d+\.\d+$", s),
319 self.step.GitRemotes())
320 # Remove 'branch-heads/' prefix.
321 return map(lambda s: s[13:], branches)
323 def MasterBranch(self):
326 def CandidateBranch(self):
329 def RemoteMasterBranch(self):
330 return "origin/master"
332 def RemoteCandidateBranch(self):
333 return "origin/candidates"
335 def RemoteBranch(self, name):
336 # Assume that if someone "fully qualified" the ref, they know what they
338 if name.startswith('refs/'):
340 if name in ["candidates", "master"]:
341 return "refs/remotes/origin/%s" % name
343 # Check if branch is in heads.
344 if self.step.Git("show-ref refs/remotes/origin/%s" % name).strip():
345 return "refs/remotes/origin/%s" % name
346 except GitFailedException:
349 # Check if branch is in branch-heads.
350 if self.step.Git("show-ref refs/remotes/branch-heads/%s" % name).strip():
351 return "refs/remotes/branch-heads/%s" % name
352 except GitFailedException:
354 self.Die("Can't find remote of %s" % name)
356 def Tag(self, tag, remote, message):
357 # Wait for the commit to appear. Assumes unique commit message titles (this
358 # is the case for all automated merge and push commits - also no title is
359 # the prefix of another title).
361 for wait_interval in [3, 7, 15, 35, 45, 60]:
362 self.step.Git("fetch")
363 commit = self.step.GitLog(n=1, format="%H", grep=message, branch=remote)
366 print("The commit has not replicated to git. Waiting for %s seconds." %
368 self.step._side_effect_handler.Sleep(wait_interval)
370 self.step.Die("Couldn't determine commit for setting the tag. Maybe the "
371 "git updater is lagging behind?")
373 self.step.Git("tag %s %s" % (tag, commit))
374 self.step.Git("push origin %s" % tag)
377 self.step.GitCLLand()
380 class Step(GitRecipesMixin):
381 def __init__(self, text, number, config, state, options, handler):
383 self._number = number
384 self._config = config
386 self._options = options
387 self._side_effect_handler = handler
388 self.vc = GitInterface()
389 self.vc.InjectStep(self)
391 # The testing configuration might set a different default cwd.
392 self.default_cwd = (self._config.get("DEFAULT_CWD") or
393 os.path.join(self._options.work_dir, "v8"))
395 assert self._number >= 0
396 assert self._config is not None
397 assert self._state is not None
398 assert self._side_effect_handler is not None
400 def __getitem__(self, key):
401 # Convenience method to allow direct [] access on step classes for
402 # manipulating the backed state dict.
403 return self._state.get(key)
405 def __setitem__(self, key, value):
406 # Convenience method to allow direct [] access on step classes for
407 # manipulating the backed state dict.
408 self._state[key] = value
410 def Config(self, key):
411 return self._config[key]
415 state_file = "%s-state.json" % self._config["PERSISTFILE_BASENAME"]
416 if not self._state and os.path.exists(state_file):
417 self._state.update(json.loads(FileToText(state_file)))
419 print ">>> Step %d: %s" % (self._number, self._text)
421 return self.RunStep()
424 TextToFile(json.dumps(self._state), state_file)
426 def RunStep(self): # pragma: no cover
427 raise NotImplementedError
429 def Retry(self, cb, retry_on=None, wait_plan=None):
430 """ Retry a function.
432 cb: The function to retry.
433 retry_on: A callback that takes the result of the function and returns
434 True if the function should be retried. A function throwing an
435 exception is always retried.
436 wait_plan: A list of waiting delays between retries in seconds. The
437 maximum number of retries is len(wait_plan).
439 retry_on = retry_on or (lambda x: False)
440 wait_plan = list(wait_plan or [])
443 got_exception = False
446 except NoRetryException as e:
448 except Exception as e:
450 if got_exception or retry_on(result):
451 if not wait_plan: # pragma: no cover
452 raise Exception("Retried too often. Giving up. Reason: %s" %
454 wait_time = wait_plan.pop()
455 print "Waiting for %f seconds." % wait_time
456 self._side_effect_handler.Sleep(wait_time)
461 def ReadLine(self, default=None):
462 # Don't prompt in forced mode.
463 if self._options.force_readline_defaults and default is not None:
464 print "%s (forced)" % default
467 return self._side_effect_handler.ReadLine()
469 def Command(self, name, args, cwd=None):
470 cmd = lambda: self._side_effect_handler.Command(
471 name, args, "", True, cwd=cwd or self.default_cwd)
472 return self.Retry(cmd, None, [5])
474 def Git(self, args="", prefix="", pipe=True, retry_on=None, cwd=None):
475 cmd = lambda: self._side_effect_handler.Command(
476 "git", args, prefix, pipe, cwd=cwd or self.default_cwd)
477 result = self.Retry(cmd, retry_on, [5, 30])
479 raise GitFailedException("'git %s' failed." % args)
482 def Editor(self, args):
483 if self._options.requires_editor:
484 return self._side_effect_handler.Command(
485 os.environ["EDITOR"],
488 cwd=self.default_cwd)
490 def ReadURL(self, url, params=None, retry_on=None, wait_plan=None):
491 wait_plan = wait_plan or [3, 60, 600]
492 cmd = lambda: self._side_effect_handler.ReadURL(url, params)
493 return self.Retry(cmd, retry_on, wait_plan)
496 return self._side_effect_handler.GetDate()
498 def Die(self, msg=""):
500 print "Error: %s" % msg
504 def DieNoManualMode(self, msg=""):
505 if not self._options.manual: # pragma: no cover
506 msg = msg or "Only available in manual mode."
509 def Confirm(self, msg):
510 print "%s [Y/n] " % msg,
511 answer = self.ReadLine(default="Y")
512 return answer == "" or answer == "Y" or answer == "y"
514 def DeleteBranch(self, name, cwd=None):
515 for line in self.GitBranch(cwd=cwd).splitlines():
516 if re.match(r"\*?\s*%s$" % re.escape(name), line):
517 msg = "Branch %s exists, do you want to delete it?" % name
518 if self.Confirm(msg):
519 self.GitDeleteBranch(name, cwd=cwd)
520 print "Branch %s deleted." % name
522 msg = "Can't continue. Please delete branch %s and try again." % name
525 def InitialEnvironmentChecks(self, cwd):
526 # Cancel if this is not a git checkout.
527 if not os.path.exists(os.path.join(cwd, ".git")): # pragma: no cover
528 self.Die("This is not a git checkout, this script won't work for you.")
530 # Cancel if EDITOR is unset or not executable.
531 if (self._options.requires_editor and (not os.environ.get("EDITOR") or
533 "which", os.environ["EDITOR"]) is None)): # pragma: no cover
534 self.Die("Please set your EDITOR environment variable, you'll need it.")
536 def CommonPrepare(self):
537 # Check for a clean workdir.
538 if not self.GitIsWorkdirClean(): # pragma: no cover
539 self.Die("Workspace is not clean. Please commit or undo your changes.")
541 # Checkout master in case the script was left on a work branch.
542 self.GitCheckout('origin/master')
544 # Fetch unfetched revisions.
547 def PrepareBranch(self):
548 # Delete the branch that will be created later if it exists already.
549 self.DeleteBranch(self._config["BRANCHNAME"])
551 def CommonCleanup(self):
552 self.GitCheckout('origin/master')
553 self.GitDeleteBranch(self._config["BRANCHNAME"])
555 # Clean up all temporary files.
556 for f in glob.iglob("%s*" % self._config["PERSISTFILE_BASENAME"]):
557 if os.path.isfile(f):
562 def ReadAndPersistVersion(self, prefix=""):
563 def ReadAndPersist(var_name, def_name):
564 match = re.match(r"^#define %s\s+(\d*)" % def_name, line)
566 value = match.group(1)
567 self["%s%s" % (prefix, var_name)] = value
568 for line in LinesInFile(os.path.join(self.default_cwd, VERSION_FILE)):
569 for (var_name, def_name) in [("major", "V8_MAJOR_VERSION"),
570 ("minor", "V8_MINOR_VERSION"),
571 ("build", "V8_BUILD_NUMBER"),
572 ("patch", "V8_PATCH_LEVEL")]:
573 ReadAndPersist(var_name, def_name)
575 def WaitForLGTM(self):
576 print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit "
577 "your change. (If you need to iterate on the patch or double check "
578 "that it's sane, do so in another shell, but remember to not "
579 "change the headline of the uploaded CL.")
581 while answer != "LGTM":
583 answer = self.ReadLine(None if self._options.wait_for_lgtm else "LGTM")
585 print "That was not 'LGTM'."
587 def WaitForResolvingConflicts(self, patch_file):
588 print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", "
589 "or resolve the conflicts, stage *all* touched files with "
590 "'git add', and type \"RESOLVED<Return>\"")
591 self.DieNoManualMode()
593 while answer != "RESOLVED":
594 if answer == "ABORT":
595 self.Die("Applying the patch failed.")
597 print "That was not 'RESOLVED' or 'ABORT'."
599 answer = self.ReadLine()
601 # Takes a file containing the patch to apply as first argument.
602 def ApplyPatch(self, patch_file, revert=False):
604 self.GitApplyPatch(patch_file, revert)
605 except GitFailedException:
606 self.WaitForResolvingConflicts(patch_file)
608 def GetVersionTag(self, revision):
609 tag = self.Git("describe --tags %s" % revision).strip()
610 if VERSION_RE.match(tag):
615 def GetRecentReleases(self, max_age):
616 # Make sure tags are fetched.
617 self.Git("fetch origin +refs/tags/*:refs/tags/*")
620 time_now = int(self._side_effect_handler.GetUTCStamp())
622 # List every tag from a given period.
623 revisions = self.Git("rev-list --max-age=%d --tags" %
624 int(time_now - max_age)).strip()
626 # Filter out revisions who's tag is off by one or more commits.
627 return filter(lambda r: self.GetVersionTag(r), revisions.splitlines())
629 def GetLatestVersion(self):
630 # Use cached version if available.
631 if self["latest_version"]:
632 return self["latest_version"]
634 # Make sure tags are fetched.
635 self.Git("fetch origin +refs/tags/*:refs/tags/*")
636 version = sorted(filter(VERSION_RE.match, self.vc.GetTags()),
637 key=SortingKey, reverse=True)[0]
638 self["latest_version"] = version
641 def GetLatestRelease(self):
642 """The latest release is the git hash of the latest tagged version.
644 This revision should be rolled into chromium.
646 latest_version = self.GetLatestVersion()
648 # The latest release.
649 latest_hash = self.GitLog(n=1, format="%H", branch=latest_version)
653 def GetLatestReleaseBase(self, version=None):
654 """The latest release base is the latest revision that is covered in the
655 last change log file. It doesn't include cherry-picked patches.
657 latest_version = version or self.GetLatestVersion()
659 # Strip patch level if it exists.
660 latest_version = ".".join(latest_version.split(".")[:3])
662 # The latest release base.
663 latest_hash = self.GitLog(n=1, format="%H", branch=latest_version)
666 title = self.GitLog(n=1, format="%s", git_hash=latest_hash)
667 match = PUSH_MSG_GIT_RE.match(title)
669 # Legacy: In the old process there's one level of indirection. The
670 # version is on the candidates branch and points to the real release
671 # base on master through the commit message.
672 return match.group("git_rev")
673 match = PUSH_MSG_NEW_RE.match(title)
675 # This is a new-style v8 version branched from master. The commit
676 # "latest_hash" is the version-file change. Its parent is the release
678 return self.GitLog(n=1, format="%H", git_hash="%s^" % latest_hash)
680 self.Die("Unknown latest release: %s" % latest_hash)
682 def ArrayToVersion(self, prefix):
683 return ".".join([self[prefix + "major"],
684 self[prefix + "minor"],
685 self[prefix + "build"],
686 self[prefix + "patch"]])
688 def StoreVersion(self, version, prefix):
689 version_parts = version.split(".")
690 if len(version_parts) == 3:
691 version_parts.append("0")
692 major, minor, build, patch = version_parts
693 self[prefix + "major"] = major
694 self[prefix + "minor"] = minor
695 self[prefix + "build"] = build
696 self[prefix + "patch"] = patch
698 def SetVersion(self, version_file, prefix):
700 for line in FileToText(version_file).splitlines():
701 if line.startswith("#define V8_MAJOR_VERSION"):
702 line = re.sub("\d+$", self[prefix + "major"], line)
703 elif line.startswith("#define V8_MINOR_VERSION"):
704 line = re.sub("\d+$", self[prefix + "minor"], line)
705 elif line.startswith("#define V8_BUILD_NUMBER"):
706 line = re.sub("\d+$", self[prefix + "build"], line)
707 elif line.startswith("#define V8_PATCH_LEVEL"):
708 line = re.sub("\d+$", self[prefix + "patch"], line)
709 elif (self[prefix + "candidate"] and
710 line.startswith("#define V8_IS_CANDIDATE_VERSION")):
711 line = re.sub("\d+$", self[prefix + "candidate"], line)
712 output += "%s\n" % line
713 TextToFile(output, version_file)
716 class BootstrapStep(Step):
717 MESSAGE = "Bootstapping v8 checkout."
720 if os.path.realpath(self.default_cwd) == os.path.realpath(V8_BASE):
721 self.Die("Can't use v8 checkout with calling script as work checkout.")
722 # Directory containing the working v8 checkout.
723 if not os.path.exists(self._options.work_dir):
724 os.makedirs(self._options.work_dir)
725 if not os.path.exists(self.default_cwd):
726 self.Command("fetch", "v8", cwd=self._options.work_dir)
729 class UploadStep(Step):
730 MESSAGE = "Upload for code review."
733 if self._options.reviewer:
734 print "Using account %s for review." % self._options.reviewer
735 reviewer = self._options.reviewer
737 print "Please enter the email address of a V8 reviewer for your patch: ",
738 self.DieNoManualMode("A reviewer must be specified in forced mode.")
739 reviewer = self.ReadLine()
740 self.GitUpload(reviewer, self._options.author, self._options.force_upload,
741 bypass_hooks=self._options.bypass_upload_hooks,
745 class DetermineV8Sheriff(Step):
746 MESSAGE = "Determine the V8 sheriff for code review."
749 self["sheriff"] = None
750 if not self._options.sheriff: # pragma: no cover
753 # The sheriff determined by the rotation on the waterfall has a
754 # @google.com account.
755 url = "https://chromium-build.appspot.com/p/chromium/sheriff_v8.js"
756 match = re.match(r"document\.write\('(\w+)'\)", self.ReadURL(url))
758 # If "channel is sheriff", we can't match an account.
760 g_name = match.group(1)
761 # Optimistically assume that google and chromium account name are the
763 self["sheriff"] = g_name + "@chromium.org"
764 self._options.reviewer = ("%s,%s" %
765 (self["sheriff"], self._options.reviewer))
766 print "Found active sheriff: %s" % self["sheriff"]
768 print "No active sheriff found."
771 def MakeStep(step_class=Step, number=0, state=None, config=None,
772 options=None, side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER):
773 # Allow to pass in empty dictionaries.
774 state = state if state is not None else {}
775 config = config if config is not None else {}
778 message = step_class.MESSAGE
779 except AttributeError:
780 message = step_class.__name__
782 return step_class(message, number=number, config=config,
783 state=state, options=options,
784 handler=side_effect_handler)
787 class ScriptsBase(object):
790 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER,
792 self._config = config or self._Config()
793 self._side_effect_handler = side_effect_handler
794 self._state = state if state is not None else {}
796 def _Description(self):
799 def _PrepareOptions(self, parser):
802 def _ProcessOptions(self, options):
805 def _Steps(self): # pragma: no cover
806 raise Exception("Not implemented.")
811 def MakeOptions(self, args=None):
812 parser = argparse.ArgumentParser(description=self._Description())
813 parser.add_argument("-a", "--author", default="",
814 help="The author email used for rietveld.")
815 parser.add_argument("--dry-run", default=False, action="store_true",
816 help="Perform only read-only actions.")
817 parser.add_argument("-r", "--reviewer", default="",
818 help="The account name to be used for reviews.")
819 parser.add_argument("--sheriff", default=False, action="store_true",
820 help=("Determine current sheriff to review CLs. On "
821 "success, this will overwrite the reviewer "
823 parser.add_argument("-s", "--step",
824 help="Specify the step where to start work. Default: 0.",
826 parser.add_argument("--work-dir",
827 help=("Location where to bootstrap a working v8 "
829 self._PrepareOptions(parser)
831 if args is None: # pragma: no cover
832 options = parser.parse_args()
834 options = parser.parse_args(args)
836 # Process common options.
837 if options.step < 0: # pragma: no cover
838 print "Bad step number %d" % options.step
842 # Defaults for options, common to all scripts.
843 options.manual = getattr(options, "manual", True)
844 options.force = getattr(options, "force", False)
845 options.bypass_upload_hooks = False
848 options.requires_editor = not options.force
849 options.wait_for_lgtm = not options.force
850 options.force_readline_defaults = not options.manual
851 options.force_upload = not options.manual
853 # Process script specific options.
854 if not self._ProcessOptions(options):
858 if not options.work_dir:
859 options.work_dir = "/tmp/v8-release-scripts-work-dir"
862 def RunSteps(self, step_classes, args=None):
863 options = self.MakeOptions(args)
867 state_file = "%s-state.json" % self._config["PERSISTFILE_BASENAME"]
868 if options.step == 0 and os.path.exists(state_file):
869 os.remove(state_file)
872 for (number, step_class) in enumerate([BootstrapStep] + step_classes):
873 steps.append(MakeStep(step_class, number, self._state, self._config,
874 options, self._side_effect_handler))
875 for step in steps[options.step:]:
880 def Run(self, args=None):
881 return self.RunSteps(self._Steps(), args)