maruel@chromium.org | 7d65467 | 2012-01-05 19:07:23 +0000 | [diff] [blame] | 1 | # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
maruel@chromium.org | 5aeb7dd | 2009-11-17 18:09:01 +0000 | [diff] [blame] | 4 | """SCM-specific utility classes.""" |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 5 | |
maruel@chromium.org | fd9cbbb | 2010-01-08 23:04:03 +0000 | [diff] [blame] | 6 | import glob |
Raul Tambre | b946b23 | 2019-03-26 14:48:46 +0000 | [diff] [blame] | 7 | import io |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 8 | import os |
Pierre-Antoine Manzagol | fc1c6f4 | 2017-05-30 12:29:58 -0400 | [diff] [blame] | 9 | import platform |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 10 | import re |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 11 | import sys |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 12 | |
| 13 | import gclient_utils |
maruel@chromium.org | 31cb48a | 2011-04-04 18:01:36 +0000 | [diff] [blame] | 14 | import subprocess2 |
| 15 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 16 | # TODO: Should fix these warnings. |
| 17 | # pylint: disable=line-too-long |
| 18 | |
Joanna Wang | 60adf7b | 2023-10-06 00:04:28 +0000 | [diff] [blame] | 19 | # constants used to identify the tree state of a directory. |
| 20 | VERSIONED_NO = 0 |
| 21 | VERSIONED_DIR = 1 |
| 22 | VERSIONED_SUBMODULE = 2 |
| 23 | |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 24 | |
maruel@chromium.org | b24a8e1 | 2009-12-22 13:45:48 +0000 | [diff] [blame] | 25 | def ValidateEmail(email): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 26 | return (re.match(r"^[a-zA-Z0-9._%\-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", |
| 27 | email) is not None) |
maruel@chromium.org | b24a8e1 | 2009-12-22 13:45:48 +0000 | [diff] [blame] | 28 | |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 29 | |
maruel@chromium.org | fd9cbbb | 2010-01-08 23:04:03 +0000 | [diff] [blame] | 30 | def GetCasedPath(path): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 31 | """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.org | fd9cbbb | 2010-01-08 23:04:03 +0000 | [diff] [blame] | 48 | |
| 49 | |
maruel@chromium.org | 3c55d98 | 2010-05-06 14:25:44 +0000 | [diff] [blame] | 50 | def GenFakeDiff(filename): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 51 | """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.org | 3c55d98 | 2010-05-06 14:25:44 +0000 | [diff] [blame] | 70 | |
| 71 | |
maruel@chromium.org | 5c8c6de | 2011-03-18 16:20:18 +0000 | [diff] [blame] | 72 | def determine_scm(root): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 73 | """Similar to upload.py's version but much simpler. |
maruel@chromium.org | 5c8c6de | 2011-03-18 16:20:18 +0000 | [diff] [blame] | 74 | |
Aaron Gable | 208db56 | 2016-12-21 14:46:36 -0800 | [diff] [blame] | 75 | Returns 'git' or None. |
maruel@chromium.org | 5c8c6de | 2011-03-18 16:20:18 +0000 | [diff] [blame] | 76 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 77 | if os.path.isdir(os.path.join(root, '.git')): |
| 78 | return 'git' |
Aravind Vasudevan | c5f0cbb | 2022-01-24 23:56:57 +0000 | [diff] [blame] | 79 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 80 | 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.org | 5c8c6de | 2011-03-18 16:20:18 +0000 | [diff] [blame] | 88 | |
| 89 | |
maruel@chromium.org | 36ac239 | 2011-10-12 16:36:11 +0000 | [diff] [blame] | 90 | def only_int(val): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 91 | if val.isdigit(): |
| 92 | return int(val) |
Aravind Vasudevan | c5f0cbb | 2022-01-24 23:56:57 +0000 | [diff] [blame] | 93 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 94 | return 0 |
maruel@chromium.org | 36ac239 | 2011-10-12 16:36:11 +0000 | [diff] [blame] | 95 | |
| 96 | |
maruel@chromium.org | 5aeb7dd | 2009-11-17 18:09:01 +0000 | [diff] [blame] | 97 | class GIT(object): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 98 | current_version = None |
maruel@chromium.org | 36ac239 | 2011-10-12 16:36:11 +0000 | [diff] [blame] | 99 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 100 | @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.org | 6d8115d | 2014-04-23 20:59:23 +0000 | [diff] [blame] | 116 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 117 | @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.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 127 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 128 | @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.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 132 | |
maruel@chromium.org | 5aeb7dd | 2009-11-17 18:09:01 +0000 | [diff] [blame] | 133 | Returns an array of (status, file) tuples.""" |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 134 | 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.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 159 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 160 | @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 Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 166 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 167 | @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 Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 172 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 173 | @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 Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 180 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 181 | @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 Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 186 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 187 | @staticmethod |
| 188 | def IsWorkTreeDirty(cwd): |
| 189 | return GIT.Capture(['status', '-s'], cwd=cwd) != '' |
nodir@chromium.org | ead4c7e | 2014-04-03 01:01:06 +0000 | [diff] [blame] | 190 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 191 | @staticmethod |
| 192 | def GetEmail(cwd): |
| 193 | """Retrieves the user email address if known.""" |
| 194 | return GIT.GetConfig(cwd, 'user.email', '') |
maruel@chromium.org | f2f9d55 | 2009-12-22 00:12:57 +0000 | [diff] [blame] | 195 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 196 | @staticmethod |
| 197 | def ShortBranchName(branch): |
| 198 | """Converts a name like 'refs/heads/foo' to just 'foo'.""" |
| 199 | return branch.replace('refs/heads/', '') |
maruel@chromium.org | f2f9d55 | 2009-12-22 00:12:57 +0000 | [diff] [blame] | 200 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 201 | @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.org | f2f9d55 | 2009-12-22 00:12:57 +0000 | [diff] [blame] | 208 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 209 | @staticmethod |
| 210 | def GetRemoteHeadRef(cwd, url, remote): |
| 211 | """Returns the full default remote branch reference, e.g. |
Josip Sokcevic | 091f5ac | 2021-01-14 23:14:21 +0000 | [diff] [blame] | 212 | 'refs/remotes/origin/main'.""" |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 213 | 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 Sokcevic | 091f5ac | 2021-01-14 23:14:21 +0000 | [diff] [blame] | 226 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 227 | 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 Sokcevic | 091f5ac | 2021-01-14 23:14:21 +0000 | [diff] [blame] | 239 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 240 | @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.org | b24a8e1 | 2009-12-22 13:45:48 +0000 | [diff] [blame] | 247 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 248 | @staticmethod |
| 249 | def GetRemoteBranches(cwd): |
| 250 | return GIT.Capture(['branch', '-r'], cwd=cwd).split() |
Edward Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 251 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 252 | @staticmethod |
| 253 | def FetchUpstreamTuple(cwd, branch=None): |
| 254 | """Returns a tuple containing remote and remote ref, |
Josip Sokcevic | 9c0dc30 | 2020-11-20 18:41:25 +0000 | [diff] [blame] | 255 | e.g. 'origin', 'refs/heads/main' |
maruel@chromium.org | f2f9d55 | 2009-12-22 00:12:57 +0000 | [diff] [blame] | 256 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 257 | 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 Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 266 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 267 | 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 Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 271 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 272 | # 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 Vasudevan | c5f0cbb | 2022-01-24 23:56:57 +0000 | [diff] [blame] | 277 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 278 | if 'origin/master' in remote_branches: |
| 279 | # Fall back on origin/master if it exits. |
| 280 | return 'origin', 'refs/heads/master' |
Edward Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 281 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 282 | return None, None |
maruel@chromium.org | f2f9d55 | 2009-12-22 00:12:57 +0000 | [diff] [blame] | 283 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 284 | @staticmethod |
| 285 | def RefToRemoteRef(ref, remote): |
| 286 | """Convert a checkout ref to the equivalent remote ref. |
mmoss@chromium.org | 6e7202b | 2014-09-09 18:23:39 +0000 | [diff] [blame] | 287 | |
| 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 Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 292 | # 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 Lemur | 9a5e3bd | 2019-04-02 23:37:45 +0000 | [diff] [blame] | 299 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 300 | 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 Lemur | 9a5e3bd | 2019-04-02 23:37:45 +0000 | [diff] [blame] | 304 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 305 | return None |
Edward Lemur | 9a5e3bd | 2019-04-02 23:37:45 +0000 | [diff] [blame] | 306 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 307 | @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.org | 6e7202b | 2014-09-09 18:23:39 +0000 | [diff] [blame] | 319 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 320 | @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.org | f2f9d55 | 2009-12-22 00:12:57 +0000 | [diff] [blame] | 329 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 330 | @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 Lemur | ca7d881 | 2018-07-24 17:42:45 +0000 | [diff] [blame] | 340 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 341 | @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 Cheng | 7a1f04d | 2017-03-21 19:12:31 -0700 | [diff] [blame] | 353 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 354 | @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.org | a937176 | 2009-12-22 18:27:38 +0000 | [diff] [blame] | 361 | |
| 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 Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 364 | 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.org | c78f246 | 2009-11-21 01:20:57 +0000 | [diff] [blame] | 385 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 386 | @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.org | 8ede00e | 2010-01-12 14:35:28 +0000 | [diff] [blame] | 396 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 397 | @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 Lemur | 98cfac1 | 2020-01-17 19:27:01 +0000 | [diff] [blame] | 404 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 405 | @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 Wang | e36c6bb | 2023-08-30 22:09:59 +0000 | [diff] [blame] | 421 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 422 | @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.org | b24a8e1 | 2009-12-22 13:45:48 +0000 | [diff] [blame] | 427 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 428 | @staticmethod |
| 429 | def GetCheckoutRoot(cwd): |
| 430 | """Returns the top level directory of a git checkout as an absolute path. |
maruel@chromium.org | b24a8e1 | 2009-12-22 13:45:48 +0000 | [diff] [blame] | 431 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 432 | root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd) |
| 433 | return os.path.abspath(os.path.join(cwd, root)) |
maruel@chromium.org | b24a8e1 | 2009-12-22 13:45:48 +0000 | [diff] [blame] | 434 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 435 | @staticmethod |
| 436 | def GetGitDir(cwd): |
| 437 | return os.path.abspath(GIT.Capture(['rev-parse', '--git-dir'], cwd=cwd)) |
nodir@chromium.org | ead4c7e | 2014-04-03 01:01:06 +0000 | [diff] [blame] | 438 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 439 | @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.org | ead4c7e | 2014-04-03 01:01:06 +0000 | [diff] [blame] | 445 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 446 | @staticmethod |
Joanna Wang | 60adf7b | 2023-10-06 00:04:28 +0000 | [diff] [blame] | 447 | def IsVersioned(cwd, relative_dir): |
| 448 | # type: (str, str) -> int |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 449 | """Checks whether the given |relative_dir| is part of cwd's repo.""" |
Joanna Wang | 60adf7b | 2023-10-06 00:04:28 +0000 | [diff] [blame] | 450 | 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.org | 1c12738 | 2015-02-17 11:15:40 +0000 | [diff] [blame] | 456 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 457 | @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.org | 1c12738 | 2015-02-17 11:15:40 +0000 | [diff] [blame] | 461 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 462 | @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 Lemur | d52edda | 2020-03-11 20:13:02 +0000 | [diff] [blame] | 478 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 479 | @staticmethod |
| 480 | def IsValidRevision(cwd, rev, sha_only=False): |
| 481 | """Verifies the revision is a proper git revision. |
ilevy@chromium.org | a41249c | 2013-07-03 00:09:12 +0000 | [diff] [blame] | 482 | |
| 483 | sha_only: Fail unless rev is a sha hash. |
| 484 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 485 | 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 |