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