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