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 | |
Edward Lesmes | 50da770 | 2020-03-30 19:23:43 +0000 | [diff] [blame] | 6 | import distutils.version |
maruel@chromium.org | fd9cbbb | 2010-01-08 23:04:03 +0000 | [diff] [blame] | 7 | import glob |
Raul Tambre | b946b23 | 2019-03-26 14:48:46 +0000 | [diff] [blame] | 8 | import io |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 9 | import os |
Pierre-Antoine Manzagol | fc1c6f4 | 2017-05-30 12:29:58 -0400 | [diff] [blame] | 10 | import platform |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 11 | import re |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 12 | import sys |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 13 | |
| 14 | import gclient_utils |
maruel@chromium.org | 31cb48a | 2011-04-04 18:01:36 +0000 | [diff] [blame] | 15 | import subprocess2 |
| 16 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 17 | # TODO: Should fix these warnings. |
| 18 | # pylint: disable=line-too-long |
| 19 | |
Joanna Wang | 60adf7b | 2023-10-06 00:04:28 +0000 | [diff] [blame^] | 20 | # constants used to identify the tree state of a directory. |
| 21 | VERSIONED_NO = 0 |
| 22 | VERSIONED_DIR = 1 |
| 23 | VERSIONED_SUBMODULE = 2 |
| 24 | |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 25 | |
maruel@chromium.org | b24a8e1 | 2009-12-22 13:45:48 +0000 | [diff] [blame] | 26 | def ValidateEmail(email): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 27 | return (re.match(r"^[a-zA-Z0-9._%\-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", |
| 28 | email) is not None) |
maruel@chromium.org | b24a8e1 | 2009-12-22 13:45:48 +0000 | [diff] [blame] | 29 | |
maruel@chromium.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 30 | |
maruel@chromium.org | fd9cbbb | 2010-01-08 23:04:03 +0000 | [diff] [blame] | 31 | def GetCasedPath(path): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 32 | """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.org | fd9cbbb | 2010-01-08 23:04:03 +0000 | [diff] [blame] | 49 | |
| 50 | |
maruel@chromium.org | 3c55d98 | 2010-05-06 14:25:44 +0000 | [diff] [blame] | 51 | def GenFakeDiff(filename): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 52 | """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.org | 3c55d98 | 2010-05-06 14:25:44 +0000 | [diff] [blame] | 71 | |
| 72 | |
maruel@chromium.org | 5c8c6de | 2011-03-18 16:20:18 +0000 | [diff] [blame] | 73 | def determine_scm(root): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 74 | """Similar to upload.py's version but much simpler. |
maruel@chromium.org | 5c8c6de | 2011-03-18 16:20:18 +0000 | [diff] [blame] | 75 | |
Aaron Gable | 208db56 | 2016-12-21 14:46:36 -0800 | [diff] [blame] | 76 | Returns 'git' or None. |
maruel@chromium.org | 5c8c6de | 2011-03-18 16:20:18 +0000 | [diff] [blame] | 77 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 78 | if os.path.isdir(os.path.join(root, '.git')): |
| 79 | return 'git' |
Aravind Vasudevan | c5f0cbb | 2022-01-24 23:56:57 +0000 | [diff] [blame] | 80 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 81 | 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.org | 5c8c6de | 2011-03-18 16:20:18 +0000 | [diff] [blame] | 89 | |
| 90 | |
maruel@chromium.org | 36ac239 | 2011-10-12 16:36:11 +0000 | [diff] [blame] | 91 | def only_int(val): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 92 | if val.isdigit(): |
| 93 | return int(val) |
Aravind Vasudevan | c5f0cbb | 2022-01-24 23:56:57 +0000 | [diff] [blame] | 94 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 95 | return 0 |
maruel@chromium.org | 36ac239 | 2011-10-12 16:36:11 +0000 | [diff] [blame] | 96 | |
| 97 | |
maruel@chromium.org | 5aeb7dd | 2009-11-17 18:09:01 +0000 | [diff] [blame] | 98 | class GIT(object): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 99 | current_version = None |
maruel@chromium.org | 36ac239 | 2011-10-12 16:36:11 +0000 | [diff] [blame] | 100 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 101 | @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.org | 6d8115d | 2014-04-23 20:59:23 +0000 | [diff] [blame] | 117 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 118 | @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.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 128 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 129 | @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.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 133 | |
maruel@chromium.org | 5aeb7dd | 2009-11-17 18:09:01 +0000 | [diff] [blame] | 134 | Returns an array of (status, file) tuples.""" |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 135 | 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.org | d5800f1 | 2009-11-12 20:03:43 +0000 | [diff] [blame] | 160 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 161 | @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 Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 167 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 168 | @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 Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 173 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 174 | @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 Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 181 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 182 | @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 Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 187 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 188 | @staticmethod |
| 189 | def IsWorkTreeDirty(cwd): |
| 190 | return GIT.Capture(['status', '-s'], cwd=cwd) != '' |
nodir@chromium.org | ead4c7e | 2014-04-03 01:01:06 +0000 | [diff] [blame] | 191 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 192 | @staticmethod |
| 193 | def GetEmail(cwd): |
| 194 | """Retrieves the user email address if known.""" |
| 195 | return GIT.GetConfig(cwd, 'user.email', '') |
maruel@chromium.org | f2f9d55 | 2009-12-22 00:12:57 +0000 | [diff] [blame] | 196 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 197 | @staticmethod |
| 198 | def ShortBranchName(branch): |
| 199 | """Converts a name like 'refs/heads/foo' to just 'foo'.""" |
| 200 | return branch.replace('refs/heads/', '') |
maruel@chromium.org | f2f9d55 | 2009-12-22 00:12:57 +0000 | [diff] [blame] | 201 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 202 | @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.org | f2f9d55 | 2009-12-22 00:12:57 +0000 | [diff] [blame] | 209 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 210 | @staticmethod |
| 211 | def GetRemoteHeadRef(cwd, url, remote): |
| 212 | """Returns the full default remote branch reference, e.g. |
Josip Sokcevic | 091f5ac | 2021-01-14 23:14:21 +0000 | [diff] [blame] | 213 | 'refs/remotes/origin/main'.""" |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 214 | 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 Sokcevic | 091f5ac | 2021-01-14 23:14:21 +0000 | [diff] [blame] | 227 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 228 | 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 Sokcevic | 091f5ac | 2021-01-14 23:14:21 +0000 | [diff] [blame] | 240 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 241 | @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.org | b24a8e1 | 2009-12-22 13:45:48 +0000 | [diff] [blame] | 248 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 249 | @staticmethod |
| 250 | def GetRemoteBranches(cwd): |
| 251 | return GIT.Capture(['branch', '-r'], cwd=cwd).split() |
Edward Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 252 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 253 | @staticmethod |
| 254 | def FetchUpstreamTuple(cwd, branch=None): |
| 255 | """Returns a tuple containing remote and remote ref, |
Josip Sokcevic | 9c0dc30 | 2020-11-20 18:41:25 +0000 | [diff] [blame] | 256 | e.g. 'origin', 'refs/heads/main' |
maruel@chromium.org | f2f9d55 | 2009-12-22 00:12:57 +0000 | [diff] [blame] | 257 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 258 | 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 Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 267 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 268 | 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 Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 272 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 273 | # 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 Vasudevan | c5f0cbb | 2022-01-24 23:56:57 +0000 | [diff] [blame] | 278 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 279 | if 'origin/master' in remote_branches: |
| 280 | # Fall back on origin/master if it exits. |
| 281 | return 'origin', 'refs/heads/master' |
Edward Lemur | 15a9b8c | 2020-02-13 00:52:30 +0000 | [diff] [blame] | 282 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 283 | return None, None |
maruel@chromium.org | f2f9d55 | 2009-12-22 00:12:57 +0000 | [diff] [blame] | 284 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 285 | @staticmethod |
| 286 | def RefToRemoteRef(ref, remote): |
| 287 | """Convert a checkout ref to the equivalent remote ref. |
mmoss@chromium.org | 6e7202b | 2014-09-09 18:23:39 +0000 | [diff] [blame] | 288 | |
| 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 Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 293 | # 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 Lemur | 9a5e3bd | 2019-04-02 23:37:45 +0000 | [diff] [blame] | 300 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 301 | 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 Lemur | 9a5e3bd | 2019-04-02 23:37:45 +0000 | [diff] [blame] | 305 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 306 | return None |
Edward Lemur | 9a5e3bd | 2019-04-02 23:37:45 +0000 | [diff] [blame] | 307 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 308 | @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.org | 6e7202b | 2014-09-09 18:23:39 +0000 | [diff] [blame] | 320 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 321 | @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.org | f2f9d55 | 2009-12-22 00:12:57 +0000 | [diff] [blame] | 330 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 331 | @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 Lemur | ca7d881 | 2018-07-24 17:42:45 +0000 | [diff] [blame] | 341 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 342 | @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 Cheng | 7a1f04d | 2017-03-21 19:12:31 -0700 | [diff] [blame] | 354 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 355 | @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.org | a937176 | 2009-12-22 18:27:38 +0000 | [diff] [blame] | 362 | |
| 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 Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 365 | 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.org | c78f246 | 2009-11-21 01:20:57 +0000 | [diff] [blame] | 386 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 387 | @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.org | 8ede00e | 2010-01-12 14:35:28 +0000 | [diff] [blame] | 397 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 398 | @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 Lemur | 98cfac1 | 2020-01-17 19:27:01 +0000 | [diff] [blame] | 405 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 406 | @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 Wang | e36c6bb | 2023-08-30 22:09:59 +0000 | [diff] [blame] | 422 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 423 | @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.org | b24a8e1 | 2009-12-22 13:45:48 +0000 | [diff] [blame] | 428 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 429 | @staticmethod |
| 430 | def GetCheckoutRoot(cwd): |
| 431 | """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] | 432 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 433 | root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd) |
| 434 | return os.path.abspath(os.path.join(cwd, root)) |
maruel@chromium.org | b24a8e1 | 2009-12-22 13:45:48 +0000 | [diff] [blame] | 435 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 436 | @staticmethod |
| 437 | def GetGitDir(cwd): |
| 438 | 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] | 439 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 440 | @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.org | ead4c7e | 2014-04-03 01:01:06 +0000 | [diff] [blame] | 446 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 447 | @staticmethod |
Joanna Wang | 60adf7b | 2023-10-06 00:04:28 +0000 | [diff] [blame^] | 448 | def IsVersioned(cwd, relative_dir): |
| 449 | # type: (str, str) -> int |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 450 | """Checks whether the given |relative_dir| is part of cwd's repo.""" |
Joanna Wang | 60adf7b | 2023-10-06 00:04:28 +0000 | [diff] [blame^] | 451 | 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.org | 1c12738 | 2015-02-17 11:15:40 +0000 | [diff] [blame] | 457 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 458 | @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.org | 1c12738 | 2015-02-17 11:15:40 +0000 | [diff] [blame] | 462 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 463 | @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 Lemur | d52edda | 2020-03-11 20:13:02 +0000 | [diff] [blame] | 479 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 480 | @staticmethod |
| 481 | def IsValidRevision(cwd, rev, sha_only=False): |
| 482 | """Verifies the revision is a proper git revision. |
ilevy@chromium.org | a41249c | 2013-07-03 00:09:12 +0000 | [diff] [blame] | 483 | |
| 484 | sha_only: Fail unless rev is a sha hash. |
| 485 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 486 | 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.org | e5d1e61 | 2011-12-19 19:49:19 +0000 | [diff] [blame] | 492 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 493 | @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) |