1#!/usr/bin/env python3 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, continue anyway for compatibility with 71 # Gerrit (which itself uses JGit for this). 72 continue 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="", force=False, cq=False, 209 cq_dry_run=False, set_bot_commit=False, bypass_hooks=False, 210 cc="", tbr_reviewer="", no_autocc=False, message_file=None, 211 **kwargs): 212 args = ["cl upload --send-mail"] 213 if reviewer: 214 args += ["-r", Quoted(reviewer)] 215 if tbr_reviewer: 216 args += ["--tbrs", Quoted(tbr_reviewer)] 217 if force: 218 args.append("-f") 219 if cq: 220 args.append("--use-commit-queue") 221 if cq_dry_run: 222 args.append("--cq-dry-run") 223 if set_bot_commit: 224 args.append("--set-bot-commit") 225 if bypass_hooks: 226 args.append("--bypass-hooks") 227 if no_autocc: 228 args.append("--no-autocc") 229 if cc: 230 args += ["--cc", Quoted(cc)] 231 if message_file: 232 args += ["--message-file", Quoted(message_file)] 233 # TODO(machenbach): Check output in forced mode. Verify that all required 234 # base files were uploaded, if not retry. 235 self.Git(MakeArgs(args), pipe=False, **kwargs) 236 237 def GitCommit(self, message="", file_name="", author=None, **kwargs): 238 assert message or file_name 239 args = ["commit"] 240 if file_name: 241 args += ["-aF", Quoted(file_name)] 242 if message: 243 args += ["-am", Quoted(message)] 244 if author: 245 args += ["--author", "\"%s <%s>\"" % (author, author)] 246 self.Git(MakeArgs(args), **kwargs) 247 248 def GitPresubmit(self, **kwargs): 249 self.Git("cl presubmit", "PRESUBMIT_TREE_CHECK=\"skip\"", **kwargs) 250 251 def GitCLLand(self, **kwargs): 252 self.Git( 253 "cl land -f --bypass-hooks", retry_on=lambda x: x is None, **kwargs) 254 255 def GitDiff(self, loc1, loc2, **kwargs): 256 return self.Git(MakeArgs(["diff", loc1, loc2]), **kwargs) 257 258 def GitPull(self, **kwargs): 259 self.Git("pull", **kwargs) 260 261 def GitFetchOrigin(self, *refspecs, **kwargs): 262 self.Git(MakeArgs(["fetch", "origin"] + list(refspecs)), **kwargs) 263 264 @Strip 265 # Copied from bot_update.py and modified for svn-like numbers only. 266 def GetCommitPositionNumber(self, git_hash, **kwargs): 267 """Dumps the 'git' log for a specific revision and parses out the commit 268 position number. 269 270 If a commit position metadata key is found, its number will be returned. 271 272 Otherwise, we will search for a 'git-svn' metadata entry. If one is found, 273 its SVN revision value is returned. 274 """ 275 git_log = self.GitLog(format='%B', n=1, git_hash=git_hash, **kwargs) 276 footer_map = GetCommitMessageFooterMap(git_log) 277 278 # Search for commit position metadata 279 value = footer_map.get(COMMIT_POSITION_FOOTER_KEY) 280 if value: 281 match = COMMIT_POSITION_RE.match(value) 282 if match: 283 return match.group(2) 284 285 # Extract the svn revision from 'git-svn' metadata 286 value = footer_map.get(GIT_SVN_ID_FOOTER_KEY) 287 if value: 288 match = GIT_SVN_ID_RE.match(value) 289 if match: 290 return match.group(1) 291 raise GitFailedException("Couldn't determine commit position for %s" % 292 git_hash) 293 294 def GitGetHashOfTag(self, tag_name, **kwargs): 295 return self.Git("rev-list -1 " + tag_name).strip().encode("ascii", "ignore") 296