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 VERSION_FILE = os.path.join("src", "version.cc")
51 V8_BASE = os.path.dirname(
52 os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
55 def TextToFile(text, file_name):
56 with open(file_name, "w") as f:
60 def AppendToFile(text, file_name):
61 with open(file_name, "a") as f:
65 def LinesInFile(file_name):
66 with open(file_name) as f:
71 def FileToText(file_name):
72 with open(file_name) as f:
76 def MSub(rexp, replacement, text):
77 return re.sub(rexp, replacement, text, flags=re.MULTILINE)
81 # Replace tabs and remove surrounding space.
82 line = re.sub(r"\t", r" ", line.strip())
84 # Format with 8 characters indentation and line width 80.
85 return textwrap.fill(line, width=80, initial_indent=" ",
86 subsequent_indent=" ")
89 def MakeComment(text):
90 return MSub(r"^( ?)", "#", text)
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")))
98 def MakeChangeLogBody(commit_messages, auto_format=False):
101 for (title, body, author) in commit_messages:
102 # TODO(machenbach): Better check for reverts. A revert should remove the
103 # original CL from the actual log entry.
104 title = title.strip()
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):
110 # Never include reverts.
111 if title.startswith("Revert "):
113 # Don't include duplicates.
114 if title in added_titles:
117 # Add and format the commit's title and bug reference. Move dot to the end.
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))
124 # Append the commit's author for reference if not in auto-format mode.
126 result += "%s\n" % Fill80("(%s)" % author.strip())
132 def MakeChangeLogBugReference(body):
133 """Grep for "BUG=xxxx" lines in the commit message and convert them to
140 ref = re.match(r"^BUG[ \t]*=[ \t]*(.+)$", text.strip())
143 for bug in ref.group(1).split(","):
145 match = re.match(r"^v8:(\d+)$", bug)
146 if match: v8bugs.append(int(match.group(1)))
148 match = re.match(r"^(?:chromium:)?(\d+)$", bug)
149 if match: crbugs.append(int(match.group(1)))
151 # Add issues to crbugs and v8bugs.
152 map(AddIssues, body.splitlines())
154 # Filter duplicates, sort, stringify.
155 crbugs = map(str, sorted(set(crbugs)))
156 v8bugs = map(str, sorted(set(v8bugs)))
159 def FormatIssues(prefix, bugs):
161 plural = "s" if len(bugs) > 1 else ""
162 bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs)))
164 FormatIssues("", v8bugs)
165 FormatIssues("Chromium ", crbugs)
167 if len(bug_groups) > 0:
168 return "(%s)" % ", ".join(bug_groups)
173 def SortingKey(version):
174 """Key for sorting version number strings: '3.11' > '3.2.1.1'"""
175 version_keys = map(int, version.split("."))
176 # Fill up to full version numbers to normalize comparison.
177 while len(version_keys) < 4: # pragma: no cover
178 version_keys.append(0)
180 return ".".join(map("{0:04d}".format, version_keys))
183 # Some commands don't like the pipe, e.g. calling vi from within the script or
184 # from subscripts like git cl upload.
185 def Command(cmd, args="", prefix="", pipe=True, cwd=None):
186 cwd = cwd or os.getcwd()
187 # TODO(machenbach): Use timeout.
188 cmd_line = "%s %s %s" % (prefix, cmd, args)
189 print "Command: %s" % cmd_line
194 return subprocess.check_output(cmd_line, shell=True, cwd=cwd)
196 return subprocess.check_call(cmd_line, shell=True, cwd=cwd)
197 except subprocess.CalledProcessError:
204 # Wrapper for side effects.
205 class SideEffectHandler(object): # pragma: no cover
206 def Call(self, fun, *args, **kwargs):
207 return fun(*args, **kwargs)
209 def Command(self, cmd, args="", prefix="", pipe=True, cwd=None):
210 return Command(cmd, args, prefix, pipe, cwd=cwd)
213 return sys.stdin.readline().strip()
215 def ReadURL(self, url, params=None):
216 # pylint: disable=E1121
217 url_fh = urllib2.urlopen(url, params, 60)
223 def ReadClusterFuzzAPI(self, api_key, **params):
224 params["api_key"] = api_key.strip()
225 params = urllib.urlencode(params)
227 headers = {"Content-type": "application/x-www-form-urlencoded"}
229 conn = httplib.HTTPSConnection("backend-dot-cluster-fuzz.appspot.com")
230 conn.request("POST", "/_api/", params, headers)
232 response = conn.getresponse()
233 data = response.read()
236 return json.loads(data)
239 print "ERROR: Could not read response. Is your key valid?"
242 def Sleep(self, seconds):
246 return datetime.date.today().strftime("%Y-%m-%d")
248 def GetUTCStamp(self):
249 return time.mktime(datetime.datetime.utcnow().timetuple())
251 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler()
254 class NoRetryException(Exception):
258 class VCInterface(object):
259 def InjectStep(self, step):
263 raise NotImplementedError()
266 raise NotImplementedError()
269 raise NotImplementedError()
271 def GetBranches(self):
272 raise NotImplementedError()
274 def GitSvn(self, hsh, branch=""):
275 raise NotImplementedError()
277 def SvnGit(self, rev, branch=""):
278 raise NotImplementedError()
280 def MasterBranch(self):
281 raise NotImplementedError()
283 def CandidateBranch(self):
284 raise NotImplementedError()
286 def RemoteMasterBranch(self):
287 raise NotImplementedError()
289 def RemoteCandidateBranch(self):
290 raise NotImplementedError()
292 def RemoteBranch(self, name):
293 raise NotImplementedError()
296 raise NotImplementedError()
299 raise NotImplementedError()
301 # TODO(machenbach): There is some svn knowledge in this interface. In svn,
302 # tag and commit are different remote commands, while in git we would commit
303 # and tag locally and then push/land in one unique step.
304 def Tag(self, tag, remote, message):
305 """Sets a tag for the current commit.
307 Assumptions: The commit already landed and the commit message is unique.
309 raise NotImplementedError()
312 class GitSvnInterface(VCInterface):
314 self.step.GitSVNRebase()
317 self.step.GitSVNFetch()
321 tags = filter(lambda s: re.match(r"^svn/tags/[\d+\.]+$", s),
322 self.step.GitRemotes())
324 # Remove 'svn/tags/' prefix.
325 return map(lambda s: s[9:], tags)
327 def GetBranches(self):
328 # Get relevant remote branches, e.g. "svn/3.25".
329 branches = filter(lambda s: re.match(r"^svn/\d+\.\d+$", s),
330 self.step.GitRemotes())
331 # Remove 'svn/' prefix.
332 return map(lambda s: s[4:], branches)
334 def GitSvn(self, hsh, branch=""):
335 return self.step.GitSVNFindSVNRev(hsh, branch)
337 def SvnGit(self, rev, branch=""):
338 return self.step.GitSVNFindGitHash(rev, branch)
340 def MasterBranch(self):
341 return "bleeding_edge"
343 def CandidateBranch(self):
346 def RemoteMasterBranch(self):
347 return "svn/bleeding_edge"
349 def RemoteCandidateBranch(self):
352 def RemoteBranch(self, name):
353 return "svn/%s" % name
356 self.step.GitSVNDCommit()
359 self.step.GitDCommit()
361 def Tag(self, tag, remote, _):
362 self.step.GitSVNFetch()
363 self.step.Git("rebase %s" % remote)
364 self.step.GitSVNTag(tag)
367 class GitTagsOnlyMixin(VCInterface):
372 self.step.Git("fetch")
373 self.step.GitSVNFetch()
376 return self.step.Git("tag").strip().splitlines()
378 def GetBranches(self):
379 # Get relevant remote branches, e.g. "branch-heads/3.25".
381 lambda s: re.match(r"^branch\-heads/\d+\.\d+$", s),
382 self.step.GitRemotes())
383 # Remove 'branch-heads/' prefix.
384 return map(lambda s: s[13:], branches)
386 def MasterBranch(self):
389 def CandidateBranch(self):
392 def RemoteMasterBranch(self):
393 return "origin/master"
395 def RemoteCandidateBranch(self):
396 return "origin/candidates"
398 def RemoteBranch(self, name):
399 if name in ["candidates", "master"]:
400 return "origin/%s" % name
401 return "branch-heads/%s" % name
403 def PushRef(self, ref):
404 self.step.Git("push origin %s" % ref)
406 def Tag(self, tag, remote, message):
407 # Wait for the commit to appear. Assumes unique commit message titles (this
408 # is the case for all automated merge and push commits - also no title is
409 # the prefix of another title).
411 for wait_interval in [3, 7, 15, 35, 45, 60]:
412 self.step.Git("fetch")
413 commit = self.step.GitLog(n=1, format="%H", grep=message, branch=remote)
416 print("The commit has not replicated to git. Waiting for %s seconds." %
418 self.step._side_effect_handler.Sleep(wait_interval)
420 self.step.Die("Couldn't determine commit for setting the tag. Maybe the "
421 "git updater is lagging behind?")
423 self.step.Git("tag %s %s" % (tag, commit))
427 class GitReadSvnWriteInterface(GitTagsOnlyMixin, GitSvnInterface):
431 class GitInterface(GitTagsOnlyMixin):
433 self.step.Git("fetch")
435 def GitSvn(self, hsh, branch=""):
438 def SvnGit(self, rev, branch=""):
439 raise NotImplementedError()
442 # FIXME(machenbach): This will not work with checkouts from bot_update
443 # after flag day because it will push to the cache. Investigate if it
444 # will work with "cl land".
445 self.step.Git("push origin")
448 self.step.GitCLLand()
450 def PushRef(self, ref):
451 self.step.Git("push https://chromium.googlesource.com/v8/v8 %s" % ref)
455 "git_svn": GitSvnInterface,
456 "git_read_svn_write": GitReadSvnWriteInterface,
461 class Step(GitRecipesMixin):
462 def __init__(self, text, number, config, state, options, handler):
464 self._number = number
465 self._config = config
467 self._options = options
468 self._side_effect_handler = handler
469 self.vc = VC_INTERFACES[options.vc_interface]()
470 self.vc.InjectStep(self)
472 # The testing configuration might set a different default cwd.
473 self.default_cwd = (self._config.get("DEFAULT_CWD") or
474 os.path.join(self._options.work_dir, "v8"))
476 assert self._number >= 0
477 assert self._config is not None
478 assert self._state is not None
479 assert self._side_effect_handler is not None
481 def __getitem__(self, key):
482 # Convenience method to allow direct [] access on step classes for
483 # manipulating the backed state dict.
484 return self._state[key]
486 def __setitem__(self, key, value):
487 # Convenience method to allow direct [] access on step classes for
488 # manipulating the backed state dict.
489 self._state[key] = value
491 def Config(self, key):
492 return self._config[key]
496 state_file = "%s-state.json" % self._config["PERSISTFILE_BASENAME"]
497 if not self._state and os.path.exists(state_file):
498 self._state.update(json.loads(FileToText(state_file)))
500 print ">>> Step %d: %s" % (self._number, self._text)
502 return self.RunStep()
505 TextToFile(json.dumps(self._state), state_file)
507 def RunStep(self): # pragma: no cover
508 raise NotImplementedError
510 def Retry(self, cb, retry_on=None, wait_plan=None):
511 """ Retry a function.
513 cb: The function to retry.
514 retry_on: A callback that takes the result of the function and returns
515 True if the function should be retried. A function throwing an
516 exception is always retried.
517 wait_plan: A list of waiting delays between retries in seconds. The
518 maximum number of retries is len(wait_plan).
520 retry_on = retry_on or (lambda x: False)
521 wait_plan = list(wait_plan or [])
524 got_exception = False
527 except NoRetryException as e:
529 except Exception as e:
531 if got_exception or retry_on(result):
532 if not wait_plan: # pragma: no cover
533 raise Exception("Retried too often. Giving up. Reason: %s" %
535 wait_time = wait_plan.pop()
536 print "Waiting for %f seconds." % wait_time
537 self._side_effect_handler.Sleep(wait_time)
542 def ReadLine(self, default=None):
543 # Don't prompt in forced mode.
544 if self._options.force_readline_defaults and default is not None:
545 print "%s (forced)" % default
548 return self._side_effect_handler.ReadLine()
550 def Command(self, name, args, cwd=None):
551 cmd = lambda: self._side_effect_handler.Command(
552 name, args, "", True, cwd=cwd or self.default_cwd)
553 return self.Retry(cmd, None, [5])
555 def Git(self, args="", prefix="", pipe=True, retry_on=None, cwd=None):
556 cmd = lambda: self._side_effect_handler.Command(
557 "git", args, prefix, pipe, cwd=cwd or self.default_cwd)
558 result = self.Retry(cmd, retry_on, [5, 30])
560 raise GitFailedException("'git %s' failed." % args)
563 def SVN(self, args="", prefix="", pipe=True, retry_on=None, cwd=None):
564 cmd = lambda: self._side_effect_handler.Command(
565 "svn", args, prefix, pipe, cwd=cwd or self.default_cwd)
566 return self.Retry(cmd, retry_on, [5, 30])
568 def Editor(self, args):
569 if self._options.requires_editor:
570 return self._side_effect_handler.Command(
571 os.environ["EDITOR"],
574 cwd=self.default_cwd)
576 def ReadURL(self, url, params=None, retry_on=None, wait_plan=None):
577 wait_plan = wait_plan or [3, 60, 600]
578 cmd = lambda: self._side_effect_handler.ReadURL(url, params)
579 return self.Retry(cmd, retry_on, wait_plan)
582 return self._side_effect_handler.GetDate()
584 def Die(self, msg=""):
586 print "Error: %s" % msg
590 def DieNoManualMode(self, msg=""):
591 if not self._options.manual: # pragma: no cover
592 msg = msg or "Only available in manual mode."
595 def Confirm(self, msg):
596 print "%s [Y/n] " % msg,
597 answer = self.ReadLine(default="Y")
598 return answer == "" or answer == "Y" or answer == "y"
600 def DeleteBranch(self, name):
601 for line in self.GitBranch().splitlines():
602 if re.match(r"\*?\s*%s$" % re.escape(name), line):
603 msg = "Branch %s exists, do you want to delete it?" % name
604 if self.Confirm(msg):
605 self.GitDeleteBranch(name)
606 print "Branch %s deleted." % name
608 msg = "Can't continue. Please delete branch %s and try again." % name
611 def InitialEnvironmentChecks(self, cwd):
612 # Cancel if this is not a git checkout.
613 if not os.path.exists(os.path.join(cwd, ".git")): # pragma: no cover
614 self.Die("This is not a git checkout, this script won't work for you.")
616 # Cancel if EDITOR is unset or not executable.
617 if (self._options.requires_editor and (not os.environ.get("EDITOR") or
619 "which", os.environ["EDITOR"]) is None)): # pragma: no cover
620 self.Die("Please set your EDITOR environment variable, you'll need it.")
622 def CommonPrepare(self):
623 # Check for a clean workdir.
624 if not self.GitIsWorkdirClean(): # pragma: no cover
625 self.Die("Workspace is not clean. Please commit or undo your changes.")
627 # Persist current branch.
628 self["current_branch"] = self.GitCurrentBranch()
630 # Fetch unfetched revisions.
633 def PrepareBranch(self):
634 # Delete the branch that will be created later if it exists already.
635 self.DeleteBranch(self._config["BRANCHNAME"])
637 def CommonCleanup(self):
638 if ' ' in self["current_branch"]:
639 self.GitCheckout('master')
641 self.GitCheckout(self["current_branch"])
642 if self._config["BRANCHNAME"] != self["current_branch"]:
643 self.GitDeleteBranch(self._config["BRANCHNAME"])
645 # Clean up all temporary files.
646 for f in glob.iglob("%s*" % self._config["PERSISTFILE_BASENAME"]):
647 if os.path.isfile(f):
652 def ReadAndPersistVersion(self, prefix=""):
653 def ReadAndPersist(var_name, def_name):
654 match = re.match(r"^#define %s\s+(\d*)" % def_name, line)
656 value = match.group(1)
657 self["%s%s" % (prefix, var_name)] = value
658 for line in LinesInFile(os.path.join(self.default_cwd, VERSION_FILE)):
659 for (var_name, def_name) in [("major", "MAJOR_VERSION"),
660 ("minor", "MINOR_VERSION"),
661 ("build", "BUILD_NUMBER"),
662 ("patch", "PATCH_LEVEL")]:
663 ReadAndPersist(var_name, def_name)
665 def WaitForLGTM(self):
666 print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit "
667 "your change. (If you need to iterate on the patch or double check "
668 "that it's sane, do so in another shell, but remember to not "
669 "change the headline of the uploaded CL.")
671 while answer != "LGTM":
673 answer = self.ReadLine(None if self._options.wait_for_lgtm else "LGTM")
675 print "That was not 'LGTM'."
677 def WaitForResolvingConflicts(self, patch_file):
678 print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", "
679 "or resolve the conflicts, stage *all* touched files with "
680 "'git add', and type \"RESOLVED<Return>\"")
681 self.DieNoManualMode()
683 while answer != "RESOLVED":
684 if answer == "ABORT":
685 self.Die("Applying the patch failed.")
687 print "That was not 'RESOLVED' or 'ABORT'."
689 answer = self.ReadLine()
691 # Takes a file containing the patch to apply as first argument.
692 def ApplyPatch(self, patch_file, revert=False):
694 self.GitApplyPatch(patch_file, revert)
695 except GitFailedException:
696 self.WaitForResolvingConflicts(patch_file)
698 def FindLastTrunkPush(
699 self, parent_hash="", branch="", include_patches=False):
700 push_pattern = "^Version [[:digit:]]*\.[[:digit:]]*\.[[:digit:]]*"
701 if not include_patches:
702 # Non-patched versions only have three numbers followed by the "(based
704 push_pattern += " (based"
705 branch = "" if parent_hash else branch or self.vc.RemoteCandidateBranch()
706 return self.GitLog(n=1, format="%H", grep=push_pattern,
707 parent_hash=parent_hash, branch=branch)
709 def ArrayToVersion(self, prefix):
710 return ".".join([self[prefix + "major"],
711 self[prefix + "minor"],
712 self[prefix + "build"],
713 self[prefix + "patch"]])
715 def SetVersion(self, version_file, prefix):
717 for line in FileToText(version_file).splitlines():
718 if line.startswith("#define MAJOR_VERSION"):
719 line = re.sub("\d+$", self[prefix + "major"], line)
720 elif line.startswith("#define MINOR_VERSION"):
721 line = re.sub("\d+$", self[prefix + "minor"], line)
722 elif line.startswith("#define BUILD_NUMBER"):
723 line = re.sub("\d+$", self[prefix + "build"], line)
724 elif line.startswith("#define PATCH_LEVEL"):
725 line = re.sub("\d+$", self[prefix + "patch"], line)
726 output += "%s\n" % line
727 TextToFile(output, version_file)
729 def SVNCommit(self, root, commit_message):
730 patch = self.GitDiff("HEAD^", "HEAD")
731 TextToFile(patch, self._config["PATCH_FILE"])
732 self.Command("svn", "update", cwd=self._options.svn)
733 if self.Command("svn", "status", cwd=self._options.svn) != "":
734 self.Die("SVN checkout not clean.")
735 if not self.Command("patch", "-d %s -p1 -i %s" %
736 (root, self._config["PATCH_FILE"]),
737 cwd=self._options.svn):
738 self.Die("Could not apply patch.")
739 for line in self.Command(
740 "svn", "status", cwd=self._options.svn).splitlines():
741 # Check for added and removed items. Svn status has seven status columns.
742 # The first contains ? for unknown and ! for missing.
743 match = re.match(r"^(.)...... (.*)$", line)
744 if match and match.group(1) == "?":
745 self.Command("svn", "add --force %s" % match.group(2),
746 cwd=self._options.svn)
747 if match and match.group(1) == "!":
748 self.Command("svn", "delete --force %s" % match.group(2),
749 cwd=self._options.svn)
753 "commit --non-interactive --username=%s --config-dir=%s -m \"%s\"" %
754 (self._options.author, self._options.svn_config, commit_message),
755 cwd=self._options.svn)
758 class BootstrapStep(Step):
759 MESSAGE = "Bootstapping v8 checkout."
762 if os.path.realpath(self.default_cwd) == os.path.realpath(V8_BASE):
763 self.Die("Can't use v8 checkout with calling script as work checkout.")
764 # Directory containing the working v8 checkout.
765 if not os.path.exists(self._options.work_dir):
766 os.makedirs(self._options.work_dir)
767 if not os.path.exists(self.default_cwd):
768 self.Command("fetch", "v8", cwd=self._options.work_dir)
771 class UploadStep(Step):
772 MESSAGE = "Upload for code review."
775 if self._options.reviewer:
776 print "Using account %s for review." % self._options.reviewer
777 reviewer = self._options.reviewer
779 print "Please enter the email address of a V8 reviewer for your patch: ",
780 self.DieNoManualMode("A reviewer must be specified in forced mode.")
781 reviewer = self.ReadLine()
782 self.GitUpload(reviewer, self._options.author, self._options.force_upload,
783 bypass_hooks=self._options.bypass_upload_hooks,
787 class DetermineV8Sheriff(Step):
788 MESSAGE = "Determine the V8 sheriff for code review."
791 self["sheriff"] = None
792 if not self._options.sheriff: # pragma: no cover
796 # The googlers mapping maps @google.com accounts to @chromium.org
798 googlers = imp.load_source('googlers_mapping',
799 self._options.googlers_mapping)
800 googlers = googlers.list_to_dict(googlers.get_list())
801 except: # pragma: no cover
802 print "Skip determining sheriff without googler mapping."
805 # The sheriff determined by the rotation on the waterfall has a
806 # @google.com account.
807 url = "https://chromium-build.appspot.com/p/chromium/sheriff_v8.js"
808 match = re.match(r"document\.write\('(\w+)'\)", self.ReadURL(url))
810 # If "channel is sheriff", we can't match an account.
812 g_name = match.group(1)
813 self["sheriff"] = googlers.get(g_name + "@google.com",
814 g_name + "@chromium.org")
815 self._options.reviewer = self["sheriff"]
816 print "Found active sheriff: %s" % self["sheriff"]
818 print "No active sheriff found."
821 def MakeStep(step_class=Step, number=0, state=None, config=None,
822 options=None, side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER):
823 # Allow to pass in empty dictionaries.
824 state = state if state is not None else {}
825 config = config if config is not None else {}
828 message = step_class.MESSAGE
829 except AttributeError:
830 message = step_class.__name__
832 return step_class(message, number=number, config=config,
833 state=state, options=options,
834 handler=side_effect_handler)
837 class ScriptsBase(object):
840 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER,
842 self._config = config or self._Config()
843 self._side_effect_handler = side_effect_handler
844 self._state = state if state is not None else {}
846 def _Description(self):
849 def _PrepareOptions(self, parser):
852 def _ProcessOptions(self, options):
855 def _Steps(self): # pragma: no cover
856 raise Exception("Not implemented.")
861 def MakeOptions(self, args=None):
862 parser = argparse.ArgumentParser(description=self._Description())
863 parser.add_argument("-a", "--author", default="",
864 help="The author email used for rietveld.")
865 parser.add_argument("--dry-run", default=False, action="store_true",
866 help="Perform only read-only actions.")
867 parser.add_argument("-g", "--googlers-mapping",
868 help="Path to the script mapping google accounts.")
869 parser.add_argument("-r", "--reviewer", default="",
870 help="The account name to be used for reviews.")
871 parser.add_argument("--sheriff", default=False, action="store_true",
872 help=("Determine current sheriff to review CLs. On "
873 "success, this will overwrite the reviewer "
875 parser.add_argument("--svn",
876 help=("Optional full svn checkout for the commit."
877 "The folder needs to be the svn root."))
878 parser.add_argument("--svn-config",
879 help=("Optional folder used as svn --config-dir."))
880 parser.add_argument("-s", "--step",
881 help="Specify the step where to start work. Default: 0.",
883 parser.add_argument("--vc-interface",
884 help=("Choose VC interface out of git_svn|"
885 "git_read_svn_write."))
886 parser.add_argument("--work-dir",
887 help=("Location where to bootstrap a working v8 "
889 self._PrepareOptions(parser)
891 if args is None: # pragma: no cover
892 options = parser.parse_args()
894 options = parser.parse_args(args)
896 # Process common options.
897 if options.step < 0: # pragma: no cover
898 print "Bad step number %d" % options.step
901 if options.sheriff and not options.googlers_mapping: # pragma: no cover
902 print "To determine the current sheriff, requires the googler mapping"
905 if options.svn and not options.svn_config:
906 print "Using pure svn for committing requires also --svn-config"
910 # Defaults for options, common to all scripts.
911 options.manual = getattr(options, "manual", True)
912 options.force = getattr(options, "force", False)
913 options.bypass_upload_hooks = False
916 options.requires_editor = not options.force
917 options.wait_for_lgtm = not options.force
918 options.force_readline_defaults = not options.manual
919 options.force_upload = not options.manual
921 # Process script specific options.
922 if not self._ProcessOptions(options):
926 if not options.vc_interface:
927 options.vc_interface = "git_read_svn_write"
928 if not options.work_dir:
929 options.work_dir = "/tmp/v8-release-scripts-work-dir"
932 def RunSteps(self, step_classes, args=None):
933 options = self.MakeOptions(args)
937 state_file = "%s-state.json" % self._config["PERSISTFILE_BASENAME"]
938 if options.step == 0 and os.path.exists(state_file):
939 os.remove(state_file)
942 for (number, step_class) in enumerate([BootstrapStep] + step_classes):
943 steps.append(MakeStep(step_class, number, self._state, self._config,
944 options, self._side_effect_handler))
945 for step in steps[options.step:]:
950 def Run(self, args=None):
951 return self.RunSteps(self._Steps(), args)