482509f7d1974e9fbf946c6995be33caae920e76
[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 imp
32 import json
33 import os
34 import re
35 import subprocess
36 import sys
37 import textwrap
38 import time
39 import urllib2
40
41 from git_recipes import GitRecipesMixin
42 from git_recipes import GitFailedException
43
44 PERSISTFILE_BASENAME = "PERSISTFILE_BASENAME"
45 BRANCHNAME = "BRANCHNAME"
46 DOT_GIT_LOCATION = "DOT_GIT_LOCATION"
47 VERSION_FILE = "VERSION_FILE"
48 CHANGELOG_FILE = "CHANGELOG_FILE"
49 CHANGELOG_ENTRY_FILE = "CHANGELOG_ENTRY_FILE"
50 COMMITMSG_FILE = "COMMITMSG_FILE"
51 PATCH_FILE = "PATCH_FILE"
52
53
54 def TextToFile(text, file_name):
55   with open(file_name, "w") as f:
56     f.write(text)
57
58
59 def AppendToFile(text, file_name):
60   with open(file_name, "a") as f:
61     f.write(text)
62
63
64 def LinesInFile(file_name):
65   with open(file_name) as f:
66     for line in f:
67       yield line
68
69
70 def FileToText(file_name):
71   with open(file_name) as f:
72     return f.read()
73
74
75 def MSub(rexp, replacement, text):
76   return re.sub(rexp, replacement, text, flags=re.MULTILINE)
77
78
79 def Fill80(line):
80   # Replace tabs and remove surrounding space.
81   line = re.sub(r"\t", r"        ", line.strip())
82
83   # Format with 8 characters indentation and line width 80.
84   return textwrap.fill(line, width=80, initial_indent="        ",
85                        subsequent_indent="        ")
86
87
88 def MakeComment(text):
89   return MSub(r"^( ?)", "#", text)
90
91
92 def StripComments(text):
93   # Use split not splitlines to keep terminal newlines.
94   return "\n".join(filter(lambda x: not x.startswith("#"), text.split("\n")))
95
96
97 def MakeChangeLogBody(commit_messages, auto_format=False):
98   result = ""
99   added_titles = set()
100   for (title, body, author) in commit_messages:
101     # TODO(machenbach): Better check for reverts. A revert should remove the
102     # original CL from the actual log entry.
103     title = title.strip()
104     if auto_format:
105       # Only add commits that set the LOG flag correctly.
106       log_exp = r"^[ \t]*LOG[ \t]*=[ \t]*(?:(?:Y(?:ES)?)|TRUE)"
107       if not re.search(log_exp, body, flags=re.I | re.M):
108         continue
109       # Never include reverts.
110       if title.startswith("Revert "):
111         continue
112       # Don't include duplicates.
113       if title in added_titles:
114         continue
115
116     # Add and format the commit's title and bug reference. Move dot to the end.
117     added_titles.add(title)
118     raw_title = re.sub(r"(\.|\?|!)$", "", title)
119     bug_reference = MakeChangeLogBugReference(body)
120     space = " " if bug_reference else ""
121     result += "%s\n" % Fill80("%s%s%s." % (raw_title, space, bug_reference))
122
123     # Append the commit's author for reference if not in auto-format mode.
124     if not auto_format:
125       result += "%s\n" % Fill80("(%s)" % author.strip())
126
127     result += "\n"
128   return result
129
130
131 def MakeChangeLogBugReference(body):
132   """Grep for "BUG=xxxx" lines in the commit message and convert them to
133   "(issue xxxx)".
134   """
135   crbugs = []
136   v8bugs = []
137
138   def AddIssues(text):
139     ref = re.match(r"^BUG[ \t]*=[ \t]*(.+)$", text.strip())
140     if not ref:
141       return
142     for bug in ref.group(1).split(","):
143       bug = bug.strip()
144       match = re.match(r"^v8:(\d+)$", bug)
145       if match: v8bugs.append(int(match.group(1)))
146       else:
147         match = re.match(r"^(?:chromium:)?(\d+)$", bug)
148         if match: crbugs.append(int(match.group(1)))
149
150   # Add issues to crbugs and v8bugs.
151   map(AddIssues, body.splitlines())
152
153   # Filter duplicates, sort, stringify.
154   crbugs = map(str, sorted(set(crbugs)))
155   v8bugs = map(str, sorted(set(v8bugs)))
156
157   bug_groups = []
158   def FormatIssues(prefix, bugs):
159     if len(bugs) > 0:
160       plural = "s" if len(bugs) > 1 else ""
161       bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs)))
162
163   FormatIssues("", v8bugs)
164   FormatIssues("Chromium ", crbugs)
165
166   if len(bug_groups) > 0:
167     return "(%s)" % ", ".join(bug_groups)
168   else:
169     return ""
170
171
172 # Some commands don't like the pipe, e.g. calling vi from within the script or
173 # from subscripts like git cl upload.
174 def Command(cmd, args="", prefix="", pipe=True):
175   # TODO(machenbach): Use timeout.
176   cmd_line = "%s %s %s" % (prefix, cmd, args)
177   print "Command: %s" % cmd_line
178   sys.stdout.flush()
179   try:
180     if pipe:
181       return subprocess.check_output(cmd_line, shell=True)
182     else:
183       return subprocess.check_call(cmd_line, shell=True)
184   except subprocess.CalledProcessError:
185     return None
186   finally:
187     sys.stdout.flush()
188     sys.stderr.flush()
189
190
191 # Wrapper for side effects.
192 class SideEffectHandler(object):  # pragma: no cover
193   def Call(self, fun, *args, **kwargs):
194     return fun(*args, **kwargs)
195
196   def Command(self, cmd, args="", prefix="", pipe=True):
197     return Command(cmd, args, prefix, pipe)
198
199   def ReadLine(self):
200     return sys.stdin.readline().strip()
201
202   def ReadURL(self, url, params=None):
203     # pylint: disable=E1121
204     url_fh = urllib2.urlopen(url, params, 60)
205     try:
206       return url_fh.read()
207     finally:
208       url_fh.close()
209
210   def Sleep(self, seconds):
211     time.sleep(seconds)
212
213   def GetDate(self):
214     return datetime.date.today().strftime("%Y-%m-%d")
215
216 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler()
217
218
219 class NoRetryException(Exception):
220   pass
221
222
223 class Step(GitRecipesMixin):
224   def __init__(self, text, requires, number, config, state, options, handler):
225     self._text = text
226     self._requires = requires
227     self._number = number
228     self._config = config
229     self._state = state
230     self._options = options
231     self._side_effect_handler = handler
232     assert self._number >= 0
233     assert self._config is not None
234     assert self._state is not None
235     assert self._side_effect_handler is not None
236
237   def __getitem__(self, key):
238     # Convenience method to allow direct [] access on step classes for
239     # manipulating the backed state dict.
240     return self._state[key]
241
242   def __setitem__(self, key, value):
243     # Convenience method to allow direct [] access on step classes for
244     # manipulating the backed state dict.
245     self._state[key] = value
246
247   def Config(self, key):
248     return self._config[key]
249
250   def Run(self):
251     # Restore state.
252     state_file = "%s-state.json" % self._config[PERSISTFILE_BASENAME]
253     if not self._state and os.path.exists(state_file):
254       self._state.update(json.loads(FileToText(state_file)))
255
256     # Skip step if requirement is not met.
257     if self._requires and not self._state.get(self._requires):
258       return
259
260     print ">>> Step %d: %s" % (self._number, self._text)
261     try:
262       return self.RunStep()
263     finally:
264       # Persist state.
265       TextToFile(json.dumps(self._state), state_file)
266
267   def RunStep(self):  # pragma: no cover
268     raise NotImplementedError
269
270   def Retry(self, cb, retry_on=None, wait_plan=None):
271     """ Retry a function.
272     Params:
273       cb: The function to retry.
274       retry_on: A callback that takes the result of the function and returns
275                 True if the function should be retried. A function throwing an
276                 exception is always retried.
277       wait_plan: A list of waiting delays between retries in seconds. The
278                  maximum number of retries is len(wait_plan).
279     """
280     retry_on = retry_on or (lambda x: False)
281     wait_plan = list(wait_plan or [])
282     wait_plan.reverse()
283     while True:
284       got_exception = False
285       try:
286         result = cb()
287       except NoRetryException, e:
288         raise e
289       except Exception:
290         got_exception = True
291       if got_exception or retry_on(result):
292         if not wait_plan:  # pragma: no cover
293           raise Exception("Retried too often. Giving up.")
294         wait_time = wait_plan.pop()
295         print "Waiting for %f seconds." % wait_time
296         self._side_effect_handler.Sleep(wait_time)
297         print "Retrying..."
298       else:
299         return result
300
301   def ReadLine(self, default=None):
302     # Don't prompt in forced mode.
303     if self._options.force_readline_defaults and default is not None:
304       print "%s (forced)" % default
305       return default
306     else:
307       return self._side_effect_handler.ReadLine()
308
309   def Git(self, args="", prefix="", pipe=True, retry_on=None):
310     cmd = lambda: self._side_effect_handler.Command("git", args, prefix, pipe)
311     result = self.Retry(cmd, retry_on, [5, 30])
312     if result is None:
313       raise GitFailedException("'git %s' failed." % args)
314     return result
315
316   def SVN(self, args="", prefix="", pipe=True, retry_on=None):
317     cmd = lambda: self._side_effect_handler.Command("svn", args, prefix, pipe)
318     return self.Retry(cmd, retry_on, [5, 30])
319
320   def Editor(self, args):
321     if self._options.requires_editor:
322       return self._side_effect_handler.Command(os.environ["EDITOR"], args,
323                                                pipe=False)
324
325   def ReadURL(self, url, params=None, retry_on=None, wait_plan=None):
326     wait_plan = wait_plan or [3, 60, 600]
327     cmd = lambda: self._side_effect_handler.ReadURL(url, params)
328     return self.Retry(cmd, retry_on, wait_plan)
329
330   def GetDate(self):
331     return self._side_effect_handler.GetDate()
332
333   def Die(self, msg=""):
334     if msg != "":
335       print "Error: %s" % msg
336     print "Exiting"
337     raise Exception(msg)
338
339   def DieNoManualMode(self, msg=""):
340     if not self._options.manual:  # pragma: no cover
341       msg = msg or "Only available in manual mode."
342       self.Die(msg)
343
344   def Confirm(self, msg):
345     print "%s [Y/n] " % msg,
346     answer = self.ReadLine(default="Y")
347     return answer == "" or answer == "Y" or answer == "y"
348
349   def DeleteBranch(self, name):
350     for line in self.GitBranch().splitlines():
351       if re.match(r".*\s+%s$" % name, line):
352         msg = "Branch %s exists, do you want to delete it?" % name
353         if self.Confirm(msg):
354           self.GitDeleteBranch(name)
355           print "Branch %s deleted." % name
356         else:
357           msg = "Can't continue. Please delete branch %s and try again." % name
358           self.Die(msg)
359
360   def InitialEnvironmentChecks(self):
361     # Cancel if this is not a git checkout.
362     if not os.path.exists(self._config[DOT_GIT_LOCATION]):  # pragma: no cover
363       self.Die("This is not a git checkout, this script won't work for you.")
364
365     # Cancel if EDITOR is unset or not executable.
366     if (self._options.requires_editor and (not os.environ.get("EDITOR") or
367         Command("which", os.environ["EDITOR"]) is None)):  # pragma: no cover
368       self.Die("Please set your EDITOR environment variable, you'll need it.")
369
370   def CommonPrepare(self):
371     # Check for a clean workdir.
372     if not self.GitIsWorkdirClean():  # pragma: no cover
373       self.Die("Workspace is not clean. Please commit or undo your changes.")
374
375     # Persist current branch.
376     self["current_branch"] = self.GitCurrentBranch()
377
378     # Fetch unfetched revisions.
379     self.GitSVNFetch()
380
381   def PrepareBranch(self):
382     # Delete the branch that will be created later if it exists already.
383     self.DeleteBranch(self._config[BRANCHNAME])
384
385   def CommonCleanup(self):
386     self.GitCheckout(self["current_branch"])
387     if self._config[BRANCHNAME] != self["current_branch"]:
388       self.GitDeleteBranch(self._config[BRANCHNAME])
389
390     # Clean up all temporary files.
391     Command("rm", "-f %s*" % self._config[PERSISTFILE_BASENAME])
392
393   def ReadAndPersistVersion(self, prefix=""):
394     def ReadAndPersist(var_name, def_name):
395       match = re.match(r"^#define %s\s+(\d*)" % def_name, line)
396       if match:
397         value = match.group(1)
398         self["%s%s" % (prefix, var_name)] = value
399     for line in LinesInFile(self._config[VERSION_FILE]):
400       for (var_name, def_name) in [("major", "MAJOR_VERSION"),
401                                    ("minor", "MINOR_VERSION"),
402                                    ("build", "BUILD_NUMBER"),
403                                    ("patch", "PATCH_LEVEL")]:
404         ReadAndPersist(var_name, def_name)
405
406   def WaitForLGTM(self):
407     print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit "
408            "your change. (If you need to iterate on the patch or double check "
409            "that it's sane, do so in another shell, but remember to not "
410            "change the headline of the uploaded CL.")
411     answer = ""
412     while answer != "LGTM":
413       print "> ",
414       answer = self.ReadLine(None if self._options.wait_for_lgtm else "LGTM")
415       if answer != "LGTM":
416         print "That was not 'LGTM'."
417
418   def WaitForResolvingConflicts(self, patch_file):
419     print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", "
420           "or resolve the conflicts, stage *all* touched files with "
421           "'git add', and type \"RESOLVED<Return>\"")
422     self.DieNoManualMode()
423     answer = ""
424     while answer != "RESOLVED":
425       if answer == "ABORT":
426         self.Die("Applying the patch failed.")
427       if answer != "":
428         print "That was not 'RESOLVED' or 'ABORT'."
429       print "> ",
430       answer = self.ReadLine()
431
432   # Takes a file containing the patch to apply as first argument.
433   def ApplyPatch(self, patch_file, revert=False):
434     try:
435       self.GitApplyPatch(patch_file, revert)
436     except GitFailedException:
437       self.WaitForResolvingConflicts(patch_file)
438
439   def FindLastTrunkPush(self, parent_hash="", include_patches=False):
440     push_pattern = "^Version [[:digit:]]*\.[[:digit:]]*\.[[:digit:]]*"
441     if not include_patches:
442       # Non-patched versions only have three numbers followed by the "(based
443       # on...) comment."
444       push_pattern += " (based"
445     branch = "" if parent_hash else "svn/trunk"
446     return self.GitLog(n=1, format="%H", grep=push_pattern,
447                        parent_hash=parent_hash, branch=branch)
448
449
450 class UploadStep(Step):
451   MESSAGE = "Upload for code review."
452
453   def RunStep(self):
454     if self._options.reviewer:
455       print "Using account %s for review." % self._options.reviewer
456       reviewer = self._options.reviewer
457     else:
458       print "Please enter the email address of a V8 reviewer for your patch: ",
459       self.DieNoManualMode("A reviewer must be specified in forced mode.")
460       reviewer = self.ReadLine()
461     self.GitUpload(reviewer, self._options.author, self._options.force_upload)
462
463
464 class DetermineV8Sheriff(Step):
465   MESSAGE = "Determine the V8 sheriff for code review."
466
467   def RunStep(self):
468     self["sheriff"] = None
469     if not self._options.sheriff:  # pragma: no cover
470       return
471
472     try:
473       # The googlers mapping maps @google.com accounts to @chromium.org
474       # accounts.
475       googlers = imp.load_source('googlers_mapping',
476                                  self._options.googlers_mapping)
477       googlers = googlers.list_to_dict(googlers.get_list())
478     except:  # pragma: no cover
479       print "Skip determining sheriff without googler mapping."
480       return
481
482     # The sheriff determined by the rotation on the waterfall has a
483     # @google.com account.
484     url = "https://chromium-build.appspot.com/p/chromium/sheriff_v8.js"
485     match = re.match(r"document\.write\('(\w+)'\)", self.ReadURL(url))
486
487     # If "channel is sheriff", we can't match an account.
488     if match:
489       g_name = match.group(1)
490       self["sheriff"] = googlers.get(g_name + "@google.com",
491                                      g_name + "@chromium.org")
492       self._options.reviewer = self["sheriff"]
493       print "Found active sheriff: %s" % self["sheriff"]
494     else:
495       print "No active sheriff found."
496
497
498 def MakeStep(step_class=Step, number=0, state=None, config=None,
499              options=None, side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER):
500     # Allow to pass in empty dictionaries.
501     state = state if state is not None else {}
502     config = config if config is not None else {}
503
504     try:
505       message = step_class.MESSAGE
506     except AttributeError:
507       message = step_class.__name__
508     try:
509       requires = step_class.REQUIRES
510     except AttributeError:
511       requires = None
512
513     return step_class(message, requires, number=number, config=config,
514                       state=state, options=options,
515                       handler=side_effect_handler)
516
517
518 class ScriptsBase(object):
519   # TODO(machenbach): Move static config here.
520   def __init__(self, config, side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER,
521                state=None):
522     self._config = config
523     self._side_effect_handler = side_effect_handler
524     self._state = state if state is not None else {}
525
526   def _Description(self):
527     return None
528
529   def _PrepareOptions(self, parser):
530     pass
531
532   def _ProcessOptions(self, options):
533     return True
534
535   def _Steps(self):  # pragma: no cover
536     raise Exception("Not implemented.")
537
538   def MakeOptions(self, args=None):
539     parser = argparse.ArgumentParser(description=self._Description())
540     parser.add_argument("-a", "--author", default="",
541                         help="The author email used for rietveld.")
542     parser.add_argument("-g", "--googlers-mapping",
543                         help="Path to the script mapping google accounts.")
544     parser.add_argument("-r", "--reviewer", default="",
545                         help="The account name to be used for reviews.")
546     parser.add_argument("--sheriff", default=False, action="store_true",
547                         help=("Determine current sheriff to review CLs. On "
548                               "success, this will overwrite the reviewer "
549                               "option."))
550     parser.add_argument("-s", "--step",
551         help="Specify the step where to start work. Default: 0.",
552         default=0, type=int)
553
554     self._PrepareOptions(parser)
555
556     if args is None:  # pragma: no cover
557       options = parser.parse_args()
558     else:
559       options = parser.parse_args(args)
560
561     # Process common options.
562     if options.step < 0:  # pragma: no cover
563       print "Bad step number %d" % options.step
564       parser.print_help()
565       return None
566     if options.sheriff and not options.googlers_mapping:  # pragma: no cover
567       print "To determine the current sheriff, requires the googler mapping"
568       parser.print_help()
569       return None
570
571     # Defaults for options, common to all scripts.
572     options.manual = getattr(options, "manual", True)
573     options.force = getattr(options, "force", False)
574
575     # Derived options.
576     options.requires_editor = not options.force
577     options.wait_for_lgtm = not options.force
578     options.force_readline_defaults = not options.manual
579     options.force_upload = not options.manual
580
581     # Process script specific options.
582     if not self._ProcessOptions(options):
583       parser.print_help()
584       return None
585     return options
586
587   def RunSteps(self, step_classes, args=None):
588     options = self.MakeOptions(args)
589     if not options:
590       return 1
591
592     state_file = "%s-state.json" % self._config[PERSISTFILE_BASENAME]
593     if options.step == 0 and os.path.exists(state_file):
594       os.remove(state_file)
595
596     steps = []
597     for (number, step_class) in enumerate(step_classes):
598       steps.append(MakeStep(step_class, number, self._state, self._config,
599                             options, self._side_effect_handler))
600     for step in steps[options.step:]:
601       if step.Run():
602         return 1
603     return 0
604
605   def Run(self, args=None):
606     return self.RunSteps(self._Steps(), args)