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 29import re 30 31SHA1_RE = re.compile('^[a-fA-F0-9]{40}$') 32ROLL_DEPS_GIT_SVN_ID_RE = re.compile('^git-svn-id: .*@([0-9]+) .*$') 33 34# Regular expression that matches a single commit footer line. 35COMMIT_FOOTER_ENTRY_RE = re.compile(r'([^:]+):\s+(.+)') 36 37# Footer metadata key for commit position. 38COMMIT_POSITION_FOOTER_KEY = 'Cr-Commit-Position' 39 40# Regular expression to parse a commit position 41COMMIT_POSITION_RE = re.compile(r'(.+)@\{#(\d+)\}') 42 43# Key for the 'git-svn' ID metadata commit footer entry. 44GIT_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 48GIT_SVN_ID_RE = re.compile(r'[^@]+@(\d+)\s+(?:[a-zA-Z0-9\-]+)') 49 50 51# Copied from bot_update.py. 52def 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 77class GitFailedException(Exception): 78 pass 79 80 81def 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 91def MakeArgs(l): 92 """['-a', '', 'abc', ''] -> '-a abc'""" 93 return " ".join(filter(None, l)) 94 95 96def Quoted(s): 97 return "\"%s\"" % s 98 99 100class 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 GitDiff(self, loc1, loc2, **kwargs): 246 return self.Git(MakeArgs(["diff", loc1, loc2]), **kwargs) 247 248 def GitPull(self, **kwargs): 249 self.Git("pull", **kwargs) 250 251 def GitFetchOrigin(self, *refspecs, **kwargs): 252 self.Git(MakeArgs(["fetch", "origin"] + list(refspecs)), **kwargs) 253 254 @Strip 255 # Copied from bot_update.py and modified for svn-like numbers only. 256 def GetCommitPositionNumber(self, git_hash, **kwargs): 257 """Dumps the 'git' log for a specific revision and parses out the commit 258 position number. 259 260 If a commit position metadata key is found, its number will be returned. 261 262 Otherwise, we will search for a 'git-svn' metadata entry. If one is found, 263 its SVN revision value is returned. 264 """ 265 git_log = self.GitLog(format='%B', n=1, git_hash=git_hash, **kwargs) 266 footer_map = GetCommitMessageFooterMap(git_log) 267 268 # Search for commit position metadata 269 value = footer_map.get(COMMIT_POSITION_FOOTER_KEY) 270 if value: 271 match = COMMIT_POSITION_RE.match(value) 272 if match: 273 return match.group(2) 274 275 # Extract the svn revision from 'git-svn' metadata 276 value = footer_map.get(GIT_SVN_ID_FOOTER_KEY) 277 if value: 278 match = GIT_SVN_ID_RE.match(value) 279 if match: 280 return match.group(1) 281 raise GitFailedException("Couldn't determine commit position for %s" % 282 git_hash) 283 284 def GitGetHashOfTag(self, tag_name, **kwargs): 285 return self.Git("rev-list -1 " + tag_name).strip().encode("ascii", "ignore") 286