Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(6)

Side by Side Diff: tools/push-to-trunk/releases.py

Issue 227583002: Add V8 releases script. (Closed) Base URL: https://v8.googlecode.com/svn/branches/bleeding_edge
Patch Set: Fix checking changed files at branch roots. Created 6 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « tools/push-to-trunk/git_recipes.py ('k') | tools/push-to-trunk/test_scripts.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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 import argparse
10 import csv
11 import itertools
12 import json
13 import os
14 import re
15 import sys
16
17 from common_includes import *
18
19 DEPS_FILE = "DEPS_FILE"
20 CHROMIUM = "CHROMIUM"
21
22 CONFIG = {
23 BRANCHNAME: "retrieve-v8-releases",
24 TEMP_BRANCH: "unused-branch", # TODO(machenbach): Remove from infrastructure.
25 PERSISTFILE_BASENAME: "/tmp/v8-releases-tempfile",
26 DOT_GIT_LOCATION: ".git",
27 VERSION_FILE: "src/version.cc",
28 DEPS_FILE: "DEPS",
29 }
30
31 # Expression for retrieving the bleeding edge revision from a commit message.
32 PUSH_MESSAGE_RE = re.compile(r".* \(based on bleeding_edge revision r(\d+)\)$")
33
34 # Expression for retrieving the merged patches from a merge commit message
35 # (old and new format).
36 MERGE_MESSAGE_RE = re.compile(r"^.*[M|m]erged (.+)(\)| into).*$", re.M)
37
38 # Expression for retrieving reverted patches from a commit message (old and
39 # new format).
40 ROLLBACK_MESSAGE_RE = re.compile(r"^.*[R|r]ollback of (.+)(\)| in).*$", re.M)
41
42 # Expression with three versions (historical) for extracting the v8 revision
43 # from the chromium DEPS file.
44 DEPS_RE = re.compile(r'^\s*(?:"v8_revision": "'
45 '|\(Var\("googlecode_url"\) % "v8"\) \+ "\/trunk@'
46 '|"http\:\/\/v8\.googlecode\.com\/svn\/trunk@)'
47 '([0-9]+)".*$', re.M)
48
49
50 def SortingKey(version):
51 """Key for sorting version number strings: '3.11' > '3.2.1.1'"""
52 version_keys = map(int, version.split("."))
53 # Fill up to full version numbers to normalize comparison.
54 while len(version_keys) < 4:
55 version_keys.append(0)
56 # Fill digits.
57 return ".".join(map("{0:03d}".format, version_keys))
58
59
60 def SortBranches(branches):
61 """Sort branches with version number names."""
62 return sorted(branches, key=SortingKey, reverse=True)
63
64
65 def FilterDuplicatesAndReverse(cr_releases):
66 """Returns the chromium releases in reverse order filtered by v8 revision
67 duplicates.
68
69 cr_releases is a list of [cr_rev, v8_rev] reverse-sorted by cr_rev.
70 """
71 last = ""
72 result = []
73 for release in reversed(cr_releases):
74 if last == release[1]:
75 continue
76 last = release[1]
77 result.append(release)
78 return result
79
80
81 def BuildRevisionRanges(cr_releases):
82 """Returns a mapping of v8 revision -> chromium ranges.
83 The ranges are comma-separated, each range has the form R1:R2. The newest
84 entry is the only one of the form R1, as there is no end range.
85
86 cr_releases is a list of [cr_rev, v8_rev] reverse-sorted by cr_rev.
87 """
88 range_lists = {}
89 cr_releases = FilterDuplicatesAndReverse(cr_releases)
90
91 # Visit pairs of cr releases from oldest to newest.
92 for cr_from, cr_to in itertools.izip(
93 cr_releases, itertools.islice(cr_releases, 1, None)):
94
95 # Assume the chromium revisions are all different.
96 assert cr_from[0] != cr_to[0]
97
98 # TODO(machenbach): Subtraction is not git friendly.
99 ran = "%s:%d" % (cr_from[0], int(cr_to[0]) - 1)
100
101 # Collect the ranges in lists per revision.
102 range_lists.setdefault(cr_from[1], []).append(ran)
103
104 # Add the newest revision.
105 if cr_releases:
106 range_lists.setdefault(cr_releases[-1][1], []).append(cr_releases[-1][0])
107
108 # Stringify and comma-separate the range lists.
109 return dict((rev, ",".join(ran)) for rev, ran in range_lists.iteritems())
110
111
112 def MatchSafe(match):
113 if match:
114 return match.group(1)
115 else:
116 return ""
117
118
119 class Preparation(Step):
120 MESSAGE = "Preparation."
121
122 def RunStep(self):
123 self.CommonPrepare()
124 self.PrepareBranch()
125
126
127 class RetrieveV8Releases(Step):
128 MESSAGE = "Retrieve all V8 releases."
129
130 def ExceedsMax(self, releases):
131 return (self._options.max_releases > 0
132 and len(releases) > self._options.max_releases)
133
134 def GetBleedingEdgeFromPush(self, title):
135 return MatchSafe(PUSH_MESSAGE_RE.match(title))
136
137 def GetMergedPatches(self, git_hash):
138 body = self.GitLog(n=1, format="%B", git_hash=git_hash)
139 patches = MatchSafe(MERGE_MESSAGE_RE.search(body))
140 if not patches:
141 patches = MatchSafe(ROLLBACK_MESSAGE_RE.search(body))
142 if patches:
143 # Indicate reverted patches with a "-".
144 patches = "-%s" % patches
145 return patches
146
147 def GetRelease(self, git_hash, branch):
148 self.ReadAndPersistVersion()
149 base_version = [self["major"], self["minor"], self["build"]]
150 version = ".".join(base_version)
151
152 patches = ""
153 if self["patch"] != "0":
154 version += ".%s" % self["patch"]
155 patches = self.GetMergedPatches(git_hash)
156
157 title = self.GitLog(n=1, format="%s", git_hash=git_hash)
158 return {
159 # The SVN revision on the branch.
160 "revision": self.GitSVNFindSVNRev(git_hash),
161 # The SVN revision on bleeding edge (only for newer trunk pushes).
162 "bleeding_edge": self.GetBleedingEdgeFromPush(title),
163 # The branch name.
164 "branch": branch,
165 # The version for displaying in the form 3.26.3 or 3.26.3.12.
166 "version": version,
167 # Merged patches if available in the form 'r1234, r2345'.
168 "patches_merged": patches,
169 }, self["patch"]
170
171 def GetReleasesFromBranch(self, branch):
172 self.GitReset("svn/%s" % branch)
173 releases = []
174 try:
175 for git_hash in self.GitLog(format="%H").splitlines():
176 if self._config[VERSION_FILE] not in self.GitChangedFiles(git_hash):
177 continue
178 if self.ExceedsMax(releases):
179 break # pragma: no cover
180 if not self.GitCheckoutFileSafe(self._config[VERSION_FILE], git_hash):
181 break # pragma: no cover
182
183 release, patch_level = self.GetRelease(git_hash, branch)
184 releases.append(release)
185
186 # Follow branches only until their creation point.
187 # TODO(machenbach): This omits patches if the version file wasn't
188 # manipulated correctly. Find a better way to detect the point where
189 # the parent of the branch head leads to the trunk branch.
190 if branch != "trunk" and patch_level == "0":
191 break
192
193 # Allow Ctrl-C interrupt.
194 except (KeyboardInterrupt, SystemExit): # pragma: no cover
195 pass
196
197 # Clean up checked-out version file.
198 self.GitCheckoutFileSafe(self._config[VERSION_FILE], "HEAD")
199 return releases
200
201 def RunStep(self):
202 self.GitCreateBranch(self._config[BRANCHNAME])
203 # Get relevant remote branches, e.g. "svn/3.25".
204 branches = filter(lambda s: re.match(r"^svn/\d+\.\d+$", s),
205 self.GitRemotes())
206 # Remove 'svn/' prefix.
207 branches = map(lambda s: s[4:], branches)
208
209 releases = []
210 if self._options.branch == 'recent':
211 # Get only recent development on trunk, beta and stable.
212 if self._options.max_releases == 0: # pragma: no cover
213 self._options.max_releases = 10
214 beta, stable = SortBranches(branches)[0:2]
215 releases += self.GetReleasesFromBranch(stable)
216 releases += self.GetReleasesFromBranch(beta)
217 releases += self.GetReleasesFromBranch("trunk")
218 elif self._options.branch == 'all': # pragma: no cover
219 # Retrieve the full release history.
220 for branch in branches:
221 releases += self.GetReleasesFromBranch(branch)
222 releases += self.GetReleasesFromBranch("trunk")
223 else: # pragma: no cover
224 # Retrieve history for a specified branch.
225 assert self._options.branch in branches + ["trunk"]
226 releases += self.GetReleasesFromBranch(self._options.branch)
227
228 self["releases"] = sorted(releases,
229 key=lambda r: SortingKey(r["version"]),
230 reverse=True)
231
232
233 # TODO(machenbach): Parts of the Chromium setup are c/p from the chromium_roll
234 # script -> unify.
235 class CheckChromium(Step):
236 MESSAGE = "Check the chromium checkout."
237
238 def Run(self):
239 self["chrome_path"] = self._options.chromium
240
241
242 class SwitchChromium(Step):
243 MESSAGE = "Switch to Chromium checkout."
244 REQUIRES = "chrome_path"
245
246 def RunStep(self):
247 self["v8_path"] = os.getcwd()
248 os.chdir(self["chrome_path"])
249 # Check for a clean workdir.
250 if not self.GitIsWorkdirClean(): # pragma: no cover
251 self.Die("Workspace is not clean. Please commit or undo your changes.")
252 # Assert that the DEPS file is there.
253 if not os.path.exists(self.Config(DEPS_FILE)): # pragma: no cover
254 self.Die("DEPS file not present.")
255
256
257 class UpdateChromiumCheckout(Step):
258 MESSAGE = "Update the checkout and create a new branch."
259 REQUIRES = "chrome_path"
260
261 def RunStep(self):
262 os.chdir(self["chrome_path"])
263 self.GitCheckout("master")
264 self.GitPull()
265 self.GitCreateBranch(self.Config(BRANCHNAME))
266
267
268 class RetrieveChromiumV8Releases(Step):
269 MESSAGE = "Retrieve V8 releases from Chromium DEPS."
270 REQUIRES = "chrome_path"
271
272 def RunStep(self):
273 os.chdir(self["chrome_path"])
274
275 trunk_releases = filter(lambda r: r["branch"] == "trunk", self["releases"])
276 if not trunk_releases: # pragma: no cover
277 print "No trunk releases detected. Skipping chromium history."
278 return True
279
280 oldest_v8_rev = int(trunk_releases[-1]["revision"])
281
282 cr_releases = []
283 try:
284 for git_hash in self.GitLog(format="%H", grep="V8").splitlines():
285 if self._config[DEPS_FILE] not in self.GitChangedFiles(git_hash):
286 continue
287 if not self.GitCheckoutFileSafe(self._config[DEPS_FILE], git_hash):
288 break # pragma: no cover
289 deps = FileToText(self.Config(DEPS_FILE))
290 match = DEPS_RE.search(deps)
291 if match:
292 svn_rev = self.GitSVNFindSVNRev(git_hash)
293 v8_rev = match.group(1)
294 cr_releases.append([svn_rev, v8_rev])
295
296 # Stop after reaching beyond the last v8 revision we want to update.
297 # We need a small buffer for possible revert/reland frenzies.
298 # TODO(machenbach): Subtraction is not git friendly.
299 if int(v8_rev) < oldest_v8_rev - 100:
300 break # pragma: no cover
301
302 # Allow Ctrl-C interrupt.
303 except (KeyboardInterrupt, SystemExit): # pragma: no cover
304 pass
305
306 # Clean up.
307 self.GitCheckoutFileSafe(self._config[DEPS_FILE], "HEAD")
308
309 # Add the chromium ranges to the v8 trunk releases.
310 all_ranges = BuildRevisionRanges(cr_releases)
311 trunk_dict = dict((r["revision"], r) for r in trunk_releases)
312 for revision, ranges in all_ranges.iteritems():
313 trunk_dict.get(revision, {})["chromium_revision"] = ranges
314
315 class SwitchV8(Step):
316 MESSAGE = "Returning to V8 checkout."
317 REQUIRES = "chrome_path"
318
319 def RunStep(self):
320 self.GitCheckout("master")
321 self.GitDeleteBranch(self.Config(BRANCHNAME))
322 os.chdir(self["v8_path"])
323
324
325 class CleanUp(Step):
326 MESSAGE = "Clean up."
327
328 def RunStep(self):
329 self.CommonCleanup()
330
331
332 class WriteOutput(Step):
333 MESSAGE = "Print output."
334
335 def Run(self):
336 if self._options.csv:
337 with open(self._options.csv, "w") as f:
338 writer = csv.DictWriter(f,
339 ["version", "branch", "revision",
340 "chromium_revision", "patches_merged"],
341 restval="",
342 extrasaction="ignore")
343 for release in self["releases"]:
344 writer.writerow(release)
345 if self._options.json:
346 with open(self._options.json, "w") as f:
347 f.write(json.dumps(self["releases"]))
348 if not self._options.csv and not self._options.json:
349 print self["releases"] # pragma: no cover
350
351
352 class Releases(ScriptsBase):
353 def _PrepareOptions(self, parser):
354 parser.add_argument("-b", "--branch", default="recent",
355 help=("The branch to analyze. If 'all' is specified, "
356 "analyze all branches. If 'recent' (default) "
357 "is specified, track beta, stable and trunk."))
358 parser.add_argument("-c", "--chromium",
359 help=("The path to your Chromium src/ "
360 "directory to automate the V8 roll."))
361 parser.add_argument("--csv", help="Path to a CSV file for export.")
362 parser.add_argument("-m", "--max-releases", type=int, default=0,
363 help="The maximum number of releases to track.")
364 parser.add_argument("--json", help="Path to a JSON file for export.")
365
366 def _ProcessOptions(self, options): # pragma: no cover
367 return True
368
369 def _Steps(self):
370 return [
371 Preparation,
372 RetrieveV8Releases,
373 CheckChromium,
374 SwitchChromium,
375 UpdateChromiumCheckout,
376 RetrieveChromiumV8Releases,
377 SwitchV8,
378 CleanUp,
379 WriteOutput,
380 ]
381
382
383 if __name__ == "__main__": # pragma: no cover
384 sys.exit(Releases(CONFIG).Run())
OLDNEW
« no previous file with comments | « tools/push-to-trunk/git_recipes.py ('k') | tools/push-to-trunk/test_scripts.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698