Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / v8 / tools / push-to-trunk / releases.py
1 #!/usr/bin/env python
2 # Copyright 2014 the V8 project authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 # This script retrieves the history of all V8 branches and trunk revisions and
7 # their corresponding Chromium revisions.
8
9 # Requires a chromium checkout with branch heads:
10 # gclient sync --with_branch_heads
11 # gclient fetch
12
13 import argparse
14 import csv
15 import itertools
16 import json
17 import os
18 import re
19 import sys
20
21 from common_includes import *
22
23 CONFIG = {
24   "BRANCHNAME": "retrieve-v8-releases",
25   "PERSISTFILE_BASENAME": "/tmp/v8-releases-tempfile",
26 }
27
28 # Expression for retrieving the bleeding edge revision from a commit message.
29 PUSH_MSG_SVN_RE = re.compile(r".* \(based on bleeding_edge revision r(\d+)\)$")
30 PUSH_MSG_GIT_RE = re.compile(r".* \(based on ([a-fA-F0-9]+)\)$")
31
32 # Expression for retrieving the merged patches from a merge commit message
33 # (old and new format).
34 MERGE_MESSAGE_RE = re.compile(r"^.*[M|m]erged (.+)(\)| into).*$", re.M)
35
36 # Expression for retrieving reverted patches from a commit message (old and
37 # new format).
38 ROLLBACK_MESSAGE_RE = re.compile(r"^.*[R|r]ollback of (.+)(\)| in).*$", re.M)
39
40 # Expression for retrieving the code review link.
41 REVIEW_LINK_RE = re.compile(r"^Review URL: (.+)$", re.M)
42
43 # Expression with three versions (historical) for extracting the v8 revision
44 # from the chromium DEPS file.
45 DEPS_RE = re.compile(r"""^\s*(?:["']v8_revision["']: ["']"""
46                      """|\(Var\("googlecode_url"\) % "v8"\) \+ "\/trunk@"""
47                      """|"http\:\/\/v8\.googlecode\.com\/svn\/trunk@)"""
48                      """([^"']+)["'].*$""", re.M)
49
50 # Expression to pick tag and revision for bleeding edge tags. To be used with
51 # output of 'svn log'.
52 BLEEDING_EDGE_TAGS_RE = re.compile(
53     r"A \/tags\/([^\s]+) \(from \/branches\/bleeding_edge\:(\d+)\)")
54
55
56 def SortBranches(branches):
57   """Sort branches with version number names."""
58   return sorted(branches, key=SortingKey, reverse=True)
59
60
61 def FilterDuplicatesAndReverse(cr_releases):
62   """Returns the chromium releases in reverse order filtered by v8 revision
63   duplicates.
64
65   cr_releases is a list of [cr_rev, v8_rev] reverse-sorted by cr_rev.
66   """
67   last = ""
68   result = []
69   for release in reversed(cr_releases):
70     if last == release[1]:
71       continue
72     last = release[1]
73     result.append(release)
74   return result
75
76
77 def BuildRevisionRanges(cr_releases):
78   """Returns a mapping of v8 revision -> chromium ranges.
79   The ranges are comma-separated, each range has the form R1:R2. The newest
80   entry is the only one of the form R1, as there is no end range.
81
82   cr_releases is a list of [cr_rev, v8_rev] reverse-sorted by cr_rev.
83   cr_rev either refers to a chromium svn revision or a chromium branch number.
84   """
85   range_lists = {}
86   cr_releases = FilterDuplicatesAndReverse(cr_releases)
87
88   # Visit pairs of cr releases from oldest to newest.
89   for cr_from, cr_to in itertools.izip(
90       cr_releases, itertools.islice(cr_releases, 1, None)):
91
92     # Assume the chromium revisions are all different.
93     assert cr_from[0] != cr_to[0]
94
95     # TODO(machenbach): Subtraction is not git friendly.
96     ran = "%s:%d" % (cr_from[0], int(cr_to[0]) - 1)
97
98     # Collect the ranges in lists per revision.
99     range_lists.setdefault(cr_from[1], []).append(ran)
100
101   # Add the newest revision.
102   if cr_releases:
103     range_lists.setdefault(cr_releases[-1][1], []).append(cr_releases[-1][0])
104
105   # Stringify and comma-separate the range lists.
106   return dict((rev, ", ".join(ran)) for rev, ran in range_lists.iteritems())
107
108
109 def MatchSafe(match):
110   if match:
111     return match.group(1)
112   else:
113     return ""
114
115
116 class Preparation(Step):
117   MESSAGE = "Preparation."
118
119   def RunStep(self):
120     self.CommonPrepare()
121     self.PrepareBranch()
122
123
124 class RetrieveV8Releases(Step):
125   MESSAGE = "Retrieve all V8 releases."
126
127   def ExceedsMax(self, releases):
128     return (self._options.max_releases > 0
129             and len(releases) > self._options.max_releases)
130
131   def GetBleedingEdgeFromPush(self, title):
132     return MatchSafe(PUSH_MSG_SVN_RE.match(title))
133
134   def GetBleedingEdgeGitFromPush(self, title):
135     return MatchSafe(PUSH_MSG_GIT_RE.match(title))
136
137   def GetMergedPatches(self, body):
138     patches = MatchSafe(MERGE_MESSAGE_RE.search(body))
139     if not patches:
140       patches = MatchSafe(ROLLBACK_MESSAGE_RE.search(body))
141       if patches:
142         # Indicate reverted patches with a "-".
143         patches = "-%s" % patches
144     return patches
145
146   def GetReleaseDict(
147       self, git_hash, bleeding_edge_rev, bleeding_edge_git, branch, version,
148       patches, cl_body):
149     revision = self.vc.GitSvn(git_hash)
150     return {
151       # The SVN revision on the branch.
152       "revision": revision,
153       # The git revision on the branch.
154       "revision_git": git_hash,
155       # The SVN revision on bleeding edge (only for newer trunk pushes).
156       "bleeding_edge": bleeding_edge_rev,
157       # The same for git.
158       "bleeding_edge_git": bleeding_edge_git,
159       # The branch name.
160       "branch": branch,
161       # The version for displaying in the form 3.26.3 or 3.26.3.12.
162       "version": version,
163       # The date of the commit.
164       "date": self.GitLog(n=1, format="%ci", git_hash=git_hash),
165       # Merged patches if available in the form 'r1234, r2345'.
166       "patches_merged": patches,
167       # Default for easier output formatting.
168       "chromium_revision": "",
169       # Default for easier output formatting.
170       "chromium_branch": "",
171       # Link to the CL on code review. Trunk pushes are not uploaded, so this
172       # field will be populated below with the recent roll CL link.
173       "review_link": MatchSafe(REVIEW_LINK_RE.search(cl_body)),
174       # Link to the commit message on google code.
175       "revision_link": ("https://code.google.com/p/v8/source/detail?r=%s"
176                         % revision),
177     }
178
179   def GetRelease(self, git_hash, branch):
180     self.ReadAndPersistVersion()
181     base_version = [self["major"], self["minor"], self["build"]]
182     version = ".".join(base_version)
183     body = self.GitLog(n=1, format="%B", git_hash=git_hash)
184
185     patches = ""
186     if self["patch"] != "0":
187       version += ".%s" % self["patch"]
188       patches = self.GetMergedPatches(body)
189
190     title = self.GitLog(n=1, format="%s", git_hash=git_hash)
191     bleeding_edge_revision = self.GetBleedingEdgeFromPush(title)
192     bleeding_edge_git = ""
193     if bleeding_edge_revision:
194       bleeding_edge_git = self.vc.SvnGit(bleeding_edge_revision,
195                                          self.vc.RemoteMasterBranch())
196     else:
197       bleeding_edge_git = self.GetBleedingEdgeGitFromPush(title)
198     return self.GetReleaseDict(
199         git_hash, bleeding_edge_revision, bleeding_edge_git, branch, version,
200         patches, body), self["patch"]
201
202   def GetReleasesFromMaster(self):
203     tag_text = self.SVN("log https://v8.googlecode.com/svn/tags -v --limit 20")
204     releases = []
205     for (tag, revision) in re.findall(BLEEDING_EDGE_TAGS_RE, tag_text):
206       git_hash = self.vc.SvnGit(revision)
207
208       # Add bleeding edge release. It does not contain patches or a code
209       # review link, as tags are not uploaded.
210       releases.append(self.GetReleaseDict(
211         git_hash, revision, git_hash, self.vc.MasterBranch(), tag, "", ""))
212     return releases
213
214   def GetReleasesFromBranch(self, branch):
215     self.GitReset(self.vc.RemoteBranch(branch))
216     if branch == self.vc.MasterBranch():
217       return self.GetReleasesFromMaster()
218
219     releases = []
220     try:
221       for git_hash in self.GitLog(format="%H").splitlines():
222         if VERSION_FILE not in self.GitChangedFiles(git_hash):
223           continue
224         if self.ExceedsMax(releases):
225           break  # pragma: no cover
226         if not self.GitCheckoutFileSafe(VERSION_FILE, git_hash):
227           break  # pragma: no cover
228
229         release, patch_level = self.GetRelease(git_hash, branch)
230         releases.append(release)
231
232         # Follow branches only until their creation point.
233         # TODO(machenbach): This omits patches if the version file wasn't
234         # manipulated correctly. Find a better way to detect the point where
235         # the parent of the branch head leads to the trunk branch.
236         if branch != self.vc.CandidateBranch() and patch_level == "0":
237           break
238
239     # Allow Ctrl-C interrupt.
240     except (KeyboardInterrupt, SystemExit):  # pragma: no cover
241       pass
242
243     # Clean up checked-out version file.
244     self.GitCheckoutFileSafe(VERSION_FILE, "HEAD")
245     return releases
246
247   def RunStep(self):
248     self.GitCreateBranch(self._config["BRANCHNAME"])
249     branches = self.vc.GetBranches()
250     releases = []
251     if self._options.branch == 'recent':
252       # Get only recent development on trunk, beta and stable.
253       if self._options.max_releases == 0:  # pragma: no cover
254         self._options.max_releases = 10
255       beta, stable = SortBranches(branches)[0:2]
256       releases += self.GetReleasesFromBranch(stable)
257       releases += self.GetReleasesFromBranch(beta)
258       releases += self.GetReleasesFromBranch(self.vc.CandidateBranch())
259       releases += self.GetReleasesFromBranch(self.vc.MasterBranch())
260     elif self._options.branch == 'all':  # pragma: no cover
261       # Retrieve the full release history.
262       for branch in branches:
263         releases += self.GetReleasesFromBranch(branch)
264       releases += self.GetReleasesFromBranch(self.vc.CandidateBranch())
265       releases += self.GetReleasesFromBranch(self.vc.MasterBranch())
266     else:  # pragma: no cover
267       # Retrieve history for a specified branch.
268       assert self._options.branch in (branches +
269           [self.vc.CandidateBranch(), self.vc.MasterBranch()])
270       releases += self.GetReleasesFromBranch(self._options.branch)
271
272     self["releases"] = sorted(releases,
273                               key=lambda r: SortingKey(r["version"]),
274                               reverse=True)
275
276
277 class SwitchChromium(Step):
278   MESSAGE = "Switch to Chromium checkout."
279
280   def RunStep(self):
281     cwd = self._options.chromium
282     # Check for a clean workdir.
283     if not self.GitIsWorkdirClean(cwd=cwd):  # pragma: no cover
284       self.Die("Workspace is not clean. Please commit or undo your changes.")
285     # Assert that the DEPS file is there.
286     if not os.path.exists(os.path.join(cwd, "DEPS")):  # pragma: no cover
287       self.Die("DEPS file not present.")
288
289
290 class UpdateChromiumCheckout(Step):
291   MESSAGE = "Update the checkout and create a new branch."
292
293   def RunStep(self):
294     cwd = self._options.chromium
295     self.GitCheckout("master", cwd=cwd)
296     self.GitPull(cwd=cwd)
297     self.GitCreateBranch(self.Config("BRANCHNAME"), cwd=cwd)
298
299
300 def ConvertToCommitNumber(step, revision):
301   # Simple check for git hashes.
302   if revision.isdigit() and len(revision) < 8:
303     return revision
304   return step.GitConvertToSVNRevision(
305       revision, cwd=os.path.join(step._options.chromium, "v8"))
306
307
308 class RetrieveChromiumV8Releases(Step):
309   MESSAGE = "Retrieve V8 releases from Chromium DEPS."
310
311   def RunStep(self):
312     cwd = self._options.chromium
313     releases = filter(
314         lambda r: r["branch"] in [self.vc.CandidateBranch(),
315                                   self.vc.MasterBranch()],
316         self["releases"])
317     if not releases:  # pragma: no cover
318       print "No releases detected. Skipping chromium history."
319       return True
320
321     # Update v8 checkout in chromium.
322     self.GitFetchOrigin(cwd=os.path.join(cwd, "v8"))
323
324     oldest_v8_rev = int(releases[-1]["revision"])
325
326     cr_releases = []
327     try:
328       for git_hash in self.GitLog(
329           format="%H", grep="V8", cwd=cwd).splitlines():
330         if "DEPS" not in self.GitChangedFiles(git_hash, cwd=cwd):
331           continue
332         if not self.GitCheckoutFileSafe("DEPS", git_hash, cwd=cwd):
333           break  # pragma: no cover
334         deps = FileToText(os.path.join(cwd, "DEPS"))
335         match = DEPS_RE.search(deps)
336         if match:
337           cr_rev = self.GetCommitPositionNumber(git_hash, cwd=cwd)
338           if cr_rev:
339             v8_rev = ConvertToCommitNumber(self, match.group(1))
340             cr_releases.append([cr_rev, v8_rev])
341
342           # Stop after reaching beyond the last v8 revision we want to update.
343           # We need a small buffer for possible revert/reland frenzies.
344           # TODO(machenbach): Subtraction is not git friendly.
345           if int(v8_rev) < oldest_v8_rev - 100:
346             break  # pragma: no cover
347
348     # Allow Ctrl-C interrupt.
349     except (KeyboardInterrupt, SystemExit):  # pragma: no cover
350       pass
351
352     # Clean up.
353     self.GitCheckoutFileSafe("DEPS", "HEAD", cwd=cwd)
354
355     # Add the chromium ranges to the v8 trunk and bleeding_edge releases.
356     all_ranges = BuildRevisionRanges(cr_releases)
357     releases_dict = dict((r["revision"], r) for r in releases)
358     for revision, ranges in all_ranges.iteritems():
359       releases_dict.get(revision, {})["chromium_revision"] = ranges
360
361
362 # TODO(machenbach): Unify common code with method above.
363 class RietrieveChromiumBranches(Step):
364   MESSAGE = "Retrieve Chromium branch information."
365
366   def RunStep(self):
367     cwd = self._options.chromium
368     trunk_releases = filter(lambda r: r["branch"] == self.vc.CandidateBranch(),
369                             self["releases"])
370     if not trunk_releases:  # pragma: no cover
371       print "No trunk releases detected. Skipping chromium history."
372       return True
373
374     oldest_v8_rev = int(trunk_releases[-1]["revision"])
375
376     # Filter out irrelevant branches.
377     branches = filter(lambda r: re.match(r"branch-heads/\d+", r),
378                       self.GitRemotes(cwd=cwd))
379
380     # Transform into pure branch numbers.
381     branches = map(lambda r: int(re.match(r"branch-heads/(\d+)", r).group(1)),
382                    branches)
383
384     branches = sorted(branches, reverse=True)
385
386     cr_branches = []
387     try:
388       for branch in branches:
389         if not self.GitCheckoutFileSafe("DEPS",
390                                         "branch-heads/%d" % branch,
391                                         cwd=cwd):
392           break  # pragma: no cover
393         deps = FileToText(os.path.join(cwd, "DEPS"))
394         match = DEPS_RE.search(deps)
395         if match:
396           v8_rev = ConvertToCommitNumber(self, match.group(1))
397           cr_branches.append([str(branch), v8_rev])
398
399           # Stop after reaching beyond the last v8 revision we want to update.
400           # We need a small buffer for possible revert/reland frenzies.
401           # TODO(machenbach): Subtraction is not git friendly.
402           if int(v8_rev) < oldest_v8_rev - 100:
403             break  # pragma: no cover
404
405     # Allow Ctrl-C interrupt.
406     except (KeyboardInterrupt, SystemExit):  # pragma: no cover
407       pass
408
409     # Clean up.
410     self.GitCheckoutFileSafe("DEPS", "HEAD", cwd=cwd)
411
412     # Add the chromium branches to the v8 trunk releases.
413     all_ranges = BuildRevisionRanges(cr_branches)
414     trunk_dict = dict((r["revision"], r) for r in trunk_releases)
415     for revision, ranges in all_ranges.iteritems():
416       trunk_dict.get(revision, {})["chromium_branch"] = ranges
417
418
419 class CleanUp(Step):
420   MESSAGE = "Clean up."
421
422   def RunStep(self):
423     self.GitCheckout("master", cwd=self._options.chromium)
424     self.GitDeleteBranch(self.Config("BRANCHNAME"), cwd=self._options.chromium)
425     self.CommonCleanup()
426
427
428 class WriteOutput(Step):
429   MESSAGE = "Print output."
430
431   def Run(self):
432     if self._options.csv:
433       with open(self._options.csv, "w") as f:
434         writer = csv.DictWriter(f,
435                                 ["version", "branch", "revision",
436                                  "chromium_revision", "patches_merged"],
437                                 restval="",
438                                 extrasaction="ignore")
439         for release in self["releases"]:
440           writer.writerow(release)
441     if self._options.json:
442       with open(self._options.json, "w") as f:
443         f.write(json.dumps(self["releases"]))
444     if not self._options.csv and not self._options.json:
445       print self["releases"]  # pragma: no cover
446
447
448 class Releases(ScriptsBase):
449   def _PrepareOptions(self, parser):
450     parser.add_argument("-b", "--branch", default="recent",
451                         help=("The branch to analyze. If 'all' is specified, "
452                               "analyze all branches. If 'recent' (default) "
453                               "is specified, track beta, stable and trunk."))
454     parser.add_argument("-c", "--chromium",
455                         help=("The path to your Chromium src/ "
456                               "directory to automate the V8 roll."))
457     parser.add_argument("--csv", help="Path to a CSV file for export.")
458     parser.add_argument("-m", "--max-releases", type=int, default=0,
459                         help="The maximum number of releases to track.")
460     parser.add_argument("--json", help="Path to a JSON file for export.")
461
462   def _ProcessOptions(self, options):  # pragma: no cover
463     return True
464
465   def _Config(self):
466     return {
467       "BRANCHNAME": "retrieve-v8-releases",
468       "PERSISTFILE_BASENAME": "/tmp/v8-releases-tempfile",
469     }
470
471   def _Steps(self):
472     return [
473       Preparation,
474       RetrieveV8Releases,
475       SwitchChromium,
476       UpdateChromiumCheckout,
477       RetrieveChromiumV8Releases,
478       RietrieveChromiumBranches,
479       CleanUp,
480       WriteOutput,
481     ]
482
483
484 if __name__ == "__main__":  # pragma: no cover
485   sys.exit(Releases().Run())