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