Ben Murdoch | b8a8cc1 | 2014-11-26 15:28:44 +0000 | [diff] [blame^] | 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'((?:\w+)://[^@]+)@(\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 | return f(*args, **kwargs).strip() |
| 84 | return new_f |
| 85 | |
| 86 | |
| 87 | def MakeArgs(l): |
| 88 | """['-a', '', 'abc', ''] -> '-a abc'""" |
| 89 | return " ".join(filter(None, l)) |
| 90 | |
| 91 | |
| 92 | def Quoted(s): |
| 93 | return "\"%s\"" % s |
| 94 | |
| 95 | |
| 96 | class GitRecipesMixin(object): |
| 97 | def GitIsWorkdirClean(self, **kwargs): |
| 98 | return self.Git("status -s -uno", **kwargs).strip() == "" |
| 99 | |
| 100 | @Strip |
| 101 | def GitBranch(self, **kwargs): |
| 102 | return self.Git("branch", **kwargs) |
| 103 | |
| 104 | def GitCreateBranch(self, name, branch="", **kwargs): |
| 105 | assert name |
| 106 | self.Git(MakeArgs(["checkout -b", name, branch]), **kwargs) |
| 107 | |
| 108 | def GitDeleteBranch(self, name, **kwargs): |
| 109 | assert name |
| 110 | self.Git(MakeArgs(["branch -D", name]), **kwargs) |
| 111 | |
| 112 | def GitReset(self, name, **kwargs): |
| 113 | assert name |
| 114 | self.Git(MakeArgs(["reset --hard", name]), **kwargs) |
| 115 | |
| 116 | def GitStash(self, **kwargs): |
| 117 | self.Git(MakeArgs(["stash"]), **kwargs) |
| 118 | |
| 119 | def GitRemotes(self, **kwargs): |
| 120 | return map(str.strip, |
| 121 | self.Git(MakeArgs(["branch -r"]), **kwargs).splitlines()) |
| 122 | |
| 123 | def GitCheckout(self, name, **kwargs): |
| 124 | assert name |
| 125 | self.Git(MakeArgs(["checkout -f", name]), **kwargs) |
| 126 | |
| 127 | def GitCheckoutFile(self, name, branch_or_hash, **kwargs): |
| 128 | assert name |
| 129 | assert branch_or_hash |
| 130 | self.Git(MakeArgs(["checkout -f", branch_or_hash, "--", name]), **kwargs) |
| 131 | |
| 132 | def GitCheckoutFileSafe(self, name, branch_or_hash, **kwargs): |
| 133 | try: |
| 134 | self.GitCheckoutFile(name, branch_or_hash, **kwargs) |
| 135 | except GitFailedException: # pragma: no cover |
| 136 | # The file doesn't exist in that revision. |
| 137 | return False |
| 138 | return True |
| 139 | |
| 140 | def GitChangedFiles(self, git_hash, **kwargs): |
| 141 | assert git_hash |
| 142 | try: |
| 143 | files = self.Git(MakeArgs(["diff --name-only", |
| 144 | git_hash, |
| 145 | "%s^" % git_hash]), **kwargs) |
| 146 | return map(str.strip, files.splitlines()) |
| 147 | except GitFailedException: # pragma: no cover |
| 148 | # Git fails using "^" at branch roots. |
| 149 | return [] |
| 150 | |
| 151 | |
| 152 | @Strip |
| 153 | def GitCurrentBranch(self, **kwargs): |
| 154 | for line in self.Git("status -s -b -uno", **kwargs).strip().splitlines(): |
| 155 | match = re.match(r"^## (.+)", line) |
| 156 | if match: return match.group(1) |
| 157 | raise Exception("Couldn't find curent branch.") # pragma: no cover |
| 158 | |
| 159 | @Strip |
| 160 | def GitLog(self, n=0, format="", grep="", git_hash="", parent_hash="", |
| 161 | branch="", reverse=False, **kwargs): |
| 162 | assert not (git_hash and parent_hash) |
| 163 | args = ["log"] |
| 164 | if n > 0: |
| 165 | args.append("-%d" % n) |
| 166 | if format: |
| 167 | args.append("--format=%s" % format) |
| 168 | if grep: |
| 169 | args.append("--grep=\"%s\"" % grep.replace("\"", "\\\"")) |
| 170 | if reverse: |
| 171 | args.append("--reverse") |
| 172 | if git_hash: |
| 173 | args.append(git_hash) |
| 174 | if parent_hash: |
| 175 | args.append("%s^" % parent_hash) |
| 176 | args.append(branch) |
| 177 | return self.Git(MakeArgs(args), **kwargs) |
| 178 | |
| 179 | def GitGetPatch(self, git_hash, **kwargs): |
| 180 | assert git_hash |
| 181 | return self.Git(MakeArgs(["log", "-1", "-p", git_hash]), **kwargs) |
| 182 | |
| 183 | # TODO(machenbach): Unused? Remove. |
| 184 | def GitAdd(self, name, **kwargs): |
| 185 | assert name |
| 186 | self.Git(MakeArgs(["add", Quoted(name)]), **kwargs) |
| 187 | |
| 188 | def GitApplyPatch(self, patch_file, reverse=False, **kwargs): |
| 189 | assert patch_file |
| 190 | args = ["apply --index --reject"] |
| 191 | if reverse: |
| 192 | args.append("--reverse") |
| 193 | args.append(Quoted(patch_file)) |
| 194 | self.Git(MakeArgs(args), **kwargs) |
| 195 | |
| 196 | def GitUpload(self, reviewer="", author="", force=False, cq=False, |
| 197 | bypass_hooks=False, **kwargs): |
| 198 | args = ["cl upload --send-mail"] |
| 199 | if author: |
| 200 | args += ["--email", Quoted(author)] |
| 201 | if reviewer: |
| 202 | args += ["-r", Quoted(reviewer)] |
| 203 | if force: |
| 204 | args.append("-f") |
| 205 | if cq: |
| 206 | args.append("--use-commit-queue") |
| 207 | if bypass_hooks: |
| 208 | args.append("--bypass-hooks") |
| 209 | # TODO(machenbach): Check output in forced mode. Verify that all required |
| 210 | # base files were uploaded, if not retry. |
| 211 | self.Git(MakeArgs(args), pipe=False, **kwargs) |
| 212 | |
| 213 | def GitCommit(self, message="", file_name="", author=None, **kwargs): |
| 214 | assert message or file_name |
| 215 | args = ["commit"] |
| 216 | if file_name: |
| 217 | args += ["-aF", Quoted(file_name)] |
| 218 | if message: |
| 219 | args += ["-am", Quoted(message)] |
| 220 | if author: |
| 221 | args += ["--author", "\"%s <%s>\"" % (author, author)] |
| 222 | self.Git(MakeArgs(args), **kwargs) |
| 223 | |
| 224 | def GitPresubmit(self, **kwargs): |
| 225 | self.Git("cl presubmit", "PRESUBMIT_TREE_CHECK=\"skip\"", **kwargs) |
| 226 | |
| 227 | def GitDCommit(self, **kwargs): |
| 228 | self.Git( |
| 229 | "cl dcommit -f --bypass-hooks", retry_on=lambda x: x is None, **kwargs) |
| 230 | |
| 231 | def GitDiff(self, loc1, loc2, **kwargs): |
| 232 | return self.Git(MakeArgs(["diff", loc1, loc2]), **kwargs) |
| 233 | |
| 234 | def GitPull(self, **kwargs): |
| 235 | self.Git("pull", **kwargs) |
| 236 | |
| 237 | def GitFetchOrigin(self, **kwargs): |
| 238 | self.Git("fetch origin", **kwargs) |
| 239 | |
| 240 | def GitConvertToSVNRevision(self, git_hash, **kwargs): |
| 241 | result = self.Git(MakeArgs(["rev-list", "-n", "1", git_hash]), **kwargs) |
| 242 | if not result or not SHA1_RE.match(result): |
| 243 | raise GitFailedException("Git hash %s is unknown." % git_hash) |
| 244 | log = self.GitLog(n=1, format="%B", git_hash=git_hash, **kwargs) |
| 245 | for line in reversed(log.splitlines()): |
| 246 | match = ROLL_DEPS_GIT_SVN_ID_RE.match(line.strip()) |
| 247 | if match: |
| 248 | return match.group(1) |
| 249 | raise GitFailedException("Couldn't convert %s to SVN." % git_hash) |
| 250 | |
| 251 | @Strip |
| 252 | # Copied from bot_update.py and modified for svn-like numbers only. |
| 253 | def GetCommitPositionNumber(self, git_hash, **kwargs): |
| 254 | """Dumps the 'git' log for a specific revision and parses out the commit |
| 255 | position number. |
| 256 | |
| 257 | If a commit position metadata key is found, its number will be returned. |
| 258 | |
| 259 | Otherwise, we will search for a 'git-svn' metadata entry. If one is found, |
| 260 | its SVN revision value is returned. |
| 261 | """ |
| 262 | git_log = self.GitLog(format='%B', n=1, git_hash=git_hash, **kwargs) |
| 263 | footer_map = GetCommitMessageFooterMap(git_log) |
| 264 | |
| 265 | # Search for commit position metadata |
| 266 | value = footer_map.get(COMMIT_POSITION_FOOTER_KEY) |
| 267 | if value: |
| 268 | match = COMMIT_POSITION_RE.match(value) |
| 269 | if match: |
| 270 | return match.group(2) |
| 271 | |
| 272 | # Extract the svn revision from 'git-svn' metadata |
| 273 | value = footer_map.get(GIT_SVN_ID_FOOTER_KEY) |
| 274 | if value: |
| 275 | match = GIT_SVN_ID_RE.match(value) |
| 276 | if match: |
| 277 | return match.group(2) |
| 278 | return None |
| 279 | |
| 280 | ### Git svn stuff |
| 281 | |
| 282 | def GitSVNFetch(self, **kwargs): |
| 283 | self.Git("svn fetch", **kwargs) |
| 284 | |
| 285 | def GitSVNRebase(self, **kwargs): |
| 286 | self.Git("svn rebase", **kwargs) |
| 287 | |
| 288 | # TODO(machenbach): Unused? Remove. |
| 289 | @Strip |
| 290 | def GitSVNLog(self, **kwargs): |
| 291 | return self.Git("svn log -1 --oneline", **kwargs) |
| 292 | |
| 293 | @Strip |
| 294 | def GitSVNFindGitHash(self, revision, branch="", **kwargs): |
| 295 | assert revision |
| 296 | return self.Git( |
| 297 | MakeArgs(["svn find-rev", "r%s" % revision, branch]), **kwargs) |
| 298 | |
| 299 | @Strip |
| 300 | def GitSVNFindSVNRev(self, git_hash, branch="", **kwargs): |
| 301 | return self.Git(MakeArgs(["svn find-rev", git_hash, branch]), **kwargs) |
| 302 | |
| 303 | def GitSVNDCommit(self, **kwargs): |
| 304 | return self.Git("svn dcommit 2>&1", retry_on=lambda x: x is None, **kwargs) |
| 305 | |
| 306 | def GitSVNTag(self, version, **kwargs): |
| 307 | self.Git(("svn tag %s -m \"Tagging version %s\"" % (version, version)), |
| 308 | retry_on=lambda x: x is None, |
| 309 | **kwargs) |