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 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