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 PUSH_MSG_GIT_RE = re.compile(r".* \(based on (?P<git_rev>[a-fA-F0-9]+)\)$")
50 PUSH_MSG_NEW_RE = re.compile(r"^Version \d+\.\d+\.\d+$")
51 VERSION_FILE = os.path.join("src", "version.cc")
52 VERSION_RE = re.compile(r"^\d+\.\d+\.\d+(?:\.\d+)?$")
55 V8_BASE = os.path.dirname(
56 os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
59 def TextToFile(text, file_name):
60 with open(file_name, "w") as f:
64 def AppendToFile(text, file_name):
65 with open(file_name, "a") as f:
69 def LinesInFile(file_name):
70 with open(file_name) as f:
75 def FileToText(file_name):
76 with open(file_name) as f:
80 def MSub(rexp, replacement, text):
81 return re.sub(rexp, replacement, text, flags=re.MULTILINE)
85 # Replace tabs and remove surrounding space.
86 line = re.sub(r"\t", r" ", line.strip())
88 # Format with 8 characters indentation and line width 80.
89 return textwrap.fill(line, width=80, initial_indent=" ",
90 subsequent_indent=" ")
93 def MakeComment(text):
94 return MSub(r"^( ?)", "#", text)
97 def StripComments(text):
98 # Use split not splitlines to keep terminal newlines.
99 return "\n".join(filter(lambda x: not x.startswith("#"), text.split("\n")))
102 def MakeChangeLogBody(commit_messages, auto_format=False):
105 for (title, body, author) in commit_messages:
106 # TODO(machenbach): Better check for reverts. A revert should remove the
107 # original CL from the actual log entry.
108 title = title.strip()
110 # Only add commits that set the LOG flag correctly.
111 log_exp = r"^[ \t]*LOG[ \t]*=[ \t]*(?:(?:Y(?:ES)?)|TRUE)"
112 if not re.search(log_exp, body, flags=re.I | re.M):
114 # Never include reverts.
115 if title.startswith("Revert "):
117 # Don't include duplicates.
118 if title in added_titles:
121 # Add and format the commit's title and bug reference. Move dot to the end.
122 added_titles.add(title)
123 raw_title = re.sub(r"(\.|\?|!)$", "", title)
124 bug_reference = MakeChangeLogBugReference(body)
125 space = " " if bug_reference else ""
126 result += "%s\n" % Fill80("%s%s%s." % (raw_title, space, bug_reference))
128 # Append the commit's author for reference if not in auto-format mode.
130 result += "%s\n" % Fill80("(%s)" % author.strip())
136 def MakeChangeLogBugReference(body):
137 """Grep for "BUG=xxxx" lines in the commit message and convert them to
144 ref = re.match(r"^BUG[ \t]*=[ \t]*(.+)$", text.strip())
147 for bug in ref.group(1).split(","):
149 match = re.match(r"^v8:(\d+)$", bug)
150 if match: v8bugs.append(int(match.group(1)))
152 match = re.match(r"^(?:chromium:)?(\d+)$", bug)
153 if match: crbugs.append(int(match.group(1)))
155 # Add issues to crbugs and v8bugs.
156 map(AddIssues, body.splitlines())
158 # Filter duplicates, sort, stringify.
159 crbugs = map(str, sorted(set(crbugs)))
160 v8bugs = map(str, sorted(set(v8bugs)))
163 def FormatIssues(prefix, bugs):
165 plural = "s" if len(bugs) > 1 else ""
166 bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs)))
168 FormatIssues("", v8bugs)
169 FormatIssues("Chromium ", crbugs)
171 if len(bug_groups) > 0:
172 return "(%s)" % ", ".join(bug_groups)
177 def SortingKey(version):
178 """Key for sorting version number strings: '3.11' > '3.2.1.1'"""
179 version_keys = map(int, version.split("."))
180 # Fill up to full version numbers to normalize comparison.
181 while len(version_keys) < 4: # pragma: no cover
182 version_keys.append(0)
184 return ".".join(map("{0:04d}".format, version_keys))
187 # Some commands don't like the pipe, e.g. calling vi from within the script or
188 # from subscripts like git cl upload.
189 def Command(cmd, args="", prefix="", pipe=True, cwd=None):
190 cwd = cwd or os.getcwd()
191 # TODO(machenbach): Use timeout.
192 cmd_line = "%s %s %s" % (prefix, cmd, args)
193 print "Command: %s" % cmd_line
198 return subprocess.check_output(cmd_line, shell=True, cwd=cwd)
200 return subprocess.check_call(cmd_line, shell=True, cwd=cwd)
201 except subprocess.CalledProcessError:
208 # Wrapper for side effects.
209 class SideEffectHandler(object): # pragma: no cover
210 def Call(self, fun, *args, **kwargs):
211 return fun(*args, **kwargs)
213 def Command(self, cmd, args="", prefix="", pipe=True, cwd=None):
214 return Command(cmd, args, prefix, pipe, cwd=cwd)
217 return sys.stdin.readline().strip()
219 def ReadURL(self, url, params=None):
220 # pylint: disable=E1121
221 url_fh = urllib2.urlopen(url, params, 60)
227 def ReadClusterFuzzAPI(self, api_key, **params):
228 params["api_key"] = api_key.strip()
229 params = urllib.urlencode(params)
231 headers = {"Content-type": "application/x-www-form-urlencoded"}
233 conn = httplib.HTTPSConnection("backend-dot-cluster-fuzz.appspot.com")
234 conn.request("POST", "/_api/", params, headers)
236 response = conn.getresponse()
237 data = response.read()
240 return json.loads(data)
243 print "ERROR: Could not read response. Is your key valid?"
246 def Sleep(self, seconds):
250 return datetime.date.today().strftime("%Y-%m-%d")
252 def GetUTCStamp(self):
253 return time.mktime(datetime.datetime.utcnow().timetuple())
255 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler()
258 class NoRetryException(Exception):
262 class VCInterface(object):
263 def InjectStep(self, step):
267 raise NotImplementedError()
270 raise NotImplementedError()
273 raise NotImplementedError()
275 def GetBranches(self):
276 raise NotImplementedError()
278 def MasterBranch(self):
279 raise NotImplementedError()
281 def CandidateBranch(self):
282 raise NotImplementedError()
284 def RemoteMasterBranch(self):
285 raise NotImplementedError()
287 def RemoteCandidateBranch(self):
288 raise NotImplementedError()
290 def RemoteBranch(self, name):
291 raise NotImplementedError()
294 raise NotImplementedError()
296 def Tag(self, tag, remote, message):
297 """Sets a tag for the current commit.
299 Assumptions: The commit already landed and the commit message is unique.
301 raise NotImplementedError()
304 class GitInterface(VCInterface):
309 self.step.Git("fetch")
312 return self.step.Git("tag").strip().splitlines()
314 def GetBranches(self):
315 # Get relevant remote branches, e.g. "branch-heads/3.25".
317 lambda s: re.match(r"^branch\-heads/\d+\.\d+$", s),
318 self.step.GitRemotes())
319 # Remove 'branch-heads/' prefix.
320 return map(lambda s: s[13:], branches)
322 def MasterBranch(self):
325 def CandidateBranch(self):
328 def RemoteMasterBranch(self):
329 return "origin/master"
331 def RemoteCandidateBranch(self):
332 return "origin/candidates"
334 def RemoteBranch(self, name):
335 # Assume that if someone "fully qualified" the ref, they know what they
337 if name.startswith('refs/'):
339 if name in ["candidates", "master"]:
340 return "refs/remotes/origin/%s" % name
342 # Check if branch is in heads.
343 if self.step.Git("show-ref refs/remotes/origin/%s" % name).strip():
344 return "refs/remotes/origin/%s" % name
345 except GitFailedException:
348 # Check if branch is in branch-heads.
349 if self.step.Git("show-ref refs/remotes/branch-heads/%s" % name).strip():
350 return "refs/remotes/branch-heads/%s" % name
351 except GitFailedException:
353 self.Die("Can't find remote of %s" % name)
355 def Tag(self, tag, remote, message):
356 # Wait for the commit to appear. Assumes unique commit message titles (this
357 # is the case for all automated merge and push commits - also no title is
358 # the prefix of another title).
360 for wait_interval in [3, 7, 15, 35, 45, 60]:
361 self.step.Git("fetch")
362 commit = self.step.GitLog(n=1, format="%H", grep=message, branch=remote)
365 print("The commit has not replicated to git. Waiting for %s seconds." %
367 self.step._side_effect_handler.Sleep(wait_interval)
369 self.step.Die("Couldn't determine commit for setting the tag. Maybe the "
370 "git updater is lagging behind?")
372 self.step.Git("tag %s %s" % (tag, commit))
373 self.step.Git("push origin %s" % tag)
376 self.step.GitCLLand()
379 class Step(GitRecipesMixin):
380 def __init__(self, text, number, config, state, options, handler):
382 self._number = number
383 self._config = config
385 self._options = options
386 self._side_effect_handler = handler
387 self.vc = GitInterface()
388 self.vc.InjectStep(self)
390 # The testing configuration might set a different default cwd.
391 self.default_cwd = (self._config.get("DEFAULT_CWD") or
392 os.path.join(self._options.work_dir, "v8"))
394 assert self._number >= 0
395 assert self._config is not None
396 assert self._state is not None
397 assert self._side_effect_handler is not None
399 def __getitem__(self, key):
400 # Convenience method to allow direct [] access on step classes for
401 # manipulating the backed state dict.
402 return self._state.get(key)
404 def __setitem__(self, key, value):
405 # Convenience method to allow direct [] access on step classes for
406 # manipulating the backed state dict.
407 self._state[key] = value
409 def Config(self, key):
410 return self._config[key]
414 state_file = "%s-state.json" % self._config["PERSISTFILE_BASENAME"]
415 if not self._state and os.path.exists(state_file):
416 self._state.update(json.loads(FileToText(state_file)))
418 print ">>> Step %d: %s" % (self._number, self._text)
420 return self.RunStep()
423 TextToFile(json.dumps(self._state), state_file)
425 def RunStep(self): # pragma: no cover
426 raise NotImplementedError
428 def Retry(self, cb, retry_on=None, wait_plan=None):
429 """ Retry a function.
431 cb: The function to retry.
432 retry_on: A callback that takes the result of the function and returns
433 True if the function should be retried. A function throwing an
434 exception is always retried.
435 wait_plan: A list of waiting delays between retries in seconds. The
436 maximum number of retries is len(wait_plan).
438 retry_on = retry_on or (lambda x: False)
439 wait_plan = list(wait_plan or [])
442 got_exception = False
445 except NoRetryException as e:
447 except Exception as e:
449 if got_exception or retry_on(result):
450 if not wait_plan: # pragma: no cover
451 raise Exception("Retried too often. Giving up. Reason: %s" %
453 wait_time = wait_plan.pop()
454 print "Waiting for %f seconds." % wait_time
455 self._side_effect_handler.Sleep(wait_time)
460 def ReadLine(self, default=None):
461 # Don't prompt in forced mode.
462 if self._options.force_readline_defaults and default is not None:
463 print "%s (forced)" % default
466 return self._side_effect_handler.ReadLine()
468 def Command(self, name, args, cwd=None):
469 cmd = lambda: self._side_effect_handler.Command(
470 name, args, "", True, cwd=cwd or self.default_cwd)
471 return self.Retry(cmd, None, [5])
473 def Git(self, args="", prefix="", pipe=True, retry_on=None, cwd=None):
474 cmd = lambda: self._side_effect_handler.Command(
475 "git", args, prefix, pipe, cwd=cwd or self.default_cwd)
476 result = self.Retry(cmd, retry_on, [5, 30])
478 raise GitFailedException("'git %s' failed." % args)
481 def Editor(self, args):
482 if self._options.requires_editor:
483 return self._side_effect_handler.Command(
484 os.environ["EDITOR"],
487 cwd=self.default_cwd)
489 def ReadURL(self, url, params=None, retry_on=None, wait_plan=None):
490 wait_plan = wait_plan or [3, 60, 600]
491 cmd = lambda: self._side_effect_handler.ReadURL(url, params)
492 return self.Retry(cmd, retry_on, wait_plan)
495 return self._side_effect_handler.GetDate()
497 def Die(self, msg=""):
499 print "Error: %s" % msg
503 def DieNoManualMode(self, msg=""):
504 if not self._options.manual: # pragma: no cover
505 msg = msg or "Only available in manual mode."
508 def Confirm(self, msg):
509 print "%s [Y/n] " % msg,
510 answer = self.ReadLine(default="Y")
511 return answer == "" or answer == "Y" or answer == "y"
513 def DeleteBranch(self, name):
514 for line in self.GitBranch().splitlines():
515 if re.match(r"\*?\s*%s$" % re.escape(name), line):
516 msg = "Branch %s exists, do you want to delete it?" % name
517 if self.Confirm(msg):
518 self.GitDeleteBranch(name)
519 print "Branch %s deleted." % name
521 msg = "Can't continue. Please delete branch %s and try again." % name
524 def InitialEnvironmentChecks(self, cwd):
525 # Cancel if this is not a git checkout.
526 if not os.path.exists(os.path.join(cwd, ".git")): # pragma: no cover
527 self.Die("This is not a git checkout, this script won't work for you.")
529 # Cancel if EDITOR is unset or not executable.
530 if (self._options.requires_editor and (not os.environ.get("EDITOR") or
532 "which", os.environ["EDITOR"]) is None)): # pragma: no cover
533 self.Die("Please set your EDITOR environment variable, you'll need it.")
535 def CommonPrepare(self):
536 # Check for a clean workdir.
537 if not self.GitIsWorkdirClean(): # pragma: no cover
538 self.Die("Workspace is not clean. Please commit or undo your changes.")
540 # Persist current branch.
541 self["current_branch"] = self.GitCurrentBranch()
543 # Fetch unfetched revisions.
546 def PrepareBranch(self):
547 # Delete the branch that will be created later if it exists already.
548 self.DeleteBranch(self._config["BRANCHNAME"])
550 def CommonCleanup(self):
551 if ' ' in self["current_branch"]:
552 self.GitCheckout('master')
554 self.GitCheckout(self["current_branch"])
555 if self._config["BRANCHNAME"] != self["current_branch"]:
556 self.GitDeleteBranch(self._config["BRANCHNAME"])
558 # Clean up all temporary files.
559 for f in glob.iglob("%s*" % self._config["PERSISTFILE_BASENAME"]):
560 if os.path.isfile(f):
565 def ReadAndPersistVersion(self, prefix=""):
566 def ReadAndPersist(var_name, def_name):
567 match = re.match(r"^#define %s\s+(\d*)" % def_name, line)
569 value = match.group(1)
570 self["%s%s" % (prefix, var_name)] = value
571 for line in LinesInFile(os.path.join(self.default_cwd, VERSION_FILE)):
572 for (var_name, def_name) in [("major", "MAJOR_VERSION"),
573 ("minor", "MINOR_VERSION"),
574 ("build", "BUILD_NUMBER"),
575 ("patch", "PATCH_LEVEL")]:
576 ReadAndPersist(var_name, def_name)
578 def WaitForLGTM(self):
579 print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit "
580 "your change. (If you need to iterate on the patch or double check "
581 "that it's sane, do so in another shell, but remember to not "
582 "change the headline of the uploaded CL.")
584 while answer != "LGTM":
586 answer = self.ReadLine(None if self._options.wait_for_lgtm else "LGTM")
588 print "That was not 'LGTM'."
590 def WaitForResolvingConflicts(self, patch_file):
591 print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", "
592 "or resolve the conflicts, stage *all* touched files with "
593 "'git add', and type \"RESOLVED<Return>\"")
594 self.DieNoManualMode()
596 while answer != "RESOLVED":
597 if answer == "ABORT":
598 self.Die("Applying the patch failed.")
600 print "That was not 'RESOLVED' or 'ABORT'."
602 answer = self.ReadLine()
604 # Takes a file containing the patch to apply as first argument.
605 def ApplyPatch(self, patch_file, revert=False):
607 self.GitApplyPatch(patch_file, revert)
608 except GitFailedException:
609 self.WaitForResolvingConflicts(patch_file)
611 def GetVersionTag(self, revision):
612 tag = self.Git("describe --tags %s" % revision).strip()
613 if VERSION_RE.match(tag):
618 def GetRecentReleases(self, max_age):
619 # Make sure tags are fetched.
620 self.Git("fetch origin +refs/tags/*:refs/tags/*")
623 time_now = int(self._side_effect_handler.GetUTCStamp())
625 # List every tag from a given period.
626 revisions = self.Git("rev-list --max-age=%d --tags" %
627 int(time_now - max_age)).strip()
629 # Filter out revisions who's tag is off by one or more commits.
630 return filter(lambda r: self.GetVersionTag(r), revisions.splitlines())
632 def GetLatestVersion(self):
633 # Use cached version if available.
634 if self["latest_version"]:
635 return self["latest_version"]
637 # Make sure tags are fetched.
638 self.Git("fetch origin +refs/tags/*:refs/tags/*")
639 version = sorted(filter(VERSION_RE.match, self.vc.GetTags()),
640 key=SortingKey, reverse=True)[0]
641 self["latest_version"] = version
644 def GetLatestRelease(self):
645 """The latest release is the git hash of the latest tagged version.
647 This revision should be rolled into chromium.
649 latest_version = self.GetLatestVersion()
651 # The latest release.
652 latest_hash = self.GitLog(n=1, format="%H", branch=latest_version)
656 def GetLatestReleaseBase(self, version=None):
657 """The latest release base is the latest revision that is covered in the
658 last change log file. It doesn't include cherry-picked patches.
660 latest_version = version or self.GetLatestVersion()
662 # Strip patch level if it exists.
663 latest_version = ".".join(latest_version.split(".")[:3])
665 # The latest release base.
666 latest_hash = self.GitLog(n=1, format="%H", branch=latest_version)
669 title = self.GitLog(n=1, format="%s", git_hash=latest_hash)
670 match = PUSH_MSG_GIT_RE.match(title)
672 # Legacy: In the old process there's one level of indirection. The
673 # version is on the candidates branch and points to the real release
674 # base on master through the commit message.
675 return match.group("git_rev")
676 match = PUSH_MSG_NEW_RE.match(title)
678 # This is a new-style v8 version branched from master. The commit
679 # "latest_hash" is the version-file change. Its parent is the release
681 return self.GitLog(n=1, format="%H", git_hash="%s^" % latest_hash)
683 self.Die("Unknown latest release: %s" % latest_hash)
685 def ArrayToVersion(self, prefix):
686 return ".".join([self[prefix + "major"],
687 self[prefix + "minor"],
688 self[prefix + "build"],
689 self[prefix + "patch"]])
691 def StoreVersion(self, version, prefix):
692 version_parts = version.split(".")
693 if len(version_parts) == 3:
694 version_parts.append("0")
695 major, minor, build, patch = version_parts
696 self[prefix + "major"] = major
697 self[prefix + "minor"] = minor
698 self[prefix + "build"] = build
699 self[prefix + "patch"] = patch
701 def SetVersion(self, version_file, prefix):
703 for line in FileToText(version_file).splitlines():
704 if line.startswith("#define MAJOR_VERSION"):
705 line = re.sub("\d+$", self[prefix + "major"], line)
706 elif line.startswith("#define MINOR_VERSION"):
707 line = re.sub("\d+$", self[prefix + "minor"], line)
708 elif line.startswith("#define BUILD_NUMBER"):
709 line = re.sub("\d+$", self[prefix + "build"], line)
710 elif line.startswith("#define PATCH_LEVEL"):
711 line = re.sub("\d+$", self[prefix + "patch"], line)
712 elif (self[prefix + "candidate"] and
713 line.startswith("#define IS_CANDIDATE_VERSION")):
714 line = re.sub("\d+$", self[prefix + "candidate"], line)
715 output += "%s\n" % line
716 TextToFile(output, version_file)
719 class BootstrapStep(Step):
720 MESSAGE = "Bootstapping v8 checkout."
723 if os.path.realpath(self.default_cwd) == os.path.realpath(V8_BASE):
724 self.Die("Can't use v8 checkout with calling script as work checkout.")
725 # Directory containing the working v8 checkout.
726 if not os.path.exists(self._options.work_dir):
727 os.makedirs(self._options.work_dir)
728 if not os.path.exists(self.default_cwd):
729 self.Command("fetch", "v8", cwd=self._options.work_dir)
732 class UploadStep(Step):
733 MESSAGE = "Upload for code review."
736 if self._options.reviewer:
737 print "Using account %s for review." % self._options.reviewer
738 reviewer = self._options.reviewer
740 print "Please enter the email address of a V8 reviewer for your patch: ",
741 self.DieNoManualMode("A reviewer must be specified in forced mode.")
742 reviewer = self.ReadLine()
743 self.GitUpload(reviewer, self._options.author, self._options.force_upload,
744 bypass_hooks=self._options.bypass_upload_hooks,
748 class DetermineV8Sheriff(Step):
749 MESSAGE = "Determine the V8 sheriff for code review."
752 self["sheriff"] = None
753 if not self._options.sheriff: # pragma: no cover
757 # The googlers mapping maps @google.com accounts to @chromium.org
759 googlers = imp.load_source('googlers_mapping',
760 self._options.googlers_mapping)
761 googlers = googlers.list_to_dict(googlers.get_list())
762 except: # pragma: no cover
763 print "Skip determining sheriff without googler mapping."
766 # The sheriff determined by the rotation on the waterfall has a
767 # @google.com account.
768 url = "https://chromium-build.appspot.com/p/chromium/sheriff_v8.js"
769 match = re.match(r"document\.write\('(\w+)'\)", self.ReadURL(url))
771 # If "channel is sheriff", we can't match an account.
773 g_name = match.group(1)
774 self["sheriff"] = googlers.get(g_name + "@google.com",
775 g_name + "@chromium.org")
776 self._options.reviewer = self["sheriff"]
777 print "Found active sheriff: %s" % self["sheriff"]
779 print "No active sheriff found."
782 def MakeStep(step_class=Step, number=0, state=None, config=None,
783 options=None, side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER):
784 # Allow to pass in empty dictionaries.
785 state = state if state is not None else {}
786 config = config if config is not None else {}
789 message = step_class.MESSAGE
790 except AttributeError:
791 message = step_class.__name__
793 return step_class(message, number=number, config=config,
794 state=state, options=options,
795 handler=side_effect_handler)
798 class ScriptsBase(object):
801 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER,
803 self._config = config or self._Config()
804 self._side_effect_handler = side_effect_handler
805 self._state = state if state is not None else {}
807 def _Description(self):
810 def _PrepareOptions(self, parser):
813 def _ProcessOptions(self, options):
816 def _Steps(self): # pragma: no cover
817 raise Exception("Not implemented.")
822 def MakeOptions(self, args=None):
823 parser = argparse.ArgumentParser(description=self._Description())
824 parser.add_argument("-a", "--author", default="",
825 help="The author email used for rietveld.")
826 parser.add_argument("--dry-run", default=False, action="store_true",
827 help="Perform only read-only actions.")
828 parser.add_argument("-g", "--googlers-mapping",
829 help="Path to the script mapping google accounts.")
830 parser.add_argument("-r", "--reviewer", default="",
831 help="The account name to be used for reviews.")
832 parser.add_argument("--sheriff", default=False, action="store_true",
833 help=("Determine current sheriff to review CLs. On "
834 "success, this will overwrite the reviewer "
836 parser.add_argument("-s", "--step",
837 help="Specify the step where to start work. Default: 0.",
839 parser.add_argument("--work-dir",
840 help=("Location where to bootstrap a working v8 "
842 self._PrepareOptions(parser)
844 if args is None: # pragma: no cover
845 options = parser.parse_args()
847 options = parser.parse_args(args)
849 # Process common options.
850 if options.step < 0: # pragma: no cover
851 print "Bad step number %d" % options.step
854 if options.sheriff and not options.googlers_mapping: # pragma: no cover
855 print "To determine the current sheriff, requires the googler mapping"
859 # Defaults for options, common to all scripts.
860 options.manual = getattr(options, "manual", True)
861 options.force = getattr(options, "force", False)
862 options.bypass_upload_hooks = False
865 options.requires_editor = not options.force
866 options.wait_for_lgtm = not options.force
867 options.force_readline_defaults = not options.manual
868 options.force_upload = not options.manual
870 # Process script specific options.
871 if not self._ProcessOptions(options):
875 if not options.work_dir:
876 options.work_dir = "/tmp/v8-release-scripts-work-dir"
879 def RunSteps(self, step_classes, args=None):
880 options = self.MakeOptions(args)
884 state_file = "%s-state.json" % self._config["PERSISTFILE_BASENAME"]
885 if options.step == 0 and os.path.exists(state_file):
886 os.remove(state_file)
889 for (number, step_class) in enumerate([BootstrapStep] + step_classes):
890 steps.append(MakeStep(step_class, number, self._state, self._config,
891 options, self._side_effect_handler))
892 for step in steps[options.step:]:
897 def Run(self, args=None):
898 return self.RunSteps(self._Steps(), args)