blob: 85033937e422326dc55f81757f88f4d4348158db [file] [log] [blame]
maruel@chromium.org7d654672012-01-05 19:07:23 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Josip Sokcevic7958e302023-03-01 23:02:21 +00004
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00005"""SCM-specific utility classes."""
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00006
Edward Lesmes50da7702020-03-30 19:23:43 +00007import distutils.version
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +00008import glob
Raul Tambreb946b232019-03-26 14:48:46 +00009import io
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000010import os
Pierre-Antoine Manzagolfc1c6f42017-05-30 12:29:58 -040011import platform
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000012import re
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000013import sys
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000014
15import gclient_utils
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000016import subprocess2
17
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000018
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000019def ValidateEmail(email):
Josip Sokcevic7958e302023-03-01 23:02:21 +000020 return (
21 re.match(r"^[a-zA-Z0-9._%\-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
22 is not None)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000023
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000024
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000025def GetCasedPath(path):
26 """Elcheapos way to get the real path case on Windows."""
27 if sys.platform.startswith('win') and os.path.exists(path):
28 # Reconstruct the path.
29 path = os.path.abspath(path)
30 paths = path.split('\\')
31 for i in range(len(paths)):
32 if i == 0:
33 # Skip drive letter.
34 continue
Josip Sokcevic7958e302023-03-01 23:02:21 +000035 subpath = '\\'.join(paths[:i+1])
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000036 prev = len('\\'.join(paths[:i]))
37 # glob.glob will return the cased path for the last item only. This is why
38 # we are calling it in a loop. Extract the data we want and put it back
39 # into the list.
Josip Sokcevic7958e302023-03-01 23:02:21 +000040 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)]
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000041 path = '\\'.join(paths)
42 return path
43
44
maruel@chromium.org3c55d982010-05-06 14:25:44 +000045def GenFakeDiff(filename):
46 """Generates a fake diff from a file."""
47 file_content = gclient_utils.FileRead(filename, 'rb').splitlines(True)
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +000048 filename = filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +000049 nb_lines = len(file_content)
50 # We need to use / since patch on unix will fail otherwise.
Raul Tambreb946b232019-03-26 14:48:46 +000051 data = io.StringIO()
maruel@chromium.org3c55d982010-05-06 14:25:44 +000052 data.write("Index: %s\n" % filename)
53 data.write('=' * 67 + '\n')
54 # Note: Should we use /dev/null instead?
55 data.write("--- %s\n" % filename)
56 data.write("+++ %s\n" % filename)
57 data.write("@@ -0,0 +1,%d @@\n" % nb_lines)
58 # Prepend '+' to every lines.
59 for line in file_content:
60 data.write('+')
61 data.write(line)
62 result = data.getvalue()
63 data.close()
64 return result
65
66
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000067def determine_scm(root):
68 """Similar to upload.py's version but much simpler.
69
Aaron Gable208db562016-12-21 14:46:36 -080070 Returns 'git' or None.
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000071 """
Aaron Gable208db562016-12-21 14:46:36 -080072 if os.path.isdir(os.path.join(root, '.git')):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000073 return 'git'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000074
75 try:
Josip Sokcevic7958e302023-03-01 23:02:21 +000076 subprocess2.check_call(
77 ['git', 'rev-parse', '--show-cdup'],
78 stdout=subprocess2.DEVNULL,
79 stderr=subprocess2.DEVNULL,
80 cwd=root)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000081 return 'git'
82 except (OSError, subprocess2.CalledProcessError):
83 return None
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000084
85
maruel@chromium.org36ac2392011-10-12 16:36:11 +000086def only_int(val):
87 if val.isdigit():
88 return int(val)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000089
90 return 0
maruel@chromium.org36ac2392011-10-12 16:36:11 +000091
92
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000093class GIT(object):
maruel@chromium.org36ac2392011-10-12 16:36:11 +000094 current_version = None
95
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000096 @staticmethod
szager@chromium.org6d8115d2014-04-23 20:59:23 +000097 def ApplyEnvVars(kwargs):
98 env = kwargs.pop('env', None) or os.environ.copy()
99 # Don't prompt for passwords; just fail quickly and noisily.
100 # By default, git will use an interactive terminal prompt when a username/
101 # password is needed. That shouldn't happen in the chromium workflow,
102 # and if it does, then gclient may hide the prompt in the midst of a flood
103 # of terminal spew. The only indication that something has gone wrong
104 # will be when gclient hangs unresponsively. Instead, we disable the
105 # password prompt and simply allow git to fail noisily. The error
106 # message produced by git will be copied to gclient's output.
107 env.setdefault('GIT_ASKPASS', 'true')
108 env.setdefault('SSH_ASKPASS', 'true')
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000109 # 'cat' is a magical git string that disables pagers on all platforms.
szager@chromium.org6d8115d2014-04-23 20:59:23 +0000110 env.setdefault('GIT_PAGER', 'cat')
111 return env
112
113 @staticmethod
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000114 def Capture(args, cwd=None, strip_out=True, **kwargs):
szager@chromium.org6d8115d2014-04-23 20:59:23 +0000115 env = GIT.ApplyEnvVars(kwargs)
Josip Sokcevic7958e302023-03-01 23:02:21 +0000116 output = subprocess2.check_output(
117 ['git'] + args, cwd=cwd, stderr=subprocess2.PIPE, env=env, **kwargs)
Edward Lesmes50da7702020-03-30 19:23:43 +0000118 output = output.decode('utf-8', 'replace')
ilevy@chromium.org4380c802013-07-12 23:38:41 +0000119 return output.strip() if strip_out else output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000120
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000121 @staticmethod
Joanna Wangb46232e2023-01-21 01:58:46 +0000122 def CaptureStatus(cwd, upstream_branch, end_commit=None):
123 # type: (str, str, Optional[str]) -> Sequence[Tuple[str, str]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000124 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000125
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000126 Returns an array of (status, file) tuples."""
Joanna Wangb46232e2023-01-21 01:58:46 +0000127 if end_commit is None:
128 end_commit = ''
msb@chromium.org786fb682010-06-02 15:16:23 +0000129 if upstream_branch is None:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000130 upstream_branch = GIT.GetUpstreamBranch(cwd)
msb@chromium.org786fb682010-06-02 15:16:23 +0000131 if upstream_branch is None:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000132 raise gclient_utils.Error('Cannot determine upstream branch')
Joanna Wangb46232e2023-01-21 01:58:46 +0000133 command = [
134 '-c', 'core.quotePath=false', 'diff', '--name-status', '--no-renames',
Josip Sokcevic49b24e42023-08-18 20:49:31 +0000135 '--ignore-submodules=all', '-r',
Joanna Wangb46232e2023-01-21 01:58:46 +0000136 '%s...%s' % (upstream_branch, end_commit)
137 ]
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000138 status = GIT.Capture(command, cwd)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000139 results = []
140 if status:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000141 for statusline in status.splitlines():
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +0000142 # 3-way merges can cause the status can be 'MMM' instead of 'M'. This
143 # can happen when the user has 2 local branches and he diffs between
144 # these 2 branches instead diffing to upstream.
Bruce Dawson9c062012019-05-02 19:20:28 +0000145 m = re.match(r'^(\w)+\t(.+)$', statusline)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000146 if not m:
Josip Sokcevic7958e302023-03-01 23:02:21 +0000147 raise gclient_utils.Error(
148 'status currently unsupported: %s' % statusline)
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +0000149 # Only grab the first letter.
150 results.append(('%s ' % m.group(1)[0], m.group(2)))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000151 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000152
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000153 @staticmethod
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000154 def GetConfig(cwd, key, default=None):
155 try:
156 return GIT.Capture(['config', key], cwd=cwd)
157 except subprocess2.CalledProcessError:
158 return default
159
160 @staticmethod
161 def GetBranchConfig(cwd, branch, key, default=None):
162 assert branch, 'A branch must be given'
163 key = 'branch.%s.%s' % (branch, key)
164 return GIT.GetConfig(cwd, key, default)
165
166 @staticmethod
167 def SetConfig(cwd, key, value=None):
168 if value is None:
169 args = ['config', '--unset', key]
170 else:
171 args = ['config', key, value]
172 GIT.Capture(args, cwd=cwd)
173
174 @staticmethod
175 def SetBranchConfig(cwd, branch, key, value=None):
176 assert branch, 'A branch must be given'
177 key = 'branch.%s.%s' % (branch, key)
178 GIT.SetConfig(cwd, key, value)
179
180 @staticmethod
nodir@chromium.orgead4c7e2014-04-03 01:01:06 +0000181 def IsWorkTreeDirty(cwd):
182 return GIT.Capture(['status', '-s'], cwd=cwd) != ''
183
184 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000185 def GetEmail(cwd):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000186 """Retrieves the user email address if known."""
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000187 return GIT.GetConfig(cwd, 'user.email', '')
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000188
189 @staticmethod
190 def ShortBranchName(branch):
191 """Converts a name like 'refs/heads/foo' to just 'foo'."""
192 return branch.replace('refs/heads/', '')
193
194 @staticmethod
195 def GetBranchRef(cwd):
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000196 """Returns the full branch reference, e.g. 'refs/heads/main'."""
Edward Lemur85153282020-02-14 22:06:29 +0000197 try:
198 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd)
199 except subprocess2.CalledProcessError:
200 return None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000201
202 @staticmethod
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000203 def GetRemoteHeadRef(cwd, url, remote):
204 """Returns the full default remote branch reference, e.g.
205 'refs/remotes/origin/main'."""
206 if os.path.exists(cwd):
207 try:
208 # Try using local git copy first
209 ref = 'refs/remotes/%s/HEAD' % remote
Josip Sokcevice1482c52021-09-09 23:03:47 +0000210 ref = GIT.Capture(['symbolic-ref', ref], cwd=cwd)
211 if not ref.endswith('master'):
212 return ref
213 # Check if there are changes in the default branch for this particular
214 # repository.
215 GIT.Capture(['remote', 'set-head', '-a', remote], cwd=cwd)
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000216 return GIT.Capture(['symbolic-ref', ref], cwd=cwd)
217 except subprocess2.CalledProcessError:
218 pass
219
220 try:
221 # Fetch information from git server
222 resp = GIT.Capture(['ls-remote', '--symref', url, 'HEAD'])
223 regex = r'^ref: (.*)\tHEAD$'
224 for line in resp.split('\n'):
225 m = re.match(regex, line)
226 if m:
227 return ''.join(GIT.RefToRemoteRef(m.group(1), remote))
228 except subprocess2.CalledProcessError:
229 pass
230 # Return default branch
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000231 return 'refs/remotes/%s/main' % remote
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000232
233 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000234 def GetBranch(cwd):
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000235 """Returns the short branch name, e.g. 'main'."""
Edward Lemur85153282020-02-14 22:06:29 +0000236 branchref = GIT.GetBranchRef(cwd)
237 if branchref:
238 return GIT.ShortBranchName(branchref)
239 return None
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000240
241 @staticmethod
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000242 def GetRemoteBranches(cwd):
243 return GIT.Capture(['branch', '-r'], cwd=cwd).split()
244
245 @staticmethod
246 def FetchUpstreamTuple(cwd, branch=None):
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000247 """Returns a tuple containing remote and remote ref,
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000248 e.g. 'origin', 'refs/heads/main'
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000249 """
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000250 try:
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000251 branch = branch or GIT.GetBranch(cwd)
maruel@chromium.orgda64d632011-09-08 17:41:15 +0000252 except subprocess2.CalledProcessError:
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000253 pass
254 if branch:
255 upstream_branch = GIT.GetBranchConfig(cwd, branch, 'merge')
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000256 if upstream_branch:
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000257 remote = GIT.GetBranchConfig(cwd, branch, 'remote', '.')
258 return remote, upstream_branch
259
260 upstream_branch = GIT.GetConfig(cwd, 'rietveld.upstream-branch')
261 if upstream_branch:
262 remote = GIT.GetConfig(cwd, 'rietveld.upstream-remote', '.')
263 return remote, upstream_branch
264
265 # Else, try to guess the origin remote.
Josip Sokcevic5bdfcd82020-11-03 17:27:15 +0000266 remote_branches = GIT.GetRemoteBranches(cwd)
267 if 'origin/main' in remote_branches:
268 # Fall back on origin/main if it exits.
269 return 'origin', 'refs/heads/main'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000270
271 if 'origin/master' in remote_branches:
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000272 # Fall back on origin/master if it exits.
273 return 'origin', 'refs/heads/master'
274
275 return None, None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000276
277 @staticmethod
Edward Lemur9a5e3bd2019-04-02 23:37:45 +0000278 def RefToRemoteRef(ref, remote):
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000279 """Convert a checkout ref to the equivalent remote ref.
280
281 Returns:
282 A tuple of the remote ref's (common prefix, unique suffix), or None if it
283 doesn't appear to refer to a remote ref (e.g. it's a commit hash).
284 """
285 # TODO(mmoss): This is just a brute-force mapping based of the expected git
286 # config. It's a bit better than the even more brute-force replace('heads',
287 # ...), but could still be smarter (like maybe actually using values gleaned
288 # from the git config).
289 m = re.match('^(refs/(remotes/)?)?branch-heads/', ref or '')
290 if m:
291 return ('refs/remotes/branch-heads/', ref.replace(m.group(0), ''))
Edward Lemur9a5e3bd2019-04-02 23:37:45 +0000292
293 m = re.match('^((refs/)?remotes/)?%s/|(refs/)?heads/' % remote, ref or '')
294 if m:
295 return ('refs/remotes/%s/' % remote, ref.replace(m.group(0), ''))
296
297 return None
298
299 @staticmethod
300 def RemoteRefToRef(ref, remote):
301 assert remote, 'A remote must be given'
302 if not ref or not ref.startswith('refs/'):
303 return None
304 if not ref.startswith('refs/remotes/'):
305 return ref
306 if ref.startswith('refs/remotes/branch-heads/'):
307 return 'refs' + ref[len('refs/remotes'):]
308 if ref.startswith('refs/remotes/%s/' % remote):
309 return 'refs/heads' + ref[len('refs/remotes/%s' % remote):]
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000310 return None
311
312 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000313 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000314 """Gets the current branch's upstream branch."""
315 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000316 if remote != '.' and upstream_branch:
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000317 remote_ref = GIT.RefToRemoteRef(upstream_branch, remote)
318 if remote_ref:
319 upstream_branch = ''.join(remote_ref)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000320 return upstream_branch
321
322 @staticmethod
Joanna Wangab9c6ba2023-01-21 01:46:36 +0000323 def IsAncestor(maybe_ancestor, ref, cwd=None):
324 # type: (string, string, Optional[string]) -> bool
Edward Lemurca7d8812018-07-24 17:42:45 +0000325 """Verifies if |maybe_ancestor| is an ancestor of |ref|."""
326 try:
327 GIT.Capture(['merge-base', '--is-ancestor', maybe_ancestor, ref], cwd=cwd)
328 return True
329 except subprocess2.CalledProcessError:
330 return False
331
332 @staticmethod
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700333 def GetOldContents(cwd, filename, branch=None):
334 if not branch:
335 branch = GIT.GetUpstreamBranch(cwd)
Pierre-Antoine Manzagolfc1c6f42017-05-30 12:29:58 -0400336 if platform.system() == 'Windows':
337 # git show <sha>:<path> wants a posix path.
338 filename = filename.replace('\\', '/')
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700339 command = ['show', '%s:%s' % (branch, filename)]
Daniel Chengd67e7152017-04-13 01:21:03 -0700340 try:
341 return GIT.Capture(command, cwd=cwd, strip_out=False)
342 except subprocess2.CalledProcessError:
343 return ''
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700344
345 @staticmethod
Josip Sokcevic7958e302023-03-01 23:02:21 +0000346 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000347 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000348 """Diffs against the upstream branch or optionally another branch.
349
350 full_move means that move or copy operations should completely recreate the
351 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000352 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000353 branch = GIT.GetUpstreamBranch(cwd)
Josip Sokcevic7958e302023-03-01 23:02:21 +0000354 command = ['-c', 'core.quotePath=false', 'diff',
355 '-p', '--no-color', '--no-prefix', '--no-ext-diff',
356 branch + "..." + branch_head]
mcgrathr@chromium.org9249f642013-06-03 21:36:18 +0000357 if full_move:
358 command.append('--no-renames')
359 else:
maruel@chromium.orga9371762009-12-22 18:27:38 +0000360 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000361 # TODO(maruel): --binary support.
362 if files:
363 command.append('--')
364 command.extend(files)
ilevy@chromium.org4380c802013-07-12 23:38:41 +0000365 diff = GIT.Capture(command, cwd=cwd, strip_out=False).splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000366 for i in range(len(diff)):
367 # In the case of added files, replace /dev/null with the path to the
368 # file being added.
369 if diff[i].startswith('--- /dev/null'):
Josip Sokcevic7958e302023-03-01 23:02:21 +0000370 diff[i] = '--- %s' % diff[i+1][4:]
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000371 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000372
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000373 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000374 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
375 """Returns the list of modified files between two branches."""
376 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000377 branch = GIT.GetUpstreamBranch(cwd)
Josip Sokcevic7958e302023-03-01 23:02:21 +0000378 command = ['-c', 'core.quotePath=false', 'diff',
379 '--name-only', branch + "..." + branch_head]
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000380 return GIT.Capture(command, cwd=cwd).splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000381
382 @staticmethod
Edward Lemur98cfac12020-01-17 19:27:01 +0000383 def GetAllFiles(cwd):
384 """Returns the list of all files under revision control."""
Josip Sokcevic49b24e42023-08-18 20:49:31 +0000385 command = ['-c', 'core.quotePath=false', 'ls-files', '-s', '--', '.']
386 files = GIT.Capture(command, cwd=cwd).splitlines(False)
387 # return only files
Josip Sokcevic2d5c6732023-08-21 23:16:18 +0000388 return [f.split(maxsplit=3)[-1] for f in files if f.startswith('100')]
Edward Lemur98cfac12020-01-17 19:27:01 +0000389
390 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000391 def GetPatchName(cwd):
392 """Constructs a name for this patch."""
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000393 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd)
maruel@chromium.org862ff8e2010-08-06 15:29:16 +0000394 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000395
396 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000397 def GetCheckoutRoot(cwd):
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000398 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000399 """
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000400 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000401 return os.path.abspath(os.path.join(cwd, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000402
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000403 @staticmethod
nodir@chromium.orgead4c7e2014-04-03 01:01:06 +0000404 def GetGitDir(cwd):
405 return os.path.abspath(GIT.Capture(['rev-parse', '--git-dir'], cwd=cwd))
406
407 @staticmethod
408 def IsInsideWorkTree(cwd):
409 try:
410 return GIT.Capture(['rev-parse', '--is-inside-work-tree'], cwd=cwd)
411 except (OSError, subprocess2.CalledProcessError):
412 return False
413
414 @staticmethod
primiano@chromium.org1c127382015-02-17 11:15:40 +0000415 def IsDirectoryVersioned(cwd, relative_dir):
416 """Checks whether the given |relative_dir| is part of cwd's repo."""
417 return bool(GIT.Capture(['ls-tree', 'HEAD', relative_dir], cwd=cwd))
418
419 @staticmethod
420 def CleanupDir(cwd, relative_dir):
421 """Cleans up untracked file inside |relative_dir|."""
422 return bool(GIT.Capture(['clean', '-df', relative_dir], cwd=cwd))
423
424 @staticmethod
Edward Lemurd52edda2020-03-11 20:13:02 +0000425 def ResolveCommit(cwd, rev):
Edward Lesmes56dbf9a2020-03-31 22:52:54 +0000426 # We do this instead of rev-parse --verify rev^{commit}, since on Windows
427 # git can be either an executable or batch script, each of which requires
428 # escaping the caret (^) a different way.
429 if gclient_utils.IsFullGitSha(rev):
430 # git-rev parse --verify FULL_GIT_SHA always succeeds, even if we don't
431 # have FULL_GIT_SHA locally. Removing the last character forces git to
432 # check if FULL_GIT_SHA refers to an object in the local database.
433 rev = rev[:-1]
Edward Lemurd52edda2020-03-11 20:13:02 +0000434 try:
Edward Lesmes56dbf9a2020-03-31 22:52:54 +0000435 return GIT.Capture(['rev-parse', '--quiet', '--verify', rev], cwd=cwd)
Edward Lemurd52edda2020-03-11 20:13:02 +0000436 except subprocess2.CalledProcessError:
437 return None
438
439 @staticmethod
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000440 def IsValidRevision(cwd, rev, sha_only=False):
441 """Verifies the revision is a proper git revision.
442
443 sha_only: Fail unless rev is a sha hash.
444 """
Edward Lemurd52edda2020-03-11 20:13:02 +0000445 sha = GIT.ResolveCommit(cwd, rev)
446 if sha is None:
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000447 return False
Edward Lemurd52edda2020-03-11 20:13:02 +0000448 if sha_only:
449 return sha == rev.lower()
450 return True
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000451
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000452 @classmethod
453 def AssertVersion(cls, min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000454 """Asserts git's version is at least min_version."""
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000455 if cls.current_version is None:
bashi@chromium.orgfcffd482012-02-24 01:47:00 +0000456 current_version = cls.Capture(['--version'], '.')
Edward Lesmes50da7702020-03-30 19:23:43 +0000457 matched = re.search(r'git version (.+)', current_version)
458 cls.current_version = distutils.version.LooseVersion(matched.group(1))
459 min_version = distutils.version.LooseVersion(min_version)
460 return (min_version <= cls.current_version, cls.current_version)