blob: 2c98290c57a2a585187cc597687ae5cd3cd86fd0 [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.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00004"""SCM-specific utility classes."""
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00005
Edward Lesmes50da7702020-03-30 19:23:43 +00006import distutils.version
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +00007import glob
Raul Tambreb946b232019-03-26 14:48:46 +00008import io
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00009import os
Pierre-Antoine Manzagolfc1c6f42017-05-30 12:29:58 -040010import platform
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000011import re
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000012import sys
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000013
14import gclient_utils
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000015import subprocess2
16
Mike Frysinger124bb8e2023-09-06 05:48:55 +000017# TODO: Should fix these warnings.
18# pylint: disable=line-too-long
19
Joanna Wang60adf7b2023-10-06 00:04:28 +000020# constants used to identify the tree state of a directory.
21VERSIONED_NO = 0
22VERSIONED_DIR = 1
23VERSIONED_SUBMODULE = 2
24
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000025
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000026def ValidateEmail(email):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000027 return (re.match(r"^[a-zA-Z0-9._%\-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$",
28 email) is not None)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000029
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000030
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000031def GetCasedPath(path):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000032 """Elcheapos way to get the real path case on Windows."""
33 if sys.platform.startswith('win') and os.path.exists(path):
34 # Reconstruct the path.
35 path = os.path.abspath(path)
36 paths = path.split('\\')
37 for i in range(len(paths)):
38 if i == 0:
39 # Skip drive letter.
40 continue
41 subpath = '\\'.join(paths[:i + 1])
42 prev = len('\\'.join(paths[:i]))
43 # glob.glob will return the cased path for the last item only. This
44 # is why we are calling it in a loop. Extract the data we want and
45 # put it back into the list.
46 paths[i] = glob.glob(subpath + '*')[0][prev + 1:len(subpath)]
47 path = '\\'.join(paths)
48 return path
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000049
50
maruel@chromium.org3c55d982010-05-06 14:25:44 +000051def GenFakeDiff(filename):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000052 """Generates a fake diff from a file."""
53 file_content = gclient_utils.FileRead(filename, 'rb').splitlines(True)
54 filename = filename.replace(os.sep, '/')
55 nb_lines = len(file_content)
56 # We need to use / since patch on unix will fail otherwise.
57 data = io.StringIO()
58 data.write("Index: %s\n" % filename)
59 data.write('=' * 67 + '\n')
60 # Note: Should we use /dev/null instead?
61 data.write("--- %s\n" % filename)
62 data.write("+++ %s\n" % filename)
63 data.write("@@ -0,0 +1,%d @@\n" % nb_lines)
64 # Prepend '+' to every lines.
65 for line in file_content:
66 data.write('+')
67 data.write(line)
68 result = data.getvalue()
69 data.close()
70 return result
maruel@chromium.org3c55d982010-05-06 14:25:44 +000071
72
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000073def determine_scm(root):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000074 """Similar to upload.py's version but much simpler.
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000075
Aaron Gable208db562016-12-21 14:46:36 -080076 Returns 'git' or None.
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000077 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +000078 if os.path.isdir(os.path.join(root, '.git')):
79 return 'git'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000080
Mike Frysinger124bb8e2023-09-06 05:48:55 +000081 try:
82 subprocess2.check_call(['git', 'rev-parse', '--show-cdup'],
83 stdout=subprocess2.DEVNULL,
84 stderr=subprocess2.DEVNULL,
85 cwd=root)
86 return 'git'
87 except (OSError, subprocess2.CalledProcessError):
88 return None
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000089
90
maruel@chromium.org36ac2392011-10-12 16:36:11 +000091def only_int(val):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000092 if val.isdigit():
93 return int(val)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000094
Mike Frysinger124bb8e2023-09-06 05:48:55 +000095 return 0
maruel@chromium.org36ac2392011-10-12 16:36:11 +000096
97
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000098class GIT(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000099 current_version = None
maruel@chromium.org36ac2392011-10-12 16:36:11 +0000100
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000101 @staticmethod
102 def ApplyEnvVars(kwargs):
103 env = kwargs.pop('env', None) or os.environ.copy()
104 # Don't prompt for passwords; just fail quickly and noisily.
105 # By default, git will use an interactive terminal prompt when a
106 # username/ password is needed. That shouldn't happen in the chromium
107 # workflow, and if it does, then gclient may hide the prompt in the
108 # midst of a flood of terminal spew. The only indication that something
109 # has gone wrong will be when gclient hangs unresponsively. Instead, we
110 # disable the password prompt and simply allow git to fail noisily. The
111 # error message produced by git will be copied to gclient's output.
112 env.setdefault('GIT_ASKPASS', 'true')
113 env.setdefault('SSH_ASKPASS', 'true')
114 # 'cat' is a magical git string that disables pagers on all platforms.
115 env.setdefault('GIT_PAGER', 'cat')
116 return env
szager@chromium.org6d8115d2014-04-23 20:59:23 +0000117
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000118 @staticmethod
119 def Capture(args, cwd=None, strip_out=True, **kwargs):
120 env = GIT.ApplyEnvVars(kwargs)
121 output = subprocess2.check_output(['git'] + args,
122 cwd=cwd,
123 stderr=subprocess2.PIPE,
124 env=env,
125 **kwargs)
126 output = output.decode('utf-8', 'replace')
127 return output.strip() if strip_out else output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000128
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000129 @staticmethod
130 def CaptureStatus(cwd, upstream_branch, end_commit=None):
131 # type: (str, str, Optional[str]) -> Sequence[Tuple[str, str]]
132 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000133
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000134 Returns an array of (status, file) tuples."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000135 if end_commit is None:
136 end_commit = ''
137 if upstream_branch is None:
138 upstream_branch = GIT.GetUpstreamBranch(cwd)
139 if upstream_branch is None:
140 raise gclient_utils.Error('Cannot determine upstream branch')
141 command = [
142 '-c', 'core.quotePath=false', 'diff', '--name-status',
143 '--no-renames', '--ignore-submodules=all', '-r',
144 '%s...%s' % (upstream_branch, end_commit)
145 ]
146 status = GIT.Capture(command, cwd)
147 results = []
148 if status:
149 for statusline in status.splitlines():
150 # 3-way merges can cause the status can be 'MMM' instead of 'M'.
151 # This can happen when the user has 2 local branches and he
152 # diffs between these 2 branches instead diffing to upstream.
153 m = re.match(r'^(\w)+\t(.+)$', statusline)
154 if not m:
155 raise gclient_utils.Error(
156 'status currently unsupported: %s' % statusline)
157 # Only grab the first letter.
158 results.append(('%s ' % m.group(1)[0], m.group(2)))
159 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000160
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000161 @staticmethod
162 def GetConfig(cwd, key, default=None):
163 try:
164 return GIT.Capture(['config', key], cwd=cwd)
165 except subprocess2.CalledProcessError:
166 return default
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000167
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000168 @staticmethod
169 def GetBranchConfig(cwd, branch, key, default=None):
170 assert branch, 'A branch must be given'
171 key = 'branch.%s.%s' % (branch, key)
172 return GIT.GetConfig(cwd, key, default)
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000173
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000174 @staticmethod
175 def SetConfig(cwd, key, value=None):
176 if value is None:
177 args = ['config', '--unset', key]
178 else:
179 args = ['config', key, value]
180 GIT.Capture(args, cwd=cwd)
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000181
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000182 @staticmethod
183 def SetBranchConfig(cwd, branch, key, value=None):
184 assert branch, 'A branch must be given'
185 key = 'branch.%s.%s' % (branch, key)
186 GIT.SetConfig(cwd, key, value)
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000187
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000188 @staticmethod
189 def IsWorkTreeDirty(cwd):
190 return GIT.Capture(['status', '-s'], cwd=cwd) != ''
nodir@chromium.orgead4c7e2014-04-03 01:01:06 +0000191
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000192 @staticmethod
193 def GetEmail(cwd):
194 """Retrieves the user email address if known."""
195 return GIT.GetConfig(cwd, 'user.email', '')
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000196
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000197 @staticmethod
198 def ShortBranchName(branch):
199 """Converts a name like 'refs/heads/foo' to just 'foo'."""
200 return branch.replace('refs/heads/', '')
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000201
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000202 @staticmethod
203 def GetBranchRef(cwd):
204 """Returns the full branch reference, e.g. 'refs/heads/main'."""
205 try:
206 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd)
207 except subprocess2.CalledProcessError:
208 return None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000209
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000210 @staticmethod
211 def GetRemoteHeadRef(cwd, url, remote):
212 """Returns the full default remote branch reference, e.g.
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000213 'refs/remotes/origin/main'."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000214 if os.path.exists(cwd):
215 try:
216 # Try using local git copy first
217 ref = 'refs/remotes/%s/HEAD' % remote
218 ref = GIT.Capture(['symbolic-ref', ref], cwd=cwd)
219 if not ref.endswith('master'):
220 return ref
221 # Check if there are changes in the default branch for this
222 # particular repository.
223 GIT.Capture(['remote', 'set-head', '-a', remote], cwd=cwd)
224 return GIT.Capture(['symbolic-ref', ref], cwd=cwd)
225 except subprocess2.CalledProcessError:
226 pass
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000227
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000228 try:
229 # Fetch information from git server
230 resp = GIT.Capture(['ls-remote', '--symref', url, 'HEAD'])
231 regex = r'^ref: (.*)\tHEAD$'
232 for line in resp.split('\n'):
233 m = re.match(regex, line)
234 if m:
235 return ''.join(GIT.RefToRemoteRef(m.group(1), remote))
236 except subprocess2.CalledProcessError:
237 pass
238 # Return default branch
239 return 'refs/remotes/%s/main' % remote
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000240
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000241 @staticmethod
242 def GetBranch(cwd):
243 """Returns the short branch name, e.g. 'main'."""
244 branchref = GIT.GetBranchRef(cwd)
245 if branchref:
246 return GIT.ShortBranchName(branchref)
247 return None
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000248
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000249 @staticmethod
250 def GetRemoteBranches(cwd):
251 return GIT.Capture(['branch', '-r'], cwd=cwd).split()
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000252
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000253 @staticmethod
254 def FetchUpstreamTuple(cwd, branch=None):
255 """Returns a tuple containing remote and remote ref,
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000256 e.g. 'origin', 'refs/heads/main'
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000257 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000258 try:
259 branch = branch or GIT.GetBranch(cwd)
260 except subprocess2.CalledProcessError:
261 pass
262 if branch:
263 upstream_branch = GIT.GetBranchConfig(cwd, branch, 'merge')
264 if upstream_branch:
265 remote = GIT.GetBranchConfig(cwd, branch, 'remote', '.')
266 return remote, upstream_branch
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000267
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000268 upstream_branch = GIT.GetConfig(cwd, 'rietveld.upstream-branch')
269 if upstream_branch:
270 remote = GIT.GetConfig(cwd, 'rietveld.upstream-remote', '.')
271 return remote, upstream_branch
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000272
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000273 # Else, try to guess the origin remote.
274 remote_branches = GIT.GetRemoteBranches(cwd)
275 if 'origin/main' in remote_branches:
276 # Fall back on origin/main if it exits.
277 return 'origin', 'refs/heads/main'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000278
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000279 if 'origin/master' in remote_branches:
280 # Fall back on origin/master if it exits.
281 return 'origin', 'refs/heads/master'
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000282
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000283 return None, None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000284
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000285 @staticmethod
286 def RefToRemoteRef(ref, remote):
287 """Convert a checkout ref to the equivalent remote ref.
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000288
289 Returns:
290 A tuple of the remote ref's (common prefix, unique suffix), or None if it
291 doesn't appear to refer to a remote ref (e.g. it's a commit hash).
292 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000293 # TODO(mmoss): This is just a brute-force mapping based of the expected
294 # git config. It's a bit better than the even more brute-force
295 # replace('heads', ...), but could still be smarter (like maybe actually
296 # using values gleaned from the git config).
297 m = re.match('^(refs/(remotes/)?)?branch-heads/', ref or '')
298 if m:
299 return ('refs/remotes/branch-heads/', ref.replace(m.group(0), ''))
Edward Lemur9a5e3bd2019-04-02 23:37:45 +0000300
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000301 m = re.match('^((refs/)?remotes/)?%s/|(refs/)?heads/' % remote, ref
302 or '')
303 if m:
304 return ('refs/remotes/%s/' % remote, ref.replace(m.group(0), ''))
Edward Lemur9a5e3bd2019-04-02 23:37:45 +0000305
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000306 return None
Edward Lemur9a5e3bd2019-04-02 23:37:45 +0000307
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000308 @staticmethod
309 def RemoteRefToRef(ref, remote):
310 assert remote, 'A remote must be given'
311 if not ref or not ref.startswith('refs/'):
312 return None
313 if not ref.startswith('refs/remotes/'):
314 return ref
315 if ref.startswith('refs/remotes/branch-heads/'):
316 return 'refs' + ref[len('refs/remotes'):]
317 if ref.startswith('refs/remotes/%s/' % remote):
318 return 'refs/heads' + ref[len('refs/remotes/%s' % remote):]
319 return None
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000320
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000321 @staticmethod
322 def GetUpstreamBranch(cwd):
323 """Gets the current branch's upstream branch."""
324 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
325 if remote != '.' and upstream_branch:
326 remote_ref = GIT.RefToRemoteRef(upstream_branch, remote)
327 if remote_ref:
328 upstream_branch = ''.join(remote_ref)
329 return upstream_branch
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000330
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000331 @staticmethod
332 def IsAncestor(maybe_ancestor, ref, cwd=None):
333 # type: (string, string, Optional[string]) -> bool
334 """Verifies if |maybe_ancestor| is an ancestor of |ref|."""
335 try:
336 GIT.Capture(['merge-base', '--is-ancestor', maybe_ancestor, ref],
337 cwd=cwd)
338 return True
339 except subprocess2.CalledProcessError:
340 return False
Edward Lemurca7d8812018-07-24 17:42:45 +0000341
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000342 @staticmethod
343 def GetOldContents(cwd, filename, branch=None):
344 if not branch:
345 branch = GIT.GetUpstreamBranch(cwd)
346 if platform.system() == 'Windows':
347 # git show <sha>:<path> wants a posix path.
348 filename = filename.replace('\\', '/')
349 command = ['show', '%s:%s' % (branch, filename)]
350 try:
351 return GIT.Capture(command, cwd=cwd, strip_out=False)
352 except subprocess2.CalledProcessError:
353 return ''
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700354
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000355 @staticmethod
356 def GenerateDiff(cwd,
357 branch=None,
358 branch_head='HEAD',
359 full_move=False,
360 files=None):
361 """Diffs against the upstream branch or optionally another branch.
maruel@chromium.orga9371762009-12-22 18:27:38 +0000362
363 full_move means that move or copy operations should completely recreate the
364 files, usually in the prospect to apply the patch for a try job."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000365 if not branch:
366 branch = GIT.GetUpstreamBranch(cwd)
367 command = [
368 '-c', 'core.quotePath=false', 'diff', '-p', '--no-color',
369 '--no-prefix', '--no-ext-diff', branch + "..." + branch_head
370 ]
371 if full_move:
372 command.append('--no-renames')
373 else:
374 command.append('-C')
375 # TODO(maruel): --binary support.
376 if files:
377 command.append('--')
378 command.extend(files)
379 diff = GIT.Capture(command, cwd=cwd, strip_out=False).splitlines(True)
380 for i in range(len(diff)):
381 # In the case of added files, replace /dev/null with the path to the
382 # file being added.
383 if diff[i].startswith('--- /dev/null'):
384 diff[i] = '--- %s' % diff[i + 1][4:]
385 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000386
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000387 @staticmethod
388 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
389 """Returns the list of modified files between two branches."""
390 if not branch:
391 branch = GIT.GetUpstreamBranch(cwd)
392 command = [
393 '-c', 'core.quotePath=false', 'diff', '--name-only',
394 branch + "..." + branch_head
395 ]
396 return GIT.Capture(command, cwd=cwd).splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000397
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000398 @staticmethod
399 def GetAllFiles(cwd):
400 """Returns the list of all files under revision control."""
401 command = ['-c', 'core.quotePath=false', 'ls-files', '-s', '--', '.']
402 files = GIT.Capture(command, cwd=cwd).splitlines(False)
403 # return only files
404 return [f.split(maxsplit=3)[-1] for f in files if f.startswith('100')]
Edward Lemur98cfac12020-01-17 19:27:01 +0000405
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000406 @staticmethod
407 def GetSubmoduleCommits(cwd, submodules):
408 # type: (string, List[string]) => Mapping[string][string]
409 """Returns a mapping of staged or committed new commits for submodules."""
410 if not submodules:
411 return {}
412 result = subprocess2.check_output(['git', 'ls-files', '-s', '--'] +
413 submodules,
414 cwd=cwd).decode('utf-8')
415 commit_hashes = {}
416 for r in result.splitlines():
417 # ['<mode>', '<commit_hash>', '<stage_number>', '<path>'].
418 record = r.strip().split(maxsplit=3) # path can contain spaces.
419 assert record[0] == '160000', 'file is not a gitlink: %s' % record
420 commit_hashes[record[3]] = record[1]
421 return commit_hashes
Joanna Wange36c6bb2023-08-30 22:09:59 +0000422
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000423 @staticmethod
424 def GetPatchName(cwd):
425 """Constructs a name for this patch."""
426 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd)
427 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000428
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000429 @staticmethod
430 def GetCheckoutRoot(cwd):
431 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000432 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000433 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd)
434 return os.path.abspath(os.path.join(cwd, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000435
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000436 @staticmethod
437 def GetGitDir(cwd):
438 return os.path.abspath(GIT.Capture(['rev-parse', '--git-dir'], cwd=cwd))
nodir@chromium.orgead4c7e2014-04-03 01:01:06 +0000439
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000440 @staticmethod
441 def IsInsideWorkTree(cwd):
442 try:
443 return GIT.Capture(['rev-parse', '--is-inside-work-tree'], cwd=cwd)
444 except (OSError, subprocess2.CalledProcessError):
445 return False
nodir@chromium.orgead4c7e2014-04-03 01:01:06 +0000446
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000447 @staticmethod
Joanna Wang60adf7b2023-10-06 00:04:28 +0000448 def IsVersioned(cwd, relative_dir):
449 # type: (str, str) -> int
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000450 """Checks whether the given |relative_dir| is part of cwd's repo."""
Joanna Wang60adf7b2023-10-06 00:04:28 +0000451 output = GIT.Capture(['ls-tree', 'HEAD', '--', relative_dir], cwd=cwd)
452 if not output:
453 return VERSIONED_NO
454 if output.startswith('160000'):
455 return VERSIONED_SUBMODULE
456 return VERSIONED_DIR
primiano@chromium.org1c127382015-02-17 11:15:40 +0000457
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000458 @staticmethod
459 def CleanupDir(cwd, relative_dir):
460 """Cleans up untracked file inside |relative_dir|."""
461 return bool(GIT.Capture(['clean', '-df', relative_dir], cwd=cwd))
primiano@chromium.org1c127382015-02-17 11:15:40 +0000462
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000463 @staticmethod
464 def ResolveCommit(cwd, rev):
465 # We do this instead of rev-parse --verify rev^{commit}, since on
466 # Windows git can be either an executable or batch script, each of which
467 # requires escaping the caret (^) a different way.
468 if gclient_utils.IsFullGitSha(rev):
469 # git-rev parse --verify FULL_GIT_SHA always succeeds, even if we
470 # don't have FULL_GIT_SHA locally. Removing the last character
471 # forces git to check if FULL_GIT_SHA refers to an object in the
472 # local database.
473 rev = rev[:-1]
474 try:
475 return GIT.Capture(['rev-parse', '--quiet', '--verify', rev],
476 cwd=cwd)
477 except subprocess2.CalledProcessError:
478 return None
Edward Lemurd52edda2020-03-11 20:13:02 +0000479
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000480 @staticmethod
481 def IsValidRevision(cwd, rev, sha_only=False):
482 """Verifies the revision is a proper git revision.
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000483
484 sha_only: Fail unless rev is a sha hash.
485 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000486 sha = GIT.ResolveCommit(cwd, rev)
487 if sha is None:
488 return False
489 if sha_only:
490 return sha == rev.lower()
491 return True
dbeam@chromium.orge5d1e612011-12-19 19:49:19 +0000492
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000493 @classmethod
494 def AssertVersion(cls, min_version):
495 """Asserts git's version is at least min_version."""
496 if cls.current_version is None:
497 current_version = cls.Capture(['--version'], '.')
498 matched = re.search(r'git version (.+)', current_version)
499 cls.current_version = distutils.version.LooseVersion(
500 matched.group(1))
501 min_version = distutils.version.LooseVersion(min_version)
502 return (min_version <= cls.current_version, cls.current_version)