blob: 64db8a576e26218f3b42142f15faad445b5f8d8b [file] [log] [blame]
steveblock@chromium.org93567042012-02-15 01:02:26 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00005"""Gclient-specific SCM-specific operations."""
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00006
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00007from __future__ import print_function
8
John Budorick0f7b2002018-01-19 15:46:17 -08009import collections
10import contextlib
borenet@google.comb2256212014-05-07 20:57:28 +000011import errno
John Budorick0f7b2002018-01-19 15:46:17 -080012import json
maruel@chromium.org754960e2009-09-21 12:31:05 +000013import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import os
Tomasz Wiszkowskid4e66882021-08-19 21:35:09 +000015import platform
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000016import posixpath
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000017import re
maruel@chromium.org90541732011-04-01 17:54:18 +000018import sys
ilevy@chromium.org3534aa52013-07-20 01:58:08 +000019import tempfile
John Budorick0f7b2002018-01-19 15:46:17 -080020import threading
zty@chromium.org6279e8a2014-02-13 01:45:25 +000021import traceback
Raul Tambreb946b232019-03-26 14:48:46 +000022
23try:
24 import urlparse
25except ImportError: # For Py3 compatibility
26 import urllib.parse as urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000027
28import gclient_utils
Ravi Mistryecda7822022-02-28 16:22:20 +000029import gerrit_util
szager@chromium.org848fd492014-04-09 19:06:44 +000030import git_cache
Josip Sokcevic7958e302023-03-01 23:02:21 +000031import scm
borenet@google.comb2256212014-05-07 20:57:28 +000032import shutil
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000033import subprocess2
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000034
35
smutae7ea312016-07-18 11:59:41 -070036class NoUsableRevError(gclient_utils.Error):
37 """Raised if requested revision isn't found in checkout."""
38
39
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000040class DiffFiltererWrapper(object):
41 """Simple base class which tracks which file is being diffed and
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000042 replaces instances of its file name in the original and
agable41e3a6c2016-10-20 11:36:56 -070043 working copy lines of the git diff output."""
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000044 index_string = None
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000045 original_prefix = "--- "
46 working_prefix = "+++ "
47
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000048 def __init__(self, relpath, print_func):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000049 # Note that we always use '/' as the path separator to be
agable41e3a6c2016-10-20 11:36:56 -070050 # consistent with cygwin-style output on Windows
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000051 self._relpath = relpath.replace("\\", "/")
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000052 self._current_file = None
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000053 self._print_func = print_func
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000054
maruel@chromium.org6e29d572010-06-04 17:32:20 +000055 def SetCurrentFile(self, current_file):
56 self._current_file = current_file
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000057
iannucci@chromium.org3830a672013-02-19 20:15:14 +000058 @property
59 def _replacement_file(self):
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000060 return posixpath.join(self._relpath, self._current_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000061
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000062 def _Replace(self, line):
63 return line.replace(self._current_file, self._replacement_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000064
65 def Filter(self, line):
66 if (line.startswith(self.index_string)):
67 self.SetCurrentFile(line[len(self.index_string):])
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000068 line = self._Replace(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000069 else:
70 if (line.startswith(self.original_prefix) or
71 line.startswith(self.working_prefix)):
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000072 line = self._Replace(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000073 self._print_func(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000074
75
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000076class GitDiffFilterer(DiffFiltererWrapper):
77 index_string = "diff --git "
78
79 def SetCurrentFile(self, current_file):
80 # Get filename by parsing "a/<filename> b/<filename>"
81 self._current_file = current_file[:(len(current_file)/2)][2:]
82
83 def _Replace(self, line):
84 return re.sub("[a|b]/" + self._current_file, self._replacement_file, line)
85
86
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000087# SCMWrapper base class
88
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000089class SCMWrapper(object):
90 """Add necessary glue between all the supported SCM.
91
msb@chromium.orgd6504212010-01-13 17:34:31 +000092 This is the abstraction layer to bind to different SCM.
93 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000094 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
Edward Lemur231f5ea2018-01-31 19:02:36 +010095 out_cb=None, print_outbuf=False):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000096 self.url = url
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +000097 self._root_dir = root_dir
98 if self._root_dir:
99 self._root_dir = self._root_dir.replace('/', os.sep)
100 self.relpath = relpath
101 if self.relpath:
102 self.relpath = self.relpath.replace('/', os.sep)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000103 if self.relpath and self._root_dir:
104 self.checkout_path = os.path.join(self._root_dir, self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000105 if out_fh is None:
106 out_fh = sys.stdout
107 self.out_fh = out_fh
108 self.out_cb = out_cb
Edward Lemur231f5ea2018-01-31 19:02:36 +0100109 self.print_outbuf = print_outbuf
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000110
111 def Print(self, *args, **kwargs):
112 kwargs.setdefault('file', self.out_fh)
113 if kwargs.pop('timestamp', True):
114 self.out_fh.write('[%s] ' % gclient_utils.Elapsed())
115 print(*args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000116
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000117 def RunCommand(self, command, options, args, file_list=None):
agabledebf6c82016-12-21 12:50:12 -0800118 commands = ['update', 'updatesingle', 'revert',
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000119 'revinfo', 'status', 'diff', 'pack', 'runhooks']
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000120
121 if not command in commands:
122 raise gclient_utils.Error('Unknown command %s' % command)
123
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000124 if not command in dir(self):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000125 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % (
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000126 command, self.__class__.__name__))
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000127
128 return getattr(self, command)(options, args, file_list)
129
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000130 @staticmethod
131 def _get_first_remote_url(checkout_path):
132 log = scm.GIT.Capture(
133 ['config', '--local', '--get-regexp', r'remote.*.url'],
134 cwd=checkout_path)
135 # Get the second token of the first line of the log.
136 return log.splitlines()[0].split(' ', 1)[1]
137
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000138 def GetCacheMirror(self):
Robert Iannuccia19649b2018-06-29 16:31:45 +0000139 if getattr(self, 'cache_dir', None):
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000140 url, _ = gclient_utils.SplitUrlRevision(self.url)
141 return git_cache.Mirror(url)
142 return None
143
smut@google.comd33eab32014-07-07 19:35:18 +0000144 def GetActualRemoteURL(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000145 """Attempt to determine the remote URL for this SCMWrapper."""
smut@google.comd33eab32014-07-07 19:35:18 +0000146 # Git
borenet@google.combda475e2014-03-24 19:04:45 +0000147 if os.path.exists(os.path.join(self.checkout_path, '.git')):
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000148 actual_remote_url = self._get_first_remote_url(self.checkout_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000149
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000150 mirror = self.GetCacheMirror()
151 # If the cache is used, obtain the actual remote URL from there.
152 if (mirror and mirror.exists() and
153 mirror.mirror_path.replace('\\', '/') ==
154 actual_remote_url.replace('\\', '/')):
155 actual_remote_url = self._get_first_remote_url(mirror.mirror_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000156 return actual_remote_url
borenet@google.com88d10082014-03-21 17:24:48 +0000157 return None
158
borenet@google.com4e9be262014-04-08 19:40:30 +0000159 def DoesRemoteURLMatch(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000160 """Determine whether the remote URL of this checkout is the expected URL."""
161 if not os.path.exists(self.checkout_path):
162 # A checkout which doesn't exist can't be broken.
163 return True
164
smut@google.comd33eab32014-07-07 19:35:18 +0000165 actual_remote_url = self.GetActualRemoteURL(options)
borenet@google.com88d10082014-03-21 17:24:48 +0000166 if actual_remote_url:
borenet@google.com8156c9f2014-04-01 16:41:36 +0000167 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip('/')
168 == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/'))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000169
170 # This may occur if the self.checkout_path exists but does not contain a
171 # valid git checkout.
172 return False
borenet@google.com88d10082014-03-21 17:24:48 +0000173
borenet@google.comb09097a2014-04-09 19:09:08 +0000174 def _DeleteOrMove(self, force):
175 """Delete the checkout directory or move it out of the way.
176
177 Args:
178 force: bool; if True, delete the directory. Otherwise, just move it.
179 """
borenet@google.comb2256212014-05-07 20:57:28 +0000180 if force and os.environ.get('CHROME_HEADLESS') == '1':
181 self.Print('_____ Conflicting directory found in %s. Removing.'
182 % self.checkout_path)
183 gclient_utils.AddWarning('Conflicting directory %s deleted.'
184 % self.checkout_path)
185 gclient_utils.rmtree(self.checkout_path)
186 else:
187 bad_scm_dir = os.path.join(self._root_dir, '_bad_scm',
188 os.path.dirname(self.relpath))
189
190 try:
191 os.makedirs(bad_scm_dir)
192 except OSError as e:
193 if e.errno != errno.EEXIST:
194 raise
195
196 dest_path = tempfile.mkdtemp(
197 prefix=os.path.basename(self.relpath),
198 dir=bad_scm_dir)
199 self.Print('_____ Conflicting directory found in %s. Moving to %s.'
200 % (self.checkout_path, dest_path))
201 gclient_utils.AddWarning('Conflicting directory %s moved to %s.'
202 % (self.checkout_path, dest_path))
203 shutil.move(self.checkout_path, dest_path)
borenet@google.comb09097a2014-04-09 19:09:08 +0000204
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000205
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000206class GitWrapper(SCMWrapper):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000207 """Wrapper for Git"""
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000208 name = 'git'
maruel@chromium.org1a60dca2013-11-26 14:06:26 +0000209 remote = 'origin'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000210
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000211 _is_env_cog = None
212
213 @staticmethod
214 def _IsCog():
215 """Returns true if the env is cog"""
216 if not GitWrapper._is_env_cog:
Joanna Wangbbb66d72022-08-29 21:48:55 +0000217 GitWrapper._is_env_cog = any(os.getcwd().startswith(x) for x in [
218 '/google/cog/cloud', '/google/src/cloud'])
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000219
220 return GitWrapper._is_env_cog
221
Robert Iannuccia19649b2018-06-29 16:31:45 +0000222 @property
223 def cache_dir(self):
224 try:
225 return git_cache.Mirror.GetCachePath()
226 except RuntimeError:
227 return None
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000228
John Budorick0f7b2002018-01-19 15:46:17 -0800229 def __init__(self, url=None, *args, **kwargs):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000230 """Removes 'git+' fake prefix from git URL."""
Henrique Ferreiroe72279d2019-04-17 12:01:50 +0000231 if url and (url.startswith('git+http://') or
232 url.startswith('git+https://')):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000233 url = url[4:]
John Budorick0f7b2002018-01-19 15:46:17 -0800234 SCMWrapper.__init__(self, url, *args, **kwargs)
szager@chromium.org848fd492014-04-09 19:06:44 +0000235 filter_kwargs = { 'time_throttle': 1, 'out_fh': self.out_fh }
236 if self.out_cb:
237 filter_kwargs['predicate'] = self.out_cb
238 self.filter = gclient_utils.GitFilter(**filter_kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +0000239 self._running_under_rosetta = None
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000240
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000241 def GetCheckoutRoot(self):
242 return scm.GIT.GetCheckoutRoot(self.checkout_path)
243
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000244 def GetRevisionDate(self, _revision):
floitsch@google.comeaab7842011-04-28 09:07:58 +0000245 """Returns the given revision's date in ISO-8601 format (which contains the
246 time zone)."""
247 # TODO(floitsch): get the time-stamp of the given revision and not just the
248 # time-stamp of the currently checked out revision.
249 return self._Capture(['log', '-n', '1', '--format=%ai'])
250
Aaron Gablef4068aa2017-12-12 15:14:09 -0800251 def _GetDiffFilenames(self, base):
252 """Returns the names of files modified since base."""
253 return self._Capture(
Raul Tambrecd862e32019-05-10 21:19:00 +0000254 # Filter to remove base if it is None.
255 list(filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only',
256 base])
257 )).split()
Aaron Gablef4068aa2017-12-12 15:14:09 -0800258
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000259 def diff(self, options, _args, _file_list):
Aaron Gable1853f662018-02-12 15:45:56 -0800260 _, revision = gclient_utils.SplitUrlRevision(self.url)
261 if not revision:
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000262 revision = 'refs/remotes/%s/main' % self.remote
Aaron Gable1853f662018-02-12 15:45:56 -0800263 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000264
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000265 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000266 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000267 repository.
268
269 The patch file is generated from a diff of the merge base of HEAD and
270 its upstream branch.
271 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700272 try:
273 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
274 except subprocess2.CalledProcessError:
275 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000276 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700277 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000278 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000279 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000280
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800281 def _Scrub(self, target, options):
282 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000283 quiet = []
284 if not options.verbose:
285 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800286 self._Run(['reset', '--hard', target] + quiet, options)
287 if options.force and options.delete_unversioned_trees:
288 # where `target` is a commit that contains both upper and lower case
289 # versions of the same file on a case insensitive filesystem, we are
290 # actually in a broken state here. The index will have both 'a' and 'A',
291 # but only one of them will exist on the disk. To progress, we delete
292 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800293 output = self._Capture([
294 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800295 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800296 # --porcelain (v1) looks like:
297 # XY filename
298 try:
299 filename = line[3:]
300 self.Print('_____ Deleting residual after reset: %r.' % filename)
301 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800302 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800303 except OSError:
304 pass
305
John Budorick882c91e2018-07-12 22:11:41 +0000306 def _FetchAndReset(self, revision, file_list, options):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800307 """Equivalent to git fetch; git reset."""
Edward Lemur579c9862018-07-13 23:17:51 +0000308 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000309
dnj@chromium.org680f2172014-06-25 00:39:32 +0000310 self._Fetch(options, prune=True, quiet=options.verbose)
John Budorick882c91e2018-07-12 22:11:41 +0000311 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000312 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800313 files = self._Capture(
314 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambreb946b232019-03-26 14:48:46 +0000315 file_list.extend(
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000316 [os.path.join(self.checkout_path, f) for f in files])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000317
szager@chromium.org8a139702014-06-20 15:55:01 +0000318 def _DisableHooks(self):
319 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
320 if not os.path.isdir(hook_dir):
321 return
322 for f in os.listdir(hook_dir):
323 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000324 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
325 if os.path.exists(disabled_hook_path):
326 os.remove(disabled_hook_path)
327 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000328
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000329 def _maybe_break_locks(self, options):
330 """This removes all .lock files from this repo's .git directory, if the
331 user passed the --break_repo_locks command line flag.
332
333 In particular, this will cleanup index.lock files, as well as ref lock
334 files.
335 """
336 if options.break_repo_locks:
337 git_dir = os.path.join(self.checkout_path, '.git')
338 for path, _, filenames in os.walk(git_dir):
339 for filename in filenames:
340 if filename.endswith('.lock'):
341 to_break = os.path.join(path, filename)
342 self.Print('breaking lock: %s' % (to_break,))
343 try:
344 os.remove(to_break)
345 except OSError as ex:
346 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
347 raise
348
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000349 def _download_topics(self, patch_rev, googlesource_url):
350 """This method returns new patch_revs to process that have the same topic.
351
352 It does the following:
353 1. Finds the topic of the Gerrit change specified in the patch_rev.
354 2. Find all changes with that topic.
355 3. Append patch_rev of the changes with the same topic to the patch_revs
356 to process.
357 4. Returns the new patch_revs to process.
358 """
359 patch_revs_to_process = []
360 # Parse the patch_rev to extract the CL and patchset.
361 patch_rev_tokens = patch_rev.split('/')
362 change = patch_rev_tokens[-2]
363 # Parse the googlesource_url.
364 tokens = re.search(
365 '//(.+).googlesource.com/(.+?)(?:\.git)?$', googlesource_url)
366 if not tokens or len(tokens.groups()) != 2:
367 # googlesource_url is not in the expected format.
368 return patch_revs_to_process
369
370 # parse the gerrit host and repo out of googlesource_url.
371 host, repo = tokens.groups()[:2]
372 gerrit_host_url = '%s-review.googlesource.com' % host
373
374 # 1. Find the topic of the Gerrit change specified in the patch_rev.
375 change_object = gerrit_util.GetChange(gerrit_host_url, change)
376 topic = change_object.get('topic')
377 if not topic:
378 # This change has no topic set.
379 return patch_revs_to_process
380
381 # 2. Find all changes with that topic.
382 changes_with_same_topic = gerrit_util.QueryChanges(
383 gerrit_host_url,
384 [('topic', topic), ('status', 'open'), ('repo', repo)],
385 o_params=['ALL_REVISIONS'])
386 for c in changes_with_same_topic:
387 if str(c['_number']) == change:
388 # This change is already in the patch_rev.
389 continue
390 self.Print('Found CL %d with the topic name %s' % (
391 c['_number'], topic))
392 # 3. Append patch_rev of the changes with the same topic to the
393 # patch_revs to process.
394 curr_rev = c['current_revision']
395 new_patch_rev = c['revisions'][curr_rev]['ref']
396 patch_revs_to_process.append(new_patch_rev)
397
Joanna Wang1a977bd2022-06-02 21:51:17 +0000398 # 4. Return the new patch_revs to process.
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000399 return patch_revs_to_process
400
Edward Lemur3acbc742019-05-30 17:57:35 +0000401 def apply_patch_ref(self, patch_repo, patch_rev, target_rev, options,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000402 file_list):
Joanna Wangf3edc502022-07-20 00:12:10 +0000403 # type: (str, str, str, optparse.Values, Collection[str]) -> str
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000404 """Apply a patch on top of the revision we're synced at.
405
Edward Lemur3acbc742019-05-30 17:57:35 +0000406 The patch ref is given by |patch_repo|@|patch_rev|.
407 |target_rev| is usually the branch that the |patch_rev| was uploaded against
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000408 (e.g. 'refs/heads/main'), but this is not required.
Edward Lemur3acbc742019-05-30 17:57:35 +0000409
410 We cherry-pick all commits reachable from |patch_rev| on top of the curret
411 HEAD, excluding those reachable from |target_rev|
412 (i.e. git cherry-pick target_rev..patch_rev).
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000413
414 Graphically, it looks like this:
415
Edward Lemur3acbc742019-05-30 17:57:35 +0000416 ... -> o -> [possibly already landed commits] -> target_rev
417 \
418 -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000419
Edward Lemur3acbc742019-05-30 17:57:35 +0000420 The final checkout state is then:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000421
Edward Lemur3acbc742019-05-30 17:57:35 +0000422 ... -> HEAD -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000423
424 After application, if |options.reset_patch_ref| is specified, we soft reset
Edward Lemur3acbc742019-05-30 17:57:35 +0000425 the cherry-picked changes, keeping them in git index only.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000426
427 Args:
Edward Lemur3acbc742019-05-30 17:57:35 +0000428 patch_repo: The patch origin.
429 e.g. 'https://foo.googlesource.com/bar'
430 patch_rev: The revision to patch.
431 e.g. 'refs/changes/1234/34/1'.
432 target_rev: The revision to use when finding the merge base.
433 Typically, the branch that the patch was uploaded against.
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000434 e.g. 'refs/heads/main' or 'refs/heads/infra/config'.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000435 options: The options passed to gclient.
436 file_list: A list where modified files will be appended.
437 """
438
Edward Lemurca7d8812018-07-24 17:42:45 +0000439 # Abort any cherry-picks in progress.
440 try:
441 self._Capture(['cherry-pick', '--abort'])
442 except subprocess2.CalledProcessError:
443 pass
444
Joanna Wangf3edc502022-07-20 00:12:10 +0000445 base_rev = self.revinfo(None, None, None)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000446
Edward Lemur3acbc742019-05-30 17:57:35 +0000447 if not target_rev:
Edward Lemur4c5c8ab2019-06-07 15:58:13 +0000448 raise gclient_utils.Error('A target revision for the patch must be given')
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000449
450 if target_rev.startswith(('refs/heads/', 'refs/branch-heads')):
Edward Lesmesf627d9f2020-07-23 19:50:50 +0000451 # If |target_rev| is in refs/heads/** or refs/branch-heads/**, try first
452 # to find the corresponding remote ref for it, since |target_rev| might
453 # point to a local ref which is not up to date with the corresponding
454 # remote ref.
Edward Lemur3acbc742019-05-30 17:57:35 +0000455 remote_ref = ''.join(scm.GIT.RefToRemoteRef(target_rev, self.remote))
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000456 self.Print('Trying the corresponding remote ref for %r: %r\n' % (
Edward Lemur3acbc742019-05-30 17:57:35 +0000457 target_rev, remote_ref))
458 if scm.GIT.IsValidRevision(self.checkout_path, remote_ref):
Josip Sokcevic3d7cbce2021-10-05 20:48:04 +0000459 # refs/remotes may need to be updated to cleanly cherry-pick changes.
460 # See https://crbug.com/1255178.
461 self._Capture(['fetch', '--no-tags', self.remote, target_rev])
Edward Lemur3acbc742019-05-30 17:57:35 +0000462 target_rev = remote_ref
463 elif not scm.GIT.IsValidRevision(self.checkout_path, target_rev):
464 # Fetch |target_rev| if it's not already available.
465 url, _ = gclient_utils.SplitUrlRevision(self.url)
Edward Lesmes07a68342021-04-20 23:39:30 +0000466 mirror = self._GetMirror(url, options, target_rev, target_rev)
Edward Lemur3acbc742019-05-30 17:57:35 +0000467 if mirror:
468 rev_type = 'branch' if target_rev.startswith('refs/') else 'hash'
469 self._UpdateMirrorIfNotContains(mirror, options, rev_type, target_rev)
470 self._Fetch(options, refspec=target_rev)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000471
Ravi Mistryecda7822022-02-28 16:22:20 +0000472 patch_revs_to_process = [patch_rev]
473
474 if hasattr(options, 'download_topics') and options.download_topics:
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000475 patch_revs_to_process_from_topics = self._download_topics(
476 patch_rev, self.url)
477 patch_revs_to_process.extend(patch_revs_to_process_from_topics)
Ravi Mistryecda7822022-02-28 16:22:20 +0000478
Edward Lesmesc621b212018-03-21 20:26:56 -0400479 self._Capture(['reset', '--hard'])
Ravi Mistryecda7822022-02-28 16:22:20 +0000480 for pr in patch_revs_to_process:
481 self.Print('===Applying patch===')
482 self.Print('Revision to patch is %r @ %r.' % (patch_repo, pr))
483 self.Print('Current dir is %r' % self.checkout_path)
484 self._Capture(['fetch', '--no-tags', patch_repo, pr])
485 pr = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400486
Ravi Mistryecda7822022-02-28 16:22:20 +0000487 if not options.rebase_patch_ref:
488 self._Capture(['checkout', pr])
489 # Adjust base_rev to be the first parent of our checked out patch ref;
490 # This will allow us to correctly extend `file_list`, and will show the
491 # correct file-list to programs which do `git diff --cached` expecting
492 # to see the patch diff.
493 base_rev = self._Capture(['rev-parse', pr+'~'])
494 else:
495 self.Print('Will cherrypick %r .. %r on top of %r.' % (
496 target_rev, pr, base_rev))
Edward Lemur3acbc742019-05-30 17:57:35 +0000497 try:
Joanna Wangab9c6ba2023-01-21 01:46:36 +0000498 if scm.GIT.IsAncestor(pr, target_rev, cwd=self.checkout_path):
Ravi Mistryecda7822022-02-28 16:22:20 +0000499 if len(patch_revs_to_process) > 1:
500 # If there are multiple patch_revs_to_process then we do not want
501 # want to invalidate a previous patch so throw an error.
502 raise gclient_utils.Error(
503 'patch_rev %s is an ancestor of target_rev %s. This '
504 'situation is unsupported when we need to apply multiple '
505 'patch_revs: %s' % (pr, target_rev, patch_revs_to_process))
506 # If |patch_rev| is an ancestor of |target_rev|, check it out.
507 self._Capture(['checkout', pr])
508 else:
509 # If a change was uploaded on top of another change, which has
510 # already landed, one of the commits in the cherry-pick range will
511 # be redundant, since it has already landed and its changes
512 # incorporated in the tree.
513 # We pass '--keep-redundant-commits' to ignore those changes.
514 self._Capture(['cherry-pick', target_rev + '..' + pr,
515 '--keep-redundant-commits'])
Edward Lemurca7d8812018-07-24 17:42:45 +0000516
Ravi Mistryecda7822022-02-28 16:22:20 +0000517 except subprocess2.CalledProcessError as e:
518 self.Print('Failed to apply patch.')
519 self.Print('Revision to patch was %r @ %r.' % (patch_repo, pr))
520 self.Print('Tried to cherrypick %r .. %r on top of %r.' % (
521 target_rev, pr, base_rev))
522 self.Print('Current dir is %r' % self.checkout_path)
523 self.Print('git returned non-zero exit status %s:\n%s' % (
524 e.returncode, e.stderr.decode('utf-8')))
525 # Print the current status so that developers know what changes caused
526 # the patch failure, since git cherry-pick doesn't show that
527 # information.
528 self.Print(self._Capture(['status']))
529 try:
530 self._Capture(['cherry-pick', '--abort'])
531 except subprocess2.CalledProcessError:
532 pass
533 raise
534
535 if file_list is not None:
536 file_list.extend(self._GetDiffFilenames(base_rev))
Edward Lemurca7d8812018-07-24 17:42:45 +0000537
Joanna Wangf3edc502022-07-20 00:12:10 +0000538 latest_commit = self.revinfo(None, None, None)
Edward Lesmesc621b212018-03-21 20:26:56 -0400539 if options.reset_patch_ref:
540 self._Capture(['reset', '--soft', base_rev])
Joanna Wangf3edc502022-07-20 00:12:10 +0000541 return latest_commit
Edward Lesmesc621b212018-03-21 20:26:56 -0400542
Joanna Wang5a7c8242022-07-01 19:09:00 +0000543 def check_diff(self, previous_commit, files=None):
544 # type: (str, Optional[List[str]]) -> bool
545 """Check if a diff exists between the current commit and `previous_commit`.
546
547 Returns True if there were diffs or if an error was encountered.
548 """
549 cmd = ['diff', previous_commit, '--quiet']
550 if files:
551 cmd += ['--'] + files
552 try:
553 self._Capture(cmd)
554 return False
555 except subprocess2.CalledProcessError as e:
556 # git diff --quiet exits with 1 if there were diffs.
557 if e.returncode != 1:
558 self.Print('git returned non-zero exit status %s:\n%s' %
559 (e.returncode, e.stderr.decode('utf-8')))
560 return True
561
Joanna Wange1753f62023-06-26 14:32:43 +0000562 def set_config(f):
563 def wrapper(*args):
564 return_val = f(*args)
565 if os.path.exists(os.path.join(args[0].checkout_path, '.git')):
566 # If diff.ignoreSubmodules is not already set, set it to `all`.
567 currentIgnore = subprocess2.capture(
568 ['git', 'config', '--get', 'diff.ignoreSubmodules'],
569 cwd=args[0].checkout_path).strip()
570 if not currentIgnore:
571 subprocess2.capture(['git', 'config', 'diff.ignoreSubmodules', 'all'],
572 cwd=args[0].checkout_path).strip()
573 return return_val
574
575 return wrapper
576
577 @set_config
msb@chromium.orge28e4982009-09-25 20:51:45 +0000578 def update(self, options, args, file_list):
579 """Runs git to update or transparently checkout the working copy.
580
581 All updated files will be appended to file_list.
582
583 Raises:
584 Error: if can't get URL for relative path.
585 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000586 if args:
587 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
588
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000589 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000590
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000591 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000592 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000593 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000594 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000595 # Override the revision number.
596 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000597 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000598 # Check again for a revision in case an initial ref was specified
599 # in the url, for example bla.git@refs/heads/custombranch
600 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000601 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000602 if not revision:
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000603 # If a dependency is not pinned, track the default remote branch.
604 revision = scm.GIT.GetRemoteHeadRef(self.checkout_path, self.url,
605 self.remote)
Edward Lesmes4ea67bb2021-04-20 17:33:52 +0000606 if revision.startswith('origin/'):
607 revision = 'refs/remotes/' + revision
msb@chromium.orge28e4982009-09-25 20:51:45 +0000608
Tomasz Wiszkowskid4e66882021-08-19 21:35:09 +0000609 if managed and platform.system() == 'Windows':
szager@chromium.org8a139702014-06-20 15:55:01 +0000610 self._DisableHooks()
611
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000612 printed_path = False
613 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000614 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700615 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000616 verbose = ['--verbose']
617 printed_path = True
618
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000619 revision_ref = revision
620 if ':' in revision:
621 revision_ref, _, revision = revision.partition(':')
622
Edward Lesmes8073a502020-04-15 02:11:14 +0000623 if revision_ref.startswith('refs/branch-heads'):
624 options.with_branch_heads = True
625
Edward Lesmes07a68342021-04-20 23:39:30 +0000626 mirror = self._GetMirror(url, options, revision, revision_ref)
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000627 if mirror:
628 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000629
John Budorick882c91e2018-07-12 22:11:41 +0000630 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
631 if remote_ref:
632 # Rewrite remote refs to their local equivalents.
633 revision = ''.join(remote_ref)
634 rev_type = "branch"
635 elif revision.startswith('refs/'):
636 # Local branch? We probably don't want to support, since DEPS should
637 # always specify branches as they are in the upstream repo.
638 rev_type = "branch"
639 else:
640 # hash is also a tag, only make a distinction at checkout
641 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000642
primiano@chromium.org1c127382015-02-17 11:15:40 +0000643 # If we are going to introduce a new project, there is a possibility that
644 # we are syncing back to a state where the project was originally a
645 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
646 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
647 # In such case, we might have a backup of the former .git folder, which can
648 # be used to avoid re-fetching the entire repo again (useful for bisects).
649 backup_dir = self.GetGitBackupDirPath()
650 target_dir = os.path.join(self.checkout_path, '.git')
651 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
652 gclient_utils.safe_makedirs(self.checkout_path)
653 os.rename(backup_dir, target_dir)
654 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800655 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000656
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000657 if (not os.path.exists(self.checkout_path) or
658 (os.path.isdir(self.checkout_path) and
659 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000660 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000661 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000662 try:
John Budorick882c91e2018-07-12 22:11:41 +0000663 self._Clone(revision, url, options)
Joanna Wang47fd5672022-08-05 20:53:31 +0000664 except subprocess2.CalledProcessError as e:
665 logging.warning('Clone failed due to: %s', e)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000666 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000667 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000668 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800669 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000670 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000671 file_list.extend(
Edward Lemur979fa782019-08-13 22:44:05 +0000672 [os.path.join(self.checkout_path, f) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000673 if mirror:
674 self._Capture(
675 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000676 if not verbose:
677 # Make the output a little prettier. It's nice to have some whitespace
678 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000679 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000680 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000681
John Budorick21a51b32018-09-19 19:39:20 +0000682 if mirror:
683 self._Capture(
684 ['remote', 'set-url', '--push', 'origin', mirror.url])
685
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000686 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000687 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000688 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
689 return self._Capture(['rev-parse', '--verify', 'HEAD'])
690
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000691 self._maybe_break_locks(options)
692
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000693 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000694 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000695
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000696 # See if the url has changed (the unittests use git://foo for the url, let
697 # that through).
698 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
699 return_early = False
700 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
701 # unit test pass. (and update the comment above)
702 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
703 # This allows devs to use experimental repos which have a different url
704 # but whose branch(s) are the same as official repos.
Raul Tambrecd862e32019-05-10 21:19:00 +0000705 if (current_url.rstrip('/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000706 subprocess2.capture(
Aaron Gableac9b0f32019-04-18 17:38:37 +0000707 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000708 cwd=self.checkout_path).strip() != 'False'):
Anthony Polito486f1812020-08-04 23:40:33 +0000709 self.Print('_____ switching %s from %s to new upstream %s' % (
710 self.relpath, current_url, url))
iannucci@chromium.org78514212014-08-20 23:08:00 +0000711 if not (options.force or options.reset):
712 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700713 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000714 # Switch over to the new upstream
715 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000716 if mirror:
Aravind Vasudevancf465852023-03-29 16:47:12 +0000717 if git_cache.Mirror.CacheDirToUrl(
718 current_url.rstrip('/')) == git_cache.Mirror.CacheDirToUrl(
719 url.rstrip('/')):
720 # Reset alternates when the cache dir is updated.
721 with open(
722 os.path.join(self.checkout_path, '.git', 'objects', 'info',
723 'alternates'), 'w') as fh:
724 fh.write(os.path.join(url, 'objects'))
725 else:
726 # Because we use Git alternatives, our existing repository is not
727 # self-contained. It's possible that new git alternative doesn't have
728 # all necessary objects that the current repository needs. Instead of
729 # blindly hoping that new alternative contains all necessary objects,
730 # keep the old alternative and just append a new one on top of it.
731 with open(
732 os.path.join(self.checkout_path, '.git', 'objects', 'info',
733 'alternates'), 'a') as fh:
734 fh.write("\n" + os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000735 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
736 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000737
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000738 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000739 else:
John Budorick882c91e2018-07-12 22:11:41 +0000740 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000741
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000742 if return_early:
743 return self._Capture(['rev-parse', '--verify', 'HEAD'])
744
msb@chromium.org5bde4852009-12-14 16:47:12 +0000745 cur_branch = self._GetCurrentBranch()
746
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000747 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000748 # 0) HEAD is detached. Probably from our initial clone.
749 # - make sure HEAD is contained by a named ref, then update.
750 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700751 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000752 # - try to rebase onto the new hash or branch
753 # 2) current branch is tracking a remote branch with local committed
754 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000755 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000756 # 3) current branch is tracking a remote branch w/or w/out changes, and
757 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000758 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000759 # 4) current branch is tracking a remote branch, but DEPS switches to a
760 # different remote branch, and
761 # a) current branch has no local changes, and --force:
762 # - checkout new branch
763 # b) current branch has local changes, and --force and --reset:
764 # - checkout new branch
765 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000766
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000767 # GetUpstreamBranch returns something like 'refs/remotes/origin/main' for
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000768 # a tracking branch
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000769 # or 'main' if not a tracking branch (it's based on a specific rev/hash)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000770 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000771 if cur_branch is None:
772 upstream_branch = None
773 current_type = "detached"
774 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000775 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000776 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
777 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
778 current_type = "hash"
779 logging.debug("Current branch is not tracking an upstream (remote)"
780 " branch.")
781 elif upstream_branch.startswith('refs/remotes'):
782 current_type = "branch"
783 else:
784 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000785
Edward Lemur579c9862018-07-13 23:17:51 +0000786 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000787
Michael Spang73fac912019-03-08 18:44:19 +0000788 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000789 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000790 self._Fetch(options, prune=options.force)
791
792 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
793 sha_only=True):
794 # Update the remotes first so we have all the refs.
795 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
796 cwd=self.checkout_path)
797 if verbose:
798 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000799
John Budorick882c91e2018-07-12 22:11:41 +0000800 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200801
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000802 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000803 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000804 target = 'HEAD'
805 if options.upstream and upstream_branch:
806 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800807 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000808
msb@chromium.org786fb682010-06-02 15:16:23 +0000809 if current_type == 'detached':
810 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800811 # We just did a Scrub, this is as clean as it's going to get. In
812 # particular if HEAD is a commit that contains two versions of the same
813 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
814 # to actually "Clean" the checkout; that commit is uncheckoutable on this
815 # system. The best we can do is carry forward to the checkout step.
816 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000817 self._CheckClean(revision)
818 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000819 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000820 self.Print('Up-to-date; skipping checkout.')
821 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000822 # 'git checkout' may need to overwrite existing untracked files. Allow
823 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000824 self._Checkout(
825 options,
John Budorick882c91e2018-07-12 22:11:41 +0000826 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000827 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000828 quiet=True,
829 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000830 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000831 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000832 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000833 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700834 # Can't find a merge-base since we don't know our upstream. That makes
835 # this command VERY likely to produce a rebase failure. For now we
836 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000837 upstream_branch = self.remote
838 if options.revision or deps_revision:
839 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700840 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700841 printed_path=printed_path, merge=options.merge)
842 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000843 elif rev_type == 'hash':
844 # case 2
845 self._AttemptRebase(upstream_branch, file_list, options,
846 newbase=revision, printed_path=printed_path,
847 merge=options.merge)
848 printed_path = True
849 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000850 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000851 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000852 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000853 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000854 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000855 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000856 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000857 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
858 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000859 force_switch = False
860 if options.force:
861 try:
John Budorick882c91e2018-07-12 22:11:41 +0000862 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000863 # case 4a
864 force_switch = True
865 except gclient_utils.Error as e:
866 if options.reset:
867 # case 4b
868 force_switch = True
869 else:
870 switch_error = '%s\n%s' % (e.message, switch_error)
871 if force_switch:
872 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000873 (upstream_branch, new_base))
874 switch_branch = 'gclient_' + remote_ref[1]
875 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000876 self._Checkout(options, switch_branch, force=True, quiet=True)
877 else:
878 # case 4c
879 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000880 else:
881 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800882 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000883 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000884 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000885 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000886 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000887 if options.merge:
888 merge_args.append('--ff')
889 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000890 merge_args.append('--ff-only')
891 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000892 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000893 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700894 rebase_files = []
Edward Lemur979fa782019-08-13 22:44:05 +0000895 if re.match(b'fatal: Not possible to fast-forward, aborting.',
896 e.stderr):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000897 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000898 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700899 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000900 printed_path = True
901 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000902 if not options.auto_rebase:
903 try:
904 action = self._AskForData(
905 'Cannot %s, attempt to rebase? '
906 '(y)es / (q)uit / (s)kip : ' %
907 ('merge' if options.merge else 'fast-forward merge'),
908 options)
909 except ValueError:
910 raise gclient_utils.Error('Invalid Character')
911 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700912 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000913 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000914 printed_path = True
915 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000916
917 if re.match(r'quit|q', action, re.I):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000918 raise gclient_utils.Error("Can't fast-forward, please merge or "
919 "rebase manually.\n"
920 "cd %s && git " % self.checkout_path
921 + "rebase %s" % upstream_branch)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000922
923 if re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000924 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000925 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000926
927 self.Print('Input not recognized')
Edward Lemur979fa782019-08-13 22:44:05 +0000928 elif re.match(b"error: Your local changes to '.*' would be "
929 b"overwritten by merge. Aborting.\nPlease, commit your "
930 b"changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000931 e.stderr):
932 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000933 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700934 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000935 printed_path = True
Edward Lemur979fa782019-08-13 22:44:05 +0000936 raise gclient_utils.Error(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000937 else:
938 # Some other problem happened with the merge
939 logging.error("Error during fast-forward merge in %s!" % self.relpath)
Edward Lemur979fa782019-08-13 22:44:05 +0000940 self.Print(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000941 raise
942 else:
943 # Fast-forward merge was successful
944 if not re.match('Already up-to-date.', merge_output) or verbose:
945 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700946 self.Print('_____ %s at %s' % (self.relpath, revision),
947 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000948 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000949 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000950 if not verbose:
951 # Make the output a little prettier. It's nice to have some
952 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000953 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000954
agablec3937b92016-10-25 10:13:03 -0700955 if file_list is not None:
956 file_list.extend(
957 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000958
959 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000960 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700961 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000962 '\nConflict while rebasing this branch.\n'
963 'Fix the conflict and run gclient again.\n'
964 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700965 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000966
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000967 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000968 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
969 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000970
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000971 # If --reset and --delete_unversioned_trees are specified, remove any
972 # untracked directories.
973 if options.reset and options.delete_unversioned_trees:
974 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
975 # merge-base by default), so doesn't include untracked files. So we use
976 # 'git ls-files --directory --others --exclude-standard' here directly.
977 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800978 ['-c', 'core.quotePath=false', 'ls-files',
979 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000980 self.checkout_path)
981 for path in (p for p in paths.splitlines() if p.endswith('/')):
982 full_path = os.path.join(self.checkout_path, path)
983 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000984 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000985 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000986
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000987 return self._Capture(['rev-parse', '--verify', 'HEAD'])
988
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000989 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000990 """Reverts local modifications.
991
992 All reverted files will be appended to file_list.
993 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000994 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000995 # revert won't work if the directory doesn't exist. It needs to
996 # checkout instead.
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000997 self.Print('_____ %s is missing, syncing instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000998 # Don't reuse the args.
999 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001000
Josip Sokcevic7e133ff2021-07-13 17:44:53 +00001001 default_rev = "refs/heads/main"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +00001002 if options.upstream:
1003 if self._GetCurrentBranch():
1004 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
1005 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001006 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001007 if not deps_revision:
1008 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +00001009 if deps_revision.startswith('refs/heads/'):
1010 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -07001011 try:
1012 deps_revision = self.GetUsableRev(deps_revision, options)
1013 except NoUsableRevError as e:
1014 # If the DEPS entry's url and hash changed, try to update the origin.
1015 # See also http://crbug.com/520067.
John Budorickd94f8ea2020-03-27 15:55:24 +00001016 logging.warning(
1017 "Couldn't find usable revision, will retrying to update instead: %s",
smutae7ea312016-07-18 11:59:41 -07001018 e.message)
1019 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001020
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001021 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001022 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001023
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001024 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +00001025 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001026
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001027 if file_list is not None:
1028 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
1029
1030 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001031 """Returns revision"""
1032 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +00001033
msb@chromium.orge28e4982009-09-25 20:51:45 +00001034 def runhooks(self, options, args, file_list):
1035 self.status(options, args, file_list)
1036
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001037 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +00001038 """Display status information."""
1039 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001040 self.Print('________ couldn\'t run status in %s:\n'
1041 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001042 else:
Anthony Politobb457342019-11-15 22:26:01 +00001043 merge_base = []
1044 if self.url:
1045 _, base_rev = gclient_utils.SplitUrlRevision(self.url)
1046 if base_rev:
Joanna Wanga654ff32023-07-18 23:25:19 +00001047 if base_rev.startswith('refs/'):
1048 base_rev = ''.join(scm.GIT.RefToRemoteRef(base_rev, self.remote))
Anthony Politobb457342019-11-15 22:26:01 +00001049 merge_base = [base_rev]
Aaron Gablef4068aa2017-12-12 15:14:09 -08001050 self._Run(
1051 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
Edward Lemur24146be2019-08-01 21:44:52 +00001052 options, always_show_header=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001053 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001054 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001055 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +00001056
smutae7ea312016-07-18 11:59:41 -07001057 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -07001058 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -07001059 sha1 = None
1060 if not os.path.isdir(self.checkout_path):
1061 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -08001062 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -07001063
1064 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1065 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001066 else:
agable41e3a6c2016-10-20 11:36:56 -07001067 # May exist in origin, but we don't have it yet, so fetch and look
1068 # again.
1069 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -07001070 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1071 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001072
1073 if not sha1:
1074 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -08001075 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -07001076
1077 return sha1
1078
primiano@chromium.org1c127382015-02-17 11:15:40 +00001079 def GetGitBackupDirPath(self):
1080 """Returns the path where the .git folder for the current project can be
1081 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
1082 return os.path.join(self._root_dir,
1083 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
1084
Edward Lesmes07a68342021-04-20 23:39:30 +00001085 def _GetMirror(self, url, options, revision=None, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001086 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +00001087 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001088 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +00001089 mirror_kwargs = {
1090 'print_func': self.filter,
Edward Lesmes07a68342021-04-20 23:39:30 +00001091 'refs': [],
1092 'commits': [],
hinoka@google.comb1b54572014-04-16 22:29:23 +00001093 }
hinoka@google.comb1b54572014-04-16 22:29:23 +00001094 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1095 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001096 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
1097 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001098 if hasattr(options, 'with_tags') and options.with_tags:
1099 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001100 elif revision_ref and revision_ref.startswith('refs/tags/'):
1101 mirror_kwargs['refs'].append(revision_ref)
Edward Lesmes07a68342021-04-20 23:39:30 +00001102 if revision and not revision.startswith('refs/'):
1103 mirror_kwargs['commits'].append(revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001104 return git_cache.Mirror(url, **mirror_kwargs)
1105
John Budorick882c91e2018-07-12 22:11:41 +00001106 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001107 """Update a git mirror by fetching the latest commits from the remote,
1108 unless mirror already contains revision whose type is sha1 hash.
1109 """
John Budorick882c91e2018-07-12 22:11:41 +00001110 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001111 if options.verbose:
1112 self.Print('skipping mirror update, it has rev=%s already' % revision,
1113 timestamp=False)
1114 return
1115
szager@chromium.org3ec84f62014-08-22 21:00:22 +00001116 if getattr(options, 'shallow', False):
Victor Vianna392ce7e2022-06-07 21:47:51 +00001117 depth = 10000
hinoka@chromium.org46b87412014-05-15 00:42:05 +00001118 else:
1119 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +00001120 mirror.populate(verbose=options.verbose,
1121 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +00001122 depth=depth,
Vadim Shtayura08049e22017-10-11 00:14:52 +00001123 lock_timeout=getattr(options, 'lock_timeout', 0))
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001124
John Budorick882c91e2018-07-12 22:11:41 +00001125 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001126 """Clone a git repository from the given URL.
1127
msb@chromium.org786fb682010-06-02 15:16:23 +00001128 Once we've cloned the repo, we checkout a working branch if the specified
1129 revision is a branch head. If it is a tag or a specific commit, then we
1130 leave HEAD detached as it makes future updates simpler -- in this case the
1131 user should first create a new branch or switch to an existing branch before
1132 making changes in the repo."""
Joanna Wang1a977bd2022-06-02 21:51:17 +00001133 in_cog_workspace = self._IsCog()
1134
1135 if self.print_outbuf:
1136 print_stdout = True
1137 filter_fn = None
1138 else:
1139 print_stdout = False
1140 filter_fn = self.filter
1141
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001142 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001143 # git clone doesn't seem to insert a newline properly before printing
1144 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001145 self.Print('')
Joanna Wang1a977bd2022-06-02 21:51:17 +00001146
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001147 # If the parent directory does not exist, Git clone on Windows will not
1148 # create it, so we need to do it manually.
1149 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001150 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001151
Joanna Wang1a977bd2022-06-02 21:51:17 +00001152 if in_cog_workspace:
1153 clone_cmd = ['citc', 'clone-repo', url, self.checkout_path]
1154 clone_cmd.append(
1155 gclient_utils.ExtractRefName(self.remote, revision) or revision)
1156 try:
1157 self._Run(clone_cmd,
1158 options,
1159 cwd=self._root_dir,
1160 retry=True,
1161 print_stdout=print_stdout,
1162 filter_fn=filter_fn)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001163 except:
1164 traceback.print_exc(file=self.out_fh)
1165 raise
1166 self._SetFetchConfig(options)
Travis Lane738b48a2022-11-28 20:28:51 +00001167 elif hasattr(options, 'no_history') and options.no_history:
1168 self._Run(['init', self.checkout_path], options, cwd=self._root_dir)
1169 self._Run(['remote', 'add', 'origin', url], options)
1170 revision = self._AutoFetchRef(options, revision, depth=1)
1171 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1172 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001173 else:
1174 cfg = gclient_utils.DefaultIndexPackConfig(url)
1175 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
1176 if self.cache_dir:
1177 clone_cmd.append('--shared')
1178 if options.verbose:
1179 clone_cmd.append('--verbose')
1180 clone_cmd.append(url)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001181 tmp_dir = tempfile.mkdtemp(prefix='_gclient_%s_' %
1182 os.path.basename(self.checkout_path),
1183 dir=parent_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001184 clone_cmd.append(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001185
1186 try:
1187 self._Run(clone_cmd,
1188 options,
1189 cwd=self._root_dir,
1190 retry=True,
1191 print_stdout=print_stdout,
1192 filter_fn=filter_fn)
Joanna Wang47fd5672022-08-05 20:53:31 +00001193 logging.debug('Cloned into temporary dir, moving to checkout_path')
Joanna Wang1a977bd2022-06-02 21:51:17 +00001194 gclient_utils.safe_makedirs(self.checkout_path)
1195 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1196 os.path.join(self.checkout_path, '.git'))
1197 except:
1198 traceback.print_exc(file=self.out_fh)
1199 raise
1200 finally:
1201 if os.listdir(tmp_dir):
1202 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
Josip Sokcevicebccac72022-06-24 21:44:49 +00001203 gclient_utils.rmtree(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001204
1205 self._SetFetchConfig(options)
1206 self._Fetch(options, prune=options.force)
1207 revision = self._AutoFetchRef(options, revision)
1208 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1209 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
1210
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001211 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001212 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001213 self.Print(
Joanna Wang1a977bd2022-06-02 21:51:17 +00001214 ('Checked out %s to a detached HEAD. Before making any commits\n'
1215 'in this repo, you should use \'git checkout <branch>\' to switch \n'
1216 'to an existing branch or use \'git checkout %s -b <branch>\' to\n'
1217 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001218
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001219 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001220 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001221 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001222 raise gclient_utils.Error("Background task requires input. Rerun "
1223 "gclient with --jobs=1 so that\n"
1224 "interaction is possible.")
Edward Lesmesae3586b2020-03-23 21:21:14 +00001225 return gclient_utils.AskForData(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001226
1227
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001228 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001229 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001230 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001231 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001232 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001233 revision = upstream
1234 if newbase:
1235 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001236 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001237 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001238 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001239 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001240 printed_path = True
1241 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001242 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001243
1244 if merge:
1245 merge_output = self._Capture(['merge', revision])
1246 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001247 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001248 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001249
1250 # Build the rebase command here using the args
1251 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1252 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001253 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001254 rebase_cmd.append('--verbose')
1255 if newbase:
1256 rebase_cmd.extend(['--onto', newbase])
1257 rebase_cmd.append(upstream)
1258 if branch:
1259 rebase_cmd.append(branch)
1260
1261 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001262 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001263 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001264 if (re.match(br'cannot rebase: you have unstaged changes', e.stderr) or
1265 re.match(br'cannot rebase: your index contains uncommitted changes',
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001266 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001267 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001268 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001269 'Cannot rebase because of unstaged changes.\n'
1270 '\'git reset --hard HEAD\' ?\n'
1271 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001272 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001273 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001274 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001275 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001276 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001277 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001278
1279 if re.match(r'quit|q', rebase_action, re.I):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001280 raise gclient_utils.Error("Please merge or rebase manually\n"
1281 "cd %s && git " % self.checkout_path
1282 + "%s" % ' '.join(rebase_cmd))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001283
1284 if re.match(r'show|s', rebase_action, re.I):
Edward Lemur979fa782019-08-13 22:44:05 +00001285 self.Print('%s' % e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001286 continue
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001287
1288 gclient_utils.Error("Input not recognized")
1289 continue
Edward Lemur979fa782019-08-13 22:44:05 +00001290 elif re.search(br'^CONFLICT', e.stdout, re.M):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001291 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1292 "Fix the conflict and run gclient again.\n"
1293 "See 'man git-rebase' for details.\n")
1294 else:
Edward Lemur979fa782019-08-13 22:44:05 +00001295 self.Print(e.stdout.decode('utf-8').strip())
1296 self.Print('Rebase produced error output:\n%s' %
1297 e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001298 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1299 "manually.\ncd %s && git " %
1300 self.checkout_path
1301 + "%s" % ' '.join(rebase_cmd))
1302
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001303 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001304 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001305 # Make the output a little prettier. It's nice to have some
1306 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001307 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001308
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001309 @staticmethod
1310 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001311 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1312 if not ok:
1313 raise gclient_utils.Error('git version %s < minimum required %s' %
1314 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001315
John Budorick882c91e2018-07-12 22:11:41 +00001316 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001317 # Special case handling if all 3 conditions are met:
1318 # * the mirros have recently changed, but deps destination remains same,
1319 # * the git histories of mirrors are conflicting.
1320 # * git cache is used
1321 # This manifests itself in current checkout having invalid HEAD commit on
1322 # most git operations. Since git cache is used, just deleted the .git
1323 # folder, and re-create it by cloning.
1324 try:
1325 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1326 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001327 if (b'fatal: bad object HEAD' in e.stderr
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001328 and self.cache_dir and self.cache_dir in url):
1329 self.Print((
1330 'Likely due to DEPS change with git cache_dir, '
1331 'the current commit points to no longer existing object.\n'
1332 '%s' % e)
1333 )
1334 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001335 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001336 else:
1337 raise
1338
msb@chromium.org786fb682010-06-02 15:16:23 +00001339 def _IsRebasing(self):
1340 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1341 # have a plumbing command to determine whether a rebase is in progress, so
1342 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1343 g = os.path.join(self.checkout_path, '.git')
1344 return (
1345 os.path.isdir(os.path.join(g, "rebase-merge")) or
1346 os.path.isdir(os.path.join(g, "rebase-apply")))
1347
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001348 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001349 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1350 if os.path.exists(lockfile):
1351 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001352 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001353 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1354 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001355 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001356
msb@chromium.org786fb682010-06-02 15:16:23 +00001357 # Make sure the tree is clean; see git-rebase.sh for reference
1358 try:
1359 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001360 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001361 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001362 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001363 '\tYou have unstaged changes.\n'
1364 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001365 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001366 try:
1367 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001368 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001369 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001370 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001371 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001372 '\tYour index contains uncommitted changes\n'
1373 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001374 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001375
agable83faed02016-10-24 14:37:10 -07001376 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001377 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1378 # reference by a commit). If not, error out -- most likely a rebase is
1379 # in progress, try to detect so we can give a better error.
1380 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001381 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1382 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001383 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001384 # Commit is not contained by any rev. See if the user is rebasing:
1385 if self._IsRebasing():
1386 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001387 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001388 '\tAlready in a conflict, i.e. (no branch).\n'
1389 '\tFix the conflict and run gclient again.\n'
1390 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1391 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001392 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001393 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001394 name = ('saved-by-gclient-' +
1395 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001396 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001397 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001398 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001399
msb@chromium.org5bde4852009-12-14 16:47:12 +00001400 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001401 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001402 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001403 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001404 return None
1405 return branch
1406
borenet@google.comc3e09d22014-04-10 13:58:18 +00001407 def _Capture(self, args, **kwargs):
Mike Frysinger286fb162019-09-30 03:14:10 +00001408 set_git_dir = 'cwd' not in kwargs
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001409 kwargs.setdefault('cwd', self.checkout_path)
1410 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001411 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001412 env = scm.GIT.ApplyEnvVars(kwargs)
Mike Frysinger286fb162019-09-30 03:14:10 +00001413 # If an explicit cwd isn't set, then default to the .git/ subdir so we get
1414 # stricter behavior. This can be useful in cases of slight corruption --
1415 # we don't accidentally go corrupting parent git checks too. See
1416 # https://crbug.com/1000825 for an example.
1417 if set_git_dir:
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001418 git_dir = os.path.abspath(os.path.join(self.checkout_path, '.git'))
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001419 # Depending on how the .gclient file was defined, self.checkout_path
1420 # might be set to a unicode string, not a regular string; on Windows
1421 # Python2, we can't set env vars to be unicode strings, so we
1422 # forcibly cast the value to a string before setting it.
1423 env.setdefault('GIT_DIR', str(git_dir))
Raul Tambrecd862e32019-05-10 21:19:00 +00001424 ret = subprocess2.check_output(
1425 ['git'] + args, env=env, **kwargs).decode('utf-8')
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001426 if strip:
1427 ret = ret.strip()
Erik Chene16ffff2019-10-14 20:35:53 +00001428 self.Print('Finished running: %s %s' % ('git', ' '.join(args)))
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001429 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001430
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001431 def _Checkout(self, options, ref, force=False, quiet=None):
1432 """Performs a 'git-checkout' operation.
1433
1434 Args:
1435 options: The configured option set
1436 ref: (str) The branch/commit to checkout
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001437 quiet: (bool/None) Whether or not the checkout should pass '--quiet'; if
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001438 'None', the behavior is inferred from 'options.verbose'.
1439 Returns: (str) The output of the checkout operation
1440 """
1441 if quiet is None:
1442 quiet = (not options.verbose)
1443 checkout_args = ['checkout']
1444 if force:
1445 checkout_args.append('--force')
1446 if quiet:
1447 checkout_args.append('--quiet')
1448 checkout_args.append(ref)
1449 return self._Capture(checkout_args)
1450
Travis Lane738b48a2022-11-28 20:28:51 +00001451 def _Fetch(self,
1452 options,
1453 remote=None,
1454 prune=False,
1455 quiet=False,
1456 refspec=None,
1457 depth=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001458 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001459 # When updating, the ref is modified to be a remote ref .
1460 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1461 # Try to reverse that mapping.
1462 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1463 if original_ref:
1464 refspec = original_ref + ':' + refspec
1465 # When a mirror is configured, it only fetches
1466 # refs/{heads,branch-heads,tags}/*.
1467 # If asked to fetch other refs, we must fetch those directly from the
1468 # repository, and not from the mirror.
1469 if not original_ref.startswith(
1470 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1471 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001472 fetch_cmd = cfg + [
1473 'fetch',
1474 remote or self.remote,
1475 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001476 if refspec:
1477 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001478
1479 if prune:
1480 fetch_cmd.append('--prune')
1481 if options.verbose:
1482 fetch_cmd.append('--verbose')
danakjd5c0b562019-11-08 17:27:47 +00001483 if not hasattr(options, 'with_tags') or not options.with_tags:
1484 fetch_cmd.append('--no-tags')
dnj@chromium.org680f2172014-06-25 00:39:32 +00001485 elif quiet:
1486 fetch_cmd.append('--quiet')
Travis Lane738b48a2022-11-28 20:28:51 +00001487 if depth:
1488 fetch_cmd.append('--depth=' + str(depth))
tandrii64103db2016-10-11 05:30:05 -07001489 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001490
Edward Lemur579c9862018-07-13 23:17:51 +00001491 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001492 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1493 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001494 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001495 try:
1496 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1497 options)
1498 self._Run(['config', 'remote.%s.fetch' % self.remote,
1499 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1500 except subprocess2.CalledProcessError as e:
1501 # If exit code was 5, it means we attempted to unset a config that
1502 # didn't exist. Ignore it.
1503 if e.returncode != 5:
1504 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001505 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001506 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001507 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1508 '^\\+refs/branch-heads/\\*:.*$']
1509 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001510 if hasattr(options, 'with_tags') and options.with_tags:
1511 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1512 '+refs/tags/*:refs/tags/*',
1513 '^\\+refs/tags/\\*:.*$']
1514 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001515
Travis Lane738b48a2022-11-28 20:28:51 +00001516 def _AutoFetchRef(self, options, revision, depth=None):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001517 """Attempts to fetch |revision| if not available in local repo.
1518
1519 Returns possibly updated revision."""
Edward Lemure0ba7b82020-03-11 20:31:32 +00001520 if not scm.GIT.IsValidRevision(self.checkout_path, revision):
Travis Lane738b48a2022-11-28 20:28:51 +00001521 self._Fetch(options, refspec=revision, depth=depth)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001522 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1523 return revision
1524
Edward Lemur24146be2019-08-01 21:44:52 +00001525 def _Run(self, args, options, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001526 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001527 kwargs.setdefault('cwd', self.checkout_path)
Edward Lemur24146be2019-08-01 21:44:52 +00001528 kwargs.setdefault('filter_fn', self.filter)
1529 kwargs.setdefault('show_header', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001530 env = scm.GIT.ApplyEnvVars(kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +00001531
agable@chromium.org772efaf2014-04-01 02:35:44 +00001532 cmd = ['git'] + args
Edward Lemur24146be2019-08-01 21:44:52 +00001533 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001534
1535
1536class CipdPackage(object):
1537 """A representation of a single CIPD package."""
1538
John Budorickd3ba72b2018-03-20 12:27:42 -07001539 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001540 self._authority_for_subdir = authority_for_subdir
1541 self._name = name
1542 self._version = version
1543
1544 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001545 def authority_for_subdir(self):
1546 """Whether this package has authority to act on behalf of its subdir.
1547
1548 Some operations should only be performed once per subdirectory. A package
1549 that has authority for its subdirectory is the only package that should
1550 perform such operations.
1551
1552 Returns:
1553 bool; whether this package has subdir authority.
1554 """
1555 return self._authority_for_subdir
1556
1557 @property
1558 def name(self):
1559 return self._name
1560
1561 @property
1562 def version(self):
1563 return self._version
1564
1565
1566class CipdRoot(object):
1567 """A representation of a single CIPD root."""
1568 def __init__(self, root_dir, service_url):
1569 self._all_packages = set()
1570 self._mutator_lock = threading.Lock()
1571 self._packages_by_subdir = collections.defaultdict(list)
1572 self._root_dir = root_dir
1573 self._service_url = service_url
Dan Le Febvre456d0852023-05-24 23:43:40 +00001574 self._resolved_packages = None
John Budorick0f7b2002018-01-19 15:46:17 -08001575
1576 def add_package(self, subdir, package, version):
1577 """Adds a package to this CIPD root.
1578
1579 As far as clients are concerned, this grants both root and subdir authority
1580 to packages arbitrarily. (The implementation grants root authority to the
1581 first package added and subdir authority to the first package added for that
1582 subdir, but clients should not depend on or expect that behavior.)
1583
1584 Args:
1585 subdir: str; relative path to where the package should be installed from
1586 the cipd root directory.
1587 package: str; the cipd package name.
1588 version: str; the cipd package version.
1589 Returns:
1590 CipdPackage; the package that was created and added to this root.
1591 """
1592 with self._mutator_lock:
1593 cipd_package = CipdPackage(
1594 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001595 not self._packages_by_subdir[subdir])
1596 self._all_packages.add(cipd_package)
1597 self._packages_by_subdir[subdir].append(cipd_package)
1598 return cipd_package
1599
1600 def packages(self, subdir):
1601 """Get the list of configured packages for the given subdir."""
1602 return list(self._packages_by_subdir[subdir])
1603
Dan Le Febvre456d0852023-05-24 23:43:40 +00001604 def resolved_packages(self):
1605 if not self._resolved_packages:
1606 self._resolved_packages = self.ensure_file_resolve()
1607 return self._resolved_packages
1608
John Budorick0f7b2002018-01-19 15:46:17 -08001609 def clobber(self):
1610 """Remove the .cipd directory.
1611
1612 This is useful for forcing ensure to redownload and reinitialize all
1613 packages.
1614 """
1615 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001616 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001617 try:
1618 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1619 except OSError:
1620 if os.path.exists(cipd_cache_dir):
1621 raise
1622
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001623 def expand_package_name(self, package_name_string, **kwargs):
1624 """Run `cipd expand-package-name`.
1625
1626 CIPD package names can be declared with placeholder variables
1627 such as '${platform}', this cmd will return the package name
1628 with the variables resolved. The resolution is based on the host
1629 the command is executing on.
1630 """
1631
1632 kwargs.setdefault('stderr', subprocess2.PIPE)
1633 cmd = ['cipd', 'expand-package-name', package_name_string]
1634 ret = subprocess2.check_output(cmd, **kwargs).decode('utf-8')
1635 return ret.strip()
1636
John Budorick0f7b2002018-01-19 15:46:17 -08001637 @contextlib.contextmanager
1638 def _create_ensure_file(self):
1639 try:
Stephanie Kim700aee72022-06-01 19:58:30 +00001640 contents = '$ParanoidMode CheckPresence\n'
1641 # TODO(crbug/1329641): Remove once cipd packages have been updated
1642 # to always be created in copy mode.
1643 contents += '$OverrideInstallMode copy\n\n'
Edward Lesmes05934952019-12-19 20:38:09 +00001644 for subdir, packages in sorted(self._packages_by_subdir.items()):
1645 contents += '@Subdir %s\n' % subdir
1646 for package in sorted(packages, key=lambda p: p.name):
1647 contents += '%s %s\n' % (package.name, package.version)
1648 contents += '\n'
John Budorick0f7b2002018-01-19 15:46:17 -08001649 ensure_file = None
1650 with tempfile.NamedTemporaryFile(
Edward Lesmes05934952019-12-19 20:38:09 +00001651 suffix='.ensure', delete=False, mode='wb') as ensure_file:
1652 ensure_file.write(contents.encode('utf-8', 'replace'))
John Budorick0f7b2002018-01-19 15:46:17 -08001653 yield ensure_file.name
1654 finally:
1655 if ensure_file is not None and os.path.exists(ensure_file.name):
1656 os.remove(ensure_file.name)
1657
1658 def ensure(self):
1659 """Run `cipd ensure`."""
1660 with self._mutator_lock:
1661 with self._create_ensure_file() as ensure_file:
1662 cmd = [
1663 'cipd', 'ensure',
1664 '-log-level', 'error',
1665 '-root', self.root_dir,
1666 '-ensure-file', ensure_file,
1667 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001668 gclient_utils.CheckCallAndFilter(
1669 cmd, print_stdout=True, show_header=True)
John Budorick0f7b2002018-01-19 15:46:17 -08001670
Dan Le Febvre456d0852023-05-24 23:43:40 +00001671 @contextlib.contextmanager
1672 def _create_ensure_file_for_resolve(self):
1673 try:
1674 contents = '$ResolvedVersions %s\n' % os.devnull
1675 for subdir, packages in sorted(self._packages_by_subdir.items()):
1676 contents += '@Subdir %s\n' % subdir
1677 for package in sorted(packages, key=lambda p: p.name):
1678 contents += '%s %s\n' % (package.name, package.version)
1679 contents += '\n'
1680 ensure_file = None
1681 with tempfile.NamedTemporaryFile(suffix='.ensure',
1682 delete=False,
1683 mode='wb') as ensure_file:
1684 ensure_file.write(contents.encode('utf-8', 'replace'))
1685 yield ensure_file.name
1686 finally:
1687 if ensure_file is not None and os.path.exists(ensure_file.name):
1688 os.remove(ensure_file.name)
1689
1690 def _create_resolved_file(self):
1691 return tempfile.NamedTemporaryFile(suffix='.resolved',
1692 delete=False,
1693 mode='wb')
1694
1695 def ensure_file_resolve(self):
1696 """Run `cipd ensure-file-resolve`."""
1697 with self._mutator_lock:
1698 with self._create_resolved_file() as output_file:
1699 with self._create_ensure_file_for_resolve() as ensure_file:
1700 cmd = [
1701 'cipd',
1702 'ensure-file-resolve',
1703 '-log-level',
1704 'error',
1705 '-ensure-file',
1706 ensure_file,
1707 '-json-output',
1708 output_file.name,
1709 ]
1710 gclient_utils.CheckCallAndFilter(cmd,
1711 print_stdout=False,
1712 show_header=False)
1713 with open(output_file.name) as f:
1714 output_json = json.load(f)
1715 return output_json.get('result', {})
1716
John Budorickd3ba72b2018-03-20 12:27:42 -07001717 def run(self, command):
1718 if command == 'update':
1719 self.ensure()
1720 elif command == 'revert':
1721 self.clobber()
1722 self.ensure()
1723
John Budorick0f7b2002018-01-19 15:46:17 -08001724 def created_package(self, package):
1725 """Checks whether this root created the given package.
1726
1727 Args:
1728 package: CipdPackage; the package to check.
1729 Returns:
1730 bool; whether this root created the given package.
1731 """
1732 return package in self._all_packages
1733
1734 @property
1735 def root_dir(self):
1736 return self._root_dir
1737
1738 @property
1739 def service_url(self):
1740 return self._service_url
1741
1742
1743class CipdWrapper(SCMWrapper):
1744 """Wrapper for CIPD.
1745
1746 Currently only supports chrome-infra-packages.appspot.com.
1747 """
John Budorick3929e9e2018-02-04 18:18:07 -08001748 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001749
1750 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1751 out_cb=None, root=None, package=None):
1752 super(CipdWrapper, self).__init__(
1753 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1754 out_cb=out_cb)
1755 assert root.created_package(package)
1756 self._package = package
1757 self._root = root
1758
1759 #override
1760 def GetCacheMirror(self):
1761 return None
1762
1763 #override
1764 def GetActualRemoteURL(self, options):
1765 return self._root.service_url
1766
1767 #override
1768 def DoesRemoteURLMatch(self, options):
1769 del options
1770 return True
1771
1772 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001773 """Does nothing.
1774
1775 CIPD packages should be reverted at the root by running
1776 `CipdRoot.run('revert')`.
1777 """
John Budorick0f7b2002018-01-19 15:46:17 -08001778
1779 def diff(self, options, args, file_list):
1780 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001781
1782 def pack(self, options, args, file_list):
1783 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001784
1785 def revinfo(self, options, args, file_list):
1786 """Grab the instance ID."""
1787 try:
1788 tmpdir = tempfile.mkdtemp()
Dan Le Febvre456d0852023-05-24 23:43:40 +00001789 # Attempt to get instance_id from the root resolved cache.
1790 # Resolved cache will not match on any CIPD packages with
1791 # variables such as ${platform}, they will fall back to
1792 # the slower method below.
1793 resolved = self._root.resolved_packages()
1794 if resolved:
1795 # CIPD uses POSIX separators across all platforms, so
1796 # replace any Windows separators.
1797 path_split = self.relpath.replace(os.sep, "/").split(":")
1798 if len(path_split) > 1:
1799 src_path, package = path_split
1800 if src_path in resolved:
1801 for resolved_package in resolved[src_path]:
1802 if package == resolved_package.get('pin', {}).get('package'):
1803 return resolved_package.get('pin', {}).get('instance_id')
1804
John Budorick0f7b2002018-01-19 15:46:17 -08001805 describe_json_path = os.path.join(tmpdir, 'describe.json')
1806 cmd = [
1807 'cipd', 'describe',
1808 self._package.name,
1809 '-log-level', 'error',
1810 '-version', self._package.version,
1811 '-json-output', describe_json_path
1812 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001813 gclient_utils.CheckCallAndFilter(cmd)
John Budorick0f7b2002018-01-19 15:46:17 -08001814 with open(describe_json_path) as f:
1815 describe_json = json.load(f)
1816 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1817 finally:
1818 gclient_utils.rmtree(tmpdir)
1819
1820 def status(self, options, args, file_list):
1821 pass
1822
1823 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001824 """Does nothing.
1825
1826 CIPD packages should be updated at the root by running
1827 `CipdRoot.run('update')`.
1828 """