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 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) 707