blob: 5dc9583ad4c52bff4b34cd5eeee0bb1229aa1eb7 [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
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +00006import glob
Raul Tambreb946b232019-03-26 14:48:46 +00007import io
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00008import os
Pierre-Antoine Manzagolfc1c6f42017-05-30 12:29:58 -04009import platform
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000010import re
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000011import sys
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000012
13import gclient_utils
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000014import subprocess2
15
Mike Frysinger124bb8e2023-09-06 05:48:55 +000016# TODO: Should fix these warnings.
17# pylint: disable=line-too-long
18
Joanna Wang60adf7b2023-10-06 00:04:28 +000019# constants used to identify the tree state of a directory.
20VERSIONED_NO = 0
21VERSIONED_DIR = 1
22VERSIONED_SUBMODULE = 2
23
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000024
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000025def ValidateEmail(email):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000026 return (re.match(r"^[a-zA-Z0-9._%\-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$",
27 email) is not None)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000028
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000029
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000030def GetCasedPath(path):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000031 """Elcheapos way to get the real path case on Windows."""
32 if sys.platform.startswith('win') and os.path.exists(path):
33 # Reconstruct the path.
34 path = os.path.abspath(path)
35 paths = path.split('\\')
36 for i in range(len(paths)):
37 if i == 0:
38 # Skip drive letter.
39 continue
40 subpath = '\\'.join(paths[:i + 1])
41 prev = len('\\'.join(paths[:i]))
42 # glob.glob will return the cased path for the last item only. This
43 # is why we are calling it in a loop. Extract the data we want and
44 # put it back into the list.
45 paths[i] = glob.glob(subpath + '*')[0][prev + 1:len(subpath)]
46 path = '\\'.join(paths)
47 return path
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000048
49
maruel@chromium.org3c55d982010-05-06 14:25:44 +000050def GenFakeDiff(filename):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000051 """Generates a fake diff from a file."""
52 file_content = gclient_utils.FileRead(filename, 'rb').splitlines(True)
53 filename = filename.replace(os.sep, '/')
54 nb_lines = len(file_content)
55 # We need to use / since patch on unix will fail otherwise.
56 data = io.StringIO()
57 data.write("Index: %s\n" % filename)
58 data.write('=' * 67 + '\n')
59 # Note: Should we use /dev/null instead?
60 data.write("--- %s\n" % filename)
61 data.write("+++ %s\n" % filename)
62 data.write("@@ -0,0 +1,%d @@\n" % nb_lines)
63 # Prepend '+' to every lines.
64 for line in file_content:
65 data.write('+')
66 data.write(line)
67 result = data.getvalue()
68 data.close()
69 return result
maruel@chromium.org3c55d982010-05-06 14:25:44 +000070
71
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000072def determine_scm(root):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000073 """Similar to upload.py's version but much simpler.
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000074
Aaron Gable208db562016-12-21 14:46:36 -080075 Returns 'git' or None.
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000076 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +000077 if os.path.isdir(os.path.join(root, '.git')):
78 return 'git'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000079
Mike Frysinger124bb8e2023-09-06 05:48:55 +000080 try:
81 subprocess2.check_call(['git', 'rev-parse', '--show-cdup'],
82 stdout=subprocess2.DEVNULL,
83 stderr=subprocess2.DEVNULL,
84 cwd=root)
85 return 'git'
86 except (OSError, subprocess2.CalledProcessError):
87 return None
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000088
89
maruel@chromium.org36ac2392011-10-12 16:36:11 +000090def only_int(val):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000091 if val.isdigit():
92 return int(val)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +000093
Mike Frysinger124bb8e2023-09-06 05:48:55 +000094 return 0
maruel@chromium.org36ac2392011-10-12 16:36:11 +000095
96
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000097class GIT(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000098 current_version = None
maruel@chromium.org36ac2392011-10-12 16:36:11 +000099
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000100 @staticmethod
101 def ApplyEnvVars(kwargs):
102 env = kwargs.pop('env', None) or os.environ.copy()
103 # Don't prompt for passwords; just fail quickly and noisily.
104 # By default, git will use an interactive terminal prompt when a
105 # username/ password is needed. That shouldn't happen in the chromium
106 # workflow, and if it does, then gclient may hide the prompt in the
107 # midst of a flood of terminal spew. The only indication that something
108 # has gone wrong will be when gclient hangs unresponsively. Instead, we
109 # disable the password prompt and simply allow git to fail noisily. The
110 # error message produced by git will be copied to gclient's output.
111 env.setdefault('GIT_ASKPASS', 'true')
112 env.setdefault('SSH_ASKPASS', 'true')
113 # 'cat' is a magical git string that disables pagers on all platforms.
114 env.setdefault('GIT_PAGER', 'cat')
115 return env
szager@chromium.org6d8115d2014-04-23 20:59:23 +0000116
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000117 @staticmethod
118 def Capture(args, cwd=None, strip_out=True, **kwargs):
119 env = GIT.ApplyEnvVars(kwargs)
120 output = subprocess2.check_output(['git'] + args,
121 cwd=cwd,
122 stderr=subprocess2.PIPE,
123 env=env,
124 **kwargs)
125 output = output.decode('utf-8', 'replace')
126 return output.strip() if strip_out else output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000127
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000128 @staticmethod
129 def CaptureStatus(cwd, upstream_branch, end_commit=None):
130 # type: (str, str, Optional[str]) -> Sequence[Tuple[str, str]]
131 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000132
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000133 Returns an array of (status, file) tuples."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000134 if end_commit is None:
135 end_commit = ''
136 if upstream_branch is None:
137 upstream_branch = GIT.GetUpstreamBranch(cwd)
138 if upstream_branch is None:
139 raise gclient_utils.Error('Cannot determine upstream branch')
140 command = [
141 '-c', 'core.quotePath=false', 'diff', '--name-status',
142 '--no-renames', '--ignore-submodules=all', '-r',
143 '%s...%s' % (upstream_branch, end_commit)
144 ]
145 status = GIT.Capture(command, cwd)
146 results = []
147 if status:
148 for statusline in status.splitlines():
149 # 3-way merges can cause the status can be 'MMM' instead of 'M'.
150 # This can happen when the user has 2 local branches and he
151 # diffs between these 2 branches instead diffing to upstream.
152 m = re.match(r'^(\w)+\t(.+)$', statusline)
153 if not m:
154 raise gclient_utils.Error(
155 'status currently unsupported: %s' % statusline)
156 # Only grab the first letter.
157 results.append(('%s ' % m.group(1)[0], m.group(2)))
158 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000159
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000160 @staticmethod
161 def GetConfig(cwd, key, default=None):
162 try:
163 return GIT.Capture(['config', key], cwd=cwd)
164 except subprocess2.CalledProcessError:
165 return default
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000166
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000167 @staticmethod
168 def GetBranchConfig(cwd, branch, key, default=None):
169 assert branch, 'A branch must be given'
170 key = 'branch.%s.%s' % (branch, key)
171 return GIT.GetConfig(cwd, key, default)
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000172
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000173 @staticmethod
174 def SetConfig(cwd, key, value=None):
175 if value is None:
176 args = ['config', '--unset', key]
177 else:
178 args = ['config', key, value]
179 GIT.Capture(args, cwd=cwd)
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000180
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000181 @staticmethod
182 def SetBranchConfig(cwd, branch, key, value=None):
183 assert branch, 'A branch must be given'
184 key = 'branch.%s.%s' % (branch, key)
185 GIT.SetConfig(cwd, key, value)
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000186
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000187 @staticmethod
188 def IsWorkTreeDirty(cwd):
189 return GIT.Capture(['status', '-s'], cwd=cwd) != ''
nodir@chromium.orgead4c7e2014-04-03 01:01:06 +0000190
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000191 @staticmethod
192 def GetEmail(cwd):
193 """Retrieves the user email address if known."""
194 return GIT.GetConfig(cwd, 'user.email', '')
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000195
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000196 @staticmethod
197 def ShortBranchName(branch):
198 """Converts a name like 'refs/heads/foo' to just 'foo'."""
199 return branch.replace('refs/heads/', '')
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000200
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000201 @staticmethod
202 def GetBranchRef(cwd):
203 """Returns the full branch reference, e.g. 'refs/heads/main'."""
204 try:
205 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd)
206 except subprocess2.CalledProcessError:
207 return None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000208
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000209 @staticmethod
210 def GetRemoteHeadRef(cwd, url, remote):
211 """Returns the full default remote branch reference, e.g.
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000212 'refs/remotes/origin/main'."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000213 if os.path.exists(cwd):
214 try:
215 # Try using local git copy first
216 ref = 'refs/remotes/%s/HEAD' % remote
217 ref = GIT.Capture(['symbolic-ref', ref], cwd=cwd)
218 if not ref.endswith('master'):
219 return ref
220 # Check if there are changes in the default branch for this
221 # particular repository.
222 GIT.Capture(['remote', 'set-head', '-a', remote], cwd=cwd)
223 return GIT.Capture(['symbolic-ref', ref], cwd=cwd)
224 except subprocess2.CalledProcessError:
225 pass
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000226
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000227 try:
228 # Fetch information from git server
229 resp = GIT.Capture(['ls-remote', '--symref', url, 'HEAD'])
230 regex = r'^ref: (.*)\tHEAD$'
231 for line in resp.split('\n'):
232 m = re.match(regex, line)
233 if m:
234 return ''.join(GIT.RefToRemoteRef(m.group(1), remote))
235 except subprocess2.CalledProcessError:
236 pass
237 # Return default branch
238 return 'refs/remotes/%s/main' % remote
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000239
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000240 @staticmethod
241 def GetBranch(cwd):
242 """Returns the short branch name, e.g. 'main'."""
243 branchref = GIT.GetBranchRef(cwd)
244 if branchref:
245 return GIT.ShortBranchName(branchref)
246 return None
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000247
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000248 @staticmethod
249 def GetRemoteBranches(cwd):
250 return GIT.Capture(['branch', '-r'], cwd=cwd).split()
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000251
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000252 @staticmethod
253 def FetchUpstreamTuple(cwd, branch=None):
254 """Returns a tuple containing remote and remote ref,
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000255 e.g. 'origin', 'refs/heads/main'
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000256 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000257 try:
258 branch = branch or GIT.GetBranch(cwd)
259 except subprocess2.CalledProcessError:
260 pass
261 if branch:
262 upstream_branch = GIT.GetBranchConfig(cwd, branch, 'merge')
263 if upstream_branch:
264 remote = GIT.GetBranchConfig(cwd, branch, 'remote', '.')
265 return remote, upstream_branch
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000266
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000267 upstream_branch = GIT.GetConfig(cwd, 'rietveld.upstream-branch')
268 if upstream_branch:
269 remote = GIT.GetConfig(cwd, 'rietveld.upstream-remote', '.')
270 return remote, upstream_branch
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000271
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000272 # Else, try to guess the origin remote.
273 remote_branches = GIT.GetRemoteBranches(cwd)
274 if 'origin/main' in remote_branches:
275 # Fall back on origin/main if it exits.
276 return 'origin', 'refs/heads/main'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000277
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000278 if 'origin/master' in remote_branches:
279 # Fall back on origin/master if it exits.
280 return 'origin', 'refs/heads/master'
Edward Lemur15a9b8c2020-02-13 00:52:30 +0000281
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000282 return None, None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000283
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000284 @staticmethod
285 def RefToRemoteRef(ref, remote):
286 """Convert a checkout ref to the equivalent remote ref.
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000287
288 Returns:
289 A tuple of the remote ref's (common prefix, unique suffix), or None if it
290 doesn't appear to refer to a remote ref (e.g. it's a commit hash).
291 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000292 # TODO(mmoss): This is just a brute-force mapping based of the expected
293 # git config. It's a bit better than the even more brute-force
294 # replace('heads', ...), but could still be smarter (like maybe actually
295 # using values gleaned from the git config).
296 m = re.match('^(refs/(remotes/)?)?branch-heads/', ref or '')
297 if m:
298 return ('refs/remotes/branch-heads/', ref.replace(m.group(0), ''))
Edward Lemur9a5e3bd2019-04-02 23:37:45 +0000299
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000300 m = re.match('^((refs/)?remotes/)?%s/|(refs/)?heads/' % remote, ref
301 or '')
302 if m:
303 return ('refs/remotes/%s/' % remote, ref.replace(m.group(0), ''))
Edward Lemur9a5e3bd2019-04-02 23:37:45 +0000304
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000305 return None
Edward Lemur9a5e3bd2019-04-02 23:37:45 +0000306
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000307 @staticmethod
308 def RemoteRefToRef(ref, remote):
309 assert remote, 'A remote must be given'
310 if not ref or not ref.startswith('refs/'):
311 return None
312 if not ref.startswith('refs/remotes/'):
313 return ref
314 if ref.startswith('refs/remotes/branch-heads/'):
315 return 'refs' + ref[len('refs/remotes'):]
316 if ref.startswith('refs/remotes/%s/' % remote):
317 return 'refs/heads' + ref[len('refs/remotes/%s' % remote):]
318 return None
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000319
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000320 @staticmethod
321 def GetUpstreamBranch(cwd):
322 """Gets the current branch's upstream branch."""
323 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
324 if remote != '.' and upstream_branch:
325 remote_ref = GIT.RefToRemoteRef(upstream_branch, remote)
326 if remote_ref:
327 upstream_branch = ''.join(remote_ref)
328 return upstream_branch
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000329
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000330 @staticmethod
331 def IsAncestor(maybe_ancestor, ref, cwd=None):
332 # type: (string, string, Optional[string]) -> bool
333 """Verifies if |maybe_ancestor| is an ancestor of |ref|."""
334 try:
335 GIT.Capture(['merge-base', '--is-ancestor', maybe_ancestor, ref],
336 cwd=cwd)
337 return True
338 except subprocess2.CalledProcessError:
339 return False
Edward Lemurca7d8812018-07-24 17:42:45 +0000340
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000341 @staticmethod
342 def GetOldContents(cwd, filename, branch=None):
343 if not branch:
344 branch = GIT.GetUpstreamBranch(cwd)
345 if platform.system() == 'Windows':
346 # git show <sha>:<path> wants a posix path.
347 filename = filename.replace('\\', '/')
348 command = ['show', '%s:%s' % (branch, filename)]
349 try:
350 return GIT.Capture(command, cwd=cwd, strip_out=False)
351 except subprocess2.CalledProcessError:
352 return ''
Daniel Cheng7a1f04d2017-03-21 19:12:31 -0700353
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000354 @staticmethod
355 def GenerateDiff(cwd,
356 branch=None,
357 branch_head='HEAD',
358 full_move=False,
359 files=None):
360 """Diffs against the upstream branch or optionally another branch.
maruel@chromium.orga9371762009-12-22 18:27:38 +0000361
362 full_move means that move or copy operations should completely recreate the
363 files, usually in the prospect to apply the patch for a try job."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000364 if not branch:
365 branch = GIT.GetUpstreamBranch(cwd)
366 command = [
367 '-c', 'core.quotePath=false', 'diff', '-p', '--no-color',
368 '--no-prefix', '--no-ext-diff', branch + "..." + branch_head
369 ]
370 if full_move:
371 command.append('--no-renames')
372 else:
373 command.append('-C')
374 # TODO(maruel): --binary support.
375 if files:
376 command.append('--')
377 command.extend(files)
378 diff = GIT.Capture(command, cwd=cwd, strip_out=False).splitlines(True)
379 for i in range(len(diff)):
380 # In the case of added files, replace /dev/null with the path to the
381 # file being added.
382 if diff[i].startswith('--- /dev/null'):
383 diff[i] = '--- %s' % diff[i + 1][4:]
384 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000385
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000386 @staticmethod
387 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
388 """Returns the list of modified files between two branches."""
389 if not branch:
390 branch = GIT.GetUpstreamBranch(cwd)
391 command = [
392 '-c', 'core.quotePath=false', 'diff', '--name-only',
393 branch + "..." + branch_head
394 ]
395 return GIT.Capture(command, cwd=cwd).splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000396
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000397 @staticmethod
398 def GetAllFiles(cwd):
399 """Returns the list of all files under revision control."""
400 command = ['-c', 'core.quotePath=false', 'ls-files', '-s', '--', '.']
401 files = GIT.Capture(command, cwd=cwd).splitlines(False)
402 # return only files
403 return [f.split(maxsplit=3)[-1] for f in files if f.startswith('100')]
Edward Lemur98cfac12020-01-17 19:27:01 +0000404
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000405 @staticmethod
406 def GetSubmoduleCommits(cwd, submodules):
407 # type: (string, List[string]) => Mapping[string][string]
408 """Returns a mapping of staged or committed new commits for submodules."""
409 if not submodules:
410 return {}
411 result = subprocess2.check_output(['git', 'ls-files', '-s', '--'] +
412 submodules,
413 cwd=cwd).decode('utf-8')
414 commit_hashes = {}
415 for r in result.splitlines():
416 # ['<mode>', '<commit_hash>', '<stage_number>', '<path>'].
417 record = r.strip().split(maxsplit=3) # path can contain spaces.
418 assert record[0] == '160000', 'file is not a gitlink: %s' % record
419 commit_hashes[record[3]] = record[1]
420 return commit_hashes
Joanna Wange36c6bb2023-08-30 22:09:59 +0000421
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000422 @staticmethod
423 def GetPatchName(cwd):
424 """Constructs a name for this patch."""
425 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd)
426 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000427
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000428 @staticmethod
429 def GetCheckoutRoot(cwd):
430 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000431 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000432 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd)
433 return os.path.abspath(os.path.join(cwd, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000434
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000435 @staticmethod
436 def GetGitDir(cwd):
437 return os.path.abspath(GIT.Capture(['rev-parse', '--git-dir'], cwd=cwd))
nodir@chromium.orgead4c7e2014-04-03 01:01:06 +0000438
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000439 @staticmethod
440 def IsInsideWorkTree(cwd):
441 try:
442 return GIT.Capture(['rev-parse', '--is-inside-work-tree'], cwd=cwd)
443 except (OSError, subprocess2.CalledProcessError):
444 return False
nodir@chromium.orgead4c7e2014-04-03 01:01:06 +0000445
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000446 @staticmethod
Joanna Wang60adf7b2023-10-06 00:04:28 +0000447 def IsVersioned(cwd, relative_dir):
448 # type: (str, str) -> int
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000449 """Checks whether the given |relative_dir| is part of cwd's repo."""
Joanna Wang60adf7b2023-10-06 00:04:28 +0000450 output = GIT.Capture(['ls-tree', 'HEAD', '--', relative_dir], cwd=cwd)
451 if not output:
452 return VERSIONED_NO
453 if output.startswith('160000'):
454 return VERSIONED_SUBMODULE
455 return VERSIONED_DIR
primiano@chromium.org1c127382015-02-17 11:15:40 +0000456
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000457 @staticmethod
458 def CleanupDir(cwd, relative_dir):
459 """Cleans up untracked file inside |relative_dir|."""
460 return bool(GIT.Capture(['clean', '-df', relative_dir], cwd=cwd))
primiano@chromium.org1c127382015-02-17 11:15:40 +0000461
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000462 @staticmethod
463 def ResolveCommit(cwd, rev):
464 # We do this instead of rev-parse --verify rev^{commit}, since on
465 # Windows git can be either an executable or batch script, each of which
466 # requires escaping the caret (^) a different way.
467 if gclient_utils.IsFullGitSha(rev):
468 # git-rev parse --verify FULL_GIT_SHA always succeeds, even if we
469 # don't have FULL_GIT_SHA locally. Removing the last character
470 # forces git to check if FULL_GIT_SHA refers to an object in the
471 # local database.
472 rev = rev[:-1]
473 try:
474 return GIT.Capture(['rev-parse', '--quiet', '--verify', rev],
475 cwd=cwd)
476 except subprocess2.CalledProcessError:
477 return None
Edward Lemurd52edda2020-03-11 20:13:02 +0000478
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000479 @staticmethod
480 def IsValidRevision(cwd, rev, sha_only=False):
481 """Verifies the revision is a proper git revision.
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000482
483 sha_only: Fail unless rev is a sha hash.
484 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000485 sha = GIT.ResolveCommit(cwd, rev)
486 if sha is None:
487 return False
488 if sha_only:
489 return sha == rev.lower()
490 return True