Home | History | Annotate | Download | only in release
      1 #!/usr/bin/env python
      2 # Copyright 2014 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 re
     30 
     31 SHA1_RE = re.compile('^[a-fA-F0-9]{40}$')
     32 ROLL_DEPS_GIT_SVN_ID_RE = re.compile('^git-svn-id: .*@([0-9]+) .*$')
     33 
     34 # Regular expression that matches a single commit footer line.
     35 COMMIT_FOOTER_ENTRY_RE = re.compile(r'([^:]+):\s+(.+)')
     36 
     37 # Footer metadata key for commit position.
     38 COMMIT_POSITION_FOOTER_KEY = 'Cr-Commit-Position'
     39 
     40 # Regular expression to parse a commit position
     41 COMMIT_POSITION_RE = re.compile(r'(.+)@\{#(\d+)\}')
     42 
     43 # Key for the 'git-svn' ID metadata commit footer entry.
     44 GIT_SVN_ID_FOOTER_KEY = 'git-svn-id'
     45 
     46 # e.g., git-svn-id: https://v8.googlecode.com/svn/trunk@23117
     47 #     ce2b1a6d-e550-0410-aec6-3dcde31c8c00
     48 GIT_SVN_ID_RE = re.compile(r'[^@]+@(\d+)\s+(?:[a-zA-Z0-9\-]+)')
     49 
     50 
     51 # Copied from bot_update.py.
     52 def GetCommitMessageFooterMap(message):
     53   """Returns: (dict) A dictionary of commit message footer entries.
     54   """
     55   footers = {}
     56 
     57   # Extract the lines in the footer block.
     58   lines = []
     59   for line in message.strip().splitlines():
     60     line = line.strip()
     61     if len(line) == 0:
     62       del(lines[:])
     63       continue
     64     lines.append(line)
     65 
     66   # Parse the footer
     67   for line in lines:
     68     m = COMMIT_FOOTER_ENTRY_RE.match(line)
     69     if not m:
     70       # If any single line isn't valid, the entire footer is invalid.
     71       footers.clear()
     72       return footers
     73     footers[m.group(1)] = m.group(2).strip()
     74   return footers
     75 
     76 
     77 class GitFailedException(Exception):
     78   pass
     79 
     80 
     81 def Strip(f):
     82   def new_f(*args, **kwargs):
     83     result = f(*args, **kwargs)
     84     if result is None:
     85       return result
     86     else:
     87       return result.strip()
     88   return new_f
     89 
     90 
     91 def MakeArgs(l):
     92   """['-a', '', 'abc', ''] -> '-a abc'"""
     93   return " ".join(filter(None, l))
     94 
     95 
     96 def Quoted(s):
     97   return "\"%s\"" % s
     98 
     99 
    100 class GitRecipesMixin(object):
    101   def GitIsWorkdirClean(self, **kwargs):
    102     return self.Git("status -s -uno", **kwargs).strip() == ""
    103 
    104   @Strip
    105   def GitBranch(self, **kwargs):
    106     return self.Git("branch", **kwargs)
    107 
    108   def GitCreateBranch(self, name, remote="", **kwargs):
    109     assert name
    110     remote_args = ["--upstream", remote] if remote else []
    111     self.Git(MakeArgs(["new-branch", name] + remote_args), **kwargs)
    112 
    113   def GitDeleteBranch(self, name, **kwargs):
    114     assert name
    115     self.Git(MakeArgs(["branch -D", name]), **kwargs)
    116 
    117   def GitReset(self, name, **kwargs):
    118     assert name
    119     self.Git(MakeArgs(["reset --hard", name]), **kwargs)
    120 
    121   def GitStash(self, **kwargs):
    122     self.Git(MakeArgs(["stash"]), **kwargs)
    123 
    124   def GitRemotes(self, **kwargs):
    125     return map(str.strip,
    126                self.Git(MakeArgs(["branch -r"]), **kwargs).splitlines())
    127 
    128   def GitCheckout(self, name, **kwargs):
    129     assert name
    130     self.Git(MakeArgs(["checkout -f", name]), **kwargs)
    131 
    132   def GitCheckoutFile(self, name, branch_or_hash, **kwargs):
    133     assert name
    134     assert branch_or_hash
    135     self.Git(MakeArgs(["checkout -f", branch_or_hash, "--", name]), **kwargs)
    136 
    137   def GitCheckoutFileSafe(self, name, branch_or_hash, **kwargs):
    138     try:
    139       self.GitCheckoutFile(name, branch_or_hash, **kwargs)
    140     except GitFailedException:  # pragma: no cover
    141       # The file doesn't exist in that revision.
    142       return False
    143     return True
    144 
    145   def GitChangedFiles(self, git_hash, **kwargs):
    146     assert git_hash
    147     try:
    148       files = self.Git(MakeArgs(["diff --name-only",
    149                                  git_hash,
    150                                  "%s^" % git_hash]), **kwargs)
    151       return map(str.strip, files.splitlines())
    152     except GitFailedException:  # pragma: no cover
    153       # Git fails using "^" at branch roots.
    154       return []
    155 
    156 
    157   @Strip
    158   def GitCurrentBranch(self, **kwargs):
    159     for line in self.Git("status -s -b -uno", **kwargs).strip().splitlines():
    160       match = re.match(r"^## (.+)", line)
    161       if match: return match.group(1)
    162     raise Exception("Couldn't find curent branch.")  # pragma: no cover
    163 
    164   @Strip
    165   def GitLog(self, n=0, format="", grep="", git_hash="", parent_hash="",
    166              branch="", path=None, reverse=False, **kwargs):
    167     assert not (git_hash and parent_hash)
    168     args = ["log"]
    169     if n > 0:
    170       args.append("-%d" % n)
    171     if format:
    172       args.append("--format=%s" % format)
    173     if grep:
    174       args.append("--grep=\"%s\"" % grep.replace("\"", "\\\""))
    175     if reverse:
    176       args.append("--reverse")
    177     if git_hash:
    178       args.append(git_hash)
    179     if parent_hash:
    180       args.append("%s^" % parent_hash)
    181     args.append(branch)
    182     if path:
    183       args.extend(["--", path])
    184     return self.Git(MakeArgs(args), **kwargs)
    185 
    186   def GitShowFile(self, refspec, path, **kwargs):
    187     assert refspec
    188     assert path
    189     return self.Git(MakeArgs(["show", "%s:%s" % (refspec, path)]), **kwargs)
    190 
    191   def GitGetPatch(self, git_hash, **kwargs):
    192     assert git_hash
    193     return self.Git(MakeArgs(["log", "-1", "-p", git_hash]), **kwargs)
    194 
    195   # TODO(machenbach): Unused? Remove.
    196   def GitAdd(self, name, **kwargs):
    197     assert name
    198     self.Git(MakeArgs(["add", Quoted(name)]), **kwargs)
    199 
    200   def GitApplyPatch(self, patch_file, reverse=False, **kwargs):
    201     assert patch_file
    202     args = ["apply --index --reject"]
    203     if reverse:
    204       args.append("--reverse")
    205     args.append(Quoted(patch_file))
    206     self.Git(MakeArgs(args), **kwargs)
    207 
    208   def GitUpload(self, reviewer="", author="", force=False, cq=False,
    209                 bypass_hooks=False, cc="", **kwargs):
    210     args = ["cl upload --send-mail"]
    211     if author:
    212       args += ["--email", Quoted(author)]
    213     if reviewer:
    214       args += ["-r", Quoted(reviewer)]
    215     if force:
    216       args.append("-f")
    217     if cq:
    218       args.append("--use-commit-queue")
    219     if bypass_hooks:
    220       args.append("--bypass-hooks")
    221     if cc:
    222       args += ["--cc", Quoted(cc)]
    223     # TODO(machenbach): Check output in forced mode. Verify that all required
    224     # base files were uploaded, if not retry.
    225     self.Git(MakeArgs(args), pipe=False, **kwargs)
    226 
    227   def GitCommit(self, message="", file_name="", author=None, **kwargs):
    228     assert message or file_name
    229     args = ["commit"]
    230     if file_name:
    231       args += ["-aF", Quoted(file_name)]
    232     if message:
    233       args += ["-am", Quoted(message)]
    234     if author:
    235       args += ["--author", "\"%s <%s>\"" % (author, author)]
    236     self.Git(MakeArgs(args), **kwargs)
    237 
    238   def GitPresubmit(self, **kwargs):
    239     self.Git("cl presubmit", "PRESUBMIT_TREE_CHECK=\"skip\"", **kwargs)
    240 
    241   def GitCLLand(self, **kwargs):
    242     self.Git(
    243         "cl land -f --bypass-hooks", retry_on=lambda x: x is None, **kwargs)
    244 
    245   def GitCLAddComment(self, message, **kwargs):
    246     args = ["cl", "comments", "-a", Quoted(message)]
    247     self.Git(MakeArgs(args), **kwargs)
    248 
    249   def GitDiff(self, loc1, loc2, **kwargs):
    250     return self.Git(MakeArgs(["diff", loc1, loc2]), **kwargs)
    251 
    252   def GitPull(self, **kwargs):
    253     self.Git("pull", **kwargs)
    254 
    255   def GitFetchOrigin(self, *refspecs, **kwargs):
    256     self.Git(MakeArgs(["fetch", "origin"] + list(refspecs)), **kwargs)
    257 
    258   @Strip
    259   # Copied from bot_update.py and modified for svn-like numbers only.
    260   def GetCommitPositionNumber(self, git_hash, **kwargs):
    261     """Dumps the 'git' log for a specific revision and parses out the commit
    262     position number.
    263 
    264     If a commit position metadata key is found, its number will be returned.
    265 
    266     Otherwise, we will search for a 'git-svn' metadata entry. If one is found,
    267     its SVN revision value is returned.
    268     """
    269     git_log = self.GitLog(format='%B', n=1, git_hash=git_hash, **kwargs)
    270     footer_map = GetCommitMessageFooterMap(git_log)
    271 
    272     # Search for commit position metadata
    273     value = footer_map.get(COMMIT_POSITION_FOOTER_KEY)
    274     if value:
    275       match = COMMIT_POSITION_RE.match(value)
    276       if match:
    277         return match.group(2)
    278 
    279     # Extract the svn revision from 'git-svn' metadata
    280     value = footer_map.get(GIT_SVN_ID_FOOTER_KEY)
    281     if value:
    282       match = GIT_SVN_ID_RE.match(value)
    283       if match:
    284         return match.group(1)
    285     raise GitFailedException("Couldn't determine commit position for %s" %
    286                              git_hash)
    287 
    288   def GitGetHashOfTag(self, tag_name, **kwargs):
    289     return self.Git("rev-list -1 " + tag_name).strip().encode("ascii", "ignore")
    290