blob: 5c03236223779b64d9d59dc26abd0fe1c1a9b3cc [file] [log] [blame]
Ben Murdoch4a90d5f2016-03-22 12:00:34 +00001#!/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
29import argparse
30import datetime
31import httplib
32import glob
33import imp
34import json
35import os
36import re
37import shutil
38import subprocess
39import sys
40import textwrap
41import time
42import urllib
43import urllib2
44
45from git_recipes import GitRecipesMixin
46from git_recipes import GitFailedException
47
48CHANGELOG_FILE = "ChangeLog"
49DAY_IN_SECONDS = 24 * 60 * 60
50PUSH_MSG_GIT_RE = re.compile(r".* \(based on (?P<git_rev>[a-fA-F0-9]+)\)$")
51PUSH_MSG_NEW_RE = re.compile(r"^Version \d+\.\d+\.\d+$")
52VERSION_FILE = os.path.join("include", "v8-version.h")
Ben Murdoch097c5b22016-05-18 11:27:45 +010053WATCHLISTS_FILE = "WATCHLISTS"
Ben Murdoch4a90d5f2016-03-22 12:00:34 +000054
55# V8 base directory.
56V8_BASE = os.path.dirname(
57 os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
58
59
60def TextToFile(text, file_name):
61 with open(file_name, "w") as f:
62 f.write(text)
63
64
65def AppendToFile(text, file_name):
66 with open(file_name, "a") as f:
67 f.write(text)
68
69
70def LinesInFile(file_name):
71 with open(file_name) as f:
72 for line in f:
73 yield line
74
75
76def FileToText(file_name):
77 with open(file_name) as f:
78 return f.read()
79
80
81def MSub(rexp, replacement, text):
82 return re.sub(rexp, replacement, text, flags=re.MULTILINE)
83
84
85def Fill80(line):
86 # Replace tabs and remove surrounding space.
87 line = re.sub(r"\t", r" ", line.strip())
88
89 # Format with 8 characters indentation and line width 80.
90 return textwrap.fill(line, width=80, initial_indent=" ",
91 subsequent_indent=" ")
92
93
94def MakeComment(text):
95 return MSub(r"^( ?)", "#", text)
96
97
98def StripComments(text):
99 # Use split not splitlines to keep terminal newlines.
100 return "\n".join(filter(lambda x: not x.startswith("#"), text.split("\n")))
101
102
103def MakeChangeLogBody(commit_messages, auto_format=False):
104 result = ""
105 added_titles = set()
106 for (title, body, author) in commit_messages:
107 # TODO(machenbach): Better check for reverts. A revert should remove the
108 # original CL from the actual log entry.
109 title = title.strip()
110 if auto_format:
111 # Only add commits that set the LOG flag correctly.
112 log_exp = r"^[ \t]*LOG[ \t]*=[ \t]*(?:(?:Y(?:ES)?)|TRUE)"
113 if not re.search(log_exp, body, flags=re.I | re.M):
114 continue
115 # Never include reverts.
116 if title.startswith("Revert "):
117 continue
118 # Don't include duplicates.
119 if title in added_titles:
120 continue
121
122 # Add and format the commit's title and bug reference. Move dot to the end.
123 added_titles.add(title)
124 raw_title = re.sub(r"(\.|\?|!)$", "", title)
125 bug_reference = MakeChangeLogBugReference(body)
126 space = " " if bug_reference else ""
127 result += "%s\n" % Fill80("%s%s%s." % (raw_title, space, bug_reference))
128
129 # Append the commit's author for reference if not in auto-format mode.
130 if not auto_format:
131 result += "%s\n" % Fill80("(%s)" % author.strip())
132
133 result += "\n"
134 return result
135
136
137def MakeChangeLogBugReference(body):
138 """Grep for "BUG=xxxx" lines in the commit message and convert them to
139 "(issue xxxx)".
140 """
141 crbugs = []
142 v8bugs = []
143
144 def AddIssues(text):
145 ref = re.match(r"^BUG[ \t]*=[ \t]*(.+)$", text.strip())
146 if not ref:
147 return
148 for bug in ref.group(1).split(","):
149 bug = bug.strip()
150 match = re.match(r"^v8:(\d+)$", bug)
151 if match: v8bugs.append(int(match.group(1)))
152 else:
153 match = re.match(r"^(?:chromium:)?(\d+)$", bug)
154 if match: crbugs.append(int(match.group(1)))
155
156 # Add issues to crbugs and v8bugs.
157 map(AddIssues, body.splitlines())
158
159 # Filter duplicates, sort, stringify.
160 crbugs = map(str, sorted(set(crbugs)))
161 v8bugs = map(str, sorted(set(v8bugs)))
162
163 bug_groups = []
164 def FormatIssues(prefix, bugs):
165 if len(bugs) > 0:
166 plural = "s" if len(bugs) > 1 else ""
167 bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs)))
168
169 FormatIssues("", v8bugs)
170 FormatIssues("Chromium ", crbugs)
171
172 if len(bug_groups) > 0:
173 return "(%s)" % ", ".join(bug_groups)
174 else:
175 return ""
176
177
178def SortingKey(version):
179 """Key for sorting version number strings: '3.11' > '3.2.1.1'"""
180 version_keys = map(int, version.split("."))
181 # Fill up to full version numbers to normalize comparison.
182 while len(version_keys) < 4: # pragma: no cover
183 version_keys.append(0)
184 # Fill digits.
185 return ".".join(map("{0:04d}".format, version_keys))
186
187
188# Some commands don't like the pipe, e.g. calling vi from within the script or
189# from subscripts like git cl upload.
190def Command(cmd, args="", prefix="", pipe=True, cwd=None):
191 cwd = cwd or os.getcwd()
192 # TODO(machenbach): Use timeout.
193 cmd_line = "%s %s %s" % (prefix, cmd, args)
194 print "Command: %s" % cmd_line
195 print "in %s" % cwd
196 sys.stdout.flush()
197 try:
198 if pipe:
199 return subprocess.check_output(cmd_line, shell=True, cwd=cwd)
200 else:
201 return subprocess.check_call(cmd_line, shell=True, cwd=cwd)
202 except subprocess.CalledProcessError:
203 return None
204 finally:
205 sys.stdout.flush()
206 sys.stderr.flush()
207
208
209def SanitizeVersionTag(tag):
210 version_without_prefix = re.compile(r"^\d+\.\d+\.\d+(?:\.\d+)?$")
211 version_with_prefix = re.compile(r"^tags\/\d+\.\d+\.\d+(?:\.\d+)?$")
212
213 if version_without_prefix.match(tag):
214 return tag
215 elif version_with_prefix.match(tag):
216 return tag[len("tags/"):]
217 else:
218 return None
219
220
221def NormalizeVersionTags(version_tags):
222 normalized_version_tags = []
223
224 # Remove tags/ prefix because of packed refs.
225 for current_tag in version_tags:
226 version_tag = SanitizeVersionTag(current_tag)
227 if version_tag != None:
228 normalized_version_tags.append(version_tag)
229
230 return normalized_version_tags
231
232
233# Wrapper for side effects.
234class SideEffectHandler(object): # pragma: no cover
235 def Call(self, fun, *args, **kwargs):
236 return fun(*args, **kwargs)
237
238 def Command(self, cmd, args="", prefix="", pipe=True, cwd=None):
239 return Command(cmd, args, prefix, pipe, cwd=cwd)
240
241 def ReadLine(self):
242 return sys.stdin.readline().strip()
243
244 def ReadURL(self, url, params=None):
245 # pylint: disable=E1121
246 url_fh = urllib2.urlopen(url, params, 60)
247 try:
248 return url_fh.read()
249 finally:
250 url_fh.close()
251
252 def ReadClusterFuzzAPI(self, api_key, **params):
253 params["api_key"] = api_key.strip()
254 params = urllib.urlencode(params)
255
256 headers = {"Content-type": "application/x-www-form-urlencoded"}
257
258 conn = httplib.HTTPSConnection("backend-dot-cluster-fuzz.appspot.com")
259 conn.request("POST", "/_api/", params, headers)
260
261 response = conn.getresponse()
262 data = response.read()
263
264 try:
265 return json.loads(data)
266 except:
267 print data
268 print "ERROR: Could not read response. Is your key valid?"
269 raise
270
271 def Sleep(self, seconds):
272 time.sleep(seconds)
273
274 def GetDate(self):
275 return datetime.date.today().strftime("%Y-%m-%d")
276
277 def GetUTCStamp(self):
278 return time.mktime(datetime.datetime.utcnow().timetuple())
279
280DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler()
281
282
283class NoRetryException(Exception):
284 pass
285
286
287class VCInterface(object):
288 def InjectStep(self, step):
289 self.step=step
290
291 def Pull(self):
292 raise NotImplementedError()
293
294 def Fetch(self):
295 raise NotImplementedError()
296
297 def GetTags(self):
298 raise NotImplementedError()
299
300 def GetBranches(self):
301 raise NotImplementedError()
302
303 def MasterBranch(self):
304 raise NotImplementedError()
305
306 def CandidateBranch(self):
307 raise NotImplementedError()
308
309 def RemoteMasterBranch(self):
310 raise NotImplementedError()
311
312 def RemoteCandidateBranch(self):
313 raise NotImplementedError()
314
315 def RemoteBranch(self, name):
316 raise NotImplementedError()
317
318 def CLLand(self):
319 raise NotImplementedError()
320
321 def Tag(self, tag, remote, message):
322 """Sets a tag for the current commit.
323
324 Assumptions: The commit already landed and the commit message is unique.
325 """
326 raise NotImplementedError()
327
328
329class GitInterface(VCInterface):
330 def Pull(self):
331 self.step.GitPull()
332
333 def Fetch(self):
334 self.step.Git("fetch")
335
336 def GetTags(self):
337 return self.step.Git("tag").strip().splitlines()
338
339 def GetBranches(self):
340 # Get relevant remote branches, e.g. "branch-heads/3.25".
341 branches = filter(
342 lambda s: re.match(r"^branch\-heads/\d+\.\d+$", s),
343 self.step.GitRemotes())
344 # Remove 'branch-heads/' prefix.
345 return map(lambda s: s[13:], branches)
346
347 def MasterBranch(self):
348 return "master"
349
350 def CandidateBranch(self):
351 return "candidates"
352
353 def RemoteMasterBranch(self):
354 return "origin/master"
355
356 def RemoteCandidateBranch(self):
357 return "origin/candidates"
358
359 def RemoteBranch(self, name):
360 # Assume that if someone "fully qualified" the ref, they know what they
361 # want.
362 if name.startswith('refs/'):
363 return name
364 if name in ["candidates", "master"]:
365 return "refs/remotes/origin/%s" % name
366 try:
367 # Check if branch is in heads.
368 if self.step.Git("show-ref refs/remotes/origin/%s" % name).strip():
369 return "refs/remotes/origin/%s" % name
370 except GitFailedException:
371 pass
372 try:
373 # Check if branch is in branch-heads.
374 if self.step.Git("show-ref refs/remotes/branch-heads/%s" % name).strip():
375 return "refs/remotes/branch-heads/%s" % name
376 except GitFailedException:
377 pass
378 self.Die("Can't find remote of %s" % name)
379
380 def Tag(self, tag, remote, message):
381 # Wait for the commit to appear. Assumes unique commit message titles (this
382 # is the case for all automated merge and push commits - also no title is
383 # the prefix of another title).
384 commit = None
Ben Murdochda12d292016-06-02 14:46:10 +0100385 for wait_interval in [10, 30, 60, 60, 60, 60, 60]:
Ben Murdoch4a90d5f2016-03-22 12:00:34 +0000386 self.step.Git("fetch")
387 commit = self.step.GitLog(n=1, format="%H", grep=message, branch=remote)
388 if commit:
389 break
390 print("The commit has not replicated to git. Waiting for %s seconds." %
391 wait_interval)
392 self.step._side_effect_handler.Sleep(wait_interval)
393 else:
394 self.step.Die("Couldn't determine commit for setting the tag. Maybe the "
395 "git updater is lagging behind?")
396
397 self.step.Git("tag %s %s" % (tag, commit))
398 self.step.Git("push origin %s" % tag)
399
400 def CLLand(self):
401 self.step.GitCLLand()
402
403
404class Step(GitRecipesMixin):
405 def __init__(self, text, number, config, state, options, handler):
406 self._text = text
407 self._number = number
408 self._config = config
409 self._state = state
410 self._options = options
411 self._side_effect_handler = handler
412 self.vc = GitInterface()
413 self.vc.InjectStep(self)
414
415 # The testing configuration might set a different default cwd.
416 self.default_cwd = (self._config.get("DEFAULT_CWD") or
417 os.path.join(self._options.work_dir, "v8"))
418
419 assert self._number >= 0
420 assert self._config is not None
421 assert self._state is not None
422 assert self._side_effect_handler is not None
423
424 def __getitem__(self, key):
425 # Convenience method to allow direct [] access on step classes for
426 # manipulating the backed state dict.
427 return self._state.get(key)
428
429 def __setitem__(self, key, value):
430 # Convenience method to allow direct [] access on step classes for
431 # manipulating the backed state dict.
432 self._state[key] = value
433
434 def Config(self, key):
435 return self._config[key]
436
437 def Run(self):
438 # Restore state.
439 state_file = "%s-state.json" % self._config["PERSISTFILE_BASENAME"]
440 if not self._state and os.path.exists(state_file):
441 self._state.update(json.loads(FileToText(state_file)))
442
443 print ">>> Step %d: %s" % (self._number, self._text)
444 try:
445 return self.RunStep()
446 finally:
447 # Persist state.
448 TextToFile(json.dumps(self._state), state_file)
449
450 def RunStep(self): # pragma: no cover
451 raise NotImplementedError
452
453 def Retry(self, cb, retry_on=None, wait_plan=None):
454 """ Retry a function.
455 Params:
456 cb: The function to retry.
457 retry_on: A callback that takes the result of the function and returns
458 True if the function should be retried. A function throwing an
459 exception is always retried.
460 wait_plan: A list of waiting delays between retries in seconds. The
461 maximum number of retries is len(wait_plan).
462 """
463 retry_on = retry_on or (lambda x: False)
464 wait_plan = list(wait_plan or [])
465 wait_plan.reverse()
466 while True:
467 got_exception = False
468 try:
469 result = cb()
470 except NoRetryException as e:
471 raise e
472 except Exception as e:
473 got_exception = e
474 if got_exception or retry_on(result):
475 if not wait_plan: # pragma: no cover
476 raise Exception("Retried too often. Giving up. Reason: %s" %
477 str(got_exception))
478 wait_time = wait_plan.pop()
479 print "Waiting for %f seconds." % wait_time
480 self._side_effect_handler.Sleep(wait_time)
481 print "Retrying..."
482 else:
483 return result
484
485 def ReadLine(self, default=None):
486 # Don't prompt in forced mode.
487 if self._options.force_readline_defaults and default is not None:
488 print "%s (forced)" % default
489 return default
490 else:
491 return self._side_effect_handler.ReadLine()
492
493 def Command(self, name, args, cwd=None):
494 cmd = lambda: self._side_effect_handler.Command(
495 name, args, "", True, cwd=cwd or self.default_cwd)
496 return self.Retry(cmd, None, [5])
497
498 def Git(self, args="", prefix="", pipe=True, retry_on=None, cwd=None):
499 cmd = lambda: self._side_effect_handler.Command(
500 "git", args, prefix, pipe, cwd=cwd or self.default_cwd)
501 result = self.Retry(cmd, retry_on, [5, 30])
502 if result is None:
503 raise GitFailedException("'git %s' failed." % args)
504 return result
505
506 def Editor(self, args):
507 if self._options.requires_editor:
508 return self._side_effect_handler.Command(
509 os.environ["EDITOR"],
510 args,
511 pipe=False,
512 cwd=self.default_cwd)
513
514 def ReadURL(self, url, params=None, retry_on=None, wait_plan=None):
515 wait_plan = wait_plan or [3, 60, 600]
516 cmd = lambda: self._side_effect_handler.ReadURL(url, params)
517 return self.Retry(cmd, retry_on, wait_plan)
518
519 def GetDate(self):
520 return self._side_effect_handler.GetDate()
521
522 def Die(self, msg=""):
523 if msg != "":
524 print "Error: %s" % msg
525 print "Exiting"
526 raise Exception(msg)
527
528 def DieNoManualMode(self, msg=""):
529 if not self._options.manual: # pragma: no cover
530 msg = msg or "Only available in manual mode."
531 self.Die(msg)
532
533 def Confirm(self, msg):
534 print "%s [Y/n] " % msg,
535 answer = self.ReadLine(default="Y")
536 return answer == "" or answer == "Y" or answer == "y"
537
538 def DeleteBranch(self, name, cwd=None):
539 for line in self.GitBranch(cwd=cwd).splitlines():
540 if re.match(r"\*?\s*%s$" % re.escape(name), line):
541 msg = "Branch %s exists, do you want to delete it?" % name
542 if self.Confirm(msg):
543 self.GitDeleteBranch(name, cwd=cwd)
544 print "Branch %s deleted." % name
545 else:
546 msg = "Can't continue. Please delete branch %s and try again." % name
547 self.Die(msg)
548
549 def InitialEnvironmentChecks(self, cwd):
550 # Cancel if this is not a git checkout.
551 if not os.path.exists(os.path.join(cwd, ".git")): # pragma: no cover
552 self.Die("This is not a git checkout, this script won't work for you.")
553
554 # Cancel if EDITOR is unset or not executable.
555 if (self._options.requires_editor and (not os.environ.get("EDITOR") or
556 self.Command(
557 "which", os.environ["EDITOR"]) is None)): # pragma: no cover
558 self.Die("Please set your EDITOR environment variable, you'll need it.")
559
560 def CommonPrepare(self):
561 # Check for a clean workdir.
562 if not self.GitIsWorkdirClean(): # pragma: no cover
563 self.Die("Workspace is not clean. Please commit or undo your changes.")
564
565 # Checkout master in case the script was left on a work branch.
566 self.GitCheckout('origin/master')
567
568 # Fetch unfetched revisions.
569 self.vc.Fetch()
570
571 def PrepareBranch(self):
572 # Delete the branch that will be created later if it exists already.
573 self.DeleteBranch(self._config["BRANCHNAME"])
574
575 def CommonCleanup(self):
576 self.GitCheckout('origin/master')
577 self.GitDeleteBranch(self._config["BRANCHNAME"])
578
579 # Clean up all temporary files.
580 for f in glob.iglob("%s*" % self._config["PERSISTFILE_BASENAME"]):
581 if os.path.isfile(f):
582 os.remove(f)
583 if os.path.isdir(f):
584 shutil.rmtree(f)
585
586 def ReadAndPersistVersion(self, prefix=""):
587 def ReadAndPersist(var_name, def_name):
588 match = re.match(r"^#define %s\s+(\d*)" % def_name, line)
589 if match:
590 value = match.group(1)
591 self["%s%s" % (prefix, var_name)] = value
592 for line in LinesInFile(os.path.join(self.default_cwd, VERSION_FILE)):
593 for (var_name, def_name) in [("major", "V8_MAJOR_VERSION"),
594 ("minor", "V8_MINOR_VERSION"),
595 ("build", "V8_BUILD_NUMBER"),
596 ("patch", "V8_PATCH_LEVEL")]:
597 ReadAndPersist(var_name, def_name)
598
599 def WaitForLGTM(self):
600 print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit "
601 "your change. (If you need to iterate on the patch or double check "
602 "that it's sane, do so in another shell, but remember to not "
603 "change the headline of the uploaded CL.")
604 answer = ""
605 while answer != "LGTM":
606 print "> ",
607 answer = self.ReadLine(None if self._options.wait_for_lgtm else "LGTM")
608 if answer != "LGTM":
609 print "That was not 'LGTM'."
610
611 def WaitForResolvingConflicts(self, patch_file):
612 print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", "
613 "or resolve the conflicts, stage *all* touched files with "
614 "'git add', and type \"RESOLVED<Return>\"")
615 self.DieNoManualMode()
616 answer = ""
617 while answer != "RESOLVED":
618 if answer == "ABORT":
619 self.Die("Applying the patch failed.")
620 if answer != "":
621 print "That was not 'RESOLVED' or 'ABORT'."
622 print "> ",
623 answer = self.ReadLine()
624
625 # Takes a file containing the patch to apply as first argument.
626 def ApplyPatch(self, patch_file, revert=False):
627 try:
628 self.GitApplyPatch(patch_file, revert)
629 except GitFailedException:
630 self.WaitForResolvingConflicts(patch_file)
631
632 def GetVersionTag(self, revision):
633 tag = self.Git("describe --tags %s" % revision).strip()
634 return SanitizeVersionTag(tag)
635
636 def GetRecentReleases(self, max_age):
637 # Make sure tags are fetched.
638 self.Git("fetch origin +refs/tags/*:refs/tags/*")
639
640 # Current timestamp.
641 time_now = int(self._side_effect_handler.GetUTCStamp())
642
643 # List every tag from a given period.
644 revisions = self.Git("rev-list --max-age=%d --tags" %
645 int(time_now - max_age)).strip()
646
647 # Filter out revisions who's tag is off by one or more commits.
648 return filter(lambda r: self.GetVersionTag(r), revisions.splitlines())
649
650 def GetLatestVersion(self):
651 # Use cached version if available.
652 if self["latest_version"]:
653 return self["latest_version"]
654
655 # Make sure tags are fetched.
656 self.Git("fetch origin +refs/tags/*:refs/tags/*")
657
658 all_tags = self.vc.GetTags()
659 only_version_tags = NormalizeVersionTags(all_tags)
660
661 version = sorted(only_version_tags,
662 key=SortingKey, reverse=True)[0]
663 self["latest_version"] = version
664 return version
665
666 def GetLatestRelease(self):
667 """The latest release is the git hash of the latest tagged version.
668
669 This revision should be rolled into chromium.
670 """
671 latest_version = self.GetLatestVersion()
672
673 # The latest release.
674 latest_hash = self.GitLog(n=1, format="%H", branch=latest_version)
675 assert latest_hash
676 return latest_hash
677
678 def GetLatestReleaseBase(self, version=None):
679 """The latest release base is the latest revision that is covered in the
680 last change log file. It doesn't include cherry-picked patches.
681 """
682 latest_version = version or self.GetLatestVersion()
683
684 # Strip patch level if it exists.
685 latest_version = ".".join(latest_version.split(".")[:3])
686
687 # The latest release base.
688 latest_hash = self.GitLog(n=1, format="%H", branch=latest_version)
689 assert latest_hash
690
691 title = self.GitLog(n=1, format="%s", git_hash=latest_hash)
692 match = PUSH_MSG_GIT_RE.match(title)
693 if match:
694 # Legacy: In the old process there's one level of indirection. The
695 # version is on the candidates branch and points to the real release
696 # base on master through the commit message.
697 return match.group("git_rev")
698 match = PUSH_MSG_NEW_RE.match(title)
699 if match:
700 # This is a new-style v8 version branched from master. The commit
701 # "latest_hash" is the version-file change. Its parent is the release
702 # base on master.
703 return self.GitLog(n=1, format="%H", git_hash="%s^" % latest_hash)
704
705 self.Die("Unknown latest release: %s" % latest_hash)
706
707 def ArrayToVersion(self, prefix):
708 return ".".join([self[prefix + "major"],
709 self[prefix + "minor"],
710 self[prefix + "build"],
711 self[prefix + "patch"]])
712
713 def StoreVersion(self, version, prefix):
714 version_parts = version.split(".")
715 if len(version_parts) == 3:
716 version_parts.append("0")
717 major, minor, build, patch = version_parts
718 self[prefix + "major"] = major
719 self[prefix + "minor"] = minor
720 self[prefix + "build"] = build
721 self[prefix + "patch"] = patch
722
723 def SetVersion(self, version_file, prefix):
724 output = ""
725 for line in FileToText(version_file).splitlines():
726 if line.startswith("#define V8_MAJOR_VERSION"):
727 line = re.sub("\d+$", self[prefix + "major"], line)
728 elif line.startswith("#define V8_MINOR_VERSION"):
729 line = re.sub("\d+$", self[prefix + "minor"], line)
730 elif line.startswith("#define V8_BUILD_NUMBER"):
731 line = re.sub("\d+$", self[prefix + "build"], line)
732 elif line.startswith("#define V8_PATCH_LEVEL"):
733 line = re.sub("\d+$", self[prefix + "patch"], line)
734 elif (self[prefix + "candidate"] and
735 line.startswith("#define V8_IS_CANDIDATE_VERSION")):
736 line = re.sub("\d+$", self[prefix + "candidate"], line)
737 output += "%s\n" % line
738 TextToFile(output, version_file)
739
740
741class BootstrapStep(Step):
742 MESSAGE = "Bootstrapping checkout and state."
743
744 def RunStep(self):
745 # Reserve state entry for json output.
746 self['json_output'] = {}
747
748 if os.path.realpath(self.default_cwd) == os.path.realpath(V8_BASE):
749 self.Die("Can't use v8 checkout with calling script as work checkout.")
750 # Directory containing the working v8 checkout.
751 if not os.path.exists(self._options.work_dir):
752 os.makedirs(self._options.work_dir)
753 if not os.path.exists(self.default_cwd):
754 self.Command("fetch", "v8", cwd=self._options.work_dir)
755
756
757class UploadStep(Step):
758 MESSAGE = "Upload for code review."
759
760 def RunStep(self):
761 if self._options.reviewer:
762 print "Using account %s for review." % self._options.reviewer
763 reviewer = self._options.reviewer
764 else:
765 print "Please enter the email address of a V8 reviewer for your patch: ",
766 self.DieNoManualMode("A reviewer must be specified in forced mode.")
767 reviewer = self.ReadLine()
768 self.GitUpload(reviewer, self._options.author, self._options.force_upload,
769 bypass_hooks=self._options.bypass_upload_hooks,
770 cc=self._options.cc)
771
772
773def MakeStep(step_class=Step, number=0, state=None, config=None,
774 options=None, side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER):
775 # Allow to pass in empty dictionaries.
776 state = state if state is not None else {}
777 config = config if config is not None else {}
778
779 try:
780 message = step_class.MESSAGE
781 except AttributeError:
782 message = step_class.__name__
783
784 return step_class(message, number=number, config=config,
785 state=state, options=options,
786 handler=side_effect_handler)
787
788
789class ScriptsBase(object):
790 def __init__(self,
791 config=None,
792 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER,
793 state=None):
794 self._config = config or self._Config()
795 self._side_effect_handler = side_effect_handler
796 self._state = state if state is not None else {}
797
798 def _Description(self):
799 return None
800
801 def _PrepareOptions(self, parser):
802 pass
803
804 def _ProcessOptions(self, options):
805 return True
806
807 def _Steps(self): # pragma: no cover
808 raise Exception("Not implemented.")
809
810 def _Config(self):
811 return {}
812
813 def MakeOptions(self, args=None):
814 parser = argparse.ArgumentParser(description=self._Description())
815 parser.add_argument("-a", "--author", default="",
816 help="The author email used for rietveld.")
817 parser.add_argument("--dry-run", default=False, action="store_true",
818 help="Perform only read-only actions.")
819 parser.add_argument("--json-output",
820 help="File to write results summary to.")
821 parser.add_argument("-r", "--reviewer", default="",
822 help="The account name to be used for reviews.")
823 parser.add_argument("-s", "--step",
824 help="Specify the step where to start work. Default: 0.",
825 default=0, type=int)
826 parser.add_argument("--work-dir",
827 help=("Location where to bootstrap a working v8 "
828 "checkout."))
829 self._PrepareOptions(parser)
830
831 if args is None: # pragma: no cover
832 options = parser.parse_args()
833 else:
834 options = parser.parse_args(args)
835
836 # Process common options.
837 if options.step < 0: # pragma: no cover
838 print "Bad step number %d" % options.step
839 parser.print_help()
840 return None
841
842 # Defaults for options, common to all scripts.
843 options.manual = getattr(options, "manual", True)
844 options.force = getattr(options, "force", False)
845 options.bypass_upload_hooks = False
846
847 # Derived options.
848 options.requires_editor = not options.force
849 options.wait_for_lgtm = not options.force
850 options.force_readline_defaults = not options.manual
851 options.force_upload = not options.manual
852
853 # Process script specific options.
854 if not self._ProcessOptions(options):
855 parser.print_help()
856 return None
857
858 if not options.work_dir:
859 options.work_dir = "/tmp/v8-release-scripts-work-dir"
860 return options
861
862 def RunSteps(self, step_classes, args=None):
863 options = self.MakeOptions(args)
864 if not options:
865 return 1
866
867 state_file = "%s-state.json" % self._config["PERSISTFILE_BASENAME"]
868 if options.step == 0 and os.path.exists(state_file):
869 os.remove(state_file)
870
871 steps = []
872 for (number, step_class) in enumerate([BootstrapStep] + step_classes):
873 steps.append(MakeStep(step_class, number, self._state, self._config,
874 options, self._side_effect_handler))
875
876 try:
877 for step in steps[options.step:]:
878 if step.Run():
879 return 0
880 finally:
881 if options.json_output:
882 with open(options.json_output, "w") as f:
883 json.dump(self._state['json_output'], f)
884
885 return 0
886
887 def Run(self, args=None):
888 return self.RunSteps(self._Steps(), args)