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