blob: 8743f71494a2cc43786dfc14fde5dcfe692d2fa2 [file] [log] [blame]
steveblock@chromium.org93567042012-02-15 01:02:26 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00005"""Gclient-specific SCM-specific operations."""
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00006
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00007from __future__ import print_function
8
John Budorick0f7b2002018-01-19 15:46:17 -08009import collections
10import contextlib
borenet@google.comb2256212014-05-07 20:57:28 +000011import errno
John Budorick0f7b2002018-01-19 15:46:17 -080012import json
maruel@chromium.org754960e2009-09-21 12:31:05 +000013import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import os
Tomasz Wiszkowskid4e66882021-08-19 21:35:09 +000015import platform
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000016import posixpath
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000017import re
maruel@chromium.org90541732011-04-01 17:54:18 +000018import sys
ilevy@chromium.org3534aa52013-07-20 01:58:08 +000019import tempfile
John Budorick0f7b2002018-01-19 15:46:17 -080020import threading
zty@chromium.org6279e8a2014-02-13 01:45:25 +000021import traceback
Raul Tambreb946b232019-03-26 14:48:46 +000022
23try:
24 import urlparse
25except ImportError: # For Py3 compatibility
26 import urllib.parse as urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000027
28import gclient_utils
Ravi Mistryecda7822022-02-28 16:22:20 +000029import gerrit_util
szager@chromium.org848fd492014-04-09 19:06:44 +000030import git_cache
Josip Sokcevic7958e302023-03-01 23:02:21 +000031import scm
borenet@google.comb2256212014-05-07 20:57:28 +000032import shutil
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000033import subprocess2
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000034
35
smutae7ea312016-07-18 11:59:41 -070036class NoUsableRevError(gclient_utils.Error):
37 """Raised if requested revision isn't found in checkout."""
38
39
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000040class DiffFiltererWrapper(object):
41 """Simple base class which tracks which file is being diffed and
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000042 replaces instances of its file name in the original and
agable41e3a6c2016-10-20 11:36:56 -070043 working copy lines of the git diff output."""
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000044 index_string = None
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000045 original_prefix = "--- "
46 working_prefix = "+++ "
47
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000048 def __init__(self, relpath, print_func):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000049 # Note that we always use '/' as the path separator to be
agable41e3a6c2016-10-20 11:36:56 -070050 # consistent with cygwin-style output on Windows
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000051 self._relpath = relpath.replace("\\", "/")
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000052 self._current_file = None
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000053 self._print_func = print_func
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000054
maruel@chromium.org6e29d572010-06-04 17:32:20 +000055 def SetCurrentFile(self, current_file):
56 self._current_file = current_file
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000057
iannucci@chromium.org3830a672013-02-19 20:15:14 +000058 @property
59 def _replacement_file(self):
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000060 return posixpath.join(self._relpath, self._current_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000061
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000062 def _Replace(self, line):
63 return line.replace(self._current_file, self._replacement_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000064
65 def Filter(self, line):
66 if (line.startswith(self.index_string)):
67 self.SetCurrentFile(line[len(self.index_string):])
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000068 line = self._Replace(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000069 else:
70 if (line.startswith(self.original_prefix) or
71 line.startswith(self.working_prefix)):
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000072 line = self._Replace(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000073 self._print_func(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000074
75
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000076class GitDiffFilterer(DiffFiltererWrapper):
77 index_string = "diff --git "
78
79 def SetCurrentFile(self, current_file):
80 # Get filename by parsing "a/<filename> b/<filename>"
81 self._current_file = current_file[:(len(current_file)/2)][2:]
82
83 def _Replace(self, line):
84 return re.sub("[a|b]/" + self._current_file, self._replacement_file, line)
85
86
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000087# SCMWrapper base class
88
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000089class SCMWrapper(object):
90 """Add necessary glue between all the supported SCM.
91
msb@chromium.orgd6504212010-01-13 17:34:31 +000092 This is the abstraction layer to bind to different SCM.
93 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000094 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
Edward Lemur231f5ea2018-01-31 19:02:36 +010095 out_cb=None, print_outbuf=False):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000096 self.url = url
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +000097 self._root_dir = root_dir
98 if self._root_dir:
99 self._root_dir = self._root_dir.replace('/', os.sep)
100 self.relpath = relpath
101 if self.relpath:
102 self.relpath = self.relpath.replace('/', os.sep)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000103 if self.relpath and self._root_dir:
104 self.checkout_path = os.path.join(self._root_dir, self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000105 if out_fh is None:
106 out_fh = sys.stdout
107 self.out_fh = out_fh
108 self.out_cb = out_cb
Edward Lemur231f5ea2018-01-31 19:02:36 +0100109 self.print_outbuf = print_outbuf
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000110
111 def Print(self, *args, **kwargs):
112 kwargs.setdefault('file', self.out_fh)
113 if kwargs.pop('timestamp', True):
114 self.out_fh.write('[%s] ' % gclient_utils.Elapsed())
115 print(*args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000116
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000117 def RunCommand(self, command, options, args, file_list=None):
agabledebf6c82016-12-21 12:50:12 -0800118 commands = ['update', 'updatesingle', 'revert',
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000119 'revinfo', 'status', 'diff', 'pack', 'runhooks']
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000120
121 if not command in commands:
122 raise gclient_utils.Error('Unknown command %s' % command)
123
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000124 if not command in dir(self):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000125 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % (
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000126 command, self.__class__.__name__))
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000127
128 return getattr(self, command)(options, args, file_list)
129
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000130 @staticmethod
131 def _get_first_remote_url(checkout_path):
132 log = scm.GIT.Capture(
133 ['config', '--local', '--get-regexp', r'remote.*.url'],
134 cwd=checkout_path)
135 # Get the second token of the first line of the log.
136 return log.splitlines()[0].split(' ', 1)[1]
137
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000138 def GetCacheMirror(self):
Robert Iannuccia19649b2018-06-29 16:31:45 +0000139 if getattr(self, 'cache_dir', None):
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000140 url, _ = gclient_utils.SplitUrlRevision(self.url)
141 return git_cache.Mirror(url)
142 return None
143
smut@google.comd33eab32014-07-07 19:35:18 +0000144 def GetActualRemoteURL(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000145 """Attempt to determine the remote URL for this SCMWrapper."""
smut@google.comd33eab32014-07-07 19:35:18 +0000146 # Git
borenet@google.combda475e2014-03-24 19:04:45 +0000147 if os.path.exists(os.path.join(self.checkout_path, '.git')):
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000148 actual_remote_url = self._get_first_remote_url(self.checkout_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000149
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000150 mirror = self.GetCacheMirror()
151 # If the cache is used, obtain the actual remote URL from there.
152 if (mirror and mirror.exists() and
153 mirror.mirror_path.replace('\\', '/') ==
154 actual_remote_url.replace('\\', '/')):
155 actual_remote_url = self._get_first_remote_url(mirror.mirror_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000156 return actual_remote_url
borenet@google.com88d10082014-03-21 17:24:48 +0000157 return None
158
borenet@google.com4e9be262014-04-08 19:40:30 +0000159 def DoesRemoteURLMatch(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000160 """Determine whether the remote URL of this checkout is the expected URL."""
161 if not os.path.exists(self.checkout_path):
162 # A checkout which doesn't exist can't be broken.
163 return True
164
smut@google.comd33eab32014-07-07 19:35:18 +0000165 actual_remote_url = self.GetActualRemoteURL(options)
borenet@google.com88d10082014-03-21 17:24:48 +0000166 if actual_remote_url:
borenet@google.com8156c9f2014-04-01 16:41:36 +0000167 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip('/')
168 == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/'))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000169
170 # This may occur if the self.checkout_path exists but does not contain a
171 # valid git checkout.
172 return False
borenet@google.com88d10082014-03-21 17:24:48 +0000173
borenet@google.comb09097a2014-04-09 19:09:08 +0000174 def _DeleteOrMove(self, force):
175 """Delete the checkout directory or move it out of the way.
176
177 Args:
178 force: bool; if True, delete the directory. Otherwise, just move it.
179 """
borenet@google.comb2256212014-05-07 20:57:28 +0000180 if force and os.environ.get('CHROME_HEADLESS') == '1':
181 self.Print('_____ Conflicting directory found in %s. Removing.'
182 % self.checkout_path)
183 gclient_utils.AddWarning('Conflicting directory %s deleted.'
184 % self.checkout_path)
185 gclient_utils.rmtree(self.checkout_path)
186 else:
187 bad_scm_dir = os.path.join(self._root_dir, '_bad_scm',
188 os.path.dirname(self.relpath))
189
190 try:
191 os.makedirs(bad_scm_dir)
192 except OSError as e:
193 if e.errno != errno.EEXIST:
194 raise
195
196 dest_path = tempfile.mkdtemp(
197 prefix=os.path.basename(self.relpath),
198 dir=bad_scm_dir)
199 self.Print('_____ Conflicting directory found in %s. Moving to %s.'
200 % (self.checkout_path, dest_path))
201 gclient_utils.AddWarning('Conflicting directory %s moved to %s.'
202 % (self.checkout_path, dest_path))
203 shutil.move(self.checkout_path, dest_path)
borenet@google.comb09097a2014-04-09 19:09:08 +0000204
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000205
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000206class GitWrapper(SCMWrapper):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000207 """Wrapper for Git"""
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000208 name = 'git'
maruel@chromium.org1a60dca2013-11-26 14:06:26 +0000209 remote = 'origin'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000210
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000211 _is_env_cog = None
212
213 @staticmethod
214 def _IsCog():
215 """Returns true if the env is cog"""
216 if not GitWrapper._is_env_cog:
Joanna Wangbbb66d72022-08-29 21:48:55 +0000217 GitWrapper._is_env_cog = any(os.getcwd().startswith(x) for x in [
218 '/google/cog/cloud', '/google/src/cloud'])
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000219
220 return GitWrapper._is_env_cog
221
Robert Iannuccia19649b2018-06-29 16:31:45 +0000222 @property
223 def cache_dir(self):
224 try:
225 return git_cache.Mirror.GetCachePath()
226 except RuntimeError:
227 return None
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000228
John Budorick0f7b2002018-01-19 15:46:17 -0800229 def __init__(self, url=None, *args, **kwargs):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000230 """Removes 'git+' fake prefix from git URL."""
Henrique Ferreiroe72279d2019-04-17 12:01:50 +0000231 if url and (url.startswith('git+http://') or
232 url.startswith('git+https://')):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000233 url = url[4:]
John Budorick0f7b2002018-01-19 15:46:17 -0800234 SCMWrapper.__init__(self, url, *args, **kwargs)
szager@chromium.org848fd492014-04-09 19:06:44 +0000235 filter_kwargs = { 'time_throttle': 1, 'out_fh': self.out_fh }
236 if self.out_cb:
237 filter_kwargs['predicate'] = self.out_cb
238 self.filter = gclient_utils.GitFilter(**filter_kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +0000239 self._running_under_rosetta = None
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000240
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000241 def GetCheckoutRoot(self):
242 return scm.GIT.GetCheckoutRoot(self.checkout_path)
243
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000244 def GetRevisionDate(self, _revision):
floitsch@google.comeaab7842011-04-28 09:07:58 +0000245 """Returns the given revision's date in ISO-8601 format (which contains the
246 time zone)."""
247 # TODO(floitsch): get the time-stamp of the given revision and not just the
248 # time-stamp of the currently checked out revision.
249 return self._Capture(['log', '-n', '1', '--format=%ai'])
250
Aaron Gablef4068aa2017-12-12 15:14:09 -0800251 def _GetDiffFilenames(self, base):
252 """Returns the names of files modified since base."""
253 return self._Capture(
Raul Tambrecd862e32019-05-10 21:19:00 +0000254 # Filter to remove base if it is None.
255 list(filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only',
256 base])
257 )).split()
Aaron Gablef4068aa2017-12-12 15:14:09 -0800258
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000259 def diff(self, options, _args, _file_list):
Aaron Gable1853f662018-02-12 15:45:56 -0800260 _, revision = gclient_utils.SplitUrlRevision(self.url)
261 if not revision:
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000262 revision = 'refs/remotes/%s/main' % self.remote
Aaron Gable1853f662018-02-12 15:45:56 -0800263 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000264
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000265 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000266 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000267 repository.
268
269 The patch file is generated from a diff of the merge base of HEAD and
270 its upstream branch.
271 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700272 try:
273 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
274 except subprocess2.CalledProcessError:
275 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000276 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700277 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000278 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000279 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000280
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800281 def _Scrub(self, target, options):
282 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000283 quiet = []
284 if not options.verbose:
285 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800286 self._Run(['reset', '--hard', target] + quiet, options)
287 if options.force and options.delete_unversioned_trees:
288 # where `target` is a commit that contains both upper and lower case
289 # versions of the same file on a case insensitive filesystem, we are
290 # actually in a broken state here. The index will have both 'a' and 'A',
291 # but only one of them will exist on the disk. To progress, we delete
292 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800293 output = self._Capture([
294 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800295 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800296 # --porcelain (v1) looks like:
297 # XY filename
298 try:
299 filename = line[3:]
300 self.Print('_____ Deleting residual after reset: %r.' % filename)
301 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800302 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800303 except OSError:
304 pass
305
John Budorick882c91e2018-07-12 22:11:41 +0000306 def _FetchAndReset(self, revision, file_list, options):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800307 """Equivalent to git fetch; git reset."""
Edward Lemur579c9862018-07-13 23:17:51 +0000308 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000309
dnj@chromium.org680f2172014-06-25 00:39:32 +0000310 self._Fetch(options, prune=True, quiet=options.verbose)
John Budorick882c91e2018-07-12 22:11:41 +0000311 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000312 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800313 files = self._Capture(
314 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambreb946b232019-03-26 14:48:46 +0000315 file_list.extend(
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000316 [os.path.join(self.checkout_path, f) for f in files])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000317
szager@chromium.org8a139702014-06-20 15:55:01 +0000318 def _DisableHooks(self):
319 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
320 if not os.path.isdir(hook_dir):
321 return
322 for f in os.listdir(hook_dir):
323 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000324 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
325 if os.path.exists(disabled_hook_path):
326 os.remove(disabled_hook_path)
327 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000328
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000329 def _maybe_break_locks(self, options):
330 """This removes all .lock files from this repo's .git directory, if the
331 user passed the --break_repo_locks command line flag.
332
333 In particular, this will cleanup index.lock files, as well as ref lock
334 files.
335 """
336 if options.break_repo_locks:
337 git_dir = os.path.join(self.checkout_path, '.git')
338 for path, _, filenames in os.walk(git_dir):
339 for filename in filenames:
340 if filename.endswith('.lock'):
341 to_break = os.path.join(path, filename)
342 self.Print('breaking lock: %s' % (to_break,))
343 try:
344 os.remove(to_break)
345 except OSError as ex:
346 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
347 raise
348
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000349 def _download_topics(self, patch_rev, googlesource_url):
350 """This method returns new patch_revs to process that have the same topic.
351
352 It does the following:
353 1. Finds the topic of the Gerrit change specified in the patch_rev.
354 2. Find all changes with that topic.
355 3. Append patch_rev of the changes with the same topic to the patch_revs
356 to process.
357 4. Returns the new patch_revs to process.
358 """
359 patch_revs_to_process = []
360 # Parse the patch_rev to extract the CL and patchset.
361 patch_rev_tokens = patch_rev.split('/')
362 change = patch_rev_tokens[-2]
363 # Parse the googlesource_url.
364 tokens = re.search(
365 '//(.+).googlesource.com/(.+?)(?:\.git)?$', googlesource_url)
366 if not tokens or len(tokens.groups()) != 2:
367 # googlesource_url is not in the expected format.
368 return patch_revs_to_process
369
370 # parse the gerrit host and repo out of googlesource_url.
371 host, repo = tokens.groups()[:2]
372 gerrit_host_url = '%s-review.googlesource.com' % host
373
374 # 1. Find the topic of the Gerrit change specified in the patch_rev.
375 change_object = gerrit_util.GetChange(gerrit_host_url, change)
376 topic = change_object.get('topic')
377 if not topic:
378 # This change has no topic set.
379 return patch_revs_to_process
380
381 # 2. Find all changes with that topic.
382 changes_with_same_topic = gerrit_util.QueryChanges(
383 gerrit_host_url,
384 [('topic', topic), ('status', 'open'), ('repo', repo)],
385 o_params=['ALL_REVISIONS'])
386 for c in changes_with_same_topic:
387 if str(c['_number']) == change:
388 # This change is already in the patch_rev.
389 continue
390 self.Print('Found CL %d with the topic name %s' % (
391 c['_number'], topic))
392 # 3. Append patch_rev of the changes with the same topic to the
393 # patch_revs to process.
394 curr_rev = c['current_revision']
395 new_patch_rev = c['revisions'][curr_rev]['ref']
396 patch_revs_to_process.append(new_patch_rev)
397
Joanna Wang1a977bd2022-06-02 21:51:17 +0000398 # 4. Return the new patch_revs to process.
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000399 return patch_revs_to_process
400
Kenneth Russell02e70b42023-08-07 22:09:29 +0000401 def _ref_to_remote_ref(self, target_rev):
402 """Helper function for scm.GIT.RefToRemoteRef with error checking.
403
404 Joins the results of scm.GIT.RefToRemoteRef into a string, but raises a
405 comprehensible error if RefToRemoteRef fails.
406
407 Args:
408 target_rev: a ref somewhere under refs/.
409 """
410 tmp_ref = scm.GIT.RefToRemoteRef(target_rev, self.remote)
411 if not tmp_ref:
412 raise gclient_utils.Error(
413 'Failed to turn target revision %r in repo %r into remote ref' %
414 (target_rev, self.checkout_path))
415 return ''.join(tmp_ref)
416
Edward Lemur3acbc742019-05-30 17:57:35 +0000417 def apply_patch_ref(self, patch_repo, patch_rev, target_rev, options,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000418 file_list):
Joanna Wangf3edc502022-07-20 00:12:10 +0000419 # type: (str, str, str, optparse.Values, Collection[str]) -> str
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000420 """Apply a patch on top of the revision we're synced at.
421
Edward Lemur3acbc742019-05-30 17:57:35 +0000422 The patch ref is given by |patch_repo|@|patch_rev|.
423 |target_rev| is usually the branch that the |patch_rev| was uploaded against
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000424 (e.g. 'refs/heads/main'), but this is not required.
Edward Lemur3acbc742019-05-30 17:57:35 +0000425
426 We cherry-pick all commits reachable from |patch_rev| on top of the curret
427 HEAD, excluding those reachable from |target_rev|
428 (i.e. git cherry-pick target_rev..patch_rev).
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000429
430 Graphically, it looks like this:
431
Edward Lemur3acbc742019-05-30 17:57:35 +0000432 ... -> o -> [possibly already landed commits] -> target_rev
433 \
434 -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000435
Edward Lemur3acbc742019-05-30 17:57:35 +0000436 The final checkout state is then:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000437
Edward Lemur3acbc742019-05-30 17:57:35 +0000438 ... -> HEAD -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000439
440 After application, if |options.reset_patch_ref| is specified, we soft reset
Edward Lemur3acbc742019-05-30 17:57:35 +0000441 the cherry-picked changes, keeping them in git index only.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000442
443 Args:
Edward Lemur3acbc742019-05-30 17:57:35 +0000444 patch_repo: The patch origin.
445 e.g. 'https://foo.googlesource.com/bar'
446 patch_rev: The revision to patch.
447 e.g. 'refs/changes/1234/34/1'.
448 target_rev: The revision to use when finding the merge base.
449 Typically, the branch that the patch was uploaded against.
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000450 e.g. 'refs/heads/main' or 'refs/heads/infra/config'.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000451 options: The options passed to gclient.
452 file_list: A list where modified files will be appended.
453 """
454
Edward Lemurca7d8812018-07-24 17:42:45 +0000455 # Abort any cherry-picks in progress.
456 try:
457 self._Capture(['cherry-pick', '--abort'])
458 except subprocess2.CalledProcessError:
459 pass
460
Joanna Wangf3edc502022-07-20 00:12:10 +0000461 base_rev = self.revinfo(None, None, None)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000462
Edward Lemur3acbc742019-05-30 17:57:35 +0000463 if not target_rev:
Edward Lemur4c5c8ab2019-06-07 15:58:13 +0000464 raise gclient_utils.Error('A target revision for the patch must be given')
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000465
466 if target_rev.startswith(('refs/heads/', 'refs/branch-heads')):
Edward Lesmesf627d9f2020-07-23 19:50:50 +0000467 # If |target_rev| is in refs/heads/** or refs/branch-heads/**, try first
468 # to find the corresponding remote ref for it, since |target_rev| might
469 # point to a local ref which is not up to date with the corresponding
470 # remote ref.
Kenneth Russell02e70b42023-08-07 22:09:29 +0000471 remote_ref = self._ref_to_remote_ref(target_rev)
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000472 self.Print('Trying the corresponding remote ref for %r: %r\n' % (
Edward Lemur3acbc742019-05-30 17:57:35 +0000473 target_rev, remote_ref))
474 if scm.GIT.IsValidRevision(self.checkout_path, remote_ref):
Josip Sokcevic3d7cbce2021-10-05 20:48:04 +0000475 # refs/remotes may need to be updated to cleanly cherry-pick changes.
476 # See https://crbug.com/1255178.
477 self._Capture(['fetch', '--no-tags', self.remote, target_rev])
Edward Lemur3acbc742019-05-30 17:57:35 +0000478 target_rev = remote_ref
479 elif not scm.GIT.IsValidRevision(self.checkout_path, target_rev):
480 # Fetch |target_rev| if it's not already available.
481 url, _ = gclient_utils.SplitUrlRevision(self.url)
Edward Lesmes07a68342021-04-20 23:39:30 +0000482 mirror = self._GetMirror(url, options, target_rev, target_rev)
Edward Lemur3acbc742019-05-30 17:57:35 +0000483 if mirror:
484 rev_type = 'branch' if target_rev.startswith('refs/') else 'hash'
485 self._UpdateMirrorIfNotContains(mirror, options, rev_type, target_rev)
486 self._Fetch(options, refspec=target_rev)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000487
Ravi Mistryecda7822022-02-28 16:22:20 +0000488 patch_revs_to_process = [patch_rev]
489
490 if hasattr(options, 'download_topics') and options.download_topics:
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000491 patch_revs_to_process_from_topics = self._download_topics(
492 patch_rev, self.url)
493 patch_revs_to_process.extend(patch_revs_to_process_from_topics)
Ravi Mistryecda7822022-02-28 16:22:20 +0000494
Edward Lesmesc621b212018-03-21 20:26:56 -0400495 self._Capture(['reset', '--hard'])
Ravi Mistryecda7822022-02-28 16:22:20 +0000496 for pr in patch_revs_to_process:
497 self.Print('===Applying patch===')
498 self.Print('Revision to patch is %r @ %r.' % (patch_repo, pr))
499 self.Print('Current dir is %r' % self.checkout_path)
500 self._Capture(['fetch', '--no-tags', patch_repo, pr])
501 pr = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400502
Ravi Mistryecda7822022-02-28 16:22:20 +0000503 if not options.rebase_patch_ref:
504 self._Capture(['checkout', pr])
505 # Adjust base_rev to be the first parent of our checked out patch ref;
506 # This will allow us to correctly extend `file_list`, and will show the
507 # correct file-list to programs which do `git diff --cached` expecting
508 # to see the patch diff.
509 base_rev = self._Capture(['rev-parse', pr+'~'])
510 else:
511 self.Print('Will cherrypick %r .. %r on top of %r.' % (
512 target_rev, pr, base_rev))
Edward Lemur3acbc742019-05-30 17:57:35 +0000513 try:
Joanna Wangab9c6ba2023-01-21 01:46:36 +0000514 if scm.GIT.IsAncestor(pr, target_rev, cwd=self.checkout_path):
Ravi Mistryecda7822022-02-28 16:22:20 +0000515 if len(patch_revs_to_process) > 1:
516 # If there are multiple patch_revs_to_process then we do not want
517 # want to invalidate a previous patch so throw an error.
518 raise gclient_utils.Error(
519 'patch_rev %s is an ancestor of target_rev %s. This '
520 'situation is unsupported when we need to apply multiple '
521 'patch_revs: %s' % (pr, target_rev, patch_revs_to_process))
522 # If |patch_rev| is an ancestor of |target_rev|, check it out.
523 self._Capture(['checkout', pr])
524 else:
525 # If a change was uploaded on top of another change, which has
526 # already landed, one of the commits in the cherry-pick range will
527 # be redundant, since it has already landed and its changes
528 # incorporated in the tree.
529 # We pass '--keep-redundant-commits' to ignore those changes.
530 self._Capture(['cherry-pick', target_rev + '..' + pr,
531 '--keep-redundant-commits'])
Edward Lemurca7d8812018-07-24 17:42:45 +0000532
Ravi Mistryecda7822022-02-28 16:22:20 +0000533 except subprocess2.CalledProcessError as e:
534 self.Print('Failed to apply patch.')
535 self.Print('Revision to patch was %r @ %r.' % (patch_repo, pr))
536 self.Print('Tried to cherrypick %r .. %r on top of %r.' % (
537 target_rev, pr, base_rev))
538 self.Print('Current dir is %r' % self.checkout_path)
539 self.Print('git returned non-zero exit status %s:\n%s' % (
540 e.returncode, e.stderr.decode('utf-8')))
541 # Print the current status so that developers know what changes caused
542 # the patch failure, since git cherry-pick doesn't show that
543 # information.
544 self.Print(self._Capture(['status']))
545 try:
546 self._Capture(['cherry-pick', '--abort'])
547 except subprocess2.CalledProcessError:
548 pass
549 raise
550
551 if file_list is not None:
552 file_list.extend(self._GetDiffFilenames(base_rev))
Edward Lemurca7d8812018-07-24 17:42:45 +0000553
Joanna Wangf3edc502022-07-20 00:12:10 +0000554 latest_commit = self.revinfo(None, None, None)
Edward Lesmesc621b212018-03-21 20:26:56 -0400555 if options.reset_patch_ref:
556 self._Capture(['reset', '--soft', base_rev])
Joanna Wangf3edc502022-07-20 00:12:10 +0000557 return latest_commit
Edward Lesmesc621b212018-03-21 20:26:56 -0400558
Joanna Wang5a7c8242022-07-01 19:09:00 +0000559 def check_diff(self, previous_commit, files=None):
560 # type: (str, Optional[List[str]]) -> bool
561 """Check if a diff exists between the current commit and `previous_commit`.
562
563 Returns True if there were diffs or if an error was encountered.
564 """
565 cmd = ['diff', previous_commit, '--quiet']
566 if files:
567 cmd += ['--'] + files
568 try:
569 self._Capture(cmd)
570 return False
571 except subprocess2.CalledProcessError as e:
572 # git diff --quiet exits with 1 if there were diffs.
573 if e.returncode != 1:
574 self.Print('git returned non-zero exit status %s:\n%s' %
575 (e.returncode, e.stderr.decode('utf-8')))
576 return True
577
Joanna Wange1753f62023-06-26 14:32:43 +0000578 def set_config(f):
579 def wrapper(*args):
580 return_val = f(*args)
581 if os.path.exists(os.path.join(args[0].checkout_path, '.git')):
582 # If diff.ignoreSubmodules is not already set, set it to `all`.
Josip Sokcevic84c0bba2023-08-10 00:52:15 +0000583 config = subprocess2.capture(
584 ['git', 'config', '-l'],
585 cwd=args[0].checkout_path).decode('utf-8').strip().splitlines()
586 if 'diff.ignoresubmodules=all' not in config:
Joanna Wange1753f62023-06-26 14:32:43 +0000587 subprocess2.capture(['git', 'config', 'diff.ignoreSubmodules', 'all'],
588 cwd=args[0].checkout_path).strip()
Josip Sokcevic84c0bba2023-08-10 00:52:15 +0000589 if 'fetch.recursesubmodules=off' not in config:
590 subprocess2.capture(
591 ['git', 'config', 'fetch.recurseSubmodules', 'off'],
592 cwd=args[0].checkout_path).strip()
Joanna Wange1753f62023-06-26 14:32:43 +0000593 return return_val
594
595 return wrapper
596
597 @set_config
msb@chromium.orge28e4982009-09-25 20:51:45 +0000598 def update(self, options, args, file_list):
599 """Runs git to update or transparently checkout the working copy.
600
601 All updated files will be appended to file_list.
602
603 Raises:
604 Error: if can't get URL for relative path.
605 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000606 if args:
607 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
608
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000609 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000610
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000611 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000612 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000613 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000614 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000615 # Override the revision number.
616 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000617 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000618 # Check again for a revision in case an initial ref was specified
619 # in the url, for example bla.git@refs/heads/custombranch
620 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000621 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000622 if not revision:
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000623 # If a dependency is not pinned, track the default remote branch.
624 revision = scm.GIT.GetRemoteHeadRef(self.checkout_path, self.url,
625 self.remote)
Edward Lesmes4ea67bb2021-04-20 17:33:52 +0000626 if revision.startswith('origin/'):
627 revision = 'refs/remotes/' + revision
msb@chromium.orge28e4982009-09-25 20:51:45 +0000628
Tomasz Wiszkowskid4e66882021-08-19 21:35:09 +0000629 if managed and platform.system() == 'Windows':
szager@chromium.org8a139702014-06-20 15:55:01 +0000630 self._DisableHooks()
631
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000632 printed_path = False
633 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000634 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700635 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000636 verbose = ['--verbose']
637 printed_path = True
638
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000639 revision_ref = revision
640 if ':' in revision:
641 revision_ref, _, revision = revision.partition(':')
642
Edward Lesmes8073a502020-04-15 02:11:14 +0000643 if revision_ref.startswith('refs/branch-heads'):
644 options.with_branch_heads = True
645
Edward Lesmes07a68342021-04-20 23:39:30 +0000646 mirror = self._GetMirror(url, options, revision, revision_ref)
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000647 if mirror:
648 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000649
John Budorick882c91e2018-07-12 22:11:41 +0000650 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
651 if remote_ref:
652 # Rewrite remote refs to their local equivalents.
653 revision = ''.join(remote_ref)
654 rev_type = "branch"
655 elif revision.startswith('refs/'):
656 # Local branch? We probably don't want to support, since DEPS should
657 # always specify branches as they are in the upstream repo.
658 rev_type = "branch"
659 else:
660 # hash is also a tag, only make a distinction at checkout
661 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000662
primiano@chromium.org1c127382015-02-17 11:15:40 +0000663 # If we are going to introduce a new project, there is a possibility that
664 # we are syncing back to a state where the project was originally a
665 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
666 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
667 # In such case, we might have a backup of the former .git folder, which can
668 # be used to avoid re-fetching the entire repo again (useful for bisects).
669 backup_dir = self.GetGitBackupDirPath()
670 target_dir = os.path.join(self.checkout_path, '.git')
671 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
672 gclient_utils.safe_makedirs(self.checkout_path)
673 os.rename(backup_dir, target_dir)
674 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800675 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000676
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000677 if (not os.path.exists(self.checkout_path) or
678 (os.path.isdir(self.checkout_path) and
679 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000680 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000681 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000682 try:
John Budorick882c91e2018-07-12 22:11:41 +0000683 self._Clone(revision, url, options)
Joanna Wang47fd5672022-08-05 20:53:31 +0000684 except subprocess2.CalledProcessError as e:
685 logging.warning('Clone failed due to: %s', e)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000686 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000687 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000688 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800689 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000690 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000691 file_list.extend(
Edward Lemur979fa782019-08-13 22:44:05 +0000692 [os.path.join(self.checkout_path, f) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000693 if mirror:
694 self._Capture(
695 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000696 if not verbose:
697 # Make the output a little prettier. It's nice to have some whitespace
698 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000699 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000700 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000701
John Budorick21a51b32018-09-19 19:39:20 +0000702 if mirror:
703 self._Capture(
704 ['remote', 'set-url', '--push', 'origin', mirror.url])
705
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000706 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000707 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000708 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
709 return self._Capture(['rev-parse', '--verify', 'HEAD'])
710
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000711 self._maybe_break_locks(options)
712
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000713 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000714 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000715
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000716 # See if the url has changed (the unittests use git://foo for the url, let
717 # that through).
718 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
719 return_early = False
720 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
721 # unit test pass. (and update the comment above)
722 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
723 # This allows devs to use experimental repos which have a different url
724 # but whose branch(s) are the same as official repos.
Raul Tambrecd862e32019-05-10 21:19:00 +0000725 if (current_url.rstrip('/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000726 subprocess2.capture(
Aaron Gableac9b0f32019-04-18 17:38:37 +0000727 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000728 cwd=self.checkout_path).strip() != 'False'):
Anthony Polito486f1812020-08-04 23:40:33 +0000729 self.Print('_____ switching %s from %s to new upstream %s' % (
730 self.relpath, current_url, url))
iannucci@chromium.org78514212014-08-20 23:08:00 +0000731 if not (options.force or options.reset):
732 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700733 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000734 # Switch over to the new upstream
735 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000736 if mirror:
Aravind Vasudevancf465852023-03-29 16:47:12 +0000737 if git_cache.Mirror.CacheDirToUrl(
738 current_url.rstrip('/')) == git_cache.Mirror.CacheDirToUrl(
739 url.rstrip('/')):
740 # Reset alternates when the cache dir is updated.
741 with open(
742 os.path.join(self.checkout_path, '.git', 'objects', 'info',
743 'alternates'), 'w') as fh:
744 fh.write(os.path.join(url, 'objects'))
745 else:
746 # Because we use Git alternatives, our existing repository is not
747 # self-contained. It's possible that new git alternative doesn't have
748 # all necessary objects that the current repository needs. Instead of
749 # blindly hoping that new alternative contains all necessary objects,
750 # keep the old alternative and just append a new one on top of it.
751 with open(
752 os.path.join(self.checkout_path, '.git', 'objects', 'info',
753 'alternates'), 'a') as fh:
754 fh.write("\n" + os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000755 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
756 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000757
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000758 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000759 else:
John Budorick882c91e2018-07-12 22:11:41 +0000760 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000761
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000762 if return_early:
763 return self._Capture(['rev-parse', '--verify', 'HEAD'])
764
msb@chromium.org5bde4852009-12-14 16:47:12 +0000765 cur_branch = self._GetCurrentBranch()
766
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000767 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000768 # 0) HEAD is detached. Probably from our initial clone.
769 # - make sure HEAD is contained by a named ref, then update.
770 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700771 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000772 # - try to rebase onto the new hash or branch
773 # 2) current branch is tracking a remote branch with local committed
774 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000775 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000776 # 3) current branch is tracking a remote branch w/or w/out changes, and
777 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000778 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000779 # 4) current branch is tracking a remote branch, but DEPS switches to a
780 # different remote branch, and
781 # a) current branch has no local changes, and --force:
782 # - checkout new branch
783 # b) current branch has local changes, and --force and --reset:
784 # - checkout new branch
785 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000786
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000787 # GetUpstreamBranch returns something like 'refs/remotes/origin/main' for
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000788 # a tracking branch
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000789 # or 'main' if not a tracking branch (it's based on a specific rev/hash)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000790 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000791 if cur_branch is None:
792 upstream_branch = None
793 current_type = "detached"
794 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000795 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000796 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
797 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
798 current_type = "hash"
799 logging.debug("Current branch is not tracking an upstream (remote)"
800 " branch.")
801 elif upstream_branch.startswith('refs/remotes'):
802 current_type = "branch"
803 else:
804 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000805
Edward Lemur579c9862018-07-13 23:17:51 +0000806 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000807
Michael Spang73fac912019-03-08 18:44:19 +0000808 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000809 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000810 self._Fetch(options, prune=options.force)
811
812 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
813 sha_only=True):
814 # Update the remotes first so we have all the refs.
815 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
816 cwd=self.checkout_path)
817 if verbose:
818 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000819
John Budorick882c91e2018-07-12 22:11:41 +0000820 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200821
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000822 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000823 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000824 target = 'HEAD'
825 if options.upstream and upstream_branch:
826 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800827 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000828
msb@chromium.org786fb682010-06-02 15:16:23 +0000829 if current_type == 'detached':
830 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800831 # We just did a Scrub, this is as clean as it's going to get. In
832 # particular if HEAD is a commit that contains two versions of the same
833 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
834 # to actually "Clean" the checkout; that commit is uncheckoutable on this
835 # system. The best we can do is carry forward to the checkout step.
836 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000837 self._CheckClean(revision)
838 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000839 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000840 self.Print('Up-to-date; skipping checkout.')
841 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000842 # 'git checkout' may need to overwrite existing untracked files. Allow
843 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000844 self._Checkout(
845 options,
John Budorick882c91e2018-07-12 22:11:41 +0000846 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000847 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000848 quiet=True,
849 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000850 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000851 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000852 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000853 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700854 # Can't find a merge-base since we don't know our upstream. That makes
855 # this command VERY likely to produce a rebase failure. For now we
856 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000857 upstream_branch = self.remote
858 if options.revision or deps_revision:
859 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700860 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700861 printed_path=printed_path, merge=options.merge)
862 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000863 elif rev_type == 'hash':
864 # case 2
865 self._AttemptRebase(upstream_branch, file_list, options,
866 newbase=revision, printed_path=printed_path,
867 merge=options.merge)
868 printed_path = True
869 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000870 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000871 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000872 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000873 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000874 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000875 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000876 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000877 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
878 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000879 force_switch = False
880 if options.force:
881 try:
John Budorick882c91e2018-07-12 22:11:41 +0000882 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000883 # case 4a
884 force_switch = True
885 except gclient_utils.Error as e:
886 if options.reset:
887 # case 4b
888 force_switch = True
889 else:
890 switch_error = '%s\n%s' % (e.message, switch_error)
891 if force_switch:
892 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000893 (upstream_branch, new_base))
894 switch_branch = 'gclient_' + remote_ref[1]
895 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000896 self._Checkout(options, switch_branch, force=True, quiet=True)
897 else:
898 # case 4c
899 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000900 else:
901 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800902 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000903 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000904 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000905 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000906 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000907 if options.merge:
908 merge_args.append('--ff')
909 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000910 merge_args.append('--ff-only')
911 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000912 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000913 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700914 rebase_files = []
Edward Lemur979fa782019-08-13 22:44:05 +0000915 if re.match(b'fatal: Not possible to fast-forward, aborting.',
916 e.stderr):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000917 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000918 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700919 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000920 printed_path = True
921 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000922 if not options.auto_rebase:
923 try:
924 action = self._AskForData(
925 'Cannot %s, attempt to rebase? '
926 '(y)es / (q)uit / (s)kip : ' %
927 ('merge' if options.merge else 'fast-forward merge'),
928 options)
929 except ValueError:
930 raise gclient_utils.Error('Invalid Character')
931 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700932 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000933 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000934 printed_path = True
935 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000936
937 if re.match(r'quit|q', action, re.I):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000938 raise gclient_utils.Error("Can't fast-forward, please merge or "
939 "rebase manually.\n"
940 "cd %s && git " % self.checkout_path
941 + "rebase %s" % upstream_branch)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000942
943 if re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000944 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000945 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000946
947 self.Print('Input not recognized')
Edward Lemur979fa782019-08-13 22:44:05 +0000948 elif re.match(b"error: Your local changes to '.*' would be "
949 b"overwritten by merge. Aborting.\nPlease, commit your "
950 b"changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000951 e.stderr):
952 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000953 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700954 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000955 printed_path = True
Edward Lemur979fa782019-08-13 22:44:05 +0000956 raise gclient_utils.Error(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000957 else:
958 # Some other problem happened with the merge
959 logging.error("Error during fast-forward merge in %s!" % self.relpath)
Edward Lemur979fa782019-08-13 22:44:05 +0000960 self.Print(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000961 raise
962 else:
963 # Fast-forward merge was successful
964 if not re.match('Already up-to-date.', merge_output) or verbose:
965 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700966 self.Print('_____ %s at %s' % (self.relpath, revision),
967 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000968 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000969 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000970 if not verbose:
971 # Make the output a little prettier. It's nice to have some
972 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000973 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000974
agablec3937b92016-10-25 10:13:03 -0700975 if file_list is not None:
976 file_list.extend(
977 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000978
979 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000980 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700981 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000982 '\nConflict while rebasing this branch.\n'
983 'Fix the conflict and run gclient again.\n'
984 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700985 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000986
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000987 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000988 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
989 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000990
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000991 # If --reset and --delete_unversioned_trees are specified, remove any
992 # untracked directories.
993 if options.reset and options.delete_unversioned_trees:
994 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
995 # merge-base by default), so doesn't include untracked files. So we use
996 # 'git ls-files --directory --others --exclude-standard' here directly.
997 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800998 ['-c', 'core.quotePath=false', 'ls-files',
999 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +00001000 self.checkout_path)
1001 for path in (p for p in paths.splitlines() if p.endswith('/')):
1002 full_path = os.path.join(self.checkout_path, path)
1003 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001004 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +00001005 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +00001006
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00001007 return self._Capture(['rev-parse', '--verify', 'HEAD'])
1008
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001009 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +00001010 """Reverts local modifications.
1011
1012 All reverted files will be appended to file_list.
1013 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +00001014 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +00001015 # revert won't work if the directory doesn't exist. It needs to
1016 # checkout instead.
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001017 self.Print('_____ %s is missing, syncing instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +00001018 # Don't reuse the args.
1019 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001020
Josip Sokcevic7e133ff2021-07-13 17:44:53 +00001021 default_rev = "refs/heads/main"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +00001022 if options.upstream:
1023 if self._GetCurrentBranch():
1024 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
1025 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001026 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001027 if not deps_revision:
1028 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +00001029 if deps_revision.startswith('refs/heads/'):
1030 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -07001031 try:
1032 deps_revision = self.GetUsableRev(deps_revision, options)
1033 except NoUsableRevError as e:
1034 # If the DEPS entry's url and hash changed, try to update the origin.
1035 # See also http://crbug.com/520067.
John Budorickd94f8ea2020-03-27 15:55:24 +00001036 logging.warning(
1037 "Couldn't find usable revision, will retrying to update instead: %s",
smutae7ea312016-07-18 11:59:41 -07001038 e.message)
1039 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001040
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001041 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001042 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001043
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001044 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +00001045 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001046
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001047 if file_list is not None:
1048 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
1049
1050 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001051 """Returns revision"""
1052 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +00001053
msb@chromium.orge28e4982009-09-25 20:51:45 +00001054 def runhooks(self, options, args, file_list):
1055 self.status(options, args, file_list)
1056
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001057 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +00001058 """Display status information."""
1059 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001060 self.Print('________ couldn\'t run status in %s:\n'
1061 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001062 else:
Anthony Politobb457342019-11-15 22:26:01 +00001063 merge_base = []
1064 if self.url:
1065 _, base_rev = gclient_utils.SplitUrlRevision(self.url)
1066 if base_rev:
Joanna Wanga654ff32023-07-18 23:25:19 +00001067 if base_rev.startswith('refs/'):
Kenneth Russell02e70b42023-08-07 22:09:29 +00001068 base_rev = self._ref_to_remote_ref(base_rev)
Anthony Politobb457342019-11-15 22:26:01 +00001069 merge_base = [base_rev]
Aaron Gablef4068aa2017-12-12 15:14:09 -08001070 self._Run(
1071 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
Edward Lemur24146be2019-08-01 21:44:52 +00001072 options, always_show_header=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001073 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001074 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001075 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +00001076
smutae7ea312016-07-18 11:59:41 -07001077 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -07001078 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -07001079 sha1 = None
1080 if not os.path.isdir(self.checkout_path):
1081 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -08001082 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -07001083
1084 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1085 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001086 else:
agable41e3a6c2016-10-20 11:36:56 -07001087 # May exist in origin, but we don't have it yet, so fetch and look
1088 # again.
1089 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -07001090 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1091 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001092
1093 if not sha1:
1094 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -08001095 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -07001096
1097 return sha1
1098
primiano@chromium.org1c127382015-02-17 11:15:40 +00001099 def GetGitBackupDirPath(self):
1100 """Returns the path where the .git folder for the current project can be
1101 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
1102 return os.path.join(self._root_dir,
1103 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
1104
Edward Lesmes07a68342021-04-20 23:39:30 +00001105 def _GetMirror(self, url, options, revision=None, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001106 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +00001107 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001108 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +00001109 mirror_kwargs = {
1110 'print_func': self.filter,
Edward Lesmes07a68342021-04-20 23:39:30 +00001111 'refs': [],
1112 'commits': [],
hinoka@google.comb1b54572014-04-16 22:29:23 +00001113 }
hinoka@google.comb1b54572014-04-16 22:29:23 +00001114 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1115 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001116 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
1117 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001118 if hasattr(options, 'with_tags') and options.with_tags:
1119 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001120 elif revision_ref and revision_ref.startswith('refs/tags/'):
1121 mirror_kwargs['refs'].append(revision_ref)
Edward Lesmes07a68342021-04-20 23:39:30 +00001122 if revision and not revision.startswith('refs/'):
1123 mirror_kwargs['commits'].append(revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001124 return git_cache.Mirror(url, **mirror_kwargs)
1125
John Budorick882c91e2018-07-12 22:11:41 +00001126 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001127 """Update a git mirror by fetching the latest commits from the remote,
1128 unless mirror already contains revision whose type is sha1 hash.
1129 """
John Budorick882c91e2018-07-12 22:11:41 +00001130 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001131 if options.verbose:
1132 self.Print('skipping mirror update, it has rev=%s already' % revision,
1133 timestamp=False)
1134 return
1135
szager@chromium.org3ec84f62014-08-22 21:00:22 +00001136 if getattr(options, 'shallow', False):
Victor Vianna392ce7e2022-06-07 21:47:51 +00001137 depth = 10000
hinoka@chromium.org46b87412014-05-15 00:42:05 +00001138 else:
1139 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +00001140 mirror.populate(verbose=options.verbose,
1141 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +00001142 depth=depth,
Vadim Shtayura08049e22017-10-11 00:14:52 +00001143 lock_timeout=getattr(options, 'lock_timeout', 0))
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001144
John Budorick882c91e2018-07-12 22:11:41 +00001145 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001146 """Clone a git repository from the given URL.
1147
msb@chromium.org786fb682010-06-02 15:16:23 +00001148 Once we've cloned the repo, we checkout a working branch if the specified
1149 revision is a branch head. If it is a tag or a specific commit, then we
1150 leave HEAD detached as it makes future updates simpler -- in this case the
1151 user should first create a new branch or switch to an existing branch before
1152 making changes in the repo."""
Joanna Wang1a977bd2022-06-02 21:51:17 +00001153 in_cog_workspace = self._IsCog()
1154
1155 if self.print_outbuf:
1156 print_stdout = True
1157 filter_fn = None
1158 else:
1159 print_stdout = False
1160 filter_fn = self.filter
1161
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001162 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001163 # git clone doesn't seem to insert a newline properly before printing
1164 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001165 self.Print('')
Joanna Wang1a977bd2022-06-02 21:51:17 +00001166
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001167 # If the parent directory does not exist, Git clone on Windows will not
1168 # create it, so we need to do it manually.
1169 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001170 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001171
Joanna Wang1a977bd2022-06-02 21:51:17 +00001172 if in_cog_workspace:
1173 clone_cmd = ['citc', 'clone-repo', url, self.checkout_path]
1174 clone_cmd.append(
1175 gclient_utils.ExtractRefName(self.remote, revision) or revision)
1176 try:
1177 self._Run(clone_cmd,
1178 options,
1179 cwd=self._root_dir,
1180 retry=True,
1181 print_stdout=print_stdout,
1182 filter_fn=filter_fn)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001183 except:
1184 traceback.print_exc(file=self.out_fh)
1185 raise
1186 self._SetFetchConfig(options)
Travis Lane738b48a2022-11-28 20:28:51 +00001187 elif hasattr(options, 'no_history') and options.no_history:
1188 self._Run(['init', self.checkout_path], options, cwd=self._root_dir)
1189 self._Run(['remote', 'add', 'origin', url], options)
1190 revision = self._AutoFetchRef(options, revision, depth=1)
1191 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1192 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001193 else:
1194 cfg = gclient_utils.DefaultIndexPackConfig(url)
1195 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
1196 if self.cache_dir:
1197 clone_cmd.append('--shared')
1198 if options.verbose:
1199 clone_cmd.append('--verbose')
1200 clone_cmd.append(url)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001201 tmp_dir = tempfile.mkdtemp(prefix='_gclient_%s_' %
1202 os.path.basename(self.checkout_path),
1203 dir=parent_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001204 clone_cmd.append(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001205
1206 try:
1207 self._Run(clone_cmd,
1208 options,
1209 cwd=self._root_dir,
1210 retry=True,
1211 print_stdout=print_stdout,
1212 filter_fn=filter_fn)
Joanna Wang47fd5672022-08-05 20:53:31 +00001213 logging.debug('Cloned into temporary dir, moving to checkout_path')
Joanna Wang1a977bd2022-06-02 21:51:17 +00001214 gclient_utils.safe_makedirs(self.checkout_path)
1215 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1216 os.path.join(self.checkout_path, '.git'))
1217 except:
1218 traceback.print_exc(file=self.out_fh)
1219 raise
1220 finally:
1221 if os.listdir(tmp_dir):
1222 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
Josip Sokcevicebccac72022-06-24 21:44:49 +00001223 gclient_utils.rmtree(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001224
1225 self._SetFetchConfig(options)
1226 self._Fetch(options, prune=options.force)
1227 revision = self._AutoFetchRef(options, revision)
1228 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1229 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
1230
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001231 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001232 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001233 self.Print(
Joanna Wang1a977bd2022-06-02 21:51:17 +00001234 ('Checked out %s to a detached HEAD. Before making any commits\n'
1235 'in this repo, you should use \'git checkout <branch>\' to switch \n'
1236 'to an existing branch or use \'git checkout %s -b <branch>\' to\n'
1237 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001238
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001239 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001240 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001241 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001242 raise gclient_utils.Error("Background task requires input. Rerun "
1243 "gclient with --jobs=1 so that\n"
1244 "interaction is possible.")
Edward Lesmesae3586b2020-03-23 21:21:14 +00001245 return gclient_utils.AskForData(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001246
1247
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001248 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001249 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001250 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001251 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001252 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001253 revision = upstream
1254 if newbase:
1255 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001256 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001257 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001258 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001259 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001260 printed_path = True
1261 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001262 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001263
1264 if merge:
1265 merge_output = self._Capture(['merge', revision])
1266 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001267 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001268 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001269
1270 # Build the rebase command here using the args
1271 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1272 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001273 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001274 rebase_cmd.append('--verbose')
1275 if newbase:
1276 rebase_cmd.extend(['--onto', newbase])
1277 rebase_cmd.append(upstream)
1278 if branch:
1279 rebase_cmd.append(branch)
1280
1281 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001282 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001283 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001284 if (re.match(br'cannot rebase: you have unstaged changes', e.stderr) or
1285 re.match(br'cannot rebase: your index contains uncommitted changes',
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001286 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001287 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001288 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001289 'Cannot rebase because of unstaged changes.\n'
1290 '\'git reset --hard HEAD\' ?\n'
1291 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001292 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001293 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001294 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001295 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001296 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001297 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001298
1299 if re.match(r'quit|q', rebase_action, re.I):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001300 raise gclient_utils.Error("Please merge or rebase manually\n"
1301 "cd %s && git " % self.checkout_path
1302 + "%s" % ' '.join(rebase_cmd))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001303
1304 if re.match(r'show|s', rebase_action, re.I):
Edward Lemur979fa782019-08-13 22:44:05 +00001305 self.Print('%s' % e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001306 continue
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001307
1308 gclient_utils.Error("Input not recognized")
1309 continue
Edward Lemur979fa782019-08-13 22:44:05 +00001310 elif re.search(br'^CONFLICT', e.stdout, re.M):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001311 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1312 "Fix the conflict and run gclient again.\n"
1313 "See 'man git-rebase' for details.\n")
1314 else:
Edward Lemur979fa782019-08-13 22:44:05 +00001315 self.Print(e.stdout.decode('utf-8').strip())
1316 self.Print('Rebase produced error output:\n%s' %
1317 e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001318 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1319 "manually.\ncd %s && git " %
1320 self.checkout_path
1321 + "%s" % ' '.join(rebase_cmd))
1322
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001323 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001324 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001325 # Make the output a little prettier. It's nice to have some
1326 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001327 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001328
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001329 @staticmethod
1330 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001331 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1332 if not ok:
1333 raise gclient_utils.Error('git version %s < minimum required %s' %
1334 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001335
John Budorick882c91e2018-07-12 22:11:41 +00001336 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001337 # Special case handling if all 3 conditions are met:
1338 # * the mirros have recently changed, but deps destination remains same,
1339 # * the git histories of mirrors are conflicting.
1340 # * git cache is used
1341 # This manifests itself in current checkout having invalid HEAD commit on
1342 # most git operations. Since git cache is used, just deleted the .git
1343 # folder, and re-create it by cloning.
1344 try:
1345 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1346 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001347 if (b'fatal: bad object HEAD' in e.stderr
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001348 and self.cache_dir and self.cache_dir in url):
1349 self.Print((
1350 'Likely due to DEPS change with git cache_dir, '
1351 'the current commit points to no longer existing object.\n'
1352 '%s' % e)
1353 )
1354 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001355 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001356 else:
1357 raise
1358
msb@chromium.org786fb682010-06-02 15:16:23 +00001359 def _IsRebasing(self):
1360 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1361 # have a plumbing command to determine whether a rebase is in progress, so
1362 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1363 g = os.path.join(self.checkout_path, '.git')
1364 return (
1365 os.path.isdir(os.path.join(g, "rebase-merge")) or
1366 os.path.isdir(os.path.join(g, "rebase-apply")))
1367
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001368 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001369 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1370 if os.path.exists(lockfile):
1371 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001372 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001373 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1374 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001375 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001376
msb@chromium.org786fb682010-06-02 15:16:23 +00001377 # Make sure the tree is clean; see git-rebase.sh for reference
1378 try:
1379 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001380 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001381 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001382 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001383 '\tYou have unstaged changes.\n'
1384 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001385 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001386 try:
1387 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001388 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001389 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001390 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001391 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001392 '\tYour index contains uncommitted changes\n'
1393 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001394 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001395
agable83faed02016-10-24 14:37:10 -07001396 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001397 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1398 # reference by a commit). If not, error out -- most likely a rebase is
1399 # in progress, try to detect so we can give a better error.
1400 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001401 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1402 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001403 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001404 # Commit is not contained by any rev. See if the user is rebasing:
1405 if self._IsRebasing():
1406 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001407 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001408 '\tAlready in a conflict, i.e. (no branch).\n'
1409 '\tFix the conflict and run gclient again.\n'
1410 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1411 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001412 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001413 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001414 name = ('saved-by-gclient-' +
1415 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001416 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001417 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001418 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001419
msb@chromium.org5bde4852009-12-14 16:47:12 +00001420 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001421 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001422 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001423 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001424 return None
1425 return branch
1426
borenet@google.comc3e09d22014-04-10 13:58:18 +00001427 def _Capture(self, args, **kwargs):
Mike Frysinger286fb162019-09-30 03:14:10 +00001428 set_git_dir = 'cwd' not in kwargs
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001429 kwargs.setdefault('cwd', self.checkout_path)
1430 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001431 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001432 env = scm.GIT.ApplyEnvVars(kwargs)
Mike Frysinger286fb162019-09-30 03:14:10 +00001433 # If an explicit cwd isn't set, then default to the .git/ subdir so we get
1434 # stricter behavior. This can be useful in cases of slight corruption --
1435 # we don't accidentally go corrupting parent git checks too. See
1436 # https://crbug.com/1000825 for an example.
1437 if set_git_dir:
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001438 git_dir = os.path.abspath(os.path.join(self.checkout_path, '.git'))
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001439 # Depending on how the .gclient file was defined, self.checkout_path
1440 # might be set to a unicode string, not a regular string; on Windows
1441 # Python2, we can't set env vars to be unicode strings, so we
1442 # forcibly cast the value to a string before setting it.
1443 env.setdefault('GIT_DIR', str(git_dir))
Raul Tambrecd862e32019-05-10 21:19:00 +00001444 ret = subprocess2.check_output(
1445 ['git'] + args, env=env, **kwargs).decode('utf-8')
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001446 if strip:
1447 ret = ret.strip()
Erik Chene16ffff2019-10-14 20:35:53 +00001448 self.Print('Finished running: %s %s' % ('git', ' '.join(args)))
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001449 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001450
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001451 def _Checkout(self, options, ref, force=False, quiet=None):
1452 """Performs a 'git-checkout' operation.
1453
1454 Args:
1455 options: The configured option set
1456 ref: (str) The branch/commit to checkout
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001457 quiet: (bool/None) Whether or not the checkout should pass '--quiet'; if
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001458 'None', the behavior is inferred from 'options.verbose'.
1459 Returns: (str) The output of the checkout operation
1460 """
1461 if quiet is None:
1462 quiet = (not options.verbose)
1463 checkout_args = ['checkout']
1464 if force:
1465 checkout_args.append('--force')
1466 if quiet:
1467 checkout_args.append('--quiet')
1468 checkout_args.append(ref)
1469 return self._Capture(checkout_args)
1470
Travis Lane738b48a2022-11-28 20:28:51 +00001471 def _Fetch(self,
1472 options,
1473 remote=None,
1474 prune=False,
1475 quiet=False,
1476 refspec=None,
1477 depth=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001478 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001479 # When updating, the ref is modified to be a remote ref .
1480 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1481 # Try to reverse that mapping.
1482 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1483 if original_ref:
1484 refspec = original_ref + ':' + refspec
1485 # When a mirror is configured, it only fetches
1486 # refs/{heads,branch-heads,tags}/*.
1487 # If asked to fetch other refs, we must fetch those directly from the
1488 # repository, and not from the mirror.
1489 if not original_ref.startswith(
1490 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1491 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001492 fetch_cmd = cfg + [
1493 'fetch',
1494 remote or self.remote,
1495 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001496 if refspec:
1497 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001498
1499 if prune:
1500 fetch_cmd.append('--prune')
1501 if options.verbose:
1502 fetch_cmd.append('--verbose')
danakjd5c0b562019-11-08 17:27:47 +00001503 if not hasattr(options, 'with_tags') or not options.with_tags:
1504 fetch_cmd.append('--no-tags')
dnj@chromium.org680f2172014-06-25 00:39:32 +00001505 elif quiet:
1506 fetch_cmd.append('--quiet')
Travis Lane738b48a2022-11-28 20:28:51 +00001507 if depth:
1508 fetch_cmd.append('--depth=' + str(depth))
tandrii64103db2016-10-11 05:30:05 -07001509 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001510
Edward Lemur579c9862018-07-13 23:17:51 +00001511 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001512 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1513 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001514 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001515 try:
1516 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1517 options)
1518 self._Run(['config', 'remote.%s.fetch' % self.remote,
1519 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1520 except subprocess2.CalledProcessError as e:
1521 # If exit code was 5, it means we attempted to unset a config that
1522 # didn't exist. Ignore it.
1523 if e.returncode != 5:
1524 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001525 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001526 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001527 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1528 '^\\+refs/branch-heads/\\*:.*$']
1529 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001530 if hasattr(options, 'with_tags') and options.with_tags:
1531 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1532 '+refs/tags/*:refs/tags/*',
1533 '^\\+refs/tags/\\*:.*$']
1534 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001535
Travis Lane738b48a2022-11-28 20:28:51 +00001536 def _AutoFetchRef(self, options, revision, depth=None):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001537 """Attempts to fetch |revision| if not available in local repo.
1538
1539 Returns possibly updated revision."""
Edward Lemure0ba7b82020-03-11 20:31:32 +00001540 if not scm.GIT.IsValidRevision(self.checkout_path, revision):
Travis Lane738b48a2022-11-28 20:28:51 +00001541 self._Fetch(options, refspec=revision, depth=depth)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001542 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1543 return revision
1544
Edward Lemur24146be2019-08-01 21:44:52 +00001545 def _Run(self, args, options, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001546 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001547 kwargs.setdefault('cwd', self.checkout_path)
Edward Lemur24146be2019-08-01 21:44:52 +00001548 kwargs.setdefault('filter_fn', self.filter)
1549 kwargs.setdefault('show_header', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001550 env = scm.GIT.ApplyEnvVars(kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +00001551
agable@chromium.org772efaf2014-04-01 02:35:44 +00001552 cmd = ['git'] + args
Edward Lemur24146be2019-08-01 21:44:52 +00001553 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001554
1555
1556class CipdPackage(object):
1557 """A representation of a single CIPD package."""
1558
John Budorickd3ba72b2018-03-20 12:27:42 -07001559 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001560 self._authority_for_subdir = authority_for_subdir
1561 self._name = name
1562 self._version = version
1563
1564 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001565 def authority_for_subdir(self):
1566 """Whether this package has authority to act on behalf of its subdir.
1567
1568 Some operations should only be performed once per subdirectory. A package
1569 that has authority for its subdirectory is the only package that should
1570 perform such operations.
1571
1572 Returns:
1573 bool; whether this package has subdir authority.
1574 """
1575 return self._authority_for_subdir
1576
1577 @property
1578 def name(self):
1579 return self._name
1580
1581 @property
1582 def version(self):
1583 return self._version
1584
1585
1586class CipdRoot(object):
1587 """A representation of a single CIPD root."""
1588 def __init__(self, root_dir, service_url):
1589 self._all_packages = set()
1590 self._mutator_lock = threading.Lock()
1591 self._packages_by_subdir = collections.defaultdict(list)
1592 self._root_dir = root_dir
1593 self._service_url = service_url
Dan Le Febvre456d0852023-05-24 23:43:40 +00001594 self._resolved_packages = None
John Budorick0f7b2002018-01-19 15:46:17 -08001595
1596 def add_package(self, subdir, package, version):
1597 """Adds a package to this CIPD root.
1598
1599 As far as clients are concerned, this grants both root and subdir authority
1600 to packages arbitrarily. (The implementation grants root authority to the
1601 first package added and subdir authority to the first package added for that
1602 subdir, but clients should not depend on or expect that behavior.)
1603
1604 Args:
1605 subdir: str; relative path to where the package should be installed from
1606 the cipd root directory.
1607 package: str; the cipd package name.
1608 version: str; the cipd package version.
1609 Returns:
1610 CipdPackage; the package that was created and added to this root.
1611 """
1612 with self._mutator_lock:
1613 cipd_package = CipdPackage(
1614 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001615 not self._packages_by_subdir[subdir])
1616 self._all_packages.add(cipd_package)
1617 self._packages_by_subdir[subdir].append(cipd_package)
1618 return cipd_package
1619
1620 def packages(self, subdir):
1621 """Get the list of configured packages for the given subdir."""
1622 return list(self._packages_by_subdir[subdir])
1623
Dan Le Febvre456d0852023-05-24 23:43:40 +00001624 def resolved_packages(self):
1625 if not self._resolved_packages:
1626 self._resolved_packages = self.ensure_file_resolve()
1627 return self._resolved_packages
1628
John Budorick0f7b2002018-01-19 15:46:17 -08001629 def clobber(self):
1630 """Remove the .cipd directory.
1631
1632 This is useful for forcing ensure to redownload and reinitialize all
1633 packages.
1634 """
1635 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001636 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001637 try:
1638 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1639 except OSError:
1640 if os.path.exists(cipd_cache_dir):
1641 raise
1642
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001643 def expand_package_name(self, package_name_string, **kwargs):
1644 """Run `cipd expand-package-name`.
1645
1646 CIPD package names can be declared with placeholder variables
1647 such as '${platform}', this cmd will return the package name
1648 with the variables resolved. The resolution is based on the host
1649 the command is executing on.
1650 """
1651
1652 kwargs.setdefault('stderr', subprocess2.PIPE)
1653 cmd = ['cipd', 'expand-package-name', package_name_string]
1654 ret = subprocess2.check_output(cmd, **kwargs).decode('utf-8')
1655 return ret.strip()
1656
John Budorick0f7b2002018-01-19 15:46:17 -08001657 @contextlib.contextmanager
1658 def _create_ensure_file(self):
1659 try:
Stephanie Kim700aee72022-06-01 19:58:30 +00001660 contents = '$ParanoidMode CheckPresence\n'
1661 # TODO(crbug/1329641): Remove once cipd packages have been updated
1662 # to always be created in copy mode.
1663 contents += '$OverrideInstallMode copy\n\n'
Edward Lesmes05934952019-12-19 20:38:09 +00001664 for subdir, packages in sorted(self._packages_by_subdir.items()):
1665 contents += '@Subdir %s\n' % subdir
1666 for package in sorted(packages, key=lambda p: p.name):
1667 contents += '%s %s\n' % (package.name, package.version)
1668 contents += '\n'
John Budorick0f7b2002018-01-19 15:46:17 -08001669 ensure_file = None
1670 with tempfile.NamedTemporaryFile(
Edward Lesmes05934952019-12-19 20:38:09 +00001671 suffix='.ensure', delete=False, mode='wb') as ensure_file:
1672 ensure_file.write(contents.encode('utf-8', 'replace'))
John Budorick0f7b2002018-01-19 15:46:17 -08001673 yield ensure_file.name
1674 finally:
1675 if ensure_file is not None and os.path.exists(ensure_file.name):
1676 os.remove(ensure_file.name)
1677
1678 def ensure(self):
1679 """Run `cipd ensure`."""
1680 with self._mutator_lock:
1681 with self._create_ensure_file() as ensure_file:
1682 cmd = [
1683 'cipd', 'ensure',
1684 '-log-level', 'error',
1685 '-root', self.root_dir,
1686 '-ensure-file', ensure_file,
1687 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001688 gclient_utils.CheckCallAndFilter(
1689 cmd, print_stdout=True, show_header=True)
John Budorick0f7b2002018-01-19 15:46:17 -08001690
Dan Le Febvre456d0852023-05-24 23:43:40 +00001691 @contextlib.contextmanager
1692 def _create_ensure_file_for_resolve(self):
1693 try:
1694 contents = '$ResolvedVersions %s\n' % os.devnull
1695 for subdir, packages in sorted(self._packages_by_subdir.items()):
1696 contents += '@Subdir %s\n' % subdir
1697 for package in sorted(packages, key=lambda p: p.name):
1698 contents += '%s %s\n' % (package.name, package.version)
1699 contents += '\n'
1700 ensure_file = None
1701 with tempfile.NamedTemporaryFile(suffix='.ensure',
1702 delete=False,
1703 mode='wb') as ensure_file:
1704 ensure_file.write(contents.encode('utf-8', 'replace'))
1705 yield ensure_file.name
1706 finally:
1707 if ensure_file is not None and os.path.exists(ensure_file.name):
1708 os.remove(ensure_file.name)
1709
1710 def _create_resolved_file(self):
1711 return tempfile.NamedTemporaryFile(suffix='.resolved',
1712 delete=False,
1713 mode='wb')
1714
1715 def ensure_file_resolve(self):
1716 """Run `cipd ensure-file-resolve`."""
1717 with self._mutator_lock:
1718 with self._create_resolved_file() as output_file:
1719 with self._create_ensure_file_for_resolve() as ensure_file:
1720 cmd = [
1721 'cipd',
1722 'ensure-file-resolve',
1723 '-log-level',
1724 'error',
1725 '-ensure-file',
1726 ensure_file,
1727 '-json-output',
1728 output_file.name,
1729 ]
1730 gclient_utils.CheckCallAndFilter(cmd,
1731 print_stdout=False,
1732 show_header=False)
1733 with open(output_file.name) as f:
1734 output_json = json.load(f)
1735 return output_json.get('result', {})
1736
John Budorickd3ba72b2018-03-20 12:27:42 -07001737 def run(self, command):
1738 if command == 'update':
1739 self.ensure()
1740 elif command == 'revert':
1741 self.clobber()
1742 self.ensure()
1743
John Budorick0f7b2002018-01-19 15:46:17 -08001744 def created_package(self, package):
1745 """Checks whether this root created the given package.
1746
1747 Args:
1748 package: CipdPackage; the package to check.
1749 Returns:
1750 bool; whether this root created the given package.
1751 """
1752 return package in self._all_packages
1753
1754 @property
1755 def root_dir(self):
1756 return self._root_dir
1757
1758 @property
1759 def service_url(self):
1760 return self._service_url
1761
1762
1763class CipdWrapper(SCMWrapper):
1764 """Wrapper for CIPD.
1765
1766 Currently only supports chrome-infra-packages.appspot.com.
1767 """
John Budorick3929e9e2018-02-04 18:18:07 -08001768 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001769
1770 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1771 out_cb=None, root=None, package=None):
1772 super(CipdWrapper, self).__init__(
1773 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1774 out_cb=out_cb)
1775 assert root.created_package(package)
1776 self._package = package
1777 self._root = root
1778
1779 #override
1780 def GetCacheMirror(self):
1781 return None
1782
1783 #override
1784 def GetActualRemoteURL(self, options):
1785 return self._root.service_url
1786
1787 #override
1788 def DoesRemoteURLMatch(self, options):
1789 del options
1790 return True
1791
1792 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001793 """Does nothing.
1794
1795 CIPD packages should be reverted at the root by running
1796 `CipdRoot.run('revert')`.
1797 """
John Budorick0f7b2002018-01-19 15:46:17 -08001798
1799 def diff(self, options, args, file_list):
1800 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001801
1802 def pack(self, options, args, file_list):
1803 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001804
1805 def revinfo(self, options, args, file_list):
1806 """Grab the instance ID."""
1807 try:
1808 tmpdir = tempfile.mkdtemp()
Dan Le Febvre456d0852023-05-24 23:43:40 +00001809 # Attempt to get instance_id from the root resolved cache.
1810 # Resolved cache will not match on any CIPD packages with
1811 # variables such as ${platform}, they will fall back to
1812 # the slower method below.
1813 resolved = self._root.resolved_packages()
1814 if resolved:
1815 # CIPD uses POSIX separators across all platforms, so
1816 # replace any Windows separators.
1817 path_split = self.relpath.replace(os.sep, "/").split(":")
1818 if len(path_split) > 1:
1819 src_path, package = path_split
1820 if src_path in resolved:
1821 for resolved_package in resolved[src_path]:
1822 if package == resolved_package.get('pin', {}).get('package'):
1823 return resolved_package.get('pin', {}).get('instance_id')
1824
John Budorick0f7b2002018-01-19 15:46:17 -08001825 describe_json_path = os.path.join(tmpdir, 'describe.json')
1826 cmd = [
1827 'cipd', 'describe',
1828 self._package.name,
1829 '-log-level', 'error',
1830 '-version', self._package.version,
1831 '-json-output', describe_json_path
1832 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001833 gclient_utils.CheckCallAndFilter(cmd)
John Budorick0f7b2002018-01-19 15:46:17 -08001834 with open(describe_json_path) as f:
1835 describe_json = json.load(f)
1836 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1837 finally:
1838 gclient_utils.rmtree(tmpdir)
1839
1840 def status(self, options, args, file_list):
1841 pass
1842
1843 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001844 """Does nothing.
1845
1846 CIPD packages should be updated at the root by running
1847 `CipdRoot.run('update')`.
1848 """