blob: 6baf1085a9771d117de724dbe87a11dfb8a0702a [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
Pierre-Antoine Manzagolfc1c6f42017-05-30 16:29:5811import platform
[email protected]d5800f12009-11-12 20:03:4312import re
[email protected]d5800f12009-11-12 20:03:4313import sys
[email protected]4755b582013-04-18 21:38:4014import tempfile
[email protected]fd876172010-04-30 14:01:0515import time
[email protected]ade9c592011-04-07 15:59:1116from xml.etree import ElementTree
[email protected]d5800f12009-11-12 20:03:4317
18import gclient_utils
[email protected]31cb48a2011-04-04 18:01:3619import subprocess2
20
[email protected]d5800f12009-11-12 20:03:4321
[email protected]b24a8e12009-12-22 13:45:4822def ValidateEmail(email):
[email protected]6e29d572010-06-04 17:32:2023 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
24 is not None)
[email protected]b24a8e12009-12-22 13:45:4825
[email protected]d5800f12009-11-12 20:03:4326
[email protected]fd9cbbb2010-01-08 23:04:0327def GetCasedPath(path):
28 """Elcheapos way to get the real path case on Windows."""
29 if sys.platform.startswith('win') and os.path.exists(path):
30 # Reconstruct the path.
31 path = os.path.abspath(path)
32 paths = path.split('\\')
33 for i in range(len(paths)):
34 if i == 0:
35 # Skip drive letter.
36 continue
37 subpath = '\\'.join(paths[:i+1])
38 prev = len('\\'.join(paths[:i]))
39 # glob.glob will return the cased path for the last item only. This is why
40 # we are calling it in a loop. Extract the data we want and put it back
41 # into the list.
42 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)]
43 path = '\\'.join(paths)
44 return path
45
46
[email protected]3c55d982010-05-06 14:25:4447def GenFakeDiff(filename):
48 """Generates a fake diff from a file."""
49 file_content = gclient_utils.FileRead(filename, 'rb').splitlines(True)
[email protected]c6d170e2010-06-03 00:06:0050 filename = filename.replace(os.sep, '/')
[email protected]3c55d982010-05-06 14:25:4451 nb_lines = len(file_content)
52 # We need to use / since patch on unix will fail otherwise.
53 data = cStringIO.StringIO()
54 data.write("Index: %s\n" % filename)
55 data.write('=' * 67 + '\n')
56 # Note: Should we use /dev/null instead?
57 data.write("--- %s\n" % filename)
58 data.write("+++ %s\n" % filename)
59 data.write("@@ -0,0 +1,%d @@\n" % nb_lines)
60 # Prepend '+' to every lines.
61 for line in file_content:
62 data.write('+')
63 data.write(line)
64 result = data.getvalue()
65 data.close()
66 return result
67
68
[email protected]5c8c6de2011-03-18 16:20:1869def determine_scm(root):
70 """Similar to upload.py's version but much simpler.
71
Aaron Gable208db562016-12-21 22:46:3672 Returns 'git' or None.
[email protected]5c8c6de2011-03-18 16:20:1873 """
Aaron Gable208db562016-12-21 22:46:3674 if os.path.isdir(os.path.join(root, '.git')):
[email protected]5c8c6de2011-03-18 16:20:1875 return 'git'
76 else:
[email protected]c98c0c52011-04-06 13:39:4377 try:
[email protected]91def9b2011-09-14 16:28:0778 subprocess2.check_call(
[email protected]5c8c6de2011-03-18 16:20:1879 ['git', 'rev-parse', '--show-cdup'],
[email protected]c98c0c52011-04-06 13:39:4380 stdout=subprocess2.VOID,
[email protected]87e6d332011-09-09 19:01:2881 stderr=subprocess2.VOID,
[email protected]c98c0c52011-04-06 13:39:4382 cwd=root)
[email protected]5c8c6de2011-03-18 16:20:1883 return 'git'
[email protected]c98c0c52011-04-06 13:39:4384 except (OSError, subprocess2.CalledProcessError):
[email protected]5c8c6de2011-03-18 16:20:1885 return None
86
87
[email protected]36ac2392011-10-12 16:36:1188def only_int(val):
89 if val.isdigit():
90 return int(val)
91 else:
92 return 0
93
94
[email protected]5aeb7dd2009-11-17 18:09:0195class GIT(object):
[email protected]36ac2392011-10-12 16:36:1196 current_version = None
97
[email protected]5aeb7dd2009-11-17 18:09:0198 @staticmethod
[email protected]6d8115d2014-04-23 20:59:2399 def ApplyEnvVars(kwargs):
100 env = kwargs.pop('env', None) or os.environ.copy()
101 # Don't prompt for passwords; just fail quickly and noisily.
102 # By default, git will use an interactive terminal prompt when a username/
103 # password is needed. That shouldn't happen in the chromium workflow,
104 # and if it does, then gclient may hide the prompt in the midst of a flood
105 # of terminal spew. The only indication that something has gone wrong
106 # will be when gclient hangs unresponsively. Instead, we disable the
107 # password prompt and simply allow git to fail noisily. The error
108 # message produced by git will be copied to gclient's output.
109 env.setdefault('GIT_ASKPASS', 'true')
110 env.setdefault('SSH_ASKPASS', 'true')
[email protected]82b91cd2013-07-09 06:33:41111 # 'cat' is a magical git string that disables pagers on all platforms.
[email protected]6d8115d2014-04-23 20:59:23112 env.setdefault('GIT_PAGER', 'cat')
113 return env
114
115 @staticmethod
116 def Capture(args, cwd, strip_out=True, **kwargs):
117 env = GIT.ApplyEnvVars(kwargs)
[email protected]4380c802013-07-12 23:38:41118 output = subprocess2.check_output(
[email protected]82b91cd2013-07-09 06:33:41119 ['git'] + args,
[email protected]4380c802013-07-12 23:38:41120 cwd=cwd, stderr=subprocess2.PIPE, env=env, **kwargs)
121 return output.strip() if strip_out else output
[email protected]d5800f12009-11-12 20:03:43122
[email protected]5aeb7dd2009-11-17 18:09:01123 @staticmethod
[email protected]80a9ef12011-12-13 20:44:10124 def CaptureStatus(files, cwd, upstream_branch):
[email protected]5aeb7dd2009-11-17 18:09:01125 """Returns git status.
[email protected]d5800f12009-11-12 20:03:43126
[email protected]5aeb7dd2009-11-17 18:09:01127 @files can be a string (one file) or a list of files.
[email protected]d5800f12009-11-12 20:03:43128
[email protected]5aeb7dd2009-11-17 18:09:01129 Returns an array of (status, file) tuples."""
[email protected]786fb682010-06-02 15:16:23130 if upstream_branch is None:
[email protected]80a9ef12011-12-13 20:44:10131 upstream_branch = GIT.GetUpstreamBranch(cwd)
[email protected]786fb682010-06-02 15:16:23132 if upstream_branch is None:
[email protected]ad80e3b2010-09-09 14:18:28133 raise gclient_utils.Error('Cannot determine upstream branch')
Aaron Gable7817f022017-12-12 17:43:17134 command = ['-c', 'core.quotePath=false', 'diff',
135 '--name-status', '--no-renames', '-r', '%s...' % upstream_branch]
[email protected]5aeb7dd2009-11-17 18:09:01136 if not files:
137 pass
138 elif isinstance(files, basestring):
139 command.append(files)
140 else:
141 command.extend(files)
[email protected]a41249c2013-07-03 00:09:12142 status = GIT.Capture(command, cwd)
[email protected]5aeb7dd2009-11-17 18:09:01143 results = []
144 if status:
[email protected]ad80e3b2010-09-09 14:18:28145 for statusline in status.splitlines():
[email protected]cc1614b2010-09-20 17:13:17146 # 3-way merges can cause the status can be 'MMM' instead of 'M'. This
147 # can happen when the user has 2 local branches and he diffs between
148 # these 2 branches instead diffing to upstream.
149 m = re.match('^(\w)+\t(.+)$', statusline)
[email protected]5aeb7dd2009-11-17 18:09:01150 if not m:
[email protected]ad80e3b2010-09-09 14:18:28151 raise gclient_utils.Error(
152 'status currently unsupported: %s' % statusline)
[email protected]cc1614b2010-09-20 17:13:17153 # Only grab the first letter.
154 results.append(('%s ' % m.group(1)[0], m.group(2)))
[email protected]5aeb7dd2009-11-17 18:09:01155 return results
[email protected]d5800f12009-11-12 20:03:43156
[email protected]c78f2462009-11-21 01:20:57157 @staticmethod
[email protected]ead4c7e2014-04-03 01:01:06158 def IsWorkTreeDirty(cwd):
159 return GIT.Capture(['status', '-s'], cwd=cwd) != ''
160
161 @staticmethod
[email protected]ad80e3b2010-09-09 14:18:28162 def GetEmail(cwd):
[email protected]c78f2462009-11-21 01:20:57163 """Retrieves the user email address if known."""
[email protected]ad80e3b2010-09-09 14:18:28164 try:
[email protected]a41249c2013-07-03 00:09:12165 return GIT.Capture(['config', 'user.email'], cwd=cwd)
[email protected]da64d632011-09-08 17:41:15166 except subprocess2.CalledProcessError:
[email protected]ad80e3b2010-09-09 14:18:28167 return ''
[email protected]f2f9d552009-12-22 00:12:57168
169 @staticmethod
170 def ShortBranchName(branch):
171 """Converts a name like 'refs/heads/foo' to just 'foo'."""
172 return branch.replace('refs/heads/', '')
173
174 @staticmethod
175 def GetBranchRef(cwd):
[email protected]b24a8e12009-12-22 13:45:48176 """Returns the full branch reference, e.g. 'refs/heads/master'."""
[email protected]a41249c2013-07-03 00:09:12177 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd)
[email protected]f2f9d552009-12-22 00:12:57178
179 @staticmethod
[email protected]b24a8e12009-12-22 13:45:48180 def GetBranch(cwd):
181 """Returns the short branch name, e.g. 'master'."""
[email protected]c308a742009-12-22 18:29:33182 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
[email protected]b24a8e12009-12-22 13:45:48183
184 @staticmethod
[email protected]f2f9d552009-12-22 00:12:57185 def FetchUpstreamTuple(cwd):
186 """Returns a tuple containg remote and remote ref,
187 e.g. 'origin', 'refs/heads/master'
[email protected]f2f9d552009-12-22 00:12:57188 """
189 remote = '.'
[email protected]b24a8e12009-12-22 13:45:48190 branch = GIT.GetBranch(cwd)
[email protected]ad80e3b2010-09-09 14:18:28191 try:
192 upstream_branch = GIT.Capture(
[email protected]a41249c2013-07-03 00:09:12193 ['config', '--local', 'branch.%s.merge' % branch], cwd=cwd)
[email protected]da64d632011-09-08 17:41:15194 except subprocess2.CalledProcessError:
[email protected]ad80e3b2010-09-09 14:18:28195 upstream_branch = None
[email protected]f2f9d552009-12-22 00:12:57196 if upstream_branch:
[email protected]ad80e3b2010-09-09 14:18:28197 try:
198 remote = GIT.Capture(
[email protected]a41249c2013-07-03 00:09:12199 ['config', '--local', 'branch.%s.remote' % branch], cwd=cwd)
[email protected]da64d632011-09-08 17:41:15200 except subprocess2.CalledProcessError:
[email protected]ad80e3b2010-09-09 14:18:28201 pass
[email protected]f2f9d552009-12-22 00:12:57202 else:
[email protected]ade368c2011-03-01 08:57:50203 try:
204 upstream_branch = GIT.Capture(
[email protected]a41249c2013-07-03 00:09:12205 ['config', '--local', 'rietveld.upstream-branch'], cwd=cwd)
[email protected]da64d632011-09-08 17:41:15206 except subprocess2.CalledProcessError:
[email protected]ade368c2011-03-01 08:57:50207 upstream_branch = None
208 if upstream_branch:
209 try:
210 remote = GIT.Capture(
[email protected]a41249c2013-07-03 00:09:12211 ['config', '--local', 'rietveld.upstream-remote'], cwd=cwd)
[email protected]da64d632011-09-08 17:41:15212 except subprocess2.CalledProcessError:
[email protected]ade368c2011-03-01 08:57:50213 pass
[email protected]81e012c2010-04-29 16:07:24214 else:
Aaron Gable208db562016-12-21 22:46:36215 # Else, try to guess the origin remote.
216 remote_branches = GIT.Capture(['branch', '-r'], cwd=cwd).split()
217 if 'origin/master' in remote_branches:
218 # Fall back on origin/master if it exits.
219 remote = 'origin'
220 upstream_branch = 'refs/heads/master'
[email protected]a630bd72010-04-29 23:32:34221 else:
Aaron Gable208db562016-12-21 22:46:36222 # Give up.
223 remote = None
224 upstream_branch = None
[email protected]f2f9d552009-12-22 00:12:57225 return remote, upstream_branch
226
227 @staticmethod
[email protected]6e7202b2014-09-09 18:23:39228 def RefToRemoteRef(ref, remote=None):
229 """Convert a checkout ref to the equivalent remote ref.
230
231 Returns:
232 A tuple of the remote ref's (common prefix, unique suffix), or None if it
233 doesn't appear to refer to a remote ref (e.g. it's a commit hash).
234 """
235 # TODO(mmoss): This is just a brute-force mapping based of the expected git
236 # config. It's a bit better than the even more brute-force replace('heads',
237 # ...), but could still be smarter (like maybe actually using values gleaned
238 # from the git config).
239 m = re.match('^(refs/(remotes/)?)?branch-heads/', ref or '')
240 if m:
241 return ('refs/remotes/branch-heads/', ref.replace(m.group(0), ''))
242 if remote:
243 m = re.match('^((refs/)?remotes/)?%s/|(refs/)?heads/' % remote, ref or '')
244 if m:
245 return ('refs/remotes/%s/' % remote, ref.replace(m.group(0), ''))
246 return None
247
248 @staticmethod
[email protected]81e012c2010-04-29 16:07:24249 def GetUpstreamBranch(cwd):
[email protected]f2f9d552009-12-22 00:12:57250 """Gets the current branch's upstream branch."""
251 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
[email protected]a630bd72010-04-29 23:32:34252 if remote != '.' and upstream_branch:
[email protected]6e7202b2014-09-09 18:23:39253 remote_ref = GIT.RefToRemoteRef(upstream_branch, remote)
254 if remote_ref:
255 upstream_branch = ''.join(remote_ref)
[email protected]f2f9d552009-12-22 00:12:57256 return upstream_branch
257
258 @staticmethod
Daniel Cheng7a1f04d2017-03-22 02:12:31259 def GetOldContents(cwd, filename, branch=None):
260 if not branch:
261 branch = GIT.GetUpstreamBranch(cwd)
Pierre-Antoine Manzagolfc1c6f42017-05-30 16:29:58262 if platform.system() == 'Windows':
263 # git show <sha>:<path> wants a posix path.
264 filename = filename.replace('\\', '/')
Daniel Cheng7a1f04d2017-03-22 02:12:31265 command = ['show', '%s:%s' % (branch, filename)]
Daniel Chengd67e7152017-04-13 08:21:03266 try:
267 return GIT.Capture(command, cwd=cwd, strip_out=False)
268 except subprocess2.CalledProcessError:
269 return ''
Daniel Cheng7a1f04d2017-03-22 02:12:31270
271 @staticmethod
[email protected]8ede00e2010-01-12 14:35:28272 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
273 files=None):
[email protected]a9371762009-12-22 18:27:38274 """Diffs against the upstream branch or optionally another branch.
275
276 full_move means that move or copy operations should completely recreate the
277 files, usually in the prospect to apply the patch for a try job."""
[email protected]f2f9d552009-12-22 00:12:57278 if not branch:
[email protected]81e012c2010-04-29 16:07:24279 branch = GIT.GetUpstreamBranch(cwd)
Aaron Gablef4068aa2017-12-12 23:14:09280 command = ['-c', 'core.quotePath=false', 'diff',
281 '-p', '--no-color', '--no-prefix', '--no-ext-diff',
[email protected]400f3e72010-05-19 14:23:36282 branch + "..." + branch_head]
[email protected]9249f642013-06-03 21:36:18283 if full_move:
284 command.append('--no-renames')
285 else:
[email protected]a9371762009-12-22 18:27:38286 command.append('-C')
[email protected]8ede00e2010-01-12 14:35:28287 # TODO(maruel): --binary support.
288 if files:
289 command.append('--')
290 command.extend(files)
[email protected]4380c802013-07-12 23:38:41291 diff = GIT.Capture(command, cwd=cwd, strip_out=False).splitlines(True)
[email protected]f2f9d552009-12-22 00:12:57292 for i in range(len(diff)):
293 # In the case of added files, replace /dev/null with the path to the
294 # file being added.
295 if diff[i].startswith('--- /dev/null'):
296 diff[i] = '--- %s' % diff[i+1][4:]
297 return ''.join(diff)
[email protected]c78f2462009-11-21 01:20:57298
[email protected]b24a8e12009-12-22 13:45:48299 @staticmethod
[email protected]8ede00e2010-01-12 14:35:28300 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
301 """Returns the list of modified files between two branches."""
302 if not branch:
[email protected]81e012c2010-04-29 16:07:24303 branch = GIT.GetUpstreamBranch(cwd)
Aaron Gablef4068aa2017-12-12 23:14:09304 command = ['-c', 'core.quotePath=false', 'diff',
305 '--name-only', branch + "..." + branch_head]
[email protected]ad80e3b2010-09-09 14:18:28306 return GIT.Capture(command, cwd=cwd).splitlines(False)
[email protected]8ede00e2010-01-12 14:35:28307
308 @staticmethod
[email protected]b24a8e12009-12-22 13:45:48309 def GetPatchName(cwd):
310 """Constructs a name for this patch."""
[email protected]a41249c2013-07-03 00:09:12311 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd)
[email protected]862ff8e2010-08-06 15:29:16312 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
[email protected]b24a8e12009-12-22 13:45:48313
314 @staticmethod
[email protected]ad80e3b2010-09-09 14:18:28315 def GetCheckoutRoot(cwd):
[email protected]01d8c1d2010-01-07 01:56:59316 """Returns the top level directory of a git checkout as an absolute path.
[email protected]b24a8e12009-12-22 13:45:48317 """
[email protected]a41249c2013-07-03 00:09:12318 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd)
[email protected]ad80e3b2010-09-09 14:18:28319 return os.path.abspath(os.path.join(cwd, root))
[email protected]b24a8e12009-12-22 13:45:48320
[email protected]e5d1e612011-12-19 19:49:19321 @staticmethod
[email protected]ead4c7e2014-04-03 01:01:06322 def GetGitDir(cwd):
323 return os.path.abspath(GIT.Capture(['rev-parse', '--git-dir'], cwd=cwd))
324
325 @staticmethod
326 def IsInsideWorkTree(cwd):
327 try:
328 return GIT.Capture(['rev-parse', '--is-inside-work-tree'], cwd=cwd)
329 except (OSError, subprocess2.CalledProcessError):
330 return False
331
332 @staticmethod
[email protected]1c127382015-02-17 11:15:40333 def IsDirectoryVersioned(cwd, relative_dir):
334 """Checks whether the given |relative_dir| is part of cwd's repo."""
335 return bool(GIT.Capture(['ls-tree', 'HEAD', relative_dir], cwd=cwd))
336
337 @staticmethod
338 def CleanupDir(cwd, relative_dir):
339 """Cleans up untracked file inside |relative_dir|."""
340 return bool(GIT.Capture(['clean', '-df', relative_dir], cwd=cwd))
341
342 @staticmethod
[email protected]a41249c2013-07-03 00:09:12343 def IsValidRevision(cwd, rev, sha_only=False):
344 """Verifies the revision is a proper git revision.
345
346 sha_only: Fail unless rev is a sha hash.
347 """
[email protected]81473862012-06-27 17:30:56348 # 'git rev-parse foo' where foo is *any* 40 character hex string will return
349 # the string and return code 0. So strip one character to force 'git
350 # rev-parse' to do a hash table look-up and returns 128 if the hash is not
351 # present.
[email protected]a41249c2013-07-03 00:09:12352 lookup_rev = rev
[email protected]81473862012-06-27 17:30:56353 if re.match(r'^[0-9a-fA-F]{40}$', rev):
[email protected]a41249c2013-07-03 00:09:12354 lookup_rev = rev[:-1]
[email protected]e5d1e612011-12-19 19:49:19355 try:
[email protected]224ba242013-07-08 22:02:31356 sha = GIT.Capture(['rev-parse', lookup_rev], cwd=cwd).lower()
[email protected]a41249c2013-07-03 00:09:12357 if lookup_rev != rev:
358 # Make sure we get the original 40 chars back.
[email protected]68953172014-06-11 22:14:35359 return rev.lower() == sha
[email protected]a41249c2013-07-03 00:09:12360 if sha_only:
[email protected]68953172014-06-11 22:14:35361 return sha.startswith(rev.lower())
362 return True
[email protected]e5d1e612011-12-19 19:49:19363 except subprocess2.CalledProcessError:
364 return False
365
[email protected]36ac2392011-10-12 16:36:11366 @classmethod
367 def AssertVersion(cls, min_version):
[email protected]d0f854a2010-03-11 19:35:53368 """Asserts git's version is at least min_version."""
[email protected]36ac2392011-10-12 16:36:11369 if cls.current_version is None:
[email protected]fcffd482012-02-24 01:47:00370 current_version = cls.Capture(['--version'], '.')
371 matched = re.search(r'version ([0-9\.]+)', current_version)
372 cls.current_version = matched.group(1)
[email protected]36ac2392011-10-12 16:36:11373 current_version_list = map(only_int, cls.current_version.split('.'))
[email protected]d0f854a2010-03-11 19:35:53374 for min_ver in map(int, min_version.split('.')):
375 ver = current_version_list.pop(0)
376 if ver < min_ver:
[email protected]36ac2392011-10-12 16:36:11377 return (False, cls.current_version)
[email protected]d0f854a2010-03-11 19:35:53378 elif ver > min_ver:
[email protected]36ac2392011-10-12 16:36:11379 return (True, cls.current_version)
380 return (True, cls.current_version)