Home | History | Annotate | Download | only in push-to-trunk
      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 optparse
     30 import sys
     31 import tempfile
     32 import urllib2
     33 
     34 from common_includes import *
     35 
     36 TRUNKBRANCH = "TRUNKBRANCH"
     37 CHROMIUM = "CHROMIUM"
     38 DEPS_FILE = "DEPS_FILE"
     39 
     40 CONFIG = {
     41   BRANCHNAME: "prepare-push",
     42   TRUNKBRANCH: "trunk-push",
     43   PERSISTFILE_BASENAME: "/tmp/v8-push-to-trunk-tempfile",
     44   TEMP_BRANCH: "prepare-push-temporary-branch-created-by-script",
     45   DOT_GIT_LOCATION: ".git",
     46   VERSION_FILE: "src/version.cc",
     47   CHANGELOG_FILE: "ChangeLog",
     48   CHANGELOG_ENTRY_FILE: "/tmp/v8-push-to-trunk-tempfile-changelog-entry",
     49   PATCH_FILE: "/tmp/v8-push-to-trunk-tempfile-patch-file",
     50   COMMITMSG_FILE: "/tmp/v8-push-to-trunk-tempfile-commitmsg",
     51   DEPS_FILE: "DEPS",
     52 }
     53 
     54 
     55 class Preparation(Step):
     56   MESSAGE = "Preparation."
     57 
     58   def RunStep(self):
     59     self.InitialEnvironmentChecks()
     60     self.CommonPrepare()
     61     self.PrepareBranch()
     62     self.DeleteBranch(self.Config(TRUNKBRANCH))
     63 
     64 
     65 class FreshBranch(Step):
     66   MESSAGE = "Create a fresh branch."
     67 
     68   def RunStep(self):
     69     args = "checkout -b %s svn/bleeding_edge" % self.Config(BRANCHNAME)
     70     if self.Git(args) is None:
     71       self.Die("Creating branch %s failed." % self.Config(BRANCHNAME))
     72 
     73 
     74 class DetectLastPush(Step):
     75   MESSAGE = "Detect commit ID of last push to trunk."
     76 
     77   def RunStep(self):
     78     last_push = (self._options.l or
     79                  self.Git("log -1 --format=%H ChangeLog").strip())
     80     while True:
     81       # Print assumed commit, circumventing git's pager.
     82       print self.Git("log -1 %s" % last_push)
     83       if self.Confirm("Is the commit printed above the last push to trunk?"):
     84         break
     85       args = "log -1 --format=%H %s^ ChangeLog" % last_push
     86       last_push = self.Git(args).strip()
     87     self.Persist("last_push", last_push)
     88     self._state["last_push"] = last_push
     89 
     90 
     91 class PrepareChangeLog(Step):
     92   MESSAGE = "Prepare raw ChangeLog entry."
     93 
     94   def Reload(self, body):
     95     """Attempts to reload the commit message from rietveld in order to allow
     96     late changes to the LOG flag. Note: This is brittle to future changes of
     97     the web page name or structure.
     98     """
     99     match = re.search(r"^Review URL: https://codereview\.chromium\.org/(\d+)$",
    100                       body, flags=re.M)
    101     if match:
    102       cl_url = "https://codereview.chromium.org/%s/description" % match.group(1)
    103       try:
    104         # Fetch from Rietveld but only retry once with one second delay since
    105         # there might be many revisions.
    106         body = self.ReadURL(cl_url, wait_plan=[1])
    107       except urllib2.URLError:
    108         pass
    109     return body
    110 
    111   def RunStep(self):
    112     self.RestoreIfUnset("last_push")
    113 
    114     # These version numbers are used again later for the trunk commit.
    115     self.ReadAndPersistVersion()
    116 
    117     date = self.GetDate()
    118     self.Persist("date", date)
    119     output = "%s: Version %s.%s.%s\n\n" % (date,
    120                                            self._state["major"],
    121                                            self._state["minor"],
    122                                            self._state["build"])
    123     TextToFile(output, self.Config(CHANGELOG_ENTRY_FILE))
    124 
    125     args = "log %s..HEAD --format=%%H" % self._state["last_push"]
    126     commits = self.Git(args).strip()
    127 
    128     # Cache raw commit messages.
    129     commit_messages = [
    130       [
    131         self.Git("log -1 %s --format=\"%%s\"" % commit),
    132         self.Reload(self.Git("log -1 %s --format=\"%%B\"" % commit)),
    133         self.Git("log -1 %s --format=\"%%an\"" % commit),
    134       ] for commit in commits.splitlines()
    135     ]
    136 
    137     # Auto-format commit messages.
    138     body = MakeChangeLogBody(commit_messages, auto_format=True)
    139     AppendToFile(body, self.Config(CHANGELOG_ENTRY_FILE))
    140 
    141     msg = ("        Performance and stability improvements on all platforms."
    142            "\n#\n# The change log above is auto-generated. Please review if "
    143            "all relevant\n# commit messages from the list below are included."
    144            "\n# All lines starting with # will be stripped.\n#\n")
    145     AppendToFile(msg, self.Config(CHANGELOG_ENTRY_FILE))
    146 
    147     # Include unformatted commit messages as a reference in a comment.
    148     comment_body = MakeComment(MakeChangeLogBody(commit_messages))
    149     AppendToFile(comment_body, self.Config(CHANGELOG_ENTRY_FILE))
    150 
    151 
    152 class EditChangeLog(Step):
    153   MESSAGE = "Edit ChangeLog entry."
    154 
    155   def RunStep(self):
    156     print ("Please press <Return> to have your EDITOR open the ChangeLog "
    157            "entry, then edit its contents to your liking. When you're done, "
    158            "save the file and exit your EDITOR. ")
    159     self.ReadLine(default="")
    160     self.Editor(self.Config(CHANGELOG_ENTRY_FILE))
    161     handle, new_changelog = tempfile.mkstemp()
    162     os.close(handle)
    163 
    164     # Strip comments and reformat with correct indentation.
    165     changelog_entry = FileToText(self.Config(CHANGELOG_ENTRY_FILE)).rstrip()
    166     changelog_entry = StripComments(changelog_entry)
    167     changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines()))
    168     changelog_entry = changelog_entry.lstrip()
    169 
    170     if changelog_entry == "":
    171       self.Die("Empty ChangeLog entry.")
    172 
    173     with open(new_changelog, "w") as f:
    174       f.write(changelog_entry)
    175       f.write("\n\n\n")  # Explicitly insert two empty lines.
    176 
    177     AppendToFile(FileToText(self.Config(CHANGELOG_FILE)), new_changelog)
    178     TextToFile(FileToText(new_changelog), self.Config(CHANGELOG_FILE))
    179     os.remove(new_changelog)
    180 
    181 
    182 class IncrementVersion(Step):
    183   MESSAGE = "Increment version number."
    184 
    185   def RunStep(self):
    186     self.RestoreIfUnset("build")
    187     new_build = str(int(self._state["build"]) + 1)
    188 
    189     if self.Confirm(("Automatically increment BUILD_NUMBER? (Saying 'n' will "
    190                      "fire up your EDITOR on %s so you can make arbitrary "
    191                      "changes. When you're done, save the file and exit your "
    192                      "EDITOR.)" % self.Config(VERSION_FILE))):
    193       text = FileToText(self.Config(VERSION_FILE))
    194       text = MSub(r"(?<=#define BUILD_NUMBER)(?P<space>\s+)\d*$",
    195                   r"\g<space>%s" % new_build,
    196                   text)
    197       TextToFile(text, self.Config(VERSION_FILE))
    198     else:
    199       self.Editor(self.Config(VERSION_FILE))
    200 
    201     self.ReadAndPersistVersion("new_")
    202 
    203 
    204 class CommitLocal(Step):
    205   MESSAGE = "Commit to local branch."
    206 
    207   def RunStep(self):
    208     self.RestoreVersionIfUnset("new_")
    209     prep_commit_msg = ("Prepare push to trunk.  "
    210         "Now working on version %s.%s.%s." % (self._state["new_major"],
    211                                               self._state["new_minor"],
    212                                               self._state["new_build"]))
    213     self.Persist("prep_commit_msg", prep_commit_msg)
    214 
    215     # Include optional TBR only in the git command. The persisted commit
    216     # message is used for finding the commit again later.
    217     review = "\n\nTBR=%s" % self._options.r if not self.IsManual() else ""
    218     if self.Git("commit -a -m \"%s%s\"" % (prep_commit_msg, review)) is None:
    219       self.Die("'git commit -a' failed.")
    220 
    221 
    222 class CommitRepository(Step):
    223   MESSAGE = "Commit to the repository."
    224 
    225   def RunStep(self):
    226     self.WaitForLGTM()
    227     # Re-read the ChangeLog entry (to pick up possible changes).
    228     # FIXME(machenbach): This was hanging once with a broken pipe.
    229     TextToFile(GetLastChangeLogEntries(self.Config(CHANGELOG_FILE)),
    230                self.Config(CHANGELOG_ENTRY_FILE))
    231 
    232     if self.Git("cl dcommit -f", "PRESUBMIT_TREE_CHECK=\"skip\"") is None:
    233       self.Die("'git cl dcommit' failed, please try again.")
    234 
    235 
    236 class StragglerCommits(Step):
    237   MESSAGE = ("Fetch straggler commits that sneaked in since this script was "
    238              "started.")
    239 
    240   def RunStep(self):
    241     if self.Git("svn fetch") is None:
    242       self.Die("'git svn fetch' failed.")
    243     self.Git("checkout svn/bleeding_edge")
    244     self.RestoreIfUnset("prep_commit_msg")
    245     args = "log -1 --format=%%H --grep=\"%s\"" % self._state["prep_commit_msg"]
    246     prepare_commit_hash = self.Git(args).strip()
    247     self.Persist("prepare_commit_hash", prepare_commit_hash)
    248 
    249 
    250 class SquashCommits(Step):
    251   MESSAGE = "Squash commits into one."
    252 
    253   def RunStep(self):
    254     # Instead of relying on "git rebase -i", we'll just create a diff, because
    255     # that's easier to automate.
    256     self.RestoreIfUnset("prepare_commit_hash")
    257     args = "diff svn/trunk %s" % self._state["prepare_commit_hash"]
    258     TextToFile(self.Git(args), self.Config(PATCH_FILE))
    259 
    260     # Convert the ChangeLog entry to commit message format:
    261     # - remove date
    262     # - remove indentation
    263     # - merge paragraphs into single long lines, keeping empty lines between
    264     #   them.
    265     self.RestoreIfUnset("date")
    266     changelog_entry = FileToText(self.Config(CHANGELOG_ENTRY_FILE))
    267 
    268     # TODO(machenbach): This could create a problem if the changelog contained
    269     # any quotation marks.
    270     text = Command("echo \"%s\" \
    271         | sed -e \"s/^%s: //\" \
    272         | sed -e 's/^ *//' \
    273         | awk '{ \
    274             if (need_space == 1) {\
    275               printf(\" \");\
    276             };\
    277             printf(\"%%s\", $0);\
    278             if ($0 ~ /^$/) {\
    279               printf(\"\\n\\n\");\
    280               need_space = 0;\
    281             } else {\
    282               need_space = 1;\
    283             }\
    284           }'" % (changelog_entry, self._state["date"]))
    285 
    286     if not text:
    287       self.Die("Commit message editing failed.")
    288     TextToFile(text, self.Config(COMMITMSG_FILE))
    289     os.remove(self.Config(CHANGELOG_ENTRY_FILE))
    290 
    291 
    292 class NewBranch(Step):
    293   MESSAGE = "Create a new branch from trunk."
    294 
    295   def RunStep(self):
    296     if self.Git("checkout -b %s svn/trunk" % self.Config(TRUNKBRANCH)) is None:
    297       self.Die("Checking out a new branch '%s' failed." %
    298                self.Config(TRUNKBRANCH))
    299 
    300 
    301 class ApplyChanges(Step):
    302   MESSAGE = "Apply squashed changes."
    303 
    304   def RunStep(self):
    305     self.ApplyPatch(self.Config(PATCH_FILE))
    306     Command("rm", "-f %s*" % self.Config(PATCH_FILE))
    307 
    308 
    309 class SetVersion(Step):
    310   MESSAGE = "Set correct version for trunk."
    311 
    312   def RunStep(self):
    313     self.RestoreVersionIfUnset()
    314     output = ""
    315     for line in FileToText(self.Config(VERSION_FILE)).splitlines():
    316       if line.startswith("#define MAJOR_VERSION"):
    317         line = re.sub("\d+$", self._state["major"], line)
    318       elif line.startswith("#define MINOR_VERSION"):
    319         line = re.sub("\d+$", self._state["minor"], line)
    320       elif line.startswith("#define BUILD_NUMBER"):
    321         line = re.sub("\d+$", self._state["build"], line)
    322       elif line.startswith("#define PATCH_LEVEL"):
    323         line = re.sub("\d+$", "0", line)
    324       elif line.startswith("#define IS_CANDIDATE_VERSION"):
    325         line = re.sub("\d+$", "0", line)
    326       output += "%s\n" % line
    327     TextToFile(output, self.Config(VERSION_FILE))
    328 
    329 
    330 class CommitTrunk(Step):
    331   MESSAGE = "Commit to local trunk branch."
    332 
    333   def RunStep(self):
    334     self.Git("add \"%s\"" % self.Config(VERSION_FILE))
    335     if self.Git("commit -F \"%s\"" % self.Config(COMMITMSG_FILE)) is None:
    336       self.Die("'git commit' failed.")
    337     Command("rm", "-f %s*" % self.Config(COMMITMSG_FILE))
    338 
    339 
    340 class SanityCheck(Step):
    341   MESSAGE = "Sanity check."
    342 
    343   def RunStep(self):
    344     if not self.Confirm("Please check if your local checkout is sane: Inspect "
    345         "%s, compile, run tests. Do you want to commit this new trunk "
    346         "revision to the repository?" % self.Config(VERSION_FILE)):
    347       self.Die("Execution canceled.")
    348 
    349 
    350 class CommitSVN(Step):
    351   MESSAGE = "Commit to SVN."
    352 
    353   def RunStep(self):
    354     result = self.Git("svn dcommit 2>&1")
    355     if not result:
    356       self.Die("'git svn dcommit' failed.")
    357     result = filter(lambda x: re.search(r"^Committed r[0-9]+", x),
    358                     result.splitlines())
    359     if len(result) > 0:
    360       trunk_revision = re.sub(r"^Committed r([0-9]+)", r"\1", result[0])
    361 
    362     # Sometimes grepping for the revision fails. No idea why. If you figure
    363     # out why it is flaky, please do fix it properly.
    364     if not trunk_revision:
    365       print("Sorry, grepping for the SVN revision failed. Please look for it "
    366             "in the last command's output above and provide it manually (just "
    367             "the number, without the leading \"r\").")
    368       self.DieNoManualMode("Can't prompt in forced mode.")
    369       while not trunk_revision:
    370         print "> ",
    371         trunk_revision = self.ReadLine()
    372     self.Persist("trunk_revision", trunk_revision)
    373 
    374 
    375 class TagRevision(Step):
    376   MESSAGE = "Tag the new revision."
    377 
    378   def RunStep(self):
    379     self.RestoreVersionIfUnset()
    380     ver = "%s.%s.%s" % (self._state["major"],
    381                         self._state["minor"],
    382                         self._state["build"])
    383     if self.Git("svn tag %s -m \"Tagging version %s\"" % (ver, ver)) is None:
    384       self.Die("'git svn tag' failed.")
    385 
    386 
    387 class CheckChromium(Step):
    388   MESSAGE = "Ask for chromium checkout."
    389 
    390   def Run(self):
    391     chrome_path = self._options.c
    392     if not chrome_path:
    393       self.DieNoManualMode("Please specify the path to a Chromium checkout in "
    394                           "forced mode.")
    395       print ("Do you have a \"NewGit\" Chromium checkout and want "
    396           "this script to automate creation of the roll CL? If yes, enter the "
    397           "path to (and including) the \"src\" directory here, otherwise just "
    398           "press <Return>: "),
    399       chrome_path = self.ReadLine()
    400     self.Persist("chrome_path", chrome_path)
    401 
    402 
    403 class SwitchChromium(Step):
    404   MESSAGE = "Switch to Chromium checkout."
    405   REQUIRES = "chrome_path"
    406 
    407   def RunStep(self):
    408     v8_path = os.getcwd()
    409     self.Persist("v8_path", v8_path)
    410     os.chdir(self._state["chrome_path"])
    411     self.InitialEnvironmentChecks()
    412     # Check for a clean workdir.
    413     if self.Git("status -s -uno").strip() != "":
    414       self.Die("Workspace is not clean. Please commit or undo your changes.")
    415     # Assert that the DEPS file is there.
    416     if not os.path.exists(self.Config(DEPS_FILE)):
    417       self.Die("DEPS file not present.")
    418 
    419 
    420 class UpdateChromiumCheckout(Step):
    421   MESSAGE = "Update the checkout and create a new branch."
    422   REQUIRES = "chrome_path"
    423 
    424   def RunStep(self):
    425     os.chdir(self._state["chrome_path"])
    426     if self.Git("checkout master") is None:
    427       self.Die("'git checkout master' failed.")
    428     if self.Git("pull") is None:
    429       self.Die("'git pull' failed, please try again.")
    430 
    431     self.RestoreIfUnset("trunk_revision")
    432     args = "checkout -b v8-roll-%s" % self._state["trunk_revision"]
    433     if self.Git(args) is None:
    434       self.Die("Failed to checkout a new branch.")
    435 
    436 
    437 class UploadCL(Step):
    438   MESSAGE = "Create and upload CL."
    439   REQUIRES = "chrome_path"
    440 
    441   def RunStep(self):
    442     os.chdir(self._state["chrome_path"])
    443 
    444     # Patch DEPS file.
    445     self.RestoreIfUnset("trunk_revision")
    446     deps = FileToText(self.Config(DEPS_FILE))
    447     deps = re.sub("(?<=\"v8_revision\": \")([0-9]+)(?=\")",
    448                   self._state["trunk_revision"],
    449                   deps)
    450     TextToFile(deps, self.Config(DEPS_FILE))
    451 
    452     self.RestoreVersionIfUnset()
    453     ver = "%s.%s.%s" % (self._state["major"],
    454                         self._state["minor"],
    455                         self._state["build"])
    456     if self._options and self._options.r:
    457       print "Using account %s for review." % self._options.r
    458       rev = self._options.r
    459     else:
    460       print "Please enter the email address of a reviewer for the roll CL: ",
    461       self.DieNoManualMode("A reviewer must be specified in forced mode.")
    462       rev = self.ReadLine()
    463     args = "commit -am \"Update V8 to version %s.\n\nTBR=%s\"" % (ver, rev)
    464     if self.Git(args) is None:
    465       self.Die("'git commit' failed.")
    466     force_flag = " -f" if not self.IsManual() else ""
    467     if self.Git("cl upload --send-mail%s" % force_flag, pipe=False) is None:
    468       self.Die("'git cl upload' failed, please try again.")
    469     print "CL uploaded."
    470 
    471 
    472 class SwitchV8(Step):
    473   MESSAGE = "Returning to V8 checkout."
    474   REQUIRES = "chrome_path"
    475 
    476   def RunStep(self):
    477     self.RestoreIfUnset("v8_path")
    478     os.chdir(self._state["v8_path"])
    479 
    480 
    481 class CleanUp(Step):
    482   MESSAGE = "Done!"
    483 
    484   def RunStep(self):
    485     self.RestoreVersionIfUnset()
    486     ver = "%s.%s.%s" % (self._state["major"],
    487                         self._state["minor"],
    488                         self._state["build"])
    489     self.RestoreIfUnset("trunk_revision")
    490     self.RestoreIfUnset("chrome_path")
    491 
    492     if self._state["chrome_path"]:
    493       print("Congratulations, you have successfully created the trunk "
    494             "revision %s and rolled it into Chromium. Please don't forget to "
    495             "update the v8rel spreadsheet:" % ver)
    496     else:
    497       print("Congratulations, you have successfully created the trunk "
    498             "revision %s. Please don't forget to roll this new version into "
    499             "Chromium, and to update the v8rel spreadsheet:" % ver)
    500     print "%s\ttrunk\t%s" % (ver, self._state["trunk_revision"])
    501 
    502     self.CommonCleanup()
    503     if self.Config(TRUNKBRANCH) != self._state["current_branch"]:
    504       self.Git("branch -D %s" % self.Config(TRUNKBRANCH))
    505 
    506 
    507 def RunPushToTrunk(config,
    508                    options,
    509                    side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER):
    510   step_classes = [
    511     Preparation,
    512     FreshBranch,
    513     DetectLastPush,
    514     PrepareChangeLog,
    515     EditChangeLog,
    516     IncrementVersion,
    517     CommitLocal,
    518     UploadStep,
    519     CommitRepository,
    520     StragglerCommits,
    521     SquashCommits,
    522     NewBranch,
    523     ApplyChanges,
    524     SetVersion,
    525     CommitTrunk,
    526     SanityCheck,
    527     CommitSVN,
    528     TagRevision,
    529     CheckChromium,
    530     SwitchChromium,
    531     UpdateChromiumCheckout,
    532     UploadCL,
    533     SwitchV8,
    534     CleanUp,
    535   ]
    536 
    537   RunScript(step_classes, config, options, side_effect_handler)
    538 
    539 
    540 def BuildOptions():
    541   result = optparse.OptionParser()
    542   result.add_option("-c", "--chromium", dest="c",
    543                     help=("Specify the path to your Chromium src/ "
    544                           "directory to automate the V8 roll."))
    545   result.add_option("-f", "--force", dest="f",
    546                     help="Don't prompt the user.",
    547                     default=False, action="store_true")
    548   result.add_option("-l", "--last-push", dest="l",
    549                     help=("Manually specify the git commit ID "
    550                           "of the last push to trunk."))
    551   result.add_option("-m", "--manual", dest="m",
    552                     help="Prompt the user at every important step.",
    553                     default=False, action="store_true")
    554   result.add_option("-r", "--reviewer", dest="r",
    555                     help=("Specify the account name to be used for reviews."))
    556   result.add_option("-s", "--step", dest="s",
    557                     help="Specify the step where to start work. Default: 0.",
    558                     default=0, type="int")
    559   return result
    560 
    561 
    562 def ProcessOptions(options):
    563   if options.s < 0:
    564     print "Bad step number %d" % options.s
    565     return False
    566   if not options.m and not options.r:
    567     print "A reviewer (-r) is required in (semi-)automatic mode."
    568     return False
    569   if options.f and options.m:
    570     print "Manual and forced mode cannot be combined."
    571     return False
    572   if not options.m and not options.c:
    573     print "A chromium checkout (-c) is required in (semi-)automatic mode."
    574     return False
    575   return True
    576 
    577 
    578 def Main():
    579   parser = BuildOptions()
    580   (options, args) = parser.parse_args()
    581   if not ProcessOptions(options):
    582     parser.print_help()
    583     return 1
    584   RunPushToTrunk(CONFIG, options)
    585 
    586 if __name__ == "__main__":
    587   sys.exit(Main())
    588