Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / v8 / tools / push-to-trunk / common_includes.py
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 VERSION_FILE = os.path.join("src", "version.cc")
49
50 # V8 base directory.
51 DEFAULT_CWD = os.path.dirname(
52     os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
53
54
55 def TextToFile(text, file_name):
56   with open(file_name, "w") as f:
57     f.write(text)
58
59
60 def AppendToFile(text, file_name):
61   with open(file_name, "a") as f:
62     f.write(text)
63
64
65 def LinesInFile(file_name):
66   with open(file_name) as f:
67     for line in f:
68       yield line
69
70
71 def FileToText(file_name):
72   with open(file_name) as f:
73     return f.read()
74
75
76 def MSub(rexp, replacement, text):
77   return re.sub(rexp, replacement, text, flags=re.MULTILINE)
78
79
80 def 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
89 def MakeComment(text):
90   return MSub(r"^( ?)", "#", text)
91
92
93 def 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
98 def 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
132 def 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
173 def 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.
185 def 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.
205 class 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
251 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler()
252
253
254 class NoRetryException(Exception):
255   pass
256
257
258 class 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
537 class 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
552 class 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
586 def 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
602 class 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)