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