blob: 0bbc940e6f079d07725b97d7cbb9d5ebb4d7ef4d [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
John Budorick0f7b2002018-01-19 15:46:17 -08007import collections
8import contextlib
borenet@google.comb2256212014-05-07 20:57:28 +00009import errno
John Budorick0f7b2002018-01-19 15:46:17 -080010import json
maruel@chromium.org754960e2009-09-21 12:31:05 +000011import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000012import os
Tomasz Wiszkowskid4e66882021-08-19 21:35:09 +000013import platform
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000014import posixpath
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000015import re
Gavin Mak65c49b12023-08-24 18:06:42 +000016import shutil
maruel@chromium.org90541732011-04-01 17:54:18 +000017import sys
ilevy@chromium.org3534aa52013-07-20 01:58:08 +000018import tempfile
John Budorick0f7b2002018-01-19 15:46:17 -080019import threading
zty@chromium.org6279e8a2014-02-13 01:45:25 +000020import traceback
Raul Tambreb946b232019-03-26 14:48:46 +000021
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000022import gclient_utils
Ravi Mistryecda7822022-02-28 16:22:20 +000023import gerrit_util
szager@chromium.org848fd492014-04-09 19:06:44 +000024import git_cache
Josip Sokcevic7958e302023-03-01 23:02:21 +000025import scm
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000026import subprocess2
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000027
28
smutae7ea312016-07-18 11:59:41 -070029class NoUsableRevError(gclient_utils.Error):
30 """Raised if requested revision isn't found in checkout."""
31
32
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000033class DiffFiltererWrapper(object):
34 """Simple base class which tracks which file is being diffed and
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000035 replaces instances of its file name in the original and
agable41e3a6c2016-10-20 11:36:56 -070036 working copy lines of the git diff output."""
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000037 index_string = None
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000038 original_prefix = "--- "
39 working_prefix = "+++ "
40
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000041 def __init__(self, relpath, print_func):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000042 # Note that we always use '/' as the path separator to be
agable41e3a6c2016-10-20 11:36:56 -070043 # consistent with cygwin-style output on Windows
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000044 self._relpath = relpath.replace("\\", "/")
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000045 self._current_file = None
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000046 self._print_func = print_func
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000047
maruel@chromium.org6e29d572010-06-04 17:32:20 +000048 def SetCurrentFile(self, current_file):
49 self._current_file = current_file
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000050
iannucci@chromium.org3830a672013-02-19 20:15:14 +000051 @property
52 def _replacement_file(self):
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000053 return posixpath.join(self._relpath, self._current_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000054
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000055 def _Replace(self, line):
56 return line.replace(self._current_file, self._replacement_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000057
58 def Filter(self, line):
59 if (line.startswith(self.index_string)):
60 self.SetCurrentFile(line[len(self.index_string):])
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000061 line = self._Replace(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000062 else:
63 if (line.startswith(self.original_prefix) or
64 line.startswith(self.working_prefix)):
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000065 line = self._Replace(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000066 self._print_func(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000067
68
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000069class GitDiffFilterer(DiffFiltererWrapper):
70 index_string = "diff --git "
71
72 def SetCurrentFile(self, current_file):
73 # Get filename by parsing "a/<filename> b/<filename>"
74 self._current_file = current_file[:(len(current_file)/2)][2:]
75
76 def _Replace(self, line):
77 return re.sub("[a|b]/" + self._current_file, self._replacement_file, line)
78
79
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000080# SCMWrapper base class
81
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000082class SCMWrapper(object):
83 """Add necessary glue between all the supported SCM.
84
msb@chromium.orgd6504212010-01-13 17:34:31 +000085 This is the abstraction layer to bind to different SCM.
86 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000087 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
Edward Lemur231f5ea2018-01-31 19:02:36 +010088 out_cb=None, print_outbuf=False):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000089 self.url = url
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +000090 self._root_dir = root_dir
91 if self._root_dir:
92 self._root_dir = self._root_dir.replace('/', os.sep)
93 self.relpath = relpath
94 if self.relpath:
95 self.relpath = self.relpath.replace('/', os.sep)
msb@chromium.orge28e4982009-09-25 20:51:45 +000096 if self.relpath and self._root_dir:
97 self.checkout_path = os.path.join(self._root_dir, self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000098 if out_fh is None:
99 out_fh = sys.stdout
100 self.out_fh = out_fh
101 self.out_cb = out_cb
Edward Lemur231f5ea2018-01-31 19:02:36 +0100102 self.print_outbuf = print_outbuf
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000103
104 def Print(self, *args, **kwargs):
105 kwargs.setdefault('file', self.out_fh)
106 if kwargs.pop('timestamp', True):
107 self.out_fh.write('[%s] ' % gclient_utils.Elapsed())
108 print(*args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000109
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000110 def RunCommand(self, command, options, args, file_list=None):
agabledebf6c82016-12-21 12:50:12 -0800111 commands = ['update', 'updatesingle', 'revert',
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000112 'revinfo', 'status', 'diff', 'pack', 'runhooks']
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000113
114 if not command in commands:
115 raise gclient_utils.Error('Unknown command %s' % command)
116
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000117 if not command in dir(self):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000118 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % (
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000119 command, self.__class__.__name__))
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000120
121 return getattr(self, command)(options, args, file_list)
122
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000123 @staticmethod
124 def _get_first_remote_url(checkout_path):
125 log = scm.GIT.Capture(
126 ['config', '--local', '--get-regexp', r'remote.*.url'],
127 cwd=checkout_path)
128 # Get the second token of the first line of the log.
129 return log.splitlines()[0].split(' ', 1)[1]
130
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000131 def GetCacheMirror(self):
Robert Iannuccia19649b2018-06-29 16:31:45 +0000132 if getattr(self, 'cache_dir', None):
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000133 url, _ = gclient_utils.SplitUrlRevision(self.url)
134 return git_cache.Mirror(url)
135 return None
136
smut@google.comd33eab32014-07-07 19:35:18 +0000137 def GetActualRemoteURL(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000138 """Attempt to determine the remote URL for this SCMWrapper."""
smut@google.comd33eab32014-07-07 19:35:18 +0000139 # Git
borenet@google.combda475e2014-03-24 19:04:45 +0000140 if os.path.exists(os.path.join(self.checkout_path, '.git')):
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000141 actual_remote_url = self._get_first_remote_url(self.checkout_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000142
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000143 mirror = self.GetCacheMirror()
144 # If the cache is used, obtain the actual remote URL from there.
145 if (mirror and mirror.exists() and
146 mirror.mirror_path.replace('\\', '/') ==
147 actual_remote_url.replace('\\', '/')):
148 actual_remote_url = self._get_first_remote_url(mirror.mirror_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000149 return actual_remote_url
borenet@google.com88d10082014-03-21 17:24:48 +0000150 return None
151
borenet@google.com4e9be262014-04-08 19:40:30 +0000152 def DoesRemoteURLMatch(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000153 """Determine whether the remote URL of this checkout is the expected URL."""
154 if not os.path.exists(self.checkout_path):
155 # A checkout which doesn't exist can't be broken.
156 return True
157
smut@google.comd33eab32014-07-07 19:35:18 +0000158 actual_remote_url = self.GetActualRemoteURL(options)
borenet@google.com88d10082014-03-21 17:24:48 +0000159 if actual_remote_url:
borenet@google.com8156c9f2014-04-01 16:41:36 +0000160 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip('/')
161 == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/'))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000162
163 # This may occur if the self.checkout_path exists but does not contain a
164 # valid git checkout.
165 return False
borenet@google.com88d10082014-03-21 17:24:48 +0000166
borenet@google.comb09097a2014-04-09 19:09:08 +0000167 def _DeleteOrMove(self, force):
168 """Delete the checkout directory or move it out of the way.
169
170 Args:
171 force: bool; if True, delete the directory. Otherwise, just move it.
172 """
borenet@google.comb2256212014-05-07 20:57:28 +0000173 if force and os.environ.get('CHROME_HEADLESS') == '1':
174 self.Print('_____ Conflicting directory found in %s. Removing.'
175 % self.checkout_path)
176 gclient_utils.AddWarning('Conflicting directory %s deleted.'
177 % self.checkout_path)
178 gclient_utils.rmtree(self.checkout_path)
179 else:
180 bad_scm_dir = os.path.join(self._root_dir, '_bad_scm',
181 os.path.dirname(self.relpath))
182
183 try:
184 os.makedirs(bad_scm_dir)
185 except OSError as e:
186 if e.errno != errno.EEXIST:
187 raise
188
189 dest_path = tempfile.mkdtemp(
190 prefix=os.path.basename(self.relpath),
191 dir=bad_scm_dir)
192 self.Print('_____ Conflicting directory found in %s. Moving to %s.'
193 % (self.checkout_path, dest_path))
194 gclient_utils.AddWarning('Conflicting directory %s moved to %s.'
195 % (self.checkout_path, dest_path))
196 shutil.move(self.checkout_path, dest_path)
borenet@google.comb09097a2014-04-09 19:09:08 +0000197
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000198
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000199class GitWrapper(SCMWrapper):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000200 """Wrapper for Git"""
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000201 name = 'git'
maruel@chromium.org1a60dca2013-11-26 14:06:26 +0000202 remote = 'origin'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000203
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000204 _is_env_cog = None
205
206 @staticmethod
207 def _IsCog():
208 """Returns true if the env is cog"""
209 if not GitWrapper._is_env_cog:
Joanna Wangbbb66d72022-08-29 21:48:55 +0000210 GitWrapper._is_env_cog = any(os.getcwd().startswith(x) for x in [
211 '/google/cog/cloud', '/google/src/cloud'])
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000212
213 return GitWrapper._is_env_cog
214
Robert Iannuccia19649b2018-06-29 16:31:45 +0000215 @property
216 def cache_dir(self):
217 try:
218 return git_cache.Mirror.GetCachePath()
219 except RuntimeError:
220 return None
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000221
John Budorick0f7b2002018-01-19 15:46:17 -0800222 def __init__(self, url=None, *args, **kwargs):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000223 """Removes 'git+' fake prefix from git URL."""
Henrique Ferreiroe72279d2019-04-17 12:01:50 +0000224 if url and (url.startswith('git+http://') or
225 url.startswith('git+https://')):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000226 url = url[4:]
John Budorick0f7b2002018-01-19 15:46:17 -0800227 SCMWrapper.__init__(self, url, *args, **kwargs)
szager@chromium.org848fd492014-04-09 19:06:44 +0000228 filter_kwargs = { 'time_throttle': 1, 'out_fh': self.out_fh }
229 if self.out_cb:
230 filter_kwargs['predicate'] = self.out_cb
231 self.filter = gclient_utils.GitFilter(**filter_kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +0000232 self._running_under_rosetta = None
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000233
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000234 def GetCheckoutRoot(self):
235 return scm.GIT.GetCheckoutRoot(self.checkout_path)
236
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000237 def GetRevisionDate(self, _revision):
floitsch@google.comeaab7842011-04-28 09:07:58 +0000238 """Returns the given revision's date in ISO-8601 format (which contains the
239 time zone)."""
240 # TODO(floitsch): get the time-stamp of the given revision and not just the
241 # time-stamp of the currently checked out revision.
242 return self._Capture(['log', '-n', '1', '--format=%ai'])
243
Aaron Gablef4068aa2017-12-12 15:14:09 -0800244 def _GetDiffFilenames(self, base):
245 """Returns the names of files modified since base."""
246 return self._Capture(
Raul Tambrecd862e32019-05-10 21:19:00 +0000247 # Filter to remove base if it is None.
248 list(filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only',
249 base])
250 )).split()
Aaron Gablef4068aa2017-12-12 15:14:09 -0800251
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000252 def diff(self, options, _args, _file_list):
Aaron Gable1853f662018-02-12 15:45:56 -0800253 _, revision = gclient_utils.SplitUrlRevision(self.url)
254 if not revision:
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000255 revision = 'refs/remotes/%s/main' % self.remote
Aaron Gable1853f662018-02-12 15:45:56 -0800256 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000257
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000258 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000259 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000260 repository.
261
262 The patch file is generated from a diff of the merge base of HEAD and
263 its upstream branch.
264 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700265 try:
266 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
267 except subprocess2.CalledProcessError:
268 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000269 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700270 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000271 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000272 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000273
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800274 def _Scrub(self, target, options):
275 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000276 quiet = []
277 if not options.verbose:
278 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800279 self._Run(['reset', '--hard', target] + quiet, options)
280 if options.force and options.delete_unversioned_trees:
281 # where `target` is a commit that contains both upper and lower case
282 # versions of the same file on a case insensitive filesystem, we are
283 # actually in a broken state here. The index will have both 'a' and 'A',
284 # but only one of them will exist on the disk. To progress, we delete
285 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800286 output = self._Capture([
287 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800288 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800289 # --porcelain (v1) looks like:
290 # XY filename
291 try:
292 filename = line[3:]
293 self.Print('_____ Deleting residual after reset: %r.' % filename)
294 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800295 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800296 except OSError:
297 pass
298
John Budorick882c91e2018-07-12 22:11:41 +0000299 def _FetchAndReset(self, revision, file_list, options):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800300 """Equivalent to git fetch; git reset."""
Edward Lemur579c9862018-07-13 23:17:51 +0000301 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000302
dnj@chromium.org680f2172014-06-25 00:39:32 +0000303 self._Fetch(options, prune=True, quiet=options.verbose)
John Budorick882c91e2018-07-12 22:11:41 +0000304 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000305 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800306 files = self._Capture(
307 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambreb946b232019-03-26 14:48:46 +0000308 file_list.extend(
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000309 [os.path.join(self.checkout_path, f) for f in files])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000310
szager@chromium.org8a139702014-06-20 15:55:01 +0000311 def _DisableHooks(self):
312 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
313 if not os.path.isdir(hook_dir):
314 return
315 for f in os.listdir(hook_dir):
316 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000317 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
318 if os.path.exists(disabled_hook_path):
319 os.remove(disabled_hook_path)
320 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000321
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000322 def _maybe_break_locks(self, options):
323 """This removes all .lock files from this repo's .git directory, if the
324 user passed the --break_repo_locks command line flag.
325
326 In particular, this will cleanup index.lock files, as well as ref lock
327 files.
328 """
329 if options.break_repo_locks:
330 git_dir = os.path.join(self.checkout_path, '.git')
331 for path, _, filenames in os.walk(git_dir):
332 for filename in filenames:
333 if filename.endswith('.lock'):
334 to_break = os.path.join(path, filename)
335 self.Print('breaking lock: %s' % (to_break,))
336 try:
337 os.remove(to_break)
338 except OSError as ex:
339 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
340 raise
341
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000342 def _download_topics(self, patch_rev, googlesource_url):
343 """This method returns new patch_revs to process that have the same topic.
344
345 It does the following:
346 1. Finds the topic of the Gerrit change specified in the patch_rev.
347 2. Find all changes with that topic.
348 3. Append patch_rev of the changes with the same topic to the patch_revs
349 to process.
350 4. Returns the new patch_revs to process.
351 """
352 patch_revs_to_process = []
353 # Parse the patch_rev to extract the CL and patchset.
354 patch_rev_tokens = patch_rev.split('/')
355 change = patch_rev_tokens[-2]
356 # Parse the googlesource_url.
357 tokens = re.search(
358 '//(.+).googlesource.com/(.+?)(?:\.git)?$', googlesource_url)
359 if not tokens or len(tokens.groups()) != 2:
360 # googlesource_url is not in the expected format.
361 return patch_revs_to_process
362
363 # parse the gerrit host and repo out of googlesource_url.
364 host, repo = tokens.groups()[:2]
365 gerrit_host_url = '%s-review.googlesource.com' % host
366
367 # 1. Find the topic of the Gerrit change specified in the patch_rev.
368 change_object = gerrit_util.GetChange(gerrit_host_url, change)
369 topic = change_object.get('topic')
370 if not topic:
371 # This change has no topic set.
372 return patch_revs_to_process
373
374 # 2. Find all changes with that topic.
375 changes_with_same_topic = gerrit_util.QueryChanges(
376 gerrit_host_url,
377 [('topic', topic), ('status', 'open'), ('repo', repo)],
378 o_params=['ALL_REVISIONS'])
379 for c in changes_with_same_topic:
380 if str(c['_number']) == change:
381 # This change is already in the patch_rev.
382 continue
383 self.Print('Found CL %d with the topic name %s' % (
384 c['_number'], topic))
385 # 3. Append patch_rev of the changes with the same topic to the
386 # patch_revs to process.
387 curr_rev = c['current_revision']
388 new_patch_rev = c['revisions'][curr_rev]['ref']
389 patch_revs_to_process.append(new_patch_rev)
390
Joanna Wang1a977bd2022-06-02 21:51:17 +0000391 # 4. Return the new patch_revs to process.
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000392 return patch_revs_to_process
393
Kenneth Russell02e70b42023-08-07 22:09:29 +0000394 def _ref_to_remote_ref(self, target_rev):
395 """Helper function for scm.GIT.RefToRemoteRef with error checking.
396
397 Joins the results of scm.GIT.RefToRemoteRef into a string, but raises a
398 comprehensible error if RefToRemoteRef fails.
399
400 Args:
401 target_rev: a ref somewhere under refs/.
402 """
403 tmp_ref = scm.GIT.RefToRemoteRef(target_rev, self.remote)
404 if not tmp_ref:
405 raise gclient_utils.Error(
406 'Failed to turn target revision %r in repo %r into remote ref' %
407 (target_rev, self.checkout_path))
408 return ''.join(tmp_ref)
409
Edward Lemur3acbc742019-05-30 17:57:35 +0000410 def apply_patch_ref(self, patch_repo, patch_rev, target_rev, options,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000411 file_list):
Joanna Wangf3edc502022-07-20 00:12:10 +0000412 # type: (str, str, str, optparse.Values, Collection[str]) -> str
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000413 """Apply a patch on top of the revision we're synced at.
414
Edward Lemur3acbc742019-05-30 17:57:35 +0000415 The patch ref is given by |patch_repo|@|patch_rev|.
416 |target_rev| is usually the branch that the |patch_rev| was uploaded against
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000417 (e.g. 'refs/heads/main'), but this is not required.
Edward Lemur3acbc742019-05-30 17:57:35 +0000418
419 We cherry-pick all commits reachable from |patch_rev| on top of the curret
420 HEAD, excluding those reachable from |target_rev|
421 (i.e. git cherry-pick target_rev..patch_rev).
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000422
423 Graphically, it looks like this:
424
Edward Lemur3acbc742019-05-30 17:57:35 +0000425 ... -> o -> [possibly already landed commits] -> target_rev
426 \
427 -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000428
Edward Lemur3acbc742019-05-30 17:57:35 +0000429 The final checkout state is then:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000430
Edward Lemur3acbc742019-05-30 17:57:35 +0000431 ... -> HEAD -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000432
433 After application, if |options.reset_patch_ref| is specified, we soft reset
Edward Lemur3acbc742019-05-30 17:57:35 +0000434 the cherry-picked changes, keeping them in git index only.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000435
436 Args:
Edward Lemur3acbc742019-05-30 17:57:35 +0000437 patch_repo: The patch origin.
438 e.g. 'https://foo.googlesource.com/bar'
439 patch_rev: The revision to patch.
440 e.g. 'refs/changes/1234/34/1'.
441 target_rev: The revision to use when finding the merge base.
442 Typically, the branch that the patch was uploaded against.
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000443 e.g. 'refs/heads/main' or 'refs/heads/infra/config'.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000444 options: The options passed to gclient.
445 file_list: A list where modified files will be appended.
446 """
447
Edward Lemurca7d8812018-07-24 17:42:45 +0000448 # Abort any cherry-picks in progress.
449 try:
450 self._Capture(['cherry-pick', '--abort'])
451 except subprocess2.CalledProcessError:
452 pass
453
Joanna Wangf3edc502022-07-20 00:12:10 +0000454 base_rev = self.revinfo(None, None, None)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000455
Edward Lemur3acbc742019-05-30 17:57:35 +0000456 if not target_rev:
Edward Lemur4c5c8ab2019-06-07 15:58:13 +0000457 raise gclient_utils.Error('A target revision for the patch must be given')
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000458
459 if target_rev.startswith(('refs/heads/', 'refs/branch-heads')):
Edward Lesmesf627d9f2020-07-23 19:50:50 +0000460 # If |target_rev| is in refs/heads/** or refs/branch-heads/**, try first
461 # to find the corresponding remote ref for it, since |target_rev| might
462 # point to a local ref which is not up to date with the corresponding
463 # remote ref.
Kenneth Russell02e70b42023-08-07 22:09:29 +0000464 remote_ref = self._ref_to_remote_ref(target_rev)
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000465 self.Print('Trying the corresponding remote ref for %r: %r\n' % (
Edward Lemur3acbc742019-05-30 17:57:35 +0000466 target_rev, remote_ref))
467 if scm.GIT.IsValidRevision(self.checkout_path, remote_ref):
Josip Sokcevic3d7cbce2021-10-05 20:48:04 +0000468 # refs/remotes may need to be updated to cleanly cherry-pick changes.
469 # See https://crbug.com/1255178.
470 self._Capture(['fetch', '--no-tags', self.remote, target_rev])
Edward Lemur3acbc742019-05-30 17:57:35 +0000471 target_rev = remote_ref
472 elif not scm.GIT.IsValidRevision(self.checkout_path, target_rev):
473 # Fetch |target_rev| if it's not already available.
474 url, _ = gclient_utils.SplitUrlRevision(self.url)
Edward Lesmes07a68342021-04-20 23:39:30 +0000475 mirror = self._GetMirror(url, options, target_rev, target_rev)
Edward Lemur3acbc742019-05-30 17:57:35 +0000476 if mirror:
477 rev_type = 'branch' if target_rev.startswith('refs/') else 'hash'
478 self._UpdateMirrorIfNotContains(mirror, options, rev_type, target_rev)
479 self._Fetch(options, refspec=target_rev)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000480
Ravi Mistryecda7822022-02-28 16:22:20 +0000481 patch_revs_to_process = [patch_rev]
482
483 if hasattr(options, 'download_topics') and options.download_topics:
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000484 patch_revs_to_process_from_topics = self._download_topics(
485 patch_rev, self.url)
486 patch_revs_to_process.extend(patch_revs_to_process_from_topics)
Ravi Mistryecda7822022-02-28 16:22:20 +0000487
Edward Lesmesc621b212018-03-21 20:26:56 -0400488 self._Capture(['reset', '--hard'])
Ravi Mistryecda7822022-02-28 16:22:20 +0000489 for pr in patch_revs_to_process:
490 self.Print('===Applying patch===')
491 self.Print('Revision to patch is %r @ %r.' % (patch_repo, pr))
492 self.Print('Current dir is %r' % self.checkout_path)
493 self._Capture(['fetch', '--no-tags', patch_repo, pr])
494 pr = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400495
Ravi Mistryecda7822022-02-28 16:22:20 +0000496 if not options.rebase_patch_ref:
497 self._Capture(['checkout', pr])
498 # Adjust base_rev to be the first parent of our checked out patch ref;
499 # This will allow us to correctly extend `file_list`, and will show the
500 # correct file-list to programs which do `git diff --cached` expecting
501 # to see the patch diff.
502 base_rev = self._Capture(['rev-parse', pr+'~'])
503 else:
504 self.Print('Will cherrypick %r .. %r on top of %r.' % (
505 target_rev, pr, base_rev))
Edward Lemur3acbc742019-05-30 17:57:35 +0000506 try:
Joanna Wangab9c6ba2023-01-21 01:46:36 +0000507 if scm.GIT.IsAncestor(pr, target_rev, cwd=self.checkout_path):
Ravi Mistryecda7822022-02-28 16:22:20 +0000508 if len(patch_revs_to_process) > 1:
509 # If there are multiple patch_revs_to_process then we do not want
510 # want to invalidate a previous patch so throw an error.
511 raise gclient_utils.Error(
512 'patch_rev %s is an ancestor of target_rev %s. This '
513 'situation is unsupported when we need to apply multiple '
514 'patch_revs: %s' % (pr, target_rev, patch_revs_to_process))
515 # If |patch_rev| is an ancestor of |target_rev|, check it out.
516 self._Capture(['checkout', pr])
517 else:
518 # If a change was uploaded on top of another change, which has
519 # already landed, one of the commits in the cherry-pick range will
520 # be redundant, since it has already landed and its changes
521 # incorporated in the tree.
522 # We pass '--keep-redundant-commits' to ignore those changes.
523 self._Capture(['cherry-pick', target_rev + '..' + pr,
524 '--keep-redundant-commits'])
Edward Lemurca7d8812018-07-24 17:42:45 +0000525
Ravi Mistryecda7822022-02-28 16:22:20 +0000526 except subprocess2.CalledProcessError as e:
527 self.Print('Failed to apply patch.')
528 self.Print('Revision to patch was %r @ %r.' % (patch_repo, pr))
529 self.Print('Tried to cherrypick %r .. %r on top of %r.' % (
530 target_rev, pr, base_rev))
531 self.Print('Current dir is %r' % self.checkout_path)
532 self.Print('git returned non-zero exit status %s:\n%s' % (
533 e.returncode, e.stderr.decode('utf-8')))
534 # Print the current status so that developers know what changes caused
535 # the patch failure, since git cherry-pick doesn't show that
536 # information.
537 self.Print(self._Capture(['status']))
538 try:
539 self._Capture(['cherry-pick', '--abort'])
540 except subprocess2.CalledProcessError:
541 pass
542 raise
543
544 if file_list is not None:
545 file_list.extend(self._GetDiffFilenames(base_rev))
Edward Lemurca7d8812018-07-24 17:42:45 +0000546
Joanna Wangf3edc502022-07-20 00:12:10 +0000547 latest_commit = self.revinfo(None, None, None)
Edward Lesmesc621b212018-03-21 20:26:56 -0400548 if options.reset_patch_ref:
549 self._Capture(['reset', '--soft', base_rev])
Joanna Wangf3edc502022-07-20 00:12:10 +0000550 return latest_commit
Edward Lesmesc621b212018-03-21 20:26:56 -0400551
Joanna Wang5a7c8242022-07-01 19:09:00 +0000552 def check_diff(self, previous_commit, files=None):
553 # type: (str, Optional[List[str]]) -> bool
554 """Check if a diff exists between the current commit and `previous_commit`.
555
556 Returns True if there were diffs or if an error was encountered.
557 """
558 cmd = ['diff', previous_commit, '--quiet']
559 if files:
560 cmd += ['--'] + files
561 try:
562 self._Capture(cmd)
563 return False
564 except subprocess2.CalledProcessError as e:
565 # git diff --quiet exits with 1 if there were diffs.
566 if e.returncode != 1:
567 self.Print('git returned non-zero exit status %s:\n%s' %
568 (e.returncode, e.stderr.decode('utf-8')))
569 return True
570
Joanna Wange1753f62023-06-26 14:32:43 +0000571 def set_config(f):
572 def wrapper(*args):
573 return_val = f(*args)
574 if os.path.exists(os.path.join(args[0].checkout_path, '.git')):
575 # If diff.ignoreSubmodules is not already set, set it to `all`.
Josip Sokcevic84c0bba2023-08-10 00:52:15 +0000576 config = subprocess2.capture(
577 ['git', 'config', '-l'],
578 cwd=args[0].checkout_path).decode('utf-8').strip().splitlines()
Joanna Wangd9ea0722023-08-18 20:05:53 +0000579 if 'diff.ignoresubmodules=dirty' not in config:
Joanna Wang4e6c1072023-08-17 18:46:24 +0000580 subprocess2.capture(
Joanna Wangd9ea0722023-08-18 20:05:53 +0000581 ['git', 'config', 'diff.ignoreSubmodules', 'dirty'],
Joanna Wang4e6c1072023-08-17 18:46:24 +0000582 cwd=args[0].checkout_path)
Josip Sokcevic84c0bba2023-08-10 00:52:15 +0000583 if 'fetch.recursesubmodules=off' not in config:
584 subprocess2.capture(
585 ['git', 'config', 'fetch.recurseSubmodules', 'off'],
Joanna Wang4e6c1072023-08-17 18:46:24 +0000586 cwd=args[0].checkout_path)
Joanna Wange1753f62023-06-26 14:32:43 +0000587 return return_val
588
589 return wrapper
590
591 @set_config
msb@chromium.orge28e4982009-09-25 20:51:45 +0000592 def update(self, options, args, file_list):
593 """Runs git to update or transparently checkout the working copy.
594
595 All updated files will be appended to file_list.
596
597 Raises:
598 Error: if can't get URL for relative path.
599 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000600 if args:
601 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
602
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000603 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000604
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000605 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000606 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000607 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000608 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000609 # Override the revision number.
610 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000611 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000612 # Check again for a revision in case an initial ref was specified
613 # in the url, for example bla.git@refs/heads/custombranch
614 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000615 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000616 if not revision:
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000617 # If a dependency is not pinned, track the default remote branch.
618 revision = scm.GIT.GetRemoteHeadRef(self.checkout_path, self.url,
619 self.remote)
Edward Lesmes4ea67bb2021-04-20 17:33:52 +0000620 if revision.startswith('origin/'):
621 revision = 'refs/remotes/' + revision
msb@chromium.orge28e4982009-09-25 20:51:45 +0000622
Tomasz Wiszkowskid4e66882021-08-19 21:35:09 +0000623 if managed and platform.system() == 'Windows':
szager@chromium.org8a139702014-06-20 15:55:01 +0000624 self._DisableHooks()
625
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000626 printed_path = False
627 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000628 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700629 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000630 verbose = ['--verbose']
631 printed_path = True
632
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000633 revision_ref = revision
634 if ':' in revision:
635 revision_ref, _, revision = revision.partition(':')
636
Edward Lesmes8073a502020-04-15 02:11:14 +0000637 if revision_ref.startswith('refs/branch-heads'):
638 options.with_branch_heads = True
639
Edward Lesmes07a68342021-04-20 23:39:30 +0000640 mirror = self._GetMirror(url, options, revision, revision_ref)
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000641 if mirror:
642 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000643
John Budorick882c91e2018-07-12 22:11:41 +0000644 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
645 if remote_ref:
646 # Rewrite remote refs to their local equivalents.
647 revision = ''.join(remote_ref)
648 rev_type = "branch"
649 elif revision.startswith('refs/'):
650 # Local branch? We probably don't want to support, since DEPS should
651 # always specify branches as they are in the upstream repo.
652 rev_type = "branch"
653 else:
654 # hash is also a tag, only make a distinction at checkout
655 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000656
primiano@chromium.org1c127382015-02-17 11:15:40 +0000657 # If we are going to introduce a new project, there is a possibility that
658 # we are syncing back to a state where the project was originally a
659 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
660 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
661 # In such case, we might have a backup of the former .git folder, which can
662 # be used to avoid re-fetching the entire repo again (useful for bisects).
663 backup_dir = self.GetGitBackupDirPath()
664 target_dir = os.path.join(self.checkout_path, '.git')
665 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
666 gclient_utils.safe_makedirs(self.checkout_path)
667 os.rename(backup_dir, target_dir)
668 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800669 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000670
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000671 if (not os.path.exists(self.checkout_path) or
672 (os.path.isdir(self.checkout_path) and
673 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000674 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000675 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000676 try:
John Budorick882c91e2018-07-12 22:11:41 +0000677 self._Clone(revision, url, options)
Joanna Wang47fd5672022-08-05 20:53:31 +0000678 except subprocess2.CalledProcessError as e:
679 logging.warning('Clone failed due to: %s', e)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000680 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000681 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000682 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800683 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000684 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000685 file_list.extend(
Edward Lemur979fa782019-08-13 22:44:05 +0000686 [os.path.join(self.checkout_path, f) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000687 if mirror:
688 self._Capture(
689 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000690 if not verbose:
691 # Make the output a little prettier. It's nice to have some whitespace
692 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000693 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000694 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000695
John Budorick21a51b32018-09-19 19:39:20 +0000696 if mirror:
697 self._Capture(
698 ['remote', 'set-url', '--push', 'origin', mirror.url])
699
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000700 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000701 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000702 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
703 return self._Capture(['rev-parse', '--verify', 'HEAD'])
704
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000705 self._maybe_break_locks(options)
706
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000707 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000708 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000709
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000710 # See if the url has changed (the unittests use git://foo for the url, let
711 # that through).
712 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
713 return_early = False
714 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
715 # unit test pass. (and update the comment above)
716 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
717 # This allows devs to use experimental repos which have a different url
718 # but whose branch(s) are the same as official repos.
Raul Tambrecd862e32019-05-10 21:19:00 +0000719 if (current_url.rstrip('/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000720 subprocess2.capture(
Aaron Gableac9b0f32019-04-18 17:38:37 +0000721 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000722 cwd=self.checkout_path).strip() != 'False'):
Anthony Polito486f1812020-08-04 23:40:33 +0000723 self.Print('_____ switching %s from %s to new upstream %s' % (
724 self.relpath, current_url, url))
iannucci@chromium.org78514212014-08-20 23:08:00 +0000725 if not (options.force or options.reset):
726 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700727 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000728 # Switch over to the new upstream
729 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000730 if mirror:
Aravind Vasudevancf465852023-03-29 16:47:12 +0000731 if git_cache.Mirror.CacheDirToUrl(
732 current_url.rstrip('/')) == git_cache.Mirror.CacheDirToUrl(
733 url.rstrip('/')):
734 # Reset alternates when the cache dir is updated.
735 with open(
736 os.path.join(self.checkout_path, '.git', 'objects', 'info',
737 'alternates'), 'w') as fh:
738 fh.write(os.path.join(url, 'objects'))
739 else:
740 # Because we use Git alternatives, our existing repository is not
741 # self-contained. It's possible that new git alternative doesn't have
742 # all necessary objects that the current repository needs. Instead of
743 # blindly hoping that new alternative contains all necessary objects,
744 # keep the old alternative and just append a new one on top of it.
745 with open(
746 os.path.join(self.checkout_path, '.git', 'objects', 'info',
747 'alternates'), 'a') as fh:
748 fh.write("\n" + os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000749 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
750 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000751
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000752 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000753 else:
John Budorick882c91e2018-07-12 22:11:41 +0000754 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000755
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000756 if return_early:
757 return self._Capture(['rev-parse', '--verify', 'HEAD'])
758
msb@chromium.org5bde4852009-12-14 16:47:12 +0000759 cur_branch = self._GetCurrentBranch()
760
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000761 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000762 # 0) HEAD is detached. Probably from our initial clone.
763 # - make sure HEAD is contained by a named ref, then update.
764 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700765 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000766 # - try to rebase onto the new hash or branch
767 # 2) current branch is tracking a remote branch with local committed
768 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000769 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000770 # 3) current branch is tracking a remote branch w/or w/out changes, and
771 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000772 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000773 # 4) current branch is tracking a remote branch, but DEPS switches to a
774 # different remote branch, and
775 # a) current branch has no local changes, and --force:
776 # - checkout new branch
777 # b) current branch has local changes, and --force and --reset:
778 # - checkout new branch
779 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000780
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000781 # GetUpstreamBranch returns something like 'refs/remotes/origin/main' for
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000782 # a tracking branch
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000783 # or 'main' if not a tracking branch (it's based on a specific rev/hash)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000784 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000785 if cur_branch is None:
786 upstream_branch = None
787 current_type = "detached"
788 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000789 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000790 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
791 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
792 current_type = "hash"
793 logging.debug("Current branch is not tracking an upstream (remote)"
794 " branch.")
795 elif upstream_branch.startswith('refs/remotes'):
796 current_type = "branch"
797 else:
798 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000799
Edward Lemur579c9862018-07-13 23:17:51 +0000800 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000801
Michael Spang73fac912019-03-08 18:44:19 +0000802 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000803 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000804 self._Fetch(options, prune=options.force)
805
806 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
807 sha_only=True):
808 # Update the remotes first so we have all the refs.
809 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
810 cwd=self.checkout_path)
811 if verbose:
812 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000813
John Budorick882c91e2018-07-12 22:11:41 +0000814 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200815
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000816 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000817 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000818 target = 'HEAD'
819 if options.upstream and upstream_branch:
820 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800821 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000822
msb@chromium.org786fb682010-06-02 15:16:23 +0000823 if current_type == 'detached':
824 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800825 # We just did a Scrub, this is as clean as it's going to get. In
826 # particular if HEAD is a commit that contains two versions of the same
827 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
828 # to actually "Clean" the checkout; that commit is uncheckoutable on this
829 # system. The best we can do is carry forward to the checkout step.
830 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000831 self._CheckClean(revision)
832 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000833 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000834 self.Print('Up-to-date; skipping checkout.')
835 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000836 # 'git checkout' may need to overwrite existing untracked files. Allow
837 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000838 self._Checkout(
839 options,
John Budorick882c91e2018-07-12 22:11:41 +0000840 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000841 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000842 quiet=True,
843 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000844 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000845 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000846 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000847 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700848 # Can't find a merge-base since we don't know our upstream. That makes
849 # this command VERY likely to produce a rebase failure. For now we
850 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000851 upstream_branch = self.remote
852 if options.revision or deps_revision:
853 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700854 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700855 printed_path=printed_path, merge=options.merge)
856 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000857 elif rev_type == 'hash':
858 # case 2
859 self._AttemptRebase(upstream_branch, file_list, options,
860 newbase=revision, printed_path=printed_path,
861 merge=options.merge)
862 printed_path = True
863 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000864 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000865 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000866 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000867 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000868 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000869 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000870 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000871 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
872 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000873 force_switch = False
874 if options.force:
875 try:
John Budorick882c91e2018-07-12 22:11:41 +0000876 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000877 # case 4a
878 force_switch = True
879 except gclient_utils.Error as e:
880 if options.reset:
881 # case 4b
882 force_switch = True
883 else:
884 switch_error = '%s\n%s' % (e.message, switch_error)
885 if force_switch:
886 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000887 (upstream_branch, new_base))
888 switch_branch = 'gclient_' + remote_ref[1]
889 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000890 self._Checkout(options, switch_branch, force=True, quiet=True)
891 else:
892 # case 4c
893 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000894 else:
895 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800896 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000897 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000898 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000899 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000900 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000901 if options.merge:
902 merge_args.append('--ff')
903 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000904 merge_args.append('--ff-only')
905 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000906 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000907 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700908 rebase_files = []
Edward Lemur979fa782019-08-13 22:44:05 +0000909 if re.match(b'fatal: Not possible to fast-forward, aborting.',
910 e.stderr):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000911 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000912 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700913 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000914 printed_path = True
915 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000916 if not options.auto_rebase:
917 try:
918 action = self._AskForData(
919 'Cannot %s, attempt to rebase? '
920 '(y)es / (q)uit / (s)kip : ' %
921 ('merge' if options.merge else 'fast-forward merge'),
922 options)
923 except ValueError:
924 raise gclient_utils.Error('Invalid Character')
925 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700926 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000927 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000928 printed_path = True
929 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000930
931 if re.match(r'quit|q', action, re.I):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000932 raise gclient_utils.Error("Can't fast-forward, please merge or "
933 "rebase manually.\n"
934 "cd %s && git " % self.checkout_path
935 + "rebase %s" % upstream_branch)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000936
937 if re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000938 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000939 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000940
941 self.Print('Input not recognized')
Edward Lemur979fa782019-08-13 22:44:05 +0000942 elif re.match(b"error: Your local changes to '.*' would be "
943 b"overwritten by merge. Aborting.\nPlease, commit your "
944 b"changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000945 e.stderr):
946 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000947 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700948 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000949 printed_path = True
Edward Lemur979fa782019-08-13 22:44:05 +0000950 raise gclient_utils.Error(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000951 else:
952 # Some other problem happened with the merge
953 logging.error("Error during fast-forward merge in %s!" % self.relpath)
Edward Lemur979fa782019-08-13 22:44:05 +0000954 self.Print(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000955 raise
956 else:
957 # Fast-forward merge was successful
958 if not re.match('Already up-to-date.', merge_output) or verbose:
959 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700960 self.Print('_____ %s at %s' % (self.relpath, revision),
961 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000962 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000963 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000964 if not verbose:
965 # Make the output a little prettier. It's nice to have some
966 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000967 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000968
agablec3937b92016-10-25 10:13:03 -0700969 if file_list is not None:
970 file_list.extend(
971 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000972
973 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000974 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700975 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000976 '\nConflict while rebasing this branch.\n'
977 'Fix the conflict and run gclient again.\n'
978 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700979 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000980
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000981 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000982 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
983 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000984
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000985 # If --reset and --delete_unversioned_trees are specified, remove any
986 # untracked directories.
987 if options.reset and options.delete_unversioned_trees:
988 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
989 # merge-base by default), so doesn't include untracked files. So we use
990 # 'git ls-files --directory --others --exclude-standard' here directly.
991 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800992 ['-c', 'core.quotePath=false', 'ls-files',
993 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000994 self.checkout_path)
995 for path in (p for p in paths.splitlines() if p.endswith('/')):
996 full_path = os.path.join(self.checkout_path, path)
997 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000998 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000999 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +00001000
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00001001 return self._Capture(['rev-parse', '--verify', 'HEAD'])
1002
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001003 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +00001004 """Reverts local modifications.
1005
1006 All reverted files will be appended to file_list.
1007 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +00001008 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +00001009 # revert won't work if the directory doesn't exist. It needs to
1010 # checkout instead.
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001011 self.Print('_____ %s is missing, syncing instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +00001012 # Don't reuse the args.
1013 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001014
Josip Sokcevic7e133ff2021-07-13 17:44:53 +00001015 default_rev = "refs/heads/main"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +00001016 if options.upstream:
1017 if self._GetCurrentBranch():
1018 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
1019 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001020 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001021 if not deps_revision:
1022 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +00001023 if deps_revision.startswith('refs/heads/'):
1024 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -07001025 try:
1026 deps_revision = self.GetUsableRev(deps_revision, options)
1027 except NoUsableRevError as e:
1028 # If the DEPS entry's url and hash changed, try to update the origin.
1029 # See also http://crbug.com/520067.
John Budorickd94f8ea2020-03-27 15:55:24 +00001030 logging.warning(
1031 "Couldn't find usable revision, will retrying to update instead: %s",
smutae7ea312016-07-18 11:59:41 -07001032 e.message)
1033 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001034
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001035 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001036 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001037
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001038 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +00001039 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001040
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001041 if file_list is not None:
1042 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
1043
1044 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001045 """Returns revision"""
1046 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +00001047
msb@chromium.orge28e4982009-09-25 20:51:45 +00001048 def runhooks(self, options, args, file_list):
1049 self.status(options, args, file_list)
1050
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001051 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +00001052 """Display status information."""
1053 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001054 self.Print('________ couldn\'t run status in %s:\n'
1055 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001056 else:
Anthony Politobb457342019-11-15 22:26:01 +00001057 merge_base = []
1058 if self.url:
1059 _, base_rev = gclient_utils.SplitUrlRevision(self.url)
1060 if base_rev:
Joanna Wanga654ff32023-07-18 23:25:19 +00001061 if base_rev.startswith('refs/'):
Kenneth Russell02e70b42023-08-07 22:09:29 +00001062 base_rev = self._ref_to_remote_ref(base_rev)
Anthony Politobb457342019-11-15 22:26:01 +00001063 merge_base = [base_rev]
Aaron Gablef4068aa2017-12-12 15:14:09 -08001064 self._Run(
1065 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
Edward Lemur24146be2019-08-01 21:44:52 +00001066 options, always_show_header=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001067 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001068 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001069 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +00001070
smutae7ea312016-07-18 11:59:41 -07001071 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -07001072 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -07001073 sha1 = None
1074 if not os.path.isdir(self.checkout_path):
1075 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -08001076 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -07001077
1078 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1079 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001080 else:
agable41e3a6c2016-10-20 11:36:56 -07001081 # May exist in origin, but we don't have it yet, so fetch and look
1082 # again.
1083 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -07001084 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1085 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001086
1087 if not sha1:
1088 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -08001089 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -07001090
1091 return sha1
1092
primiano@chromium.org1c127382015-02-17 11:15:40 +00001093 def GetGitBackupDirPath(self):
1094 """Returns the path where the .git folder for the current project can be
1095 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
1096 return os.path.join(self._root_dir,
1097 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
1098
Edward Lesmes07a68342021-04-20 23:39:30 +00001099 def _GetMirror(self, url, options, revision=None, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001100 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +00001101 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001102 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +00001103 mirror_kwargs = {
1104 'print_func': self.filter,
Edward Lesmes07a68342021-04-20 23:39:30 +00001105 'refs': [],
1106 'commits': [],
hinoka@google.comb1b54572014-04-16 22:29:23 +00001107 }
hinoka@google.comb1b54572014-04-16 22:29:23 +00001108 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1109 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001110 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
1111 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001112 if hasattr(options, 'with_tags') and options.with_tags:
1113 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001114 elif revision_ref and revision_ref.startswith('refs/tags/'):
1115 mirror_kwargs['refs'].append(revision_ref)
Edward Lesmes07a68342021-04-20 23:39:30 +00001116 if revision and not revision.startswith('refs/'):
1117 mirror_kwargs['commits'].append(revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001118 return git_cache.Mirror(url, **mirror_kwargs)
1119
John Budorick882c91e2018-07-12 22:11:41 +00001120 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001121 """Update a git mirror by fetching the latest commits from the remote,
1122 unless mirror already contains revision whose type is sha1 hash.
1123 """
John Budorick882c91e2018-07-12 22:11:41 +00001124 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001125 if options.verbose:
1126 self.Print('skipping mirror update, it has rev=%s already' % revision,
1127 timestamp=False)
1128 return
1129
szager@chromium.org3ec84f62014-08-22 21:00:22 +00001130 if getattr(options, 'shallow', False):
Victor Vianna392ce7e2022-06-07 21:47:51 +00001131 depth = 10000
hinoka@chromium.org46b87412014-05-15 00:42:05 +00001132 else:
1133 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +00001134 mirror.populate(verbose=options.verbose,
1135 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +00001136 depth=depth,
Vadim Shtayura08049e22017-10-11 00:14:52 +00001137 lock_timeout=getattr(options, 'lock_timeout', 0))
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001138
John Budorick882c91e2018-07-12 22:11:41 +00001139 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001140 """Clone a git repository from the given URL.
1141
msb@chromium.org786fb682010-06-02 15:16:23 +00001142 Once we've cloned the repo, we checkout a working branch if the specified
1143 revision is a branch head. If it is a tag or a specific commit, then we
1144 leave HEAD detached as it makes future updates simpler -- in this case the
1145 user should first create a new branch or switch to an existing branch before
1146 making changes in the repo."""
Joanna Wang1a977bd2022-06-02 21:51:17 +00001147 in_cog_workspace = self._IsCog()
1148
1149 if self.print_outbuf:
1150 print_stdout = True
1151 filter_fn = None
1152 else:
1153 print_stdout = False
1154 filter_fn = self.filter
1155
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001156 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001157 # git clone doesn't seem to insert a newline properly before printing
1158 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001159 self.Print('')
Joanna Wang1a977bd2022-06-02 21:51:17 +00001160
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001161 # If the parent directory does not exist, Git clone on Windows will not
1162 # create it, so we need to do it manually.
1163 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001164 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001165
Joanna Wang1a977bd2022-06-02 21:51:17 +00001166 if in_cog_workspace:
1167 clone_cmd = ['citc', 'clone-repo', url, self.checkout_path]
1168 clone_cmd.append(
1169 gclient_utils.ExtractRefName(self.remote, revision) or revision)
1170 try:
1171 self._Run(clone_cmd,
1172 options,
1173 cwd=self._root_dir,
1174 retry=True,
1175 print_stdout=print_stdout,
1176 filter_fn=filter_fn)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001177 except:
1178 traceback.print_exc(file=self.out_fh)
1179 raise
1180 self._SetFetchConfig(options)
Travis Lane738b48a2022-11-28 20:28:51 +00001181 elif hasattr(options, 'no_history') and options.no_history:
1182 self._Run(['init', self.checkout_path], options, cwd=self._root_dir)
1183 self._Run(['remote', 'add', 'origin', url], options)
1184 revision = self._AutoFetchRef(options, revision, depth=1)
1185 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1186 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001187 else:
1188 cfg = gclient_utils.DefaultIndexPackConfig(url)
1189 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
1190 if self.cache_dir:
1191 clone_cmd.append('--shared')
1192 if options.verbose:
1193 clone_cmd.append('--verbose')
1194 clone_cmd.append(url)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001195 tmp_dir = tempfile.mkdtemp(prefix='_gclient_%s_' %
1196 os.path.basename(self.checkout_path),
1197 dir=parent_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001198 clone_cmd.append(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001199
1200 try:
1201 self._Run(clone_cmd,
1202 options,
1203 cwd=self._root_dir,
1204 retry=True,
1205 print_stdout=print_stdout,
1206 filter_fn=filter_fn)
Joanna Wang47fd5672022-08-05 20:53:31 +00001207 logging.debug('Cloned into temporary dir, moving to checkout_path')
Joanna Wang1a977bd2022-06-02 21:51:17 +00001208 gclient_utils.safe_makedirs(self.checkout_path)
1209 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1210 os.path.join(self.checkout_path, '.git'))
1211 except:
1212 traceback.print_exc(file=self.out_fh)
1213 raise
1214 finally:
1215 if os.listdir(tmp_dir):
1216 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
Josip Sokcevicebccac72022-06-24 21:44:49 +00001217 gclient_utils.rmtree(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001218
1219 self._SetFetchConfig(options)
1220 self._Fetch(options, prune=options.force)
1221 revision = self._AutoFetchRef(options, revision)
1222 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1223 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
1224
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001225 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001226 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001227 self.Print(
Joanna Wang1a977bd2022-06-02 21:51:17 +00001228 ('Checked out %s to a detached HEAD. Before making any commits\n'
1229 'in this repo, you should use \'git checkout <branch>\' to switch \n'
1230 'to an existing branch or use \'git checkout %s -b <branch>\' to\n'
1231 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001232
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001233 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001234 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001235 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001236 raise gclient_utils.Error("Background task requires input. Rerun "
1237 "gclient with --jobs=1 so that\n"
1238 "interaction is possible.")
Edward Lesmesae3586b2020-03-23 21:21:14 +00001239 return gclient_utils.AskForData(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001240
1241
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001242 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001243 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001244 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001245 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001246 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001247 revision = upstream
1248 if newbase:
1249 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001250 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001251 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001252 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001253 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001254 printed_path = True
1255 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001256 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001257
1258 if merge:
1259 merge_output = self._Capture(['merge', revision])
1260 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001261 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001262 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001263
1264 # Build the rebase command here using the args
1265 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1266 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001267 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001268 rebase_cmd.append('--verbose')
1269 if newbase:
1270 rebase_cmd.extend(['--onto', newbase])
1271 rebase_cmd.append(upstream)
1272 if branch:
1273 rebase_cmd.append(branch)
1274
1275 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001276 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001277 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001278 if (re.match(br'cannot rebase: you have unstaged changes', e.stderr) or
1279 re.match(br'cannot rebase: your index contains uncommitted changes',
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001280 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001281 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001282 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001283 'Cannot rebase because of unstaged changes.\n'
1284 '\'git reset --hard HEAD\' ?\n'
1285 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001286 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001287 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001288 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001289 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001290 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001291 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001292
1293 if re.match(r'quit|q', rebase_action, re.I):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001294 raise gclient_utils.Error("Please merge or rebase manually\n"
1295 "cd %s && git " % self.checkout_path
1296 + "%s" % ' '.join(rebase_cmd))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001297
1298 if re.match(r'show|s', rebase_action, re.I):
Edward Lemur979fa782019-08-13 22:44:05 +00001299 self.Print('%s' % e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001300 continue
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001301
1302 gclient_utils.Error("Input not recognized")
1303 continue
Edward Lemur979fa782019-08-13 22:44:05 +00001304 elif re.search(br'^CONFLICT', e.stdout, re.M):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001305 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1306 "Fix the conflict and run gclient again.\n"
1307 "See 'man git-rebase' for details.\n")
1308 else:
Edward Lemur979fa782019-08-13 22:44:05 +00001309 self.Print(e.stdout.decode('utf-8').strip())
1310 self.Print('Rebase produced error output:\n%s' %
1311 e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001312 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1313 "manually.\ncd %s && git " %
1314 self.checkout_path
1315 + "%s" % ' '.join(rebase_cmd))
1316
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001317 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001318 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001319 # Make the output a little prettier. It's nice to have some
1320 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001321 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001322
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001323 @staticmethod
1324 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001325 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1326 if not ok:
1327 raise gclient_utils.Error('git version %s < minimum required %s' %
1328 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001329
John Budorick882c91e2018-07-12 22:11:41 +00001330 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001331 # Special case handling if all 3 conditions are met:
1332 # * the mirros have recently changed, but deps destination remains same,
1333 # * the git histories of mirrors are conflicting.
1334 # * git cache is used
1335 # This manifests itself in current checkout having invalid HEAD commit on
1336 # most git operations. Since git cache is used, just deleted the .git
1337 # folder, and re-create it by cloning.
1338 try:
1339 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1340 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001341 if (b'fatal: bad object HEAD' in e.stderr
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001342 and self.cache_dir and self.cache_dir in url):
1343 self.Print((
1344 'Likely due to DEPS change with git cache_dir, '
1345 'the current commit points to no longer existing object.\n'
1346 '%s' % e)
1347 )
1348 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001349 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001350 else:
1351 raise
1352
msb@chromium.org786fb682010-06-02 15:16:23 +00001353 def _IsRebasing(self):
1354 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1355 # have a plumbing command to determine whether a rebase is in progress, so
1356 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1357 g = os.path.join(self.checkout_path, '.git')
1358 return (
1359 os.path.isdir(os.path.join(g, "rebase-merge")) or
1360 os.path.isdir(os.path.join(g, "rebase-apply")))
1361
Aravind Vasudevaned935cf2023-08-24 23:52:20 +00001362 def _CheckClean(self, revision):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001363 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1364 if os.path.exists(lockfile):
1365 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001366 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001367 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1368 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001369 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001370
msb@chromium.org786fb682010-06-02 15:16:23 +00001371 # Make sure the tree is clean; see git-rebase.sh for reference
1372 try:
1373 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001374 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001375 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001376 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001377 '\tYou have unstaged changes.\n'
1378 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001379 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001380 try:
1381 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001382 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001383 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001384 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001385 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001386 '\tYour index contains uncommitted changes\n'
1387 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001388 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001389
agable83faed02016-10-24 14:37:10 -07001390 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001391 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1392 # reference by a commit). If not, error out -- most likely a rebase is
1393 # in progress, try to detect so we can give a better error.
1394 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001395 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1396 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001397 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001398 # Commit is not contained by any rev. See if the user is rebasing:
1399 if self._IsRebasing():
1400 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001401 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001402 '\tAlready in a conflict, i.e. (no branch).\n'
1403 '\tFix the conflict and run gclient again.\n'
1404 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1405 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001406 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001407 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001408 name = ('saved-by-gclient-' +
1409 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001410 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001411 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001412 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001413
msb@chromium.org5bde4852009-12-14 16:47:12 +00001414 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001415 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001416 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001417 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001418 return None
1419 return branch
1420
borenet@google.comc3e09d22014-04-10 13:58:18 +00001421 def _Capture(self, args, **kwargs):
Mike Frysinger286fb162019-09-30 03:14:10 +00001422 set_git_dir = 'cwd' not in kwargs
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001423 kwargs.setdefault('cwd', self.checkout_path)
1424 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001425 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001426 env = scm.GIT.ApplyEnvVars(kwargs)
Mike Frysinger286fb162019-09-30 03:14:10 +00001427 # If an explicit cwd isn't set, then default to the .git/ subdir so we get
1428 # stricter behavior. This can be useful in cases of slight corruption --
1429 # we don't accidentally go corrupting parent git checks too. See
1430 # https://crbug.com/1000825 for an example.
1431 if set_git_dir:
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001432 git_dir = os.path.abspath(os.path.join(self.checkout_path, '.git'))
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001433 # Depending on how the .gclient file was defined, self.checkout_path
1434 # might be set to a unicode string, not a regular string; on Windows
1435 # Python2, we can't set env vars to be unicode strings, so we
1436 # forcibly cast the value to a string before setting it.
1437 env.setdefault('GIT_DIR', str(git_dir))
Raul Tambrecd862e32019-05-10 21:19:00 +00001438 ret = subprocess2.check_output(
1439 ['git'] + args, env=env, **kwargs).decode('utf-8')
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001440 if strip:
1441 ret = ret.strip()
Erik Chene16ffff2019-10-14 20:35:53 +00001442 self.Print('Finished running: %s %s' % ('git', ' '.join(args)))
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001443 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001444
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001445 def _Checkout(self, options, ref, force=False, quiet=None):
1446 """Performs a 'git-checkout' operation.
1447
1448 Args:
1449 options: The configured option set
1450 ref: (str) The branch/commit to checkout
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001451 quiet: (bool/None) Whether or not the checkout should pass '--quiet'; if
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001452 'None', the behavior is inferred from 'options.verbose'.
1453 Returns: (str) The output of the checkout operation
1454 """
1455 if quiet is None:
1456 quiet = (not options.verbose)
1457 checkout_args = ['checkout']
1458 if force:
1459 checkout_args.append('--force')
1460 if quiet:
1461 checkout_args.append('--quiet')
1462 checkout_args.append(ref)
1463 return self._Capture(checkout_args)
1464
Travis Lane738b48a2022-11-28 20:28:51 +00001465 def _Fetch(self,
1466 options,
1467 remote=None,
1468 prune=False,
1469 quiet=False,
1470 refspec=None,
1471 depth=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001472 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001473 # When updating, the ref is modified to be a remote ref .
1474 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1475 # Try to reverse that mapping.
1476 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1477 if original_ref:
1478 refspec = original_ref + ':' + refspec
1479 # When a mirror is configured, it only fetches
1480 # refs/{heads,branch-heads,tags}/*.
1481 # If asked to fetch other refs, we must fetch those directly from the
1482 # repository, and not from the mirror.
1483 if not original_ref.startswith(
1484 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1485 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001486 fetch_cmd = cfg + [
1487 'fetch',
1488 remote or self.remote,
1489 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001490 if refspec:
1491 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001492
1493 if prune:
1494 fetch_cmd.append('--prune')
1495 if options.verbose:
1496 fetch_cmd.append('--verbose')
danakjd5c0b562019-11-08 17:27:47 +00001497 if not hasattr(options, 'with_tags') or not options.with_tags:
1498 fetch_cmd.append('--no-tags')
dnj@chromium.org680f2172014-06-25 00:39:32 +00001499 elif quiet:
1500 fetch_cmd.append('--quiet')
Travis Lane738b48a2022-11-28 20:28:51 +00001501 if depth:
1502 fetch_cmd.append('--depth=' + str(depth))
tandrii64103db2016-10-11 05:30:05 -07001503 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001504
Edward Lemur579c9862018-07-13 23:17:51 +00001505 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001506 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1507 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001508 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001509 try:
1510 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1511 options)
1512 self._Run(['config', 'remote.%s.fetch' % self.remote,
1513 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1514 except subprocess2.CalledProcessError as e:
1515 # If exit code was 5, it means we attempted to unset a config that
1516 # didn't exist. Ignore it.
1517 if e.returncode != 5:
1518 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001519 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001520 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001521 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1522 '^\\+refs/branch-heads/\\*:.*$']
1523 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001524 if hasattr(options, 'with_tags') and options.with_tags:
1525 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1526 '+refs/tags/*:refs/tags/*',
1527 '^\\+refs/tags/\\*:.*$']
1528 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001529
Travis Lane738b48a2022-11-28 20:28:51 +00001530 def _AutoFetchRef(self, options, revision, depth=None):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001531 """Attempts to fetch |revision| if not available in local repo.
1532
1533 Returns possibly updated revision."""
Edward Lemure0ba7b82020-03-11 20:31:32 +00001534 if not scm.GIT.IsValidRevision(self.checkout_path, revision):
Travis Lane738b48a2022-11-28 20:28:51 +00001535 self._Fetch(options, refspec=revision, depth=depth)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001536 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1537 return revision
1538
Edward Lemur24146be2019-08-01 21:44:52 +00001539 def _Run(self, args, options, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001540 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001541 kwargs.setdefault('cwd', self.checkout_path)
Edward Lemur24146be2019-08-01 21:44:52 +00001542 kwargs.setdefault('filter_fn', self.filter)
1543 kwargs.setdefault('show_header', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001544 env = scm.GIT.ApplyEnvVars(kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +00001545
agable@chromium.org772efaf2014-04-01 02:35:44 +00001546 cmd = ['git'] + args
Edward Lemur24146be2019-08-01 21:44:52 +00001547 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001548
1549
1550class CipdPackage(object):
1551 """A representation of a single CIPD package."""
1552
John Budorickd3ba72b2018-03-20 12:27:42 -07001553 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001554 self._authority_for_subdir = authority_for_subdir
1555 self._name = name
1556 self._version = version
1557
1558 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001559 def authority_for_subdir(self):
1560 """Whether this package has authority to act on behalf of its subdir.
1561
1562 Some operations should only be performed once per subdirectory. A package
1563 that has authority for its subdirectory is the only package that should
1564 perform such operations.
1565
1566 Returns:
1567 bool; whether this package has subdir authority.
1568 """
1569 return self._authority_for_subdir
1570
1571 @property
1572 def name(self):
1573 return self._name
1574
1575 @property
1576 def version(self):
1577 return self._version
1578
1579
1580class CipdRoot(object):
1581 """A representation of a single CIPD root."""
1582 def __init__(self, root_dir, service_url):
1583 self._all_packages = set()
1584 self._mutator_lock = threading.Lock()
1585 self._packages_by_subdir = collections.defaultdict(list)
1586 self._root_dir = root_dir
1587 self._service_url = service_url
Dan Le Febvre456d0852023-05-24 23:43:40 +00001588 self._resolved_packages = None
John Budorick0f7b2002018-01-19 15:46:17 -08001589
1590 def add_package(self, subdir, package, version):
1591 """Adds a package to this CIPD root.
1592
1593 As far as clients are concerned, this grants both root and subdir authority
1594 to packages arbitrarily. (The implementation grants root authority to the
1595 first package added and subdir authority to the first package added for that
1596 subdir, but clients should not depend on or expect that behavior.)
1597
1598 Args:
1599 subdir: str; relative path to where the package should be installed from
1600 the cipd root directory.
1601 package: str; the cipd package name.
1602 version: str; the cipd package version.
1603 Returns:
1604 CipdPackage; the package that was created and added to this root.
1605 """
1606 with self._mutator_lock:
1607 cipd_package = CipdPackage(
1608 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001609 not self._packages_by_subdir[subdir])
1610 self._all_packages.add(cipd_package)
1611 self._packages_by_subdir[subdir].append(cipd_package)
1612 return cipd_package
1613
1614 def packages(self, subdir):
1615 """Get the list of configured packages for the given subdir."""
1616 return list(self._packages_by_subdir[subdir])
1617
Dan Le Febvre456d0852023-05-24 23:43:40 +00001618 def resolved_packages(self):
1619 if not self._resolved_packages:
1620 self._resolved_packages = self.ensure_file_resolve()
1621 return self._resolved_packages
1622
John Budorick0f7b2002018-01-19 15:46:17 -08001623 def clobber(self):
1624 """Remove the .cipd directory.
1625
1626 This is useful for forcing ensure to redownload and reinitialize all
1627 packages.
1628 """
1629 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001630 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001631 try:
1632 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1633 except OSError:
1634 if os.path.exists(cipd_cache_dir):
1635 raise
1636
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001637 def expand_package_name(self, package_name_string, **kwargs):
1638 """Run `cipd expand-package-name`.
1639
1640 CIPD package names can be declared with placeholder variables
1641 such as '${platform}', this cmd will return the package name
1642 with the variables resolved. The resolution is based on the host
1643 the command is executing on.
1644 """
1645
1646 kwargs.setdefault('stderr', subprocess2.PIPE)
1647 cmd = ['cipd', 'expand-package-name', package_name_string]
1648 ret = subprocess2.check_output(cmd, **kwargs).decode('utf-8')
1649 return ret.strip()
1650
John Budorick0f7b2002018-01-19 15:46:17 -08001651 @contextlib.contextmanager
1652 def _create_ensure_file(self):
1653 try:
Stephanie Kim700aee72022-06-01 19:58:30 +00001654 contents = '$ParanoidMode CheckPresence\n'
1655 # TODO(crbug/1329641): Remove once cipd packages have been updated
1656 # to always be created in copy mode.
1657 contents += '$OverrideInstallMode copy\n\n'
Edward Lesmes05934952019-12-19 20:38:09 +00001658 for subdir, packages in sorted(self._packages_by_subdir.items()):
1659 contents += '@Subdir %s\n' % subdir
1660 for package in sorted(packages, key=lambda p: p.name):
1661 contents += '%s %s\n' % (package.name, package.version)
1662 contents += '\n'
John Budorick0f7b2002018-01-19 15:46:17 -08001663 ensure_file = None
1664 with tempfile.NamedTemporaryFile(
Edward Lesmes05934952019-12-19 20:38:09 +00001665 suffix='.ensure', delete=False, mode='wb') as ensure_file:
1666 ensure_file.write(contents.encode('utf-8', 'replace'))
John Budorick0f7b2002018-01-19 15:46:17 -08001667 yield ensure_file.name
1668 finally:
1669 if ensure_file is not None and os.path.exists(ensure_file.name):
1670 os.remove(ensure_file.name)
1671
1672 def ensure(self):
1673 """Run `cipd ensure`."""
1674 with self._mutator_lock:
1675 with self._create_ensure_file() as ensure_file:
1676 cmd = [
1677 'cipd', 'ensure',
1678 '-log-level', 'error',
1679 '-root', self.root_dir,
1680 '-ensure-file', ensure_file,
1681 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001682 gclient_utils.CheckCallAndFilter(
1683 cmd, print_stdout=True, show_header=True)
John Budorick0f7b2002018-01-19 15:46:17 -08001684
Dan Le Febvre456d0852023-05-24 23:43:40 +00001685 @contextlib.contextmanager
1686 def _create_ensure_file_for_resolve(self):
1687 try:
1688 contents = '$ResolvedVersions %s\n' % os.devnull
1689 for subdir, packages in sorted(self._packages_by_subdir.items()):
1690 contents += '@Subdir %s\n' % subdir
1691 for package in sorted(packages, key=lambda p: p.name):
1692 contents += '%s %s\n' % (package.name, package.version)
1693 contents += '\n'
1694 ensure_file = None
1695 with tempfile.NamedTemporaryFile(suffix='.ensure',
1696 delete=False,
1697 mode='wb') as ensure_file:
1698 ensure_file.write(contents.encode('utf-8', 'replace'))
1699 yield ensure_file.name
1700 finally:
1701 if ensure_file is not None and os.path.exists(ensure_file.name):
1702 os.remove(ensure_file.name)
1703
1704 def _create_resolved_file(self):
1705 return tempfile.NamedTemporaryFile(suffix='.resolved',
1706 delete=False,
1707 mode='wb')
1708
1709 def ensure_file_resolve(self):
1710 """Run `cipd ensure-file-resolve`."""
1711 with self._mutator_lock:
1712 with self._create_resolved_file() as output_file:
1713 with self._create_ensure_file_for_resolve() as ensure_file:
1714 cmd = [
1715 'cipd',
1716 'ensure-file-resolve',
1717 '-log-level',
1718 'error',
1719 '-ensure-file',
1720 ensure_file,
1721 '-json-output',
1722 output_file.name,
1723 ]
1724 gclient_utils.CheckCallAndFilter(cmd,
1725 print_stdout=False,
1726 show_header=False)
1727 with open(output_file.name) as f:
1728 output_json = json.load(f)
1729 return output_json.get('result', {})
1730
John Budorickd3ba72b2018-03-20 12:27:42 -07001731 def run(self, command):
1732 if command == 'update':
1733 self.ensure()
1734 elif command == 'revert':
1735 self.clobber()
1736 self.ensure()
1737
John Budorick0f7b2002018-01-19 15:46:17 -08001738 def created_package(self, package):
1739 """Checks whether this root created the given package.
1740
1741 Args:
1742 package: CipdPackage; the package to check.
1743 Returns:
1744 bool; whether this root created the given package.
1745 """
1746 return package in self._all_packages
1747
1748 @property
1749 def root_dir(self):
1750 return self._root_dir
1751
1752 @property
1753 def service_url(self):
1754 return self._service_url
1755
1756
1757class CipdWrapper(SCMWrapper):
1758 """Wrapper for CIPD.
1759
1760 Currently only supports chrome-infra-packages.appspot.com.
1761 """
John Budorick3929e9e2018-02-04 18:18:07 -08001762 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001763
1764 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1765 out_cb=None, root=None, package=None):
1766 super(CipdWrapper, self).__init__(
1767 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1768 out_cb=out_cb)
1769 assert root.created_package(package)
1770 self._package = package
1771 self._root = root
1772
1773 #override
1774 def GetCacheMirror(self):
1775 return None
1776
1777 #override
1778 def GetActualRemoteURL(self, options):
1779 return self._root.service_url
1780
1781 #override
1782 def DoesRemoteURLMatch(self, options):
1783 del options
1784 return True
1785
1786 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001787 """Does nothing.
1788
1789 CIPD packages should be reverted at the root by running
1790 `CipdRoot.run('revert')`.
1791 """
John Budorick0f7b2002018-01-19 15:46:17 -08001792
1793 def diff(self, options, args, file_list):
1794 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001795
1796 def pack(self, options, args, file_list):
1797 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001798
1799 def revinfo(self, options, args, file_list):
1800 """Grab the instance ID."""
1801 try:
1802 tmpdir = tempfile.mkdtemp()
Dan Le Febvre456d0852023-05-24 23:43:40 +00001803 # Attempt to get instance_id from the root resolved cache.
1804 # Resolved cache will not match on any CIPD packages with
1805 # variables such as ${platform}, they will fall back to
1806 # the slower method below.
1807 resolved = self._root.resolved_packages()
1808 if resolved:
1809 # CIPD uses POSIX separators across all platforms, so
1810 # replace any Windows separators.
1811 path_split = self.relpath.replace(os.sep, "/").split(":")
1812 if len(path_split) > 1:
1813 src_path, package = path_split
1814 if src_path in resolved:
1815 for resolved_package in resolved[src_path]:
1816 if package == resolved_package.get('pin', {}).get('package'):
1817 return resolved_package.get('pin', {}).get('instance_id')
1818
John Budorick0f7b2002018-01-19 15:46:17 -08001819 describe_json_path = os.path.join(tmpdir, 'describe.json')
1820 cmd = [
1821 'cipd', 'describe',
1822 self._package.name,
1823 '-log-level', 'error',
1824 '-version', self._package.version,
1825 '-json-output', describe_json_path
1826 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001827 gclient_utils.CheckCallAndFilter(cmd)
John Budorick0f7b2002018-01-19 15:46:17 -08001828 with open(describe_json_path) as f:
1829 describe_json = json.load(f)
1830 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1831 finally:
1832 gclient_utils.rmtree(tmpdir)
1833
1834 def status(self, options, args, file_list):
1835 pass
1836
1837 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001838 """Does nothing.
1839
1840 CIPD packages should be updated at the root by running
1841 `CipdRoot.run('update')`.
1842 """