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