blob: 3428c0f71ee8615e6278c3e4e852f40eaf606e36 [file] [log] [blame]
[email protected]7d654672012-01-05 19:07:231# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]d5800f12009-11-12 20:03:432# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
[email protected]5aeb7dd2009-11-17 18:09:015"""SCM-specific utility classes."""
[email protected]d5800f12009-11-12 20:03:436
[email protected]3c55d982010-05-06 14:25:447import cStringIO
[email protected]fd9cbbb2010-01-08 23:04:038import glob
[email protected]07ab60e2011-02-08 21:54:009import logging
[email protected]d5800f12009-11-12 20:03:4310import os
11import re
[email protected]d5800f12009-11-12 20:03:4312import sys
[email protected]4755b582013-04-18 21:38:4013import tempfile
[email protected]fd876172010-04-30 14:01:0514import time
[email protected]ade9c592011-04-07 15:59:1115from xml.etree import ElementTree
[email protected]d5800f12009-11-12 20:03:4316
17import gclient_utils
[email protected]31cb48a2011-04-04 18:01:3618import subprocess2
19
[email protected]d5800f12009-11-12 20:03:4320
[email protected]b24a8e12009-12-22 13:45:4821def ValidateEmail(email):
[email protected]6e29d572010-06-04 17:32:2022 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
23 is not None)
[email protected]b24a8e12009-12-22 13:45:4824
[email protected]d5800f12009-11-12 20:03:4325
[email protected]fd9cbbb2010-01-08 23:04:0326def GetCasedPath(path):
27 """Elcheapos way to get the real path case on Windows."""
28 if sys.platform.startswith('win') and os.path.exists(path):
29 # Reconstruct the path.
30 path = os.path.abspath(path)
31 paths = path.split('\\')
32 for i in range(len(paths)):
33 if i == 0:
34 # Skip drive letter.
35 continue
36 subpath = '\\'.join(paths[:i+1])
37 prev = len('\\'.join(paths[:i]))
38 # glob.glob will return the cased path for the last item only. This is why
39 # we are calling it in a loop. Extract the data we want and put it back
40 # into the list.
41 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)]
42 path = '\\'.join(paths)
43 return path
44
45
[email protected]3c55d982010-05-06 14:25:4446def GenFakeDiff(filename):
47 """Generates a fake diff from a file."""
48 file_content = gclient_utils.FileRead(filename, 'rb').splitlines(True)
[email protected]c6d170e2010-06-03 00:06:0049 filename = filename.replace(os.sep, '/')
[email protected]3c55d982010-05-06 14:25:4450 nb_lines = len(file_content)
51 # We need to use / since patch on unix will fail otherwise.
52 data = cStringIO.StringIO()
53 data.write("Index: %s\n" % filename)
54 data.write('=' * 67 + '\n')
55 # Note: Should we use /dev/null instead?
56 data.write("--- %s\n" % filename)
57 data.write("+++ %s\n" % filename)
58 data.write("@@ -0,0 +1,%d @@\n" % nb_lines)
59 # Prepend '+' to every lines.
60 for line in file_content:
61 data.write('+')
62 data.write(line)
63 result = data.getvalue()
64 data.close()
65 return result
66
67
[email protected]5c8c6de2011-03-18 16:20:1868def determine_scm(root):
69 """Similar to upload.py's version but much simpler.
70
71 Returns 'svn', 'git' or None.
72 """
73 if os.path.isdir(os.path.join(root, '.svn')):
74 return 'svn'
[email protected]c98c0c52011-04-06 13:39:4375 elif os.path.isdir(os.path.join(root, '.git')):
[email protected]5c8c6de2011-03-18 16:20:1876 return 'git'
77 else:
[email protected]c98c0c52011-04-06 13:39:4378 try:
[email protected]91def9b2011-09-14 16:28:0779 subprocess2.check_call(
[email protected]5c8c6de2011-03-18 16:20:1880 ['git', 'rev-parse', '--show-cdup'],
[email protected]c98c0c52011-04-06 13:39:4381 stdout=subprocess2.VOID,
[email protected]87e6d332011-09-09 19:01:2882 stderr=subprocess2.VOID,
[email protected]c98c0c52011-04-06 13:39:4383 cwd=root)
[email protected]5c8c6de2011-03-18 16:20:1884 return 'git'
[email protected]c98c0c52011-04-06 13:39:4385 except (OSError, subprocess2.CalledProcessError):
[email protected]5c8c6de2011-03-18 16:20:1886 return None
87
88
[email protected]36ac2392011-10-12 16:36:1189def only_int(val):
90 if val.isdigit():
91 return int(val)
92 else:
93 return 0
94
95
[email protected]5aeb7dd2009-11-17 18:09:0196class GIT(object):
[email protected]36ac2392011-10-12 16:36:1197 current_version = None
98
[email protected]5aeb7dd2009-11-17 18:09:0199 @staticmethod
[email protected]6d8115d2014-04-23 20:59:23100 def ApplyEnvVars(kwargs):
101 env = kwargs.pop('env', None) or os.environ.copy()
102 # Don't prompt for passwords; just fail quickly and noisily.
103 # By default, git will use an interactive terminal prompt when a username/
104 # password is needed. That shouldn't happen in the chromium workflow,
105 # and if it does, then gclient may hide the prompt in the midst of a flood
106 # of terminal spew. The only indication that something has gone wrong
107 # will be when gclient hangs unresponsively. Instead, we disable the
108 # password prompt and simply allow git to fail noisily. The error
109 # message produced by git will be copied to gclient's output.
110 env.setdefault('GIT_ASKPASS', 'true')
111 env.setdefault('SSH_ASKPASS', 'true')
[email protected]82b91cd2013-07-09 06:33:41112 # 'cat' is a magical git string that disables pagers on all platforms.
[email protected]6d8115d2014-04-23 20:59:23113 env.setdefault('GIT_PAGER', 'cat')
114 return env
115
116 @staticmethod
117 def Capture(args, cwd, strip_out=True, **kwargs):
118 env = GIT.ApplyEnvVars(kwargs)
[email protected]4380c802013-07-12 23:38:41119 output = subprocess2.check_output(
[email protected]82b91cd2013-07-09 06:33:41120 ['git'] + args,
[email protected]4380c802013-07-12 23:38:41121 cwd=cwd, stderr=subprocess2.PIPE, env=env, **kwargs)
122 return output.strip() if strip_out else output
[email protected]d5800f12009-11-12 20:03:43123
[email protected]5aeb7dd2009-11-17 18:09:01124 @staticmethod
[email protected]80a9ef12011-12-13 20:44:10125 def CaptureStatus(files, cwd, upstream_branch):
[email protected]5aeb7dd2009-11-17 18:09:01126 """Returns git status.
[email protected]d5800f12009-11-12 20:03:43127
[email protected]5aeb7dd2009-11-17 18:09:01128 @files can be a string (one file) or a list of files.
[email protected]d5800f12009-11-12 20:03:43129
[email protected]5aeb7dd2009-11-17 18:09:01130 Returns an array of (status, file) tuples."""
[email protected]786fb682010-06-02 15:16:23131 if upstream_branch is None:
[email protected]80a9ef12011-12-13 20:44:10132 upstream_branch = GIT.GetUpstreamBranch(cwd)
[email protected]786fb682010-06-02 15:16:23133 if upstream_branch is None:
[email protected]ad80e3b2010-09-09 14:18:28134 raise gclient_utils.Error('Cannot determine upstream branch')
[email protected]9249f642013-06-03 21:36:18135 command = ['diff', '--name-status', '--no-renames',
136 '-r', '%s...' % upstream_branch]
[email protected]5aeb7dd2009-11-17 18:09:01137 if not files:
138 pass
139 elif isinstance(files, basestring):
140 command.append(files)
141 else:
142 command.extend(files)
[email protected]a41249c2013-07-03 00:09:12143 status = GIT.Capture(command, cwd)
[email protected]5aeb7dd2009-11-17 18:09:01144 results = []
145 if status:
[email protected]ad80e3b2010-09-09 14:18:28146 for statusline in status.splitlines():
[email protected]cc1614b2010-09-20 17:13:17147 # 3-way merges can cause the status can be 'MMM' instead of 'M'. This
148 # can happen when the user has 2 local branches and he diffs between
149 # these 2 branches instead diffing to upstream.
150 m = re.match('^(\w)+\t(.+)$', statusline)
[email protected]5aeb7dd2009-11-17 18:09:01151 if not m:
[email protected]ad80e3b2010-09-09 14:18:28152 raise gclient_utils.Error(
153 'status currently unsupported: %s' % statusline)
[email protected]cc1614b2010-09-20 17:13:17154 # Only grab the first letter.
155 results.append(('%s ' % m.group(1)[0], m.group(2)))
[email protected]5aeb7dd2009-11-17 18:09:01156 return results
[email protected]d5800f12009-11-12 20:03:43157
[email protected]c78f2462009-11-21 01:20:57158 @staticmethod
[email protected]ead4c7e2014-04-03 01:01:06159 def IsWorkTreeDirty(cwd):
160 return GIT.Capture(['status', '-s'], cwd=cwd) != ''
161
162 @staticmethod
[email protected]ad80e3b2010-09-09 14:18:28163 def GetEmail(cwd):
[email protected]c78f2462009-11-21 01:20:57164 """Retrieves the user email address if known."""
165 # We could want to look at the svn cred when it has a svn remote but it
166 # should be fine for now, users should simply configure their git settings.
[email protected]ad80e3b2010-09-09 14:18:28167 try:
[email protected]a41249c2013-07-03 00:09:12168 return GIT.Capture(['config', 'user.email'], cwd=cwd)
[email protected]da64d632011-09-08 17:41:15169 except subprocess2.CalledProcessError:
[email protected]ad80e3b2010-09-09 14:18:28170 return ''
[email protected]f2f9d552009-12-22 00:12:57171
172 @staticmethod
173 def ShortBranchName(branch):
174 """Converts a name like 'refs/heads/foo' to just 'foo'."""
175 return branch.replace('refs/heads/', '')
176
177 @staticmethod
178 def GetBranchRef(cwd):
[email protected]b24a8e12009-12-22 13:45:48179 """Returns the full branch reference, e.g. 'refs/heads/master'."""
[email protected]a41249c2013-07-03 00:09:12180 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd)
[email protected]f2f9d552009-12-22 00:12:57181
182 @staticmethod
[email protected]b24a8e12009-12-22 13:45:48183 def GetBranch(cwd):
184 """Returns the short branch name, e.g. 'master'."""
[email protected]c308a742009-12-22 18:29:33185 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
[email protected]b24a8e12009-12-22 13:45:48186
187 @staticmethod
[email protected]6c2b49d2014-02-26 23:57:38188 def IsGitSvn(cwd):
[email protected]b09097a2014-04-09 19:09:08189 """Returns True if this repo looks like it's using git-svn."""
[email protected]f2f9d552009-12-22 00:12:57190 # If you have any "svn-remote.*" config keys, we think you're using svn.
191 try:
[email protected]a41249c2013-07-03 00:09:12192 GIT.Capture(['config', '--local', '--get-regexp', r'^svn-remote\.'],
[email protected]6c2b49d2014-02-26 23:57:38193 cwd=cwd)
[email protected]f2f9d552009-12-22 00:12:57194 return True
[email protected]da64d632011-09-08 17:41:15195 except subprocess2.CalledProcessError:
[email protected]f2f9d552009-12-22 00:12:57196 return False
197
198 @staticmethod
[email protected]866276c2011-03-18 20:09:31199 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
200 """Return the corresponding git ref if |base_url| together with |glob_spec|
201 matches the full |url|.
202
203 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
204 """
205 fetch_suburl, as_ref = glob_spec.split(':')
206 if allow_wildcards:
207 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
208 if glob_match:
209 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
210 # "branches/{472,597,648}/src:refs/remotes/svn/*".
211 branch_re = re.escape(base_url)
212 if glob_match.group(1):
213 branch_re += '/' + re.escape(glob_match.group(1))
214 wildcard = glob_match.group(2)
215 if wildcard == '*':
216 branch_re += '([^/]*)'
217 else:
218 # Escape and replace surrounding braces with parentheses and commas
219 # with pipe symbols.
220 wildcard = re.escape(wildcard)
221 wildcard = re.sub('^\\\\{', '(', wildcard)
222 wildcard = re.sub('\\\\,', '|', wildcard)
223 wildcard = re.sub('\\\\}$', ')', wildcard)
224 branch_re += wildcard
225 if glob_match.group(3):
226 branch_re += re.escape(glob_match.group(3))
227 match = re.match(branch_re, url)
228 if match:
229 return re.sub('\*$', match.group(1), as_ref)
230
231 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
232 if fetch_suburl:
233 full_url = base_url + '/' + fetch_suburl
234 else:
235 full_url = base_url
236 if full_url == url:
237 return as_ref
238 return None
239
240 @staticmethod
[email protected]f2f9d552009-12-22 00:12:57241 def GetSVNBranch(cwd):
242 """Returns the svn branch name if found."""
243 # Try to figure out which remote branch we're based on.
244 # Strategy:
[email protected]ade368c2011-03-01 08:57:50245 # 1) iterate through our branch history and find the svn URL.
246 # 2) find the svn-remote that fetches from the URL.
[email protected]f2f9d552009-12-22 00:12:57247
248 # regexp matching the git-svn line that contains the URL.
249 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
250
[email protected]ade368c2011-03-01 08:57:50251 # We don't want to go through all of history, so read a line from the
252 # pipe at a time.
253 # The -100 is an arbitrary limit so we don't search forever.
254 cmd = ['git', 'log', '-100', '--pretty=medium']
[email protected]f94e3f12011-12-13 21:03:46255 proc = subprocess2.Popen(cmd, cwd=cwd, stdout=subprocess2.PIPE)
[email protected]e8c28622011-04-05 14:41:44256 url = None
[email protected]ade368c2011-03-01 08:57:50257 for line in proc.stdout:
258 match = git_svn_re.match(line)
259 if match:
260 url = match.group(1)
261 proc.stdout.close() # Cut pipe.
262 break
[email protected]f2f9d552009-12-22 00:12:57263
[email protected]ade368c2011-03-01 08:57:50264 if url:
265 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
[email protected]80a9ef12011-12-13 20:44:10266 remotes = GIT.Capture(
[email protected]a41249c2013-07-03 00:09:12267 ['config', '--local', '--get-regexp', r'^svn-remote\..*\.url'],
[email protected]80a9ef12011-12-13 20:44:10268 cwd=cwd).splitlines()
[email protected]ade368c2011-03-01 08:57:50269 for remote in remotes:
270 match = svn_remote_re.match(remote)
[email protected]f2f9d552009-12-22 00:12:57271 if match:
[email protected]ade368c2011-03-01 08:57:50272 remote = match.group(1)
273 base_url = match.group(2)
[email protected]866276c2011-03-18 20:09:31274 try:
275 fetch_spec = GIT.Capture(
[email protected]a41249c2013-07-03 00:09:12276 ['config', '--local', 'svn-remote.%s.fetch' % remote],
277 cwd=cwd)
[email protected]866276c2011-03-18 20:09:31278 branch = GIT.MatchSvnGlob(url, base_url, fetch_spec, False)
[email protected]da64d632011-09-08 17:41:15279 except subprocess2.CalledProcessError:
[email protected]866276c2011-03-18 20:09:31280 branch = None
281 if branch:
282 return branch
283 try:
284 branch_spec = GIT.Capture(
[email protected]a41249c2013-07-03 00:09:12285 ['config', '--local', 'svn-remote.%s.branches' % remote],
286 cwd=cwd)
[email protected]866276c2011-03-18 20:09:31287 branch = GIT.MatchSvnGlob(url, base_url, branch_spec, True)
[email protected]da64d632011-09-08 17:41:15288 except subprocess2.CalledProcessError:
[email protected]866276c2011-03-18 20:09:31289 branch = None
290 if branch:
291 return branch
292 try:
293 tag_spec = GIT.Capture(
[email protected]a41249c2013-07-03 00:09:12294 ['config', '--local', 'svn-remote.%s.tags' % remote],
295 cwd=cwd)
[email protected]866276c2011-03-18 20:09:31296 branch = GIT.MatchSvnGlob(url, base_url, tag_spec, True)
[email protected]da64d632011-09-08 17:41:15297 except subprocess2.CalledProcessError:
[email protected]866276c2011-03-18 20:09:31298 branch = None
299 if branch:
300 return branch
[email protected]f2f9d552009-12-22 00:12:57301
302 @staticmethod
303 def FetchUpstreamTuple(cwd):
304 """Returns a tuple containg remote and remote ref,
305 e.g. 'origin', 'refs/heads/master'
[email protected]81e012c2010-04-29 16:07:24306 Tries to be intelligent and understand git-svn.
[email protected]f2f9d552009-12-22 00:12:57307 """
308 remote = '.'
[email protected]b24a8e12009-12-22 13:45:48309 branch = GIT.GetBranch(cwd)
[email protected]ad80e3b2010-09-09 14:18:28310 try:
311 upstream_branch = GIT.Capture(
[email protected]a41249c2013-07-03 00:09:12312 ['config', '--local', 'branch.%s.merge' % branch], cwd=cwd)
[email protected]da64d632011-09-08 17:41:15313 except subprocess2.CalledProcessError:
[email protected]ad80e3b2010-09-09 14:18:28314 upstream_branch = None
[email protected]f2f9d552009-12-22 00:12:57315 if upstream_branch:
[email protected]ad80e3b2010-09-09 14:18:28316 try:
317 remote = GIT.Capture(
[email protected]a41249c2013-07-03 00:09:12318 ['config', '--local', 'branch.%s.remote' % branch], cwd=cwd)
[email protected]da64d632011-09-08 17:41:15319 except subprocess2.CalledProcessError:
[email protected]ad80e3b2010-09-09 14:18:28320 pass
[email protected]f2f9d552009-12-22 00:12:57321 else:
[email protected]ade368c2011-03-01 08:57:50322 try:
323 upstream_branch = GIT.Capture(
[email protected]a41249c2013-07-03 00:09:12324 ['config', '--local', 'rietveld.upstream-branch'], cwd=cwd)
[email protected]da64d632011-09-08 17:41:15325 except subprocess2.CalledProcessError:
[email protected]ade368c2011-03-01 08:57:50326 upstream_branch = None
327 if upstream_branch:
328 try:
329 remote = GIT.Capture(
[email protected]a41249c2013-07-03 00:09:12330 ['config', '--local', 'rietveld.upstream-remote'], cwd=cwd)
[email protected]da64d632011-09-08 17:41:15331 except subprocess2.CalledProcessError:
[email protected]ade368c2011-03-01 08:57:50332 pass
[email protected]81e012c2010-04-29 16:07:24333 else:
[email protected]ade368c2011-03-01 08:57:50334 # Fall back on trying a git-svn upstream branch.
335 if GIT.IsGitSvn(cwd):
336 upstream_branch = GIT.GetSVNBranch(cwd)
[email protected]a630bd72010-04-29 23:32:34337 else:
[email protected]ade368c2011-03-01 08:57:50338 # Else, try to guess the origin remote.
339 remote_branches = GIT.Capture(['branch', '-r'], cwd=cwd).split()
340 if 'origin/master' in remote_branches:
341 # Fall back on origin/master if it exits.
342 remote = 'origin'
343 upstream_branch = 'refs/heads/master'
344 elif 'origin/trunk' in remote_branches:
345 # Fall back on origin/trunk if it exists. Generally a shared
346 # git-svn clone
347 remote = 'origin'
348 upstream_branch = 'refs/heads/trunk'
349 else:
350 # Give up.
351 remote = None
352 upstream_branch = None
[email protected]f2f9d552009-12-22 00:12:57353 return remote, upstream_branch
354
355 @staticmethod
[email protected]6e7202b2014-09-09 18:23:39356 def RefToRemoteRef(ref, remote=None):
357 """Convert a checkout ref to the equivalent remote ref.
358
359 Returns:
360 A tuple of the remote ref's (common prefix, unique suffix), or None if it
361 doesn't appear to refer to a remote ref (e.g. it's a commit hash).
362 """
363 # TODO(mmoss): This is just a brute-force mapping based of the expected git
364 # config. It's a bit better than the even more brute-force replace('heads',
365 # ...), but could still be smarter (like maybe actually using values gleaned
366 # from the git config).
367 m = re.match('^(refs/(remotes/)?)?branch-heads/', ref or '')
368 if m:
369 return ('refs/remotes/branch-heads/', ref.replace(m.group(0), ''))
370 if remote:
371 m = re.match('^((refs/)?remotes/)?%s/|(refs/)?heads/' % remote, ref or '')
372 if m:
373 return ('refs/remotes/%s/' % remote, ref.replace(m.group(0), ''))
374 return None
375
376 @staticmethod
[email protected]81e012c2010-04-29 16:07:24377 def GetUpstreamBranch(cwd):
[email protected]f2f9d552009-12-22 00:12:57378 """Gets the current branch's upstream branch."""
379 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
[email protected]a630bd72010-04-29 23:32:34380 if remote != '.' and upstream_branch:
[email protected]6e7202b2014-09-09 18:23:39381 remote_ref = GIT.RefToRemoteRef(upstream_branch, remote)
382 if remote_ref:
383 upstream_branch = ''.join(remote_ref)
[email protected]f2f9d552009-12-22 00:12:57384 return upstream_branch
385
386 @staticmethod
[email protected]8ede00e2010-01-12 14:35:28387 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
388 files=None):
[email protected]a9371762009-12-22 18:27:38389 """Diffs against the upstream branch or optionally another branch.
390
391 full_move means that move or copy operations should completely recreate the
392 files, usually in the prospect to apply the patch for a try job."""
[email protected]f2f9d552009-12-22 00:12:57393 if not branch:
[email protected]81e012c2010-04-29 16:07:24394 branch = GIT.GetUpstreamBranch(cwd)
[email protected]33167332012-02-23 21:15:30395 command = ['diff', '-p', '--no-color', '--no-prefix', '--no-ext-diff',
[email protected]400f3e72010-05-19 14:23:36396 branch + "..." + branch_head]
[email protected]9249f642013-06-03 21:36:18397 if full_move:
398 command.append('--no-renames')
399 else:
[email protected]a9371762009-12-22 18:27:38400 command.append('-C')
[email protected]8ede00e2010-01-12 14:35:28401 # TODO(maruel): --binary support.
402 if files:
403 command.append('--')
404 command.extend(files)
[email protected]4380c802013-07-12 23:38:41405 diff = GIT.Capture(command, cwd=cwd, strip_out=False).splitlines(True)
[email protected]f2f9d552009-12-22 00:12:57406 for i in range(len(diff)):
407 # In the case of added files, replace /dev/null with the path to the
408 # file being added.
409 if diff[i].startswith('--- /dev/null'):
410 diff[i] = '--- %s' % diff[i+1][4:]
411 return ''.join(diff)
[email protected]c78f2462009-11-21 01:20:57412
[email protected]b24a8e12009-12-22 13:45:48413 @staticmethod
[email protected]8ede00e2010-01-12 14:35:28414 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
415 """Returns the list of modified files between two branches."""
416 if not branch:
[email protected]81e012c2010-04-29 16:07:24417 branch = GIT.GetUpstreamBranch(cwd)
[email protected]838f0f22010-04-09 17:02:50418 command = ['diff', '--name-only', branch + "..." + branch_head]
[email protected]ad80e3b2010-09-09 14:18:28419 return GIT.Capture(command, cwd=cwd).splitlines(False)
[email protected]8ede00e2010-01-12 14:35:28420
421 @staticmethod
[email protected]b24a8e12009-12-22 13:45:48422 def GetPatchName(cwd):
423 """Constructs a name for this patch."""
[email protected]a41249c2013-07-03 00:09:12424 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd)
[email protected]862ff8e2010-08-06 15:29:16425 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
[email protected]b24a8e12009-12-22 13:45:48426
427 @staticmethod
[email protected]ad80e3b2010-09-09 14:18:28428 def GetCheckoutRoot(cwd):
[email protected]01d8c1d2010-01-07 01:56:59429 """Returns the top level directory of a git checkout as an absolute path.
[email protected]b24a8e12009-12-22 13:45:48430 """
[email protected]a41249c2013-07-03 00:09:12431 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd)
[email protected]ad80e3b2010-09-09 14:18:28432 return os.path.abspath(os.path.join(cwd, root))
[email protected]b24a8e12009-12-22 13:45:48433
[email protected]e5d1e612011-12-19 19:49:19434 @staticmethod
[email protected]ead4c7e2014-04-03 01:01:06435 def GetGitDir(cwd):
436 return os.path.abspath(GIT.Capture(['rev-parse', '--git-dir'], cwd=cwd))
437
438 @staticmethod
439 def IsInsideWorkTree(cwd):
440 try:
441 return GIT.Capture(['rev-parse', '--is-inside-work-tree'], cwd=cwd)
442 except (OSError, subprocess2.CalledProcessError):
443 return False
444
445 @staticmethod
[email protected]1c127382015-02-17 11:15:40446 def IsDirectoryVersioned(cwd, relative_dir):
447 """Checks whether the given |relative_dir| is part of cwd's repo."""
448 return bool(GIT.Capture(['ls-tree', 'HEAD', relative_dir], cwd=cwd))
449
450 @staticmethod
451 def CleanupDir(cwd, relative_dir):
452 """Cleans up untracked file inside |relative_dir|."""
453 return bool(GIT.Capture(['clean', '-df', relative_dir], cwd=cwd))
454
455 @staticmethod
[email protected]e5d1e612011-12-19 19:49:19456 def GetGitSvnHeadRev(cwd):
457 """Gets the most recently pulled git-svn revision."""
458 try:
459 output = GIT.Capture(['svn', 'info'], cwd=cwd)
460 match = re.search(r'^Revision: ([0-9]+)$', output, re.MULTILINE)
461 return int(match.group(1)) if match else None
462 except (subprocess2.CalledProcessError, ValueError):
463 return None
464
465 @staticmethod
[email protected]492a3682012-08-10 00:28:28466 def ParseGitSvnSha1(output):
467 """Parses git-svn output for the first sha1."""
468 match = re.search(r'[0-9a-fA-F]{40}', output)
469 return match.group(0) if match else None
470
471 @staticmethod
[email protected]e5d1e612011-12-19 19:49:19472 def GetSha1ForSvnRev(cwd, rev):
473 """Returns a corresponding git sha1 for a SVN revision."""
[email protected]6c2b49d2014-02-26 23:57:38474 if not GIT.IsGitSvn(cwd=cwd):
[email protected]e5d1e612011-12-19 19:49:19475 return None
476 try:
[email protected]c51def32012-10-15 18:50:37477 output = GIT.Capture(['svn', 'find-rev', 'r' + str(rev)], cwd=cwd)
478 return GIT.ParseGitSvnSha1(output)
479 except subprocess2.CalledProcessError:
480 return None
481
482 @staticmethod
483 def GetBlessedSha1ForSvnRev(cwd, rev):
484 """Returns a git commit hash from the master branch history that has
485 accurate .DEPS.git and git submodules. To understand why this is more
486 complicated than a simple call to `git svn find-rev`, refer to:
487
488 http://www.chromium.org/developers/how-tos/git-repo
489 """
490 git_svn_rev = GIT.GetSha1ForSvnRev(cwd, rev)
491 if not git_svn_rev:
492 return None
493 try:
[email protected]312a6a42012-10-11 21:19:42494 output = GIT.Capture(
495 ['rev-list', '--ancestry-path', '--reverse',
496 '--grep', 'SVN changes up to revision [0-9]*',
497 '%s..refs/remotes/origin/master' % git_svn_rev], cwd=cwd)
498 if not output:
499 return None
500 sha1 = output.splitlines()[0]
501 if not sha1:
502 return None
503 output = GIT.Capture(['rev-list', '-n', '1', '%s^1' % sha1], cwd=cwd)
504 if git_svn_rev != output.rstrip():
505 raise gclient_utils.Error(sha1)
506 return sha1
[email protected]e5d1e612011-12-19 19:49:19507 except subprocess2.CalledProcessError:
508 return None
509
510 @staticmethod
[email protected]a41249c2013-07-03 00:09:12511 def IsValidRevision(cwd, rev, sha_only=False):
512 """Verifies the revision is a proper git revision.
513
514 sha_only: Fail unless rev is a sha hash.
515 """
[email protected]81473862012-06-27 17:30:56516 # 'git rev-parse foo' where foo is *any* 40 character hex string will return
517 # the string and return code 0. So strip one character to force 'git
518 # rev-parse' to do a hash table look-up and returns 128 if the hash is not
519 # present.
[email protected]a41249c2013-07-03 00:09:12520 lookup_rev = rev
[email protected]81473862012-06-27 17:30:56521 if re.match(r'^[0-9a-fA-F]{40}$', rev):
[email protected]a41249c2013-07-03 00:09:12522 lookup_rev = rev[:-1]
[email protected]e5d1e612011-12-19 19:49:19523 try:
[email protected]224ba242013-07-08 22:02:31524 sha = GIT.Capture(['rev-parse', lookup_rev], cwd=cwd).lower()
[email protected]a41249c2013-07-03 00:09:12525 if lookup_rev != rev:
526 # Make sure we get the original 40 chars back.
[email protected]68953172014-06-11 22:14:35527 return rev.lower() == sha
[email protected]a41249c2013-07-03 00:09:12528 if sha_only:
[email protected]68953172014-06-11 22:14:35529 return sha.startswith(rev.lower())
530 return True
[email protected]e5d1e612011-12-19 19:49:19531 except subprocess2.CalledProcessError:
532 return False
533
[email protected]36ac2392011-10-12 16:36:11534 @classmethod
535 def AssertVersion(cls, min_version):
[email protected]d0f854a2010-03-11 19:35:53536 """Asserts git's version is at least min_version."""
[email protected]36ac2392011-10-12 16:36:11537 if cls.current_version is None:
[email protected]fcffd482012-02-24 01:47:00538 current_version = cls.Capture(['--version'], '.')
539 matched = re.search(r'version ([0-9\.]+)', current_version)
540 cls.current_version = matched.group(1)
[email protected]36ac2392011-10-12 16:36:11541 current_version_list = map(only_int, cls.current_version.split('.'))
[email protected]d0f854a2010-03-11 19:35:53542 for min_ver in map(int, min_version.split('.')):
543 ver = current_version_list.pop(0)
544 if ver < min_ver:
[email protected]36ac2392011-10-12 16:36:11545 return (False, cls.current_version)
[email protected]d0f854a2010-03-11 19:35:53546 elif ver > min_ver:
[email protected]36ac2392011-10-12 16:36:11547 return (True, cls.current_version)
548 return (True, cls.current_version)