Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / v8 / tools / push-to-trunk / 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 VERSION_FILE = os.path.join("src", "version.cc")
49
50 # V8 base directory.
51 V8_BASE = os.path.dirname(
52     os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
53
54
55 def TextToFile(text, file_name):
56   with open(file_name, "w") as f:
57     f.write(text)
58
59
60 def AppendToFile(text, file_name):
61   with open(file_name, "a") as f:
62     f.write(text)
63
64
65 def LinesInFile(file_name):
66   with open(file_name) as f:
67     for line in f:
68       yield line
69
70
71 def FileToText(file_name):
72   with open(file_name) as f:
73     return f.read()
74
75
76 def MSub(rexp, replacement, text):
77   return re.sub(rexp, replacement, text, flags=re.MULTILINE)
78
79
80 def Fill80(line):
81   # Replace tabs and remove surrounding space.
82   line = re.sub(r"\t", r"        ", line.strip())
83
84   # Format with 8 characters indentation and line width 80.
85   return textwrap.fill(line, width=80, initial_indent="        ",
86                        subsequent_indent="        ")
87
88
89 def MakeComment(text):
90   return MSub(r"^( ?)", "#", text)
91
92
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")))
96
97
98 def MakeChangeLogBody(commit_messages, auto_format=False):
99   result = ""
100   added_titles = set()
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()
105     if auto_format:
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):
109         continue
110       # Never include reverts.
111       if title.startswith("Revert "):
112         continue
113       # Don't include duplicates.
114       if title in added_titles:
115         continue
116
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))
123
124     # Append the commit's author for reference if not in auto-format mode.
125     if not auto_format:
126       result += "%s\n" % Fill80("(%s)" % author.strip())
127
128     result += "\n"
129   return result
130
131
132 def MakeChangeLogBugReference(body):
133   """Grep for "BUG=xxxx" lines in the commit message and convert them to
134   "(issue xxxx)".
135   """
136   crbugs = []
137   v8bugs = []
138
139   def AddIssues(text):
140     ref = re.match(r"^BUG[ \t]*=[ \t]*(.+)$", text.strip())
141     if not ref:
142       return
143     for bug in ref.group(1).split(","):
144       bug = bug.strip()
145       match = re.match(r"^v8:(\d+)$", bug)
146       if match: v8bugs.append(int(match.group(1)))
147       else:
148         match = re.match(r"^(?:chromium:)?(\d+)$", bug)
149         if match: crbugs.append(int(match.group(1)))
150
151   # Add issues to crbugs and v8bugs.
152   map(AddIssues, body.splitlines())
153
154   # Filter duplicates, sort, stringify.
155   crbugs = map(str, sorted(set(crbugs)))
156   v8bugs = map(str, sorted(set(v8bugs)))
157
158   bug_groups = []
159   def FormatIssues(prefix, bugs):
160     if len(bugs) > 0:
161       plural = "s" if len(bugs) > 1 else ""
162       bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs)))
163
164   FormatIssues("", v8bugs)
165   FormatIssues("Chromium ", crbugs)
166
167   if len(bug_groups) > 0:
168     return "(%s)" % ", ".join(bug_groups)
169   else:
170     return ""
171
172
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)
179   # Fill digits.
180   return ".".join(map("{0:04d}".format, version_keys))
181
182
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
190   print "in %s" % cwd
191   sys.stdout.flush()
192   try:
193     if pipe:
194       return subprocess.check_output(cmd_line, shell=True, cwd=cwd)
195     else:
196       return subprocess.check_call(cmd_line, shell=True, cwd=cwd)
197   except subprocess.CalledProcessError:
198     return None
199   finally:
200     sys.stdout.flush()
201     sys.stderr.flush()
202
203
204 # Wrapper for side effects.
205 class SideEffectHandler(object):  # pragma: no cover
206   def Call(self, fun, *args, **kwargs):
207     return fun(*args, **kwargs)
208
209   def Command(self, cmd, args="", prefix="", pipe=True, cwd=None):
210     return Command(cmd, args, prefix, pipe, cwd=cwd)
211
212   def ReadLine(self):
213     return sys.stdin.readline().strip()
214
215   def ReadURL(self, url, params=None):
216     # pylint: disable=E1121
217     url_fh = urllib2.urlopen(url, params, 60)
218     try:
219       return url_fh.read()
220     finally:
221       url_fh.close()
222
223   def ReadClusterFuzzAPI(self, api_key, **params):
224     params["api_key"] = api_key.strip()
225     params = urllib.urlencode(params)
226
227     headers = {"Content-type": "application/x-www-form-urlencoded"}
228
229     conn = httplib.HTTPSConnection("backend-dot-cluster-fuzz.appspot.com")
230     conn.request("POST", "/_api/", params, headers)
231
232     response = conn.getresponse()
233     data = response.read()
234
235     try:
236       return json.loads(data)
237     except:
238       print data
239       print "ERROR: Could not read response. Is your key valid?"
240       raise
241
242   def Sleep(self, seconds):
243     time.sleep(seconds)
244
245   def GetDate(self):
246     return datetime.date.today().strftime("%Y-%m-%d")
247
248   def GetUTCStamp(self):
249     return time.mktime(datetime.datetime.utcnow().timetuple())
250
251 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler()
252
253
254 class NoRetryException(Exception):
255   pass
256
257
258 class VCInterface(object):
259   def InjectStep(self, step):
260     self.step=step
261
262   def Pull(self):
263     raise NotImplementedError()
264
265   def Fetch(self):
266     raise NotImplementedError()
267
268   def GetTags(self):
269     raise NotImplementedError()
270
271   def GetBranches(self):
272     raise NotImplementedError()
273
274   def GitSvn(self, hsh, branch=""):
275     raise NotImplementedError()
276
277   def SvnGit(self, rev, branch=""):
278     raise NotImplementedError()
279
280   def MasterBranch(self):
281     raise NotImplementedError()
282
283   def CandidateBranch(self):
284     raise NotImplementedError()
285
286   def RemoteMasterBranch(self):
287     raise NotImplementedError()
288
289   def RemoteCandidateBranch(self):
290     raise NotImplementedError()
291
292   def RemoteBranch(self, name):
293     raise NotImplementedError()
294
295   def Land(self):
296     raise NotImplementedError()
297
298   def CLLand(self):
299     raise NotImplementedError()
300
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.
306
307     Assumptions: The commit already landed and the commit message is unique.
308     """
309     raise NotImplementedError()
310
311
312 class GitSvnInterface(VCInterface):
313   def Pull(self):
314     self.step.GitSVNRebase()
315
316   def Fetch(self):
317     self.step.GitSVNFetch()
318
319   def GetTags(self):
320     # Get remote tags.
321     tags = filter(lambda s: re.match(r"^svn/tags/[\d+\.]+$", s),
322                   self.step.GitRemotes())
323
324     # Remove 'svn/tags/' prefix.
325     return map(lambda s: s[9:], tags)
326
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)
333
334   def GitSvn(self, hsh, branch=""):
335     return self.step.GitSVNFindSVNRev(hsh, branch)
336
337   def SvnGit(self, rev, branch=""):
338     return self.step.GitSVNFindGitHash(rev, branch)
339
340   def MasterBranch(self):
341     return "bleeding_edge"
342
343   def CandidateBranch(self):
344     return "trunk"
345
346   def RemoteMasterBranch(self):
347     return "svn/bleeding_edge"
348
349   def RemoteCandidateBranch(self):
350     return "svn/trunk"
351
352   def RemoteBranch(self, name):
353     return "svn/%s" % name
354
355   def Land(self):
356     self.step.GitSVNDCommit()
357
358   def CLLand(self):
359     self.step.GitDCommit()
360
361   def Tag(self, tag, remote, _):
362     self.step.GitSVNFetch()
363     self.step.Git("rebase %s" % remote)
364     self.step.GitSVNTag(tag)
365
366
367 class GitTagsOnlyMixin(VCInterface):
368   def Pull(self):
369     self.step.GitPull()
370
371   def Fetch(self):
372     self.step.Git("fetch")
373     self.step.GitSVNFetch()
374
375   def GetTags(self):
376      return self.step.Git("tag").strip().splitlines()
377
378   def GetBranches(self):
379     # Get relevant remote branches, e.g. "branch-heads/3.25".
380     branches = filter(
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)
385
386   def MasterBranch(self):
387     return "master"
388
389   def CandidateBranch(self):
390     return "candidates"
391
392   def RemoteMasterBranch(self):
393     return "origin/master"
394
395   def RemoteCandidateBranch(self):
396     return "origin/candidates"
397
398   def RemoteBranch(self, name):
399     if name in ["candidates", "master"]:
400       return "origin/%s" % name
401     return "branch-heads/%s" % name
402
403   def PushRef(self, ref):
404     self.step.Git("push origin %s" % ref)
405
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).
410     commit = None
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)
414       if commit:
415         break
416       print("The commit has not replicated to git. Waiting for %s seconds." %
417             wait_interval)
418       self.step._side_effect_handler.Sleep(wait_interval)
419     else:
420       self.step.Die("Couldn't determine commit for setting the tag. Maybe the "
421                     "git updater is lagging behind?")
422
423     self.step.Git("tag %s %s" % (tag, commit))
424     self.PushRef(tag)
425
426
427 class GitReadSvnWriteInterface(GitTagsOnlyMixin, GitSvnInterface):
428   pass
429
430
431 class GitInterface(GitTagsOnlyMixin):
432   def Fetch(self):
433     self.step.Git("fetch")
434
435   def GitSvn(self, hsh, branch=""):
436     return ""
437
438   def SvnGit(self, rev, branch=""):
439     raise NotImplementedError()
440
441   def Land(self):
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")
446
447   def CLLand(self):
448     self.step.GitCLLand()
449
450   def PushRef(self, ref):
451     self.step.Git("push https://chromium.googlesource.com/v8/v8 %s" % ref)
452
453
454 VC_INTERFACES = {
455   "git_svn": GitSvnInterface,
456   "git_read_svn_write": GitReadSvnWriteInterface,
457   "git": GitInterface,
458 }
459
460
461 class Step(GitRecipesMixin):
462   def __init__(self, text, number, config, state, options, handler):
463     self._text = text
464     self._number = number
465     self._config = config
466     self._state = state
467     self._options = options
468     self._side_effect_handler = handler
469     self.vc = VC_INTERFACES[options.vc_interface]()
470     self.vc.InjectStep(self)
471
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"))
475
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
480
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]
485
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
490
491   def Config(self, key):
492     return self._config[key]
493
494   def Run(self):
495     # Restore state.
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)))
499
500     print ">>> Step %d: %s" % (self._number, self._text)
501     try:
502       return self.RunStep()
503     finally:
504       # Persist state.
505       TextToFile(json.dumps(self._state), state_file)
506
507   def RunStep(self):  # pragma: no cover
508     raise NotImplementedError
509
510   def Retry(self, cb, retry_on=None, wait_plan=None):
511     """ Retry a function.
512     Params:
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).
519     """
520     retry_on = retry_on or (lambda x: False)
521     wait_plan = list(wait_plan or [])
522     wait_plan.reverse()
523     while True:
524       got_exception = False
525       try:
526         result = cb()
527       except NoRetryException as e:
528         raise e
529       except Exception as e:
530         got_exception = 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" %
534                           str(got_exception))
535         wait_time = wait_plan.pop()
536         print "Waiting for %f seconds." % wait_time
537         self._side_effect_handler.Sleep(wait_time)
538         print "Retrying..."
539       else:
540         return result
541
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
546       return default
547     else:
548       return self._side_effect_handler.ReadLine()
549
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])
554
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])
559     if result is None:
560       raise GitFailedException("'git %s' failed." % args)
561     return result
562
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])
567
568   def Editor(self, args):
569     if self._options.requires_editor:
570       return self._side_effect_handler.Command(
571           os.environ["EDITOR"],
572           args,
573           pipe=False,
574           cwd=self.default_cwd)
575
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)
580
581   def GetDate(self):
582     return self._side_effect_handler.GetDate()
583
584   def Die(self, msg=""):
585     if msg != "":
586       print "Error: %s" % msg
587     print "Exiting"
588     raise Exception(msg)
589
590   def DieNoManualMode(self, msg=""):
591     if not self._options.manual:  # pragma: no cover
592       msg = msg or "Only available in manual mode."
593       self.Die(msg)
594
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"
599
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
607         else:
608           msg = "Can't continue. Please delete branch %s and try again." % name
609           self.Die(msg)
610
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.")
615
616     # Cancel if EDITOR is unset or not executable.
617     if (self._options.requires_editor and (not os.environ.get("EDITOR") or
618         self.Command(
619             "which", os.environ["EDITOR"]) is None)):  # pragma: no cover
620       self.Die("Please set your EDITOR environment variable, you'll need it.")
621
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.")
626
627     # Persist current branch.
628     self["current_branch"] = self.GitCurrentBranch()
629
630     # Fetch unfetched revisions.
631     self.vc.Fetch()
632
633   def PrepareBranch(self):
634     # Delete the branch that will be created later if it exists already.
635     self.DeleteBranch(self._config["BRANCHNAME"])
636
637   def CommonCleanup(self):
638     if ' ' in self["current_branch"]:
639       self.GitCheckout('master')
640     else:
641       self.GitCheckout(self["current_branch"])
642     if self._config["BRANCHNAME"] != self["current_branch"]:
643       self.GitDeleteBranch(self._config["BRANCHNAME"])
644
645     # Clean up all temporary files.
646     for f in glob.iglob("%s*" % self._config["PERSISTFILE_BASENAME"]):
647       if os.path.isfile(f):
648         os.remove(f)
649       if os.path.isdir(f):
650         shutil.rmtree(f)
651
652   def ReadAndPersistVersion(self, prefix=""):
653     def ReadAndPersist(var_name, def_name):
654       match = re.match(r"^#define %s\s+(\d*)" % def_name, line)
655       if match:
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)
664
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.")
670     answer = ""
671     while answer != "LGTM":
672       print "> ",
673       answer = self.ReadLine(None if self._options.wait_for_lgtm else "LGTM")
674       if answer != "LGTM":
675         print "That was not 'LGTM'."
676
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()
682     answer = ""
683     while answer != "RESOLVED":
684       if answer == "ABORT":
685         self.Die("Applying the patch failed.")
686       if answer != "":
687         print "That was not 'RESOLVED' or 'ABORT'."
688       print "> ",
689       answer = self.ReadLine()
690
691   # Takes a file containing the patch to apply as first argument.
692   def ApplyPatch(self, patch_file, revert=False):
693     try:
694       self.GitApplyPatch(patch_file, revert)
695     except GitFailedException:
696       self.WaitForResolvingConflicts(patch_file)
697
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
703       # on...) comment."
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)
708
709   def ArrayToVersion(self, prefix):
710     return ".".join([self[prefix + "major"],
711                      self[prefix + "minor"],
712                      self[prefix + "build"],
713                      self[prefix + "patch"]])
714
715   def SetVersion(self, version_file, prefix):
716     output = ""
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)
728
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)
750
751     self.Command(
752         "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)
756
757
758 class BootstrapStep(Step):
759   MESSAGE = "Bootstapping v8 checkout."
760
761   def RunStep(self):
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)
769
770
771 class UploadStep(Step):
772   MESSAGE = "Upload for code review."
773
774   def RunStep(self):
775     if self._options.reviewer:
776       print "Using account %s for review." % self._options.reviewer
777       reviewer = self._options.reviewer
778     else:
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,
784                    cc=self._options.cc)
785
786
787 class DetermineV8Sheriff(Step):
788   MESSAGE = "Determine the V8 sheriff for code review."
789
790   def RunStep(self):
791     self["sheriff"] = None
792     if not self._options.sheriff:  # pragma: no cover
793       return
794
795     try:
796       # The googlers mapping maps @google.com accounts to @chromium.org
797       # accounts.
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."
803       return
804
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))
809
810     # If "channel is sheriff", we can't match an account.
811     if match:
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"]
817     else:
818       print "No active sheriff found."
819
820
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 {}
826
827     try:
828       message = step_class.MESSAGE
829     except AttributeError:
830       message = step_class.__name__
831
832     return step_class(message, number=number, config=config,
833                       state=state, options=options,
834                       handler=side_effect_handler)
835
836
837 class ScriptsBase(object):
838   def __init__(self,
839                config=None,
840                side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER,
841                state=None):
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 {}
845
846   def _Description(self):
847     return None
848
849   def _PrepareOptions(self, parser):
850     pass
851
852   def _ProcessOptions(self, options):
853     return True
854
855   def _Steps(self):  # pragma: no cover
856     raise Exception("Not implemented.")
857
858   def _Config(self):
859     return {}
860
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 "
874                               "option."))
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.",
882         default=0, type=int)
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 "
888                               "checkout."))
889     self._PrepareOptions(parser)
890
891     if args is None:  # pragma: no cover
892       options = parser.parse_args()
893     else:
894       options = parser.parse_args(args)
895
896     # Process common options.
897     if options.step < 0:  # pragma: no cover
898       print "Bad step number %d" % options.step
899       parser.print_help()
900       return None
901     if options.sheriff and not options.googlers_mapping:  # pragma: no cover
902       print "To determine the current sheriff, requires the googler mapping"
903       parser.print_help()
904       return None
905     if options.svn and not options.svn_config:
906       print "Using pure svn for committing requires also --svn-config"
907       parser.print_help()
908       return None
909
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
914
915     # Derived options.
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
920
921     # Process script specific options.
922     if not self._ProcessOptions(options):
923       parser.print_help()
924       return None
925
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"
930     return options
931
932   def RunSteps(self, step_classes, args=None):
933     options = self.MakeOptions(args)
934     if not options:
935       return 1
936
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)
940
941     steps = []
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:]:
946       if step.Run():
947         return 0
948     return 0
949
950   def Run(self, args=None):
951     return self.RunSteps(self._Steps(), args)