bae05bc6b542a2c41d4bb9773bc8ecfc3fc97068
[platform/upstream/nodejs.git] / deps / v8 / tools / release / common_includes.py
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 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+)?$")
53
54 # V8 base directory.
55 V8_BASE = os.path.dirname(
56     os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
57
58
59 def TextToFile(text, file_name):
60   with open(file_name, "w") as f:
61     f.write(text)
62
63
64 def AppendToFile(text, file_name):
65   with open(file_name, "a") as f:
66     f.write(text)
67
68
69 def LinesInFile(file_name):
70   with open(file_name) as f:
71     for line in f:
72       yield line
73
74
75 def FileToText(file_name):
76   with open(file_name) as f:
77     return f.read()
78
79
80 def MSub(rexp, replacement, text):
81   return re.sub(rexp, replacement, text, flags=re.MULTILINE)
82
83
84 def Fill80(line):
85   # Replace tabs and remove surrounding space.
86   line = re.sub(r"\t", r"        ", line.strip())
87
88   # Format with 8 characters indentation and line width 80.
89   return textwrap.fill(line, width=80, initial_indent="        ",
90                        subsequent_indent="        ")
91
92
93 def MakeComment(text):
94   return MSub(r"^( ?)", "#", text)
95
96
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")))
100
101
102 def MakeChangeLogBody(commit_messages, auto_format=False):
103   result = ""
104   added_titles = set()
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()
109     if auto_format:
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):
113         continue
114       # Never include reverts.
115       if title.startswith("Revert "):
116         continue
117       # Don't include duplicates.
118       if title in added_titles:
119         continue
120
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))
127
128     # Append the commit's author for reference if not in auto-format mode.
129     if not auto_format:
130       result += "%s\n" % Fill80("(%s)" % author.strip())
131
132     result += "\n"
133   return result
134
135
136 def MakeChangeLogBugReference(body):
137   """Grep for "BUG=xxxx" lines in the commit message and convert them to
138   "(issue xxxx)".
139   """
140   crbugs = []
141   v8bugs = []
142
143   def AddIssues(text):
144     ref = re.match(r"^BUG[ \t]*=[ \t]*(.+)$", text.strip())
145     if not ref:
146       return
147     for bug in ref.group(1).split(","):
148       bug = bug.strip()
149       match = re.match(r"^v8:(\d+)$", bug)
150       if match: v8bugs.append(int(match.group(1)))
151       else:
152         match = re.match(r"^(?:chromium:)?(\d+)$", bug)
153         if match: crbugs.append(int(match.group(1)))
154
155   # Add issues to crbugs and v8bugs.
156   map(AddIssues, body.splitlines())
157
158   # Filter duplicates, sort, stringify.
159   crbugs = map(str, sorted(set(crbugs)))
160   v8bugs = map(str, sorted(set(v8bugs)))
161
162   bug_groups = []
163   def FormatIssues(prefix, bugs):
164     if len(bugs) > 0:
165       plural = "s" if len(bugs) > 1 else ""
166       bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs)))
167
168   FormatIssues("", v8bugs)
169   FormatIssues("Chromium ", crbugs)
170
171   if len(bug_groups) > 0:
172     return "(%s)" % ", ".join(bug_groups)
173   else:
174     return ""
175
176
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)
183   # Fill digits.
184   return ".".join(map("{0:04d}".format, version_keys))
185
186
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
194   print "in %s" % cwd
195   sys.stdout.flush()
196   try:
197     if pipe:
198       return subprocess.check_output(cmd_line, shell=True, cwd=cwd)
199     else:
200       return subprocess.check_call(cmd_line, shell=True, cwd=cwd)
201   except subprocess.CalledProcessError:
202     return None
203   finally:
204     sys.stdout.flush()
205     sys.stderr.flush()
206
207
208 # Wrapper for side effects.
209 class SideEffectHandler(object):  # pragma: no cover
210   def Call(self, fun, *args, **kwargs):
211     return fun(*args, **kwargs)
212
213   def Command(self, cmd, args="", prefix="", pipe=True, cwd=None):
214     return Command(cmd, args, prefix, pipe, cwd=cwd)
215
216   def ReadLine(self):
217     return sys.stdin.readline().strip()
218
219   def ReadURL(self, url, params=None):
220     # pylint: disable=E1121
221     url_fh = urllib2.urlopen(url, params, 60)
222     try:
223       return url_fh.read()
224     finally:
225       url_fh.close()
226
227   def ReadClusterFuzzAPI(self, api_key, **params):
228     params["api_key"] = api_key.strip()
229     params = urllib.urlencode(params)
230
231     headers = {"Content-type": "application/x-www-form-urlencoded"}
232
233     conn = httplib.HTTPSConnection("backend-dot-cluster-fuzz.appspot.com")
234     conn.request("POST", "/_api/", params, headers)
235
236     response = conn.getresponse()
237     data = response.read()
238
239     try:
240       return json.loads(data)
241     except:
242       print data
243       print "ERROR: Could not read response. Is your key valid?"
244       raise
245
246   def Sleep(self, seconds):
247     time.sleep(seconds)
248
249   def GetDate(self):
250     return datetime.date.today().strftime("%Y-%m-%d")
251
252   def GetUTCStamp(self):
253     return time.mktime(datetime.datetime.utcnow().timetuple())
254
255 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler()
256
257
258 class NoRetryException(Exception):
259   pass
260
261
262 class VCInterface(object):
263   def InjectStep(self, step):
264     self.step=step
265
266   def Pull(self):
267     raise NotImplementedError()
268
269   def Fetch(self):
270     raise NotImplementedError()
271
272   def GetTags(self):
273     raise NotImplementedError()
274
275   def GetBranches(self):
276     raise NotImplementedError()
277
278   def MasterBranch(self):
279     raise NotImplementedError()
280
281   def CandidateBranch(self):
282     raise NotImplementedError()
283
284   def RemoteMasterBranch(self):
285     raise NotImplementedError()
286
287   def RemoteCandidateBranch(self):
288     raise NotImplementedError()
289
290   def RemoteBranch(self, name):
291     raise NotImplementedError()
292
293   def CLLand(self):
294     raise NotImplementedError()
295
296   def Tag(self, tag, remote, message):
297     """Sets a tag for the current commit.
298
299     Assumptions: The commit already landed and the commit message is unique.
300     """
301     raise NotImplementedError()
302
303
304 class GitInterface(VCInterface):
305   def Pull(self):
306     self.step.GitPull()
307
308   def Fetch(self):
309     self.step.Git("fetch")
310
311   def GetTags(self):
312      return self.step.Git("tag").strip().splitlines()
313
314   def GetBranches(self):
315     # Get relevant remote branches, e.g. "branch-heads/3.25".
316     branches = filter(
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)
321
322   def MasterBranch(self):
323     return "master"
324
325   def CandidateBranch(self):
326     return "candidates"
327
328   def RemoteMasterBranch(self):
329     return "origin/master"
330
331   def RemoteCandidateBranch(self):
332     return "origin/candidates"
333
334   def RemoteBranch(self, name):
335     # Assume that if someone "fully qualified" the ref, they know what they
336     # want.
337     if name.startswith('refs/'):
338       return name
339     if name in ["candidates", "master"]:
340       return "refs/remotes/origin/%s" % name
341     try:
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:
346       pass
347     try:
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:
352       pass
353     self.Die("Can't find remote of %s" % name)
354
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).
359     commit = None
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)
363       if commit:
364         break
365       print("The commit has not replicated to git. Waiting for %s seconds." %
366             wait_interval)
367       self.step._side_effect_handler.Sleep(wait_interval)
368     else:
369       self.step.Die("Couldn't determine commit for setting the tag. Maybe the "
370                     "git updater is lagging behind?")
371
372     self.step.Git("tag %s %s" % (tag, commit))
373     self.step.Git("push origin %s" % tag)
374
375   def CLLand(self):
376     self.step.GitCLLand()
377
378
379 class Step(GitRecipesMixin):
380   def __init__(self, text, number, config, state, options, handler):
381     self._text = text
382     self._number = number
383     self._config = config
384     self._state = state
385     self._options = options
386     self._side_effect_handler = handler
387     self.vc = GitInterface()
388     self.vc.InjectStep(self)
389
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"))
393
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
398
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)
403
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
408
409   def Config(self, key):
410     return self._config[key]
411
412   def Run(self):
413     # Restore state.
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)))
417
418     print ">>> Step %d: %s" % (self._number, self._text)
419     try:
420       return self.RunStep()
421     finally:
422       # Persist state.
423       TextToFile(json.dumps(self._state), state_file)
424
425   def RunStep(self):  # pragma: no cover
426     raise NotImplementedError
427
428   def Retry(self, cb, retry_on=None, wait_plan=None):
429     """ Retry a function.
430     Params:
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).
437     """
438     retry_on = retry_on or (lambda x: False)
439     wait_plan = list(wait_plan or [])
440     wait_plan.reverse()
441     while True:
442       got_exception = False
443       try:
444         result = cb()
445       except NoRetryException as e:
446         raise e
447       except Exception as e:
448         got_exception = 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" %
452                           str(got_exception))
453         wait_time = wait_plan.pop()
454         print "Waiting for %f seconds." % wait_time
455         self._side_effect_handler.Sleep(wait_time)
456         print "Retrying..."
457       else:
458         return result
459
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
464       return default
465     else:
466       return self._side_effect_handler.ReadLine()
467
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])
472
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])
477     if result is None:
478       raise GitFailedException("'git %s' failed." % args)
479     return result
480
481   def Editor(self, args):
482     if self._options.requires_editor:
483       return self._side_effect_handler.Command(
484           os.environ["EDITOR"],
485           args,
486           pipe=False,
487           cwd=self.default_cwd)
488
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)
493
494   def GetDate(self):
495     return self._side_effect_handler.GetDate()
496
497   def Die(self, msg=""):
498     if msg != "":
499       print "Error: %s" % msg
500     print "Exiting"
501     raise Exception(msg)
502
503   def DieNoManualMode(self, msg=""):
504     if not self._options.manual:  # pragma: no cover
505       msg = msg or "Only available in manual mode."
506       self.Die(msg)
507
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"
512
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
520         else:
521           msg = "Can't continue. Please delete branch %s and try again." % name
522           self.Die(msg)
523
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.")
528
529     # Cancel if EDITOR is unset or not executable.
530     if (self._options.requires_editor and (not os.environ.get("EDITOR") or
531         self.Command(
532             "which", os.environ["EDITOR"]) is None)):  # pragma: no cover
533       self.Die("Please set your EDITOR environment variable, you'll need it.")
534
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.")
539
540     # Persist current branch.
541     self["current_branch"] = self.GitCurrentBranch()
542
543     # Fetch unfetched revisions.
544     self.vc.Fetch()
545
546   def PrepareBranch(self):
547     # Delete the branch that will be created later if it exists already.
548     self.DeleteBranch(self._config["BRANCHNAME"])
549
550   def CommonCleanup(self):
551     if ' ' in self["current_branch"]:
552       self.GitCheckout('master')
553     else:
554       self.GitCheckout(self["current_branch"])
555     if self._config["BRANCHNAME"] != self["current_branch"]:
556       self.GitDeleteBranch(self._config["BRANCHNAME"])
557
558     # Clean up all temporary files.
559     for f in glob.iglob("%s*" % self._config["PERSISTFILE_BASENAME"]):
560       if os.path.isfile(f):
561         os.remove(f)
562       if os.path.isdir(f):
563         shutil.rmtree(f)
564
565   def ReadAndPersistVersion(self, prefix=""):
566     def ReadAndPersist(var_name, def_name):
567       match = re.match(r"^#define %s\s+(\d*)" % def_name, line)
568       if match:
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)
577
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.")
583     answer = ""
584     while answer != "LGTM":
585       print "> ",
586       answer = self.ReadLine(None if self._options.wait_for_lgtm else "LGTM")
587       if answer != "LGTM":
588         print "That was not 'LGTM'."
589
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()
595     answer = ""
596     while answer != "RESOLVED":
597       if answer == "ABORT":
598         self.Die("Applying the patch failed.")
599       if answer != "":
600         print "That was not 'RESOLVED' or 'ABORT'."
601       print "> ",
602       answer = self.ReadLine()
603
604   # Takes a file containing the patch to apply as first argument.
605   def ApplyPatch(self, patch_file, revert=False):
606     try:
607       self.GitApplyPatch(patch_file, revert)
608     except GitFailedException:
609       self.WaitForResolvingConflicts(patch_file)
610
611   def GetVersionTag(self, revision):
612     tag = self.Git("describe --tags %s" % revision).strip()
613     if VERSION_RE.match(tag):
614       return tag
615     else:
616       return None
617
618   def GetRecentReleases(self, max_age):
619     # Make sure tags are fetched.
620     self.Git("fetch origin +refs/tags/*:refs/tags/*")
621
622     # Current timestamp.
623     time_now = int(self._side_effect_handler.GetUTCStamp())
624
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()
628
629     # Filter out revisions who's tag is off by one or more commits.
630     return filter(lambda r: self.GetVersionTag(r), revisions.splitlines())
631
632   def GetLatestVersion(self):
633     # Use cached version if available.
634     if self["latest_version"]:
635       return self["latest_version"]
636
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
642     return version
643
644   def GetLatestRelease(self):
645     """The latest release is the git hash of the latest tagged version.
646
647     This revision should be rolled into chromium.
648     """
649     latest_version = self.GetLatestVersion()
650
651     # The latest release.
652     latest_hash = self.GitLog(n=1, format="%H", branch=latest_version)
653     assert latest_hash
654     return latest_hash
655
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.
659     """
660     latest_version = version or self.GetLatestVersion()
661
662     # Strip patch level if it exists.
663     latest_version = ".".join(latest_version.split(".")[:3])
664
665     # The latest release base.
666     latest_hash = self.GitLog(n=1, format="%H", branch=latest_version)
667     assert latest_hash
668
669     title = self.GitLog(n=1, format="%s", git_hash=latest_hash)
670     match = PUSH_MSG_GIT_RE.match(title)
671     if match:
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)
677     if match:
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
680       # base on master.
681       return self.GitLog(n=1, format="%H", git_hash="%s^" % latest_hash)
682
683     self.Die("Unknown latest release: %s" % latest_hash)
684
685   def ArrayToVersion(self, prefix):
686     return ".".join([self[prefix + "major"],
687                      self[prefix + "minor"],
688                      self[prefix + "build"],
689                      self[prefix + "patch"]])
690
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
700
701   def SetVersion(self, version_file, prefix):
702     output = ""
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)
717
718
719 class BootstrapStep(Step):
720   MESSAGE = "Bootstapping v8 checkout."
721
722   def RunStep(self):
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)
730
731
732 class UploadStep(Step):
733   MESSAGE = "Upload for code review."
734
735   def RunStep(self):
736     if self._options.reviewer:
737       print "Using account %s for review." % self._options.reviewer
738       reviewer = self._options.reviewer
739     else:
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,
745                    cc=self._options.cc)
746
747
748 class DetermineV8Sheriff(Step):
749   MESSAGE = "Determine the V8 sheriff for code review."
750
751   def RunStep(self):
752     self["sheriff"] = None
753     if not self._options.sheriff:  # pragma: no cover
754       return
755
756     try:
757       # The googlers mapping maps @google.com accounts to @chromium.org
758       # accounts.
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."
764       return
765
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))
770
771     # If "channel is sheriff", we can't match an account.
772     if match:
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"]
778     else:
779       print "No active sheriff found."
780
781
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 {}
787
788     try:
789       message = step_class.MESSAGE
790     except AttributeError:
791       message = step_class.__name__
792
793     return step_class(message, number=number, config=config,
794                       state=state, options=options,
795                       handler=side_effect_handler)
796
797
798 class ScriptsBase(object):
799   def __init__(self,
800                config=None,
801                side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER,
802                state=None):
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 {}
806
807   def _Description(self):
808     return None
809
810   def _PrepareOptions(self, parser):
811     pass
812
813   def _ProcessOptions(self, options):
814     return True
815
816   def _Steps(self):  # pragma: no cover
817     raise Exception("Not implemented.")
818
819   def _Config(self):
820     return {}
821
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 "
835                               "option."))
836     parser.add_argument("-s", "--step",
837         help="Specify the step where to start work. Default: 0.",
838         default=0, type=int)
839     parser.add_argument("--work-dir",
840                         help=("Location where to bootstrap a working v8 "
841                               "checkout."))
842     self._PrepareOptions(parser)
843
844     if args is None:  # pragma: no cover
845       options = parser.parse_args()
846     else:
847       options = parser.parse_args(args)
848
849     # Process common options.
850     if options.step < 0:  # pragma: no cover
851       print "Bad step number %d" % options.step
852       parser.print_help()
853       return None
854     if options.sheriff and not options.googlers_mapping:  # pragma: no cover
855       print "To determine the current sheriff, requires the googler mapping"
856       parser.print_help()
857       return None
858
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
863
864     # Derived options.
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
869
870     # Process script specific options.
871     if not self._ProcessOptions(options):
872       parser.print_help()
873       return None
874
875     if not options.work_dir:
876       options.work_dir = "/tmp/v8-release-scripts-work-dir"
877     return options
878
879   def RunSteps(self, step_classes, args=None):
880     options = self.MakeOptions(args)
881     if not options:
882       return 1
883
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)
887
888     steps = []
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:]:
893       if step.Run():
894         return 0
895     return 0
896
897   def Run(self, args=None):
898     return self.RunSteps(self._Steps(), args)