blob: 60958b5ccf4c7b78745db9ff5fb7a2a25995bc38 [file] [log] [blame]
steveblock@chromium.org93567042012-02-15 01:02:26 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00005"""Gclient-specific SCM-specific operations."""
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00006
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00007from __future__ import print_function
8
John Budorick0f7b2002018-01-19 15:46:17 -08009import collections
10import contextlib
borenet@google.comb2256212014-05-07 20:57:28 +000011import errno
John Budorick0f7b2002018-01-19 15:46:17 -080012import json
maruel@chromium.org754960e2009-09-21 12:31:05 +000013import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import os
Tomasz Wiszkowskid4e66882021-08-19 21:35:09 +000015import platform
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000016import posixpath
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000017import re
maruel@chromium.org90541732011-04-01 17:54:18 +000018import sys
ilevy@chromium.org3534aa52013-07-20 01:58:08 +000019import tempfile
John Budorick0f7b2002018-01-19 15:46:17 -080020import threading
zty@chromium.org6279e8a2014-02-13 01:45:25 +000021import traceback
Raul Tambreb946b232019-03-26 14:48:46 +000022
23try:
24 import urlparse
25except ImportError: # For Py3 compatibility
26 import urllib.parse as urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000027
28import gclient_utils
Ravi Mistryecda7822022-02-28 16:22:20 +000029import gerrit_util
szager@chromium.org848fd492014-04-09 19:06:44 +000030import git_cache
Josip Sokcevic7958e302023-03-01 23:02:21 +000031import scm
borenet@google.comb2256212014-05-07 20:57:28 +000032import shutil
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000033import subprocess2
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000034
35
smutae7ea312016-07-18 11:59:41 -070036class NoUsableRevError(gclient_utils.Error):
37 """Raised if requested revision isn't found in checkout."""
38
39
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000040class DiffFiltererWrapper(object):
41 """Simple base class which tracks which file is being diffed and
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000042 replaces instances of its file name in the original and
agable41e3a6c2016-10-20 11:36:56 -070043 working copy lines of the git diff output."""
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000044 index_string = None
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000045 original_prefix = "--- "
46 working_prefix = "+++ "
47
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000048 def __init__(self, relpath, print_func):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000049 # Note that we always use '/' as the path separator to be
agable41e3a6c2016-10-20 11:36:56 -070050 # consistent with cygwin-style output on Windows
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000051 self._relpath = relpath.replace("\\", "/")
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000052 self._current_file = None
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000053 self._print_func = print_func
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000054
maruel@chromium.org6e29d572010-06-04 17:32:20 +000055 def SetCurrentFile(self, current_file):
56 self._current_file = current_file
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000057
iannucci@chromium.org3830a672013-02-19 20:15:14 +000058 @property
59 def _replacement_file(self):
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000060 return posixpath.join(self._relpath, self._current_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000061
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000062 def _Replace(self, line):
63 return line.replace(self._current_file, self._replacement_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000064
65 def Filter(self, line):
66 if (line.startswith(self.index_string)):
67 self.SetCurrentFile(line[len(self.index_string):])
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000068 line = self._Replace(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000069 else:
70 if (line.startswith(self.original_prefix) or
71 line.startswith(self.working_prefix)):
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000072 line = self._Replace(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000073 self._print_func(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000074
75
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000076class GitDiffFilterer(DiffFiltererWrapper):
77 index_string = "diff --git "
78
79 def SetCurrentFile(self, current_file):
80 # Get filename by parsing "a/<filename> b/<filename>"
81 self._current_file = current_file[:(len(current_file)/2)][2:]
82
83 def _Replace(self, line):
84 return re.sub("[a|b]/" + self._current_file, self._replacement_file, line)
85
86
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000087# SCMWrapper base class
88
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000089class SCMWrapper(object):
90 """Add necessary glue between all the supported SCM.
91
msb@chromium.orgd6504212010-01-13 17:34:31 +000092 This is the abstraction layer to bind to different SCM.
93 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000094 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
Edward Lemur231f5ea2018-01-31 19:02:36 +010095 out_cb=None, print_outbuf=False):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000096 self.url = url
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +000097 self._root_dir = root_dir
98 if self._root_dir:
99 self._root_dir = self._root_dir.replace('/', os.sep)
100 self.relpath = relpath
101 if self.relpath:
102 self.relpath = self.relpath.replace('/', os.sep)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000103 if self.relpath and self._root_dir:
104 self.checkout_path = os.path.join(self._root_dir, self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000105 if out_fh is None:
106 out_fh = sys.stdout
107 self.out_fh = out_fh
108 self.out_cb = out_cb
Edward Lemur231f5ea2018-01-31 19:02:36 +0100109 self.print_outbuf = print_outbuf
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000110
111 def Print(self, *args, **kwargs):
112 kwargs.setdefault('file', self.out_fh)
113 if kwargs.pop('timestamp', True):
114 self.out_fh.write('[%s] ' % gclient_utils.Elapsed())
115 print(*args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000116
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000117 def RunCommand(self, command, options, args, file_list=None):
agabledebf6c82016-12-21 12:50:12 -0800118 commands = ['update', 'updatesingle', 'revert',
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000119 'revinfo', 'status', 'diff', 'pack', 'runhooks']
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000120
121 if not command in commands:
122 raise gclient_utils.Error('Unknown command %s' % command)
123
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000124 if not command in dir(self):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000125 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % (
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000126 command, self.__class__.__name__))
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000127
128 return getattr(self, command)(options, args, file_list)
129
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000130 @staticmethod
131 def _get_first_remote_url(checkout_path):
132 log = scm.GIT.Capture(
133 ['config', '--local', '--get-regexp', r'remote.*.url'],
134 cwd=checkout_path)
135 # Get the second token of the first line of the log.
136 return log.splitlines()[0].split(' ', 1)[1]
137
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000138 def GetCacheMirror(self):
Robert Iannuccia19649b2018-06-29 16:31:45 +0000139 if getattr(self, 'cache_dir', None):
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000140 url, _ = gclient_utils.SplitUrlRevision(self.url)
141 return git_cache.Mirror(url)
142 return None
143
smut@google.comd33eab32014-07-07 19:35:18 +0000144 def GetActualRemoteURL(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000145 """Attempt to determine the remote URL for this SCMWrapper."""
smut@google.comd33eab32014-07-07 19:35:18 +0000146 # Git
borenet@google.combda475e2014-03-24 19:04:45 +0000147 if os.path.exists(os.path.join(self.checkout_path, '.git')):
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000148 actual_remote_url = self._get_first_remote_url(self.checkout_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000149
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000150 mirror = self.GetCacheMirror()
151 # If the cache is used, obtain the actual remote URL from there.
152 if (mirror and mirror.exists() and
153 mirror.mirror_path.replace('\\', '/') ==
154 actual_remote_url.replace('\\', '/')):
155 actual_remote_url = self._get_first_remote_url(mirror.mirror_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000156 return actual_remote_url
borenet@google.com88d10082014-03-21 17:24:48 +0000157 return None
158
borenet@google.com4e9be262014-04-08 19:40:30 +0000159 def DoesRemoteURLMatch(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000160 """Determine whether the remote URL of this checkout is the expected URL."""
161 if not os.path.exists(self.checkout_path):
162 # A checkout which doesn't exist can't be broken.
163 return True
164
smut@google.comd33eab32014-07-07 19:35:18 +0000165 actual_remote_url = self.GetActualRemoteURL(options)
borenet@google.com88d10082014-03-21 17:24:48 +0000166 if actual_remote_url:
borenet@google.com8156c9f2014-04-01 16:41:36 +0000167 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip('/')
168 == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/'))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000169
170 # This may occur if the self.checkout_path exists but does not contain a
171 # valid git checkout.
172 return False
borenet@google.com88d10082014-03-21 17:24:48 +0000173
borenet@google.comb09097a2014-04-09 19:09:08 +0000174 def _DeleteOrMove(self, force):
175 """Delete the checkout directory or move it out of the way.
176
177 Args:
178 force: bool; if True, delete the directory. Otherwise, just move it.
179 """
borenet@google.comb2256212014-05-07 20:57:28 +0000180 if force and os.environ.get('CHROME_HEADLESS') == '1':
181 self.Print('_____ Conflicting directory found in %s. Removing.'
182 % self.checkout_path)
183 gclient_utils.AddWarning('Conflicting directory %s deleted.'
184 % self.checkout_path)
185 gclient_utils.rmtree(self.checkout_path)
186 else:
187 bad_scm_dir = os.path.join(self._root_dir, '_bad_scm',
188 os.path.dirname(self.relpath))
189
190 try:
191 os.makedirs(bad_scm_dir)
192 except OSError as e:
193 if e.errno != errno.EEXIST:
194 raise
195
196 dest_path = tempfile.mkdtemp(
197 prefix=os.path.basename(self.relpath),
198 dir=bad_scm_dir)
199 self.Print('_____ Conflicting directory found in %s. Moving to %s.'
200 % (self.checkout_path, dest_path))
201 gclient_utils.AddWarning('Conflicting directory %s moved to %s.'
202 % (self.checkout_path, dest_path))
203 shutil.move(self.checkout_path, dest_path)
borenet@google.comb09097a2014-04-09 19:09:08 +0000204
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000205
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000206class GitWrapper(SCMWrapper):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000207 """Wrapper for Git"""
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000208 name = 'git'
maruel@chromium.org1a60dca2013-11-26 14:06:26 +0000209 remote = 'origin'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000210
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000211 _is_env_cog = None
212
213 @staticmethod
214 def _IsCog():
215 """Returns true if the env is cog"""
216 if not GitWrapper._is_env_cog:
Joanna Wangbbb66d72022-08-29 21:48:55 +0000217 GitWrapper._is_env_cog = any(os.getcwd().startswith(x) for x in [
218 '/google/cog/cloud', '/google/src/cloud'])
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000219
220 return GitWrapper._is_env_cog
221
Robert Iannuccia19649b2018-06-29 16:31:45 +0000222 @property
223 def cache_dir(self):
224 try:
225 return git_cache.Mirror.GetCachePath()
226 except RuntimeError:
227 return None
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000228
John Budorick0f7b2002018-01-19 15:46:17 -0800229 def __init__(self, url=None, *args, **kwargs):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000230 """Removes 'git+' fake prefix from git URL."""
Henrique Ferreiroe72279d2019-04-17 12:01:50 +0000231 if url and (url.startswith('git+http://') or
232 url.startswith('git+https://')):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000233 url = url[4:]
John Budorick0f7b2002018-01-19 15:46:17 -0800234 SCMWrapper.__init__(self, url, *args, **kwargs)
szager@chromium.org848fd492014-04-09 19:06:44 +0000235 filter_kwargs = { 'time_throttle': 1, 'out_fh': self.out_fh }
236 if self.out_cb:
237 filter_kwargs['predicate'] = self.out_cb
238 self.filter = gclient_utils.GitFilter(**filter_kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +0000239 self._running_under_rosetta = None
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000240
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000241 def GetCheckoutRoot(self):
242 return scm.GIT.GetCheckoutRoot(self.checkout_path)
243
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000244 def GetRevisionDate(self, _revision):
floitsch@google.comeaab7842011-04-28 09:07:58 +0000245 """Returns the given revision's date in ISO-8601 format (which contains the
246 time zone)."""
247 # TODO(floitsch): get the time-stamp of the given revision and not just the
248 # time-stamp of the currently checked out revision.
249 return self._Capture(['log', '-n', '1', '--format=%ai'])
250
Aaron Gablef4068aa2017-12-12 15:14:09 -0800251 def _GetDiffFilenames(self, base):
252 """Returns the names of files modified since base."""
253 return self._Capture(
Raul Tambrecd862e32019-05-10 21:19:00 +0000254 # Filter to remove base if it is None.
255 list(filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only',
256 base])
257 )).split()
Aaron Gablef4068aa2017-12-12 15:14:09 -0800258
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000259 def diff(self, options, _args, _file_list):
Aaron Gable1853f662018-02-12 15:45:56 -0800260 _, revision = gclient_utils.SplitUrlRevision(self.url)
261 if not revision:
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000262 revision = 'refs/remotes/%s/main' % self.remote
Aaron Gable1853f662018-02-12 15:45:56 -0800263 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000264
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000265 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000266 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000267 repository.
268
269 The patch file is generated from a diff of the merge base of HEAD and
270 its upstream branch.
271 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700272 try:
273 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
274 except subprocess2.CalledProcessError:
275 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000276 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700277 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000278 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000279 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000280
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800281 def _Scrub(self, target, options):
282 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000283 quiet = []
284 if not options.verbose:
285 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800286 self._Run(['reset', '--hard', target] + quiet, options)
287 if options.force and options.delete_unversioned_trees:
288 # where `target` is a commit that contains both upper and lower case
289 # versions of the same file on a case insensitive filesystem, we are
290 # actually in a broken state here. The index will have both 'a' and 'A',
291 # but only one of them will exist on the disk. To progress, we delete
292 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800293 output = self._Capture([
294 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800295 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800296 # --porcelain (v1) looks like:
297 # XY filename
298 try:
299 filename = line[3:]
300 self.Print('_____ Deleting residual after reset: %r.' % filename)
301 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800302 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800303 except OSError:
304 pass
305
John Budorick882c91e2018-07-12 22:11:41 +0000306 def _FetchAndReset(self, revision, file_list, options):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800307 """Equivalent to git fetch; git reset."""
Edward Lemur579c9862018-07-13 23:17:51 +0000308 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000309
dnj@chromium.org680f2172014-06-25 00:39:32 +0000310 self._Fetch(options, prune=True, quiet=options.verbose)
John Budorick882c91e2018-07-12 22:11:41 +0000311 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000312 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800313 files = self._Capture(
314 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambreb946b232019-03-26 14:48:46 +0000315 file_list.extend(
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000316 [os.path.join(self.checkout_path, f) for f in files])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000317
szager@chromium.org8a139702014-06-20 15:55:01 +0000318 def _DisableHooks(self):
319 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
320 if not os.path.isdir(hook_dir):
321 return
322 for f in os.listdir(hook_dir):
323 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000324 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
325 if os.path.exists(disabled_hook_path):
326 os.remove(disabled_hook_path)
327 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000328
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000329 def _maybe_break_locks(self, options):
330 """This removes all .lock files from this repo's .git directory, if the
331 user passed the --break_repo_locks command line flag.
332
333 In particular, this will cleanup index.lock files, as well as ref lock
334 files.
335 """
336 if options.break_repo_locks:
337 git_dir = os.path.join(self.checkout_path, '.git')
338 for path, _, filenames in os.walk(git_dir):
339 for filename in filenames:
340 if filename.endswith('.lock'):
341 to_break = os.path.join(path, filename)
342 self.Print('breaking lock: %s' % (to_break,))
343 try:
344 os.remove(to_break)
345 except OSError as ex:
346 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
347 raise
348
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000349 def _download_topics(self, patch_rev, googlesource_url):
350 """This method returns new patch_revs to process that have the same topic.
351
352 It does the following:
353 1. Finds the topic of the Gerrit change specified in the patch_rev.
354 2. Find all changes with that topic.
355 3. Append patch_rev of the changes with the same topic to the patch_revs
356 to process.
357 4. Returns the new patch_revs to process.
358 """
359 patch_revs_to_process = []
360 # Parse the patch_rev to extract the CL and patchset.
361 patch_rev_tokens = patch_rev.split('/')
362 change = patch_rev_tokens[-2]
363 # Parse the googlesource_url.
364 tokens = re.search(
365 '//(.+).googlesource.com/(.+?)(?:\.git)?$', googlesource_url)
366 if not tokens or len(tokens.groups()) != 2:
367 # googlesource_url is not in the expected format.
368 return patch_revs_to_process
369
370 # parse the gerrit host and repo out of googlesource_url.
371 host, repo = tokens.groups()[:2]
372 gerrit_host_url = '%s-review.googlesource.com' % host
373
374 # 1. Find the topic of the Gerrit change specified in the patch_rev.
375 change_object = gerrit_util.GetChange(gerrit_host_url, change)
376 topic = change_object.get('topic')
377 if not topic:
378 # This change has no topic set.
379 return patch_revs_to_process
380
381 # 2. Find all changes with that topic.
382 changes_with_same_topic = gerrit_util.QueryChanges(
383 gerrit_host_url,
384 [('topic', topic), ('status', 'open'), ('repo', repo)],
385 o_params=['ALL_REVISIONS'])
386 for c in changes_with_same_topic:
387 if str(c['_number']) == change:
388 # This change is already in the patch_rev.
389 continue
390 self.Print('Found CL %d with the topic name %s' % (
391 c['_number'], topic))
392 # 3. Append patch_rev of the changes with the same topic to the
393 # patch_revs to process.
394 curr_rev = c['current_revision']
395 new_patch_rev = c['revisions'][curr_rev]['ref']
396 patch_revs_to_process.append(new_patch_rev)
397
Joanna Wang1a977bd2022-06-02 21:51:17 +0000398 # 4. Return the new patch_revs to process.
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000399 return patch_revs_to_process
400
Kenneth Russell02e70b42023-08-07 22:09:29 +0000401 def _ref_to_remote_ref(self, target_rev):
402 """Helper function for scm.GIT.RefToRemoteRef with error checking.
403
404 Joins the results of scm.GIT.RefToRemoteRef into a string, but raises a
405 comprehensible error if RefToRemoteRef fails.
406
407 Args:
408 target_rev: a ref somewhere under refs/.
409 """
410 tmp_ref = scm.GIT.RefToRemoteRef(target_rev, self.remote)
411 if not tmp_ref:
412 raise gclient_utils.Error(
413 'Failed to turn target revision %r in repo %r into remote ref' %
414 (target_rev, self.checkout_path))
415 return ''.join(tmp_ref)
416
Edward Lemur3acbc742019-05-30 17:57:35 +0000417 def apply_patch_ref(self, patch_repo, patch_rev, target_rev, options,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000418 file_list):
Joanna Wangf3edc502022-07-20 00:12:10 +0000419 # type: (str, str, str, optparse.Values, Collection[str]) -> str
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000420 """Apply a patch on top of the revision we're synced at.
421
Edward Lemur3acbc742019-05-30 17:57:35 +0000422 The patch ref is given by |patch_repo|@|patch_rev|.
423 |target_rev| is usually the branch that the |patch_rev| was uploaded against
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000424 (e.g. 'refs/heads/main'), but this is not required.
Edward Lemur3acbc742019-05-30 17:57:35 +0000425
426 We cherry-pick all commits reachable from |patch_rev| on top of the curret
427 HEAD, excluding those reachable from |target_rev|
428 (i.e. git cherry-pick target_rev..patch_rev).
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000429
430 Graphically, it looks like this:
431
Edward Lemur3acbc742019-05-30 17:57:35 +0000432 ... -> o -> [possibly already landed commits] -> target_rev
433 \
434 -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000435
Edward Lemur3acbc742019-05-30 17:57:35 +0000436 The final checkout state is then:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000437
Edward Lemur3acbc742019-05-30 17:57:35 +0000438 ... -> HEAD -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000439
440 After application, if |options.reset_patch_ref| is specified, we soft reset
Edward Lemur3acbc742019-05-30 17:57:35 +0000441 the cherry-picked changes, keeping them in git index only.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000442
443 Args:
Edward Lemur3acbc742019-05-30 17:57:35 +0000444 patch_repo: The patch origin.
445 e.g. 'https://foo.googlesource.com/bar'
446 patch_rev: The revision to patch.
447 e.g. 'refs/changes/1234/34/1'.
448 target_rev: The revision to use when finding the merge base.
449 Typically, the branch that the patch was uploaded against.
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000450 e.g. 'refs/heads/main' or 'refs/heads/infra/config'.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000451 options: The options passed to gclient.
452 file_list: A list where modified files will be appended.
453 """
454
Edward Lemurca7d8812018-07-24 17:42:45 +0000455 # Abort any cherry-picks in progress.
456 try:
457 self._Capture(['cherry-pick', '--abort'])
458 except subprocess2.CalledProcessError:
459 pass
460
Joanna Wangf3edc502022-07-20 00:12:10 +0000461 base_rev = self.revinfo(None, None, None)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000462
Edward Lemur3acbc742019-05-30 17:57:35 +0000463 if not target_rev:
Edward Lemur4c5c8ab2019-06-07 15:58:13 +0000464 raise gclient_utils.Error('A target revision for the patch must be given')
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000465
466 if target_rev.startswith(('refs/heads/', 'refs/branch-heads')):
Edward Lesmesf627d9f2020-07-23 19:50:50 +0000467 # If |target_rev| is in refs/heads/** or refs/branch-heads/**, try first
468 # to find the corresponding remote ref for it, since |target_rev| might
469 # point to a local ref which is not up to date with the corresponding
470 # remote ref.
Kenneth Russell02e70b42023-08-07 22:09:29 +0000471 remote_ref = self._ref_to_remote_ref(target_rev)
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000472 self.Print('Trying the corresponding remote ref for %r: %r\n' % (
Edward Lemur3acbc742019-05-30 17:57:35 +0000473 target_rev, remote_ref))
474 if scm.GIT.IsValidRevision(self.checkout_path, remote_ref):
Josip Sokcevic3d7cbce2021-10-05 20:48:04 +0000475 # refs/remotes may need to be updated to cleanly cherry-pick changes.
476 # See https://crbug.com/1255178.
477 self._Capture(['fetch', '--no-tags', self.remote, target_rev])
Edward Lemur3acbc742019-05-30 17:57:35 +0000478 target_rev = remote_ref
479 elif not scm.GIT.IsValidRevision(self.checkout_path, target_rev):
480 # Fetch |target_rev| if it's not already available.
481 url, _ = gclient_utils.SplitUrlRevision(self.url)
Edward Lesmes07a68342021-04-20 23:39:30 +0000482 mirror = self._GetMirror(url, options, target_rev, target_rev)
Edward Lemur3acbc742019-05-30 17:57:35 +0000483 if mirror:
484 rev_type = 'branch' if target_rev.startswith('refs/') else 'hash'
485 self._UpdateMirrorIfNotContains(mirror, options, rev_type, target_rev)
486 self._Fetch(options, refspec=target_rev)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000487
Ravi Mistryecda7822022-02-28 16:22:20 +0000488 patch_revs_to_process = [patch_rev]
489
490 if hasattr(options, 'download_topics') and options.download_topics:
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000491 patch_revs_to_process_from_topics = self._download_topics(
492 patch_rev, self.url)
493 patch_revs_to_process.extend(patch_revs_to_process_from_topics)
Ravi Mistryecda7822022-02-28 16:22:20 +0000494
Edward Lesmesc621b212018-03-21 20:26:56 -0400495 self._Capture(['reset', '--hard'])
Ravi Mistryecda7822022-02-28 16:22:20 +0000496 for pr in patch_revs_to_process:
497 self.Print('===Applying patch===')
498 self.Print('Revision to patch is %r @ %r.' % (patch_repo, pr))
499 self.Print('Current dir is %r' % self.checkout_path)
500 self._Capture(['fetch', '--no-tags', patch_repo, pr])
501 pr = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400502
Ravi Mistryecda7822022-02-28 16:22:20 +0000503 if not options.rebase_patch_ref:
504 self._Capture(['checkout', pr])
505 # Adjust base_rev to be the first parent of our checked out patch ref;
506 # This will allow us to correctly extend `file_list`, and will show the
507 # correct file-list to programs which do `git diff --cached` expecting
508 # to see the patch diff.
509 base_rev = self._Capture(['rev-parse', pr+'~'])
510 else:
511 self.Print('Will cherrypick %r .. %r on top of %r.' % (
512 target_rev, pr, base_rev))
Edward Lemur3acbc742019-05-30 17:57:35 +0000513 try:
Joanna Wangab9c6ba2023-01-21 01:46:36 +0000514 if scm.GIT.IsAncestor(pr, target_rev, cwd=self.checkout_path):
Ravi Mistryecda7822022-02-28 16:22:20 +0000515 if len(patch_revs_to_process) > 1:
516 # If there are multiple patch_revs_to_process then we do not want
517 # want to invalidate a previous patch so throw an error.
518 raise gclient_utils.Error(
519 'patch_rev %s is an ancestor of target_rev %s. This '
520 'situation is unsupported when we need to apply multiple '
521 'patch_revs: %s' % (pr, target_rev, patch_revs_to_process))
522 # If |patch_rev| is an ancestor of |target_rev|, check it out.
523 self._Capture(['checkout', pr])
524 else:
525 # If a change was uploaded on top of another change, which has
526 # already landed, one of the commits in the cherry-pick range will
527 # be redundant, since it has already landed and its changes
528 # incorporated in the tree.
529 # We pass '--keep-redundant-commits' to ignore those changes.
530 self._Capture(['cherry-pick', target_rev + '..' + pr,
531 '--keep-redundant-commits'])
Edward Lemurca7d8812018-07-24 17:42:45 +0000532
Ravi Mistryecda7822022-02-28 16:22:20 +0000533 except subprocess2.CalledProcessError as e:
534 self.Print('Failed to apply patch.')
535 self.Print('Revision to patch was %r @ %r.' % (patch_repo, pr))
536 self.Print('Tried to cherrypick %r .. %r on top of %r.' % (
537 target_rev, pr, base_rev))
538 self.Print('Current dir is %r' % self.checkout_path)
539 self.Print('git returned non-zero exit status %s:\n%s' % (
540 e.returncode, e.stderr.decode('utf-8')))
541 # Print the current status so that developers know what changes caused
542 # the patch failure, since git cherry-pick doesn't show that
543 # information.
544 self.Print(self._Capture(['status']))
545 try:
546 self._Capture(['cherry-pick', '--abort'])
547 except subprocess2.CalledProcessError:
548 pass
549 raise
550
551 if file_list is not None:
552 file_list.extend(self._GetDiffFilenames(base_rev))
Edward Lemurca7d8812018-07-24 17:42:45 +0000553
Joanna Wangf3edc502022-07-20 00:12:10 +0000554 latest_commit = self.revinfo(None, None, None)
Edward Lesmesc621b212018-03-21 20:26:56 -0400555 if options.reset_patch_ref:
556 self._Capture(['reset', '--soft', base_rev])
Joanna Wangf3edc502022-07-20 00:12:10 +0000557 return latest_commit
Edward Lesmesc621b212018-03-21 20:26:56 -0400558
Joanna Wang5a7c8242022-07-01 19:09:00 +0000559 def check_diff(self, previous_commit, files=None):
560 # type: (str, Optional[List[str]]) -> bool
561 """Check if a diff exists between the current commit and `previous_commit`.
562
563 Returns True if there were diffs or if an error was encountered.
564 """
565 cmd = ['diff', previous_commit, '--quiet']
566 if files:
567 cmd += ['--'] + files
568 try:
569 self._Capture(cmd)
570 return False
571 except subprocess2.CalledProcessError as e:
572 # git diff --quiet exits with 1 if there were diffs.
573 if e.returncode != 1:
574 self.Print('git returned non-zero exit status %s:\n%s' %
575 (e.returncode, e.stderr.decode('utf-8')))
576 return True
577
Joanna Wange1753f62023-06-26 14:32:43 +0000578 def set_config(f):
579 def wrapper(*args):
580 return_val = f(*args)
581 if os.path.exists(os.path.join(args[0].checkout_path, '.git')):
582 # If diff.ignoreSubmodules is not already set, set it to `all`.
Josip Sokcevic84c0bba2023-08-10 00:52:15 +0000583 config = subprocess2.capture(
584 ['git', 'config', '-l'],
585 cwd=args[0].checkout_path).decode('utf-8').strip().splitlines()
Joanna Wang4e6c1072023-08-17 18:46:24 +0000586 if 'diff.ignoresubmodules=all' in config:
587 subprocess2.capture(
588 ['git', 'config', '--unset', 'diff.ignoreSubmodules'],
589 cwd=args[0].checkout_path)
Josip Sokcevic84c0bba2023-08-10 00:52:15 +0000590 if 'fetch.recursesubmodules=off' not in config:
591 subprocess2.capture(
592 ['git', 'config', 'fetch.recurseSubmodules', 'off'],
Joanna Wang4e6c1072023-08-17 18:46:24 +0000593 cwd=args[0].checkout_path)
Joanna Wange1753f62023-06-26 14:32:43 +0000594 return return_val
595
596 return wrapper
597
598 @set_config
msb@chromium.orge28e4982009-09-25 20:51:45 +0000599 def update(self, options, args, file_list):
600 """Runs git to update or transparently checkout the working copy.
601
602 All updated files will be appended to file_list.
603
604 Raises:
605 Error: if can't get URL for relative path.
606 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000607 if args:
608 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
609
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000610 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000611
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000612 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000613 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000614 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000615 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000616 # Override the revision number.
617 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000618 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000619 # Check again for a revision in case an initial ref was specified
620 # in the url, for example bla.git@refs/heads/custombranch
621 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000622 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000623 if not revision:
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000624 # If a dependency is not pinned, track the default remote branch.
625 revision = scm.GIT.GetRemoteHeadRef(self.checkout_path, self.url,
626 self.remote)
Edward Lesmes4ea67bb2021-04-20 17:33:52 +0000627 if revision.startswith('origin/'):
628 revision = 'refs/remotes/' + revision
msb@chromium.orge28e4982009-09-25 20:51:45 +0000629
Tomasz Wiszkowskid4e66882021-08-19 21:35:09 +0000630 if managed and platform.system() == 'Windows':
szager@chromium.org8a139702014-06-20 15:55:01 +0000631 self._DisableHooks()
632
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000633 printed_path = False
634 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000635 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700636 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000637 verbose = ['--verbose']
638 printed_path = True
639
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000640 revision_ref = revision
641 if ':' in revision:
642 revision_ref, _, revision = revision.partition(':')
643
Edward Lesmes8073a502020-04-15 02:11:14 +0000644 if revision_ref.startswith('refs/branch-heads'):
645 options.with_branch_heads = True
646
Edward Lesmes07a68342021-04-20 23:39:30 +0000647 mirror = self._GetMirror(url, options, revision, revision_ref)
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000648 if mirror:
649 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000650
John Budorick882c91e2018-07-12 22:11:41 +0000651 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
652 if remote_ref:
653 # Rewrite remote refs to their local equivalents.
654 revision = ''.join(remote_ref)
655 rev_type = "branch"
656 elif revision.startswith('refs/'):
657 # Local branch? We probably don't want to support, since DEPS should
658 # always specify branches as they are in the upstream repo.
659 rev_type = "branch"
660 else:
661 # hash is also a tag, only make a distinction at checkout
662 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000663
primiano@chromium.org1c127382015-02-17 11:15:40 +0000664 # If we are going to introduce a new project, there is a possibility that
665 # we are syncing back to a state where the project was originally a
666 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
667 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
668 # In such case, we might have a backup of the former .git folder, which can
669 # be used to avoid re-fetching the entire repo again (useful for bisects).
670 backup_dir = self.GetGitBackupDirPath()
671 target_dir = os.path.join(self.checkout_path, '.git')
672 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
673 gclient_utils.safe_makedirs(self.checkout_path)
674 os.rename(backup_dir, target_dir)
675 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800676 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000677
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000678 if (not os.path.exists(self.checkout_path) or
679 (os.path.isdir(self.checkout_path) and
680 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000681 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000682 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000683 try:
John Budorick882c91e2018-07-12 22:11:41 +0000684 self._Clone(revision, url, options)
Joanna Wang47fd5672022-08-05 20:53:31 +0000685 except subprocess2.CalledProcessError as e:
686 logging.warning('Clone failed due to: %s', e)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000687 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000688 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000689 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800690 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000691 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000692 file_list.extend(
Edward Lemur979fa782019-08-13 22:44:05 +0000693 [os.path.join(self.checkout_path, f) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000694 if mirror:
695 self._Capture(
696 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000697 if not verbose:
698 # Make the output a little prettier. It's nice to have some whitespace
699 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000700 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000701 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000702
John Budorick21a51b32018-09-19 19:39:20 +0000703 if mirror:
704 self._Capture(
705 ['remote', 'set-url', '--push', 'origin', mirror.url])
706
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000707 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000708 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000709 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
710 return self._Capture(['rev-parse', '--verify', 'HEAD'])
711
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000712 self._maybe_break_locks(options)
713
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000714 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000715 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000716
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000717 # See if the url has changed (the unittests use git://foo for the url, let
718 # that through).
719 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
720 return_early = False
721 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
722 # unit test pass. (and update the comment above)
723 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
724 # This allows devs to use experimental repos which have a different url
725 # but whose branch(s) are the same as official repos.
Raul Tambrecd862e32019-05-10 21:19:00 +0000726 if (current_url.rstrip('/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000727 subprocess2.capture(
Aaron Gableac9b0f32019-04-18 17:38:37 +0000728 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000729 cwd=self.checkout_path).strip() != 'False'):
Anthony Polito486f1812020-08-04 23:40:33 +0000730 self.Print('_____ switching %s from %s to new upstream %s' % (
731 self.relpath, current_url, url))
iannucci@chromium.org78514212014-08-20 23:08:00 +0000732 if not (options.force or options.reset):
733 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700734 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000735 # Switch over to the new upstream
736 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000737 if mirror:
Aravind Vasudevancf465852023-03-29 16:47:12 +0000738 if git_cache.Mirror.CacheDirToUrl(
739 current_url.rstrip('/')) == git_cache.Mirror.CacheDirToUrl(
740 url.rstrip('/')):
741 # Reset alternates when the cache dir is updated.
742 with open(
743 os.path.join(self.checkout_path, '.git', 'objects', 'info',
744 'alternates'), 'w') as fh:
745 fh.write(os.path.join(url, 'objects'))
746 else:
747 # Because we use Git alternatives, our existing repository is not
748 # self-contained. It's possible that new git alternative doesn't have
749 # all necessary objects that the current repository needs. Instead of
750 # blindly hoping that new alternative contains all necessary objects,
751 # keep the old alternative and just append a new one on top of it.
752 with open(
753 os.path.join(self.checkout_path, '.git', 'objects', 'info',
754 'alternates'), 'a') as fh:
755 fh.write("\n" + os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000756 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
757 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000758
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000759 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000760 else:
John Budorick882c91e2018-07-12 22:11:41 +0000761 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000762
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000763 if return_early:
764 return self._Capture(['rev-parse', '--verify', 'HEAD'])
765
msb@chromium.org5bde4852009-12-14 16:47:12 +0000766 cur_branch = self._GetCurrentBranch()
767
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000768 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000769 # 0) HEAD is detached. Probably from our initial clone.
770 # - make sure HEAD is contained by a named ref, then update.
771 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700772 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000773 # - try to rebase onto the new hash or branch
774 # 2) current branch is tracking a remote branch with local committed
775 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000776 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000777 # 3) current branch is tracking a remote branch w/or w/out changes, and
778 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000779 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000780 # 4) current branch is tracking a remote branch, but DEPS switches to a
781 # different remote branch, and
782 # a) current branch has no local changes, and --force:
783 # - checkout new branch
784 # b) current branch has local changes, and --force and --reset:
785 # - checkout new branch
786 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000787
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000788 # GetUpstreamBranch returns something like 'refs/remotes/origin/main' for
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000789 # a tracking branch
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000790 # or 'main' if not a tracking branch (it's based on a specific rev/hash)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000791 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000792 if cur_branch is None:
793 upstream_branch = None
794 current_type = "detached"
795 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000796 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000797 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
798 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
799 current_type = "hash"
800 logging.debug("Current branch is not tracking an upstream (remote)"
801 " branch.")
802 elif upstream_branch.startswith('refs/remotes'):
803 current_type = "branch"
804 else:
805 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000806
Edward Lemur579c9862018-07-13 23:17:51 +0000807 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000808
Michael Spang73fac912019-03-08 18:44:19 +0000809 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000810 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000811 self._Fetch(options, prune=options.force)
812
813 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
814 sha_only=True):
815 # Update the remotes first so we have all the refs.
816 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
817 cwd=self.checkout_path)
818 if verbose:
819 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000820
John Budorick882c91e2018-07-12 22:11:41 +0000821 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200822
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000823 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000824 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000825 target = 'HEAD'
826 if options.upstream and upstream_branch:
827 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800828 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000829
msb@chromium.org786fb682010-06-02 15:16:23 +0000830 if current_type == 'detached':
831 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800832 # We just did a Scrub, this is as clean as it's going to get. In
833 # particular if HEAD is a commit that contains two versions of the same
834 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
835 # to actually "Clean" the checkout; that commit is uncheckoutable on this
836 # system. The best we can do is carry forward to the checkout step.
837 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000838 self._CheckClean(revision)
839 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000840 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000841 self.Print('Up-to-date; skipping checkout.')
842 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000843 # 'git checkout' may need to overwrite existing untracked files. Allow
844 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000845 self._Checkout(
846 options,
John Budorick882c91e2018-07-12 22:11:41 +0000847 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000848 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000849 quiet=True,
850 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000851 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000852 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000853 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000854 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700855 # Can't find a merge-base since we don't know our upstream. That makes
856 # this command VERY likely to produce a rebase failure. For now we
857 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000858 upstream_branch = self.remote
859 if options.revision or deps_revision:
860 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700861 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700862 printed_path=printed_path, merge=options.merge)
863 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000864 elif rev_type == 'hash':
865 # case 2
866 self._AttemptRebase(upstream_branch, file_list, options,
867 newbase=revision, printed_path=printed_path,
868 merge=options.merge)
869 printed_path = True
870 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000871 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000872 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000873 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000874 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000875 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000876 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000877 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000878 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
879 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000880 force_switch = False
881 if options.force:
882 try:
John Budorick882c91e2018-07-12 22:11:41 +0000883 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000884 # case 4a
885 force_switch = True
886 except gclient_utils.Error as e:
887 if options.reset:
888 # case 4b
889 force_switch = True
890 else:
891 switch_error = '%s\n%s' % (e.message, switch_error)
892 if force_switch:
893 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000894 (upstream_branch, new_base))
895 switch_branch = 'gclient_' + remote_ref[1]
896 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000897 self._Checkout(options, switch_branch, force=True, quiet=True)
898 else:
899 # case 4c
900 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000901 else:
902 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800903 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000904 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000905 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000906 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000907 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000908 if options.merge:
909 merge_args.append('--ff')
910 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000911 merge_args.append('--ff-only')
912 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000913 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000914 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700915 rebase_files = []
Edward Lemur979fa782019-08-13 22:44:05 +0000916 if re.match(b'fatal: Not possible to fast-forward, aborting.',
917 e.stderr):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000918 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000919 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700920 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000921 printed_path = True
922 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000923 if not options.auto_rebase:
924 try:
925 action = self._AskForData(
926 'Cannot %s, attempt to rebase? '
927 '(y)es / (q)uit / (s)kip : ' %
928 ('merge' if options.merge else 'fast-forward merge'),
929 options)
930 except ValueError:
931 raise gclient_utils.Error('Invalid Character')
932 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700933 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000934 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000935 printed_path = True
936 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000937
938 if re.match(r'quit|q', action, re.I):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000939 raise gclient_utils.Error("Can't fast-forward, please merge or "
940 "rebase manually.\n"
941 "cd %s && git " % self.checkout_path
942 + "rebase %s" % upstream_branch)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000943
944 if re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000945 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000946 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000947
948 self.Print('Input not recognized')
Edward Lemur979fa782019-08-13 22:44:05 +0000949 elif re.match(b"error: Your local changes to '.*' would be "
950 b"overwritten by merge. Aborting.\nPlease, commit your "
951 b"changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000952 e.stderr):
953 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000954 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700955 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000956 printed_path = True
Edward Lemur979fa782019-08-13 22:44:05 +0000957 raise gclient_utils.Error(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000958 else:
959 # Some other problem happened with the merge
960 logging.error("Error during fast-forward merge in %s!" % self.relpath)
Edward Lemur979fa782019-08-13 22:44:05 +0000961 self.Print(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000962 raise
963 else:
964 # Fast-forward merge was successful
965 if not re.match('Already up-to-date.', merge_output) or verbose:
966 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700967 self.Print('_____ %s at %s' % (self.relpath, revision),
968 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000969 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000970 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000971 if not verbose:
972 # Make the output a little prettier. It's nice to have some
973 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000974 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000975
agablec3937b92016-10-25 10:13:03 -0700976 if file_list is not None:
977 file_list.extend(
978 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000979
980 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000981 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700982 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000983 '\nConflict while rebasing this branch.\n'
984 'Fix the conflict and run gclient again.\n'
985 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700986 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000987
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000988 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000989 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
990 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000991
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000992 # If --reset and --delete_unversioned_trees are specified, remove any
993 # untracked directories.
994 if options.reset and options.delete_unversioned_trees:
995 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
996 # merge-base by default), so doesn't include untracked files. So we use
997 # 'git ls-files --directory --others --exclude-standard' here directly.
998 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800999 ['-c', 'core.quotePath=false', 'ls-files',
1000 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +00001001 self.checkout_path)
1002 for path in (p for p in paths.splitlines() if p.endswith('/')):
1003 full_path = os.path.join(self.checkout_path, path)
1004 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001005 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +00001006 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +00001007
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +00001008 return self._Capture(['rev-parse', '--verify', 'HEAD'])
1009
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001010 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +00001011 """Reverts local modifications.
1012
1013 All reverted files will be appended to file_list.
1014 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +00001015 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +00001016 # revert won't work if the directory doesn't exist. It needs to
1017 # checkout instead.
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001018 self.Print('_____ %s is missing, syncing instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +00001019 # Don't reuse the args.
1020 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001021
Josip Sokcevic7e133ff2021-07-13 17:44:53 +00001022 default_rev = "refs/heads/main"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +00001023 if options.upstream:
1024 if self._GetCurrentBranch():
1025 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
1026 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001027 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001028 if not deps_revision:
1029 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +00001030 if deps_revision.startswith('refs/heads/'):
1031 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -07001032 try:
1033 deps_revision = self.GetUsableRev(deps_revision, options)
1034 except NoUsableRevError as e:
1035 # If the DEPS entry's url and hash changed, try to update the origin.
1036 # See also http://crbug.com/520067.
John Budorickd94f8ea2020-03-27 15:55:24 +00001037 logging.warning(
1038 "Couldn't find usable revision, will retrying to update instead: %s",
smutae7ea312016-07-18 11:59:41 -07001039 e.message)
1040 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001041
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001042 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001043 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001044
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001045 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +00001046 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001047
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001048 if file_list is not None:
1049 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
1050
1051 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001052 """Returns revision"""
1053 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +00001054
msb@chromium.orge28e4982009-09-25 20:51:45 +00001055 def runhooks(self, options, args, file_list):
1056 self.status(options, args, file_list)
1057
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001058 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +00001059 """Display status information."""
1060 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001061 self.Print('________ couldn\'t run status in %s:\n'
1062 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001063 else:
Anthony Politobb457342019-11-15 22:26:01 +00001064 merge_base = []
1065 if self.url:
1066 _, base_rev = gclient_utils.SplitUrlRevision(self.url)
1067 if base_rev:
Joanna Wanga654ff32023-07-18 23:25:19 +00001068 if base_rev.startswith('refs/'):
Kenneth Russell02e70b42023-08-07 22:09:29 +00001069 base_rev = self._ref_to_remote_ref(base_rev)
Anthony Politobb457342019-11-15 22:26:01 +00001070 merge_base = [base_rev]
Aaron Gablef4068aa2017-12-12 15:14:09 -08001071 self._Run(
1072 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
Edward Lemur24146be2019-08-01 21:44:52 +00001073 options, always_show_header=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001074 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001075 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001076 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +00001077
smutae7ea312016-07-18 11:59:41 -07001078 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -07001079 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -07001080 sha1 = None
1081 if not os.path.isdir(self.checkout_path):
1082 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -08001083 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -07001084
1085 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1086 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001087 else:
agable41e3a6c2016-10-20 11:36:56 -07001088 # May exist in origin, but we don't have it yet, so fetch and look
1089 # again.
1090 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -07001091 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1092 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001093
1094 if not sha1:
1095 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -08001096 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -07001097
1098 return sha1
1099
primiano@chromium.org1c127382015-02-17 11:15:40 +00001100 def GetGitBackupDirPath(self):
1101 """Returns the path where the .git folder for the current project can be
1102 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
1103 return os.path.join(self._root_dir,
1104 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
1105
Edward Lesmes07a68342021-04-20 23:39:30 +00001106 def _GetMirror(self, url, options, revision=None, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001107 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +00001108 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001109 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +00001110 mirror_kwargs = {
1111 'print_func': self.filter,
Edward Lesmes07a68342021-04-20 23:39:30 +00001112 'refs': [],
1113 'commits': [],
hinoka@google.comb1b54572014-04-16 22:29:23 +00001114 }
hinoka@google.comb1b54572014-04-16 22:29:23 +00001115 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1116 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001117 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
1118 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001119 if hasattr(options, 'with_tags') and options.with_tags:
1120 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001121 elif revision_ref and revision_ref.startswith('refs/tags/'):
1122 mirror_kwargs['refs'].append(revision_ref)
Edward Lesmes07a68342021-04-20 23:39:30 +00001123 if revision and not revision.startswith('refs/'):
1124 mirror_kwargs['commits'].append(revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001125 return git_cache.Mirror(url, **mirror_kwargs)
1126
John Budorick882c91e2018-07-12 22:11:41 +00001127 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001128 """Update a git mirror by fetching the latest commits from the remote,
1129 unless mirror already contains revision whose type is sha1 hash.
1130 """
John Budorick882c91e2018-07-12 22:11:41 +00001131 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001132 if options.verbose:
1133 self.Print('skipping mirror update, it has rev=%s already' % revision,
1134 timestamp=False)
1135 return
1136
szager@chromium.org3ec84f62014-08-22 21:00:22 +00001137 if getattr(options, 'shallow', False):
Victor Vianna392ce7e2022-06-07 21:47:51 +00001138 depth = 10000
hinoka@chromium.org46b87412014-05-15 00:42:05 +00001139 else:
1140 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +00001141 mirror.populate(verbose=options.verbose,
1142 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +00001143 depth=depth,
Vadim Shtayura08049e22017-10-11 00:14:52 +00001144 lock_timeout=getattr(options, 'lock_timeout', 0))
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001145
John Budorick882c91e2018-07-12 22:11:41 +00001146 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001147 """Clone a git repository from the given URL.
1148
msb@chromium.org786fb682010-06-02 15:16:23 +00001149 Once we've cloned the repo, we checkout a working branch if the specified
1150 revision is a branch head. If it is a tag or a specific commit, then we
1151 leave HEAD detached as it makes future updates simpler -- in this case the
1152 user should first create a new branch or switch to an existing branch before
1153 making changes in the repo."""
Joanna Wang1a977bd2022-06-02 21:51:17 +00001154 in_cog_workspace = self._IsCog()
1155
1156 if self.print_outbuf:
1157 print_stdout = True
1158 filter_fn = None
1159 else:
1160 print_stdout = False
1161 filter_fn = self.filter
1162
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001163 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001164 # git clone doesn't seem to insert a newline properly before printing
1165 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001166 self.Print('')
Joanna Wang1a977bd2022-06-02 21:51:17 +00001167
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001168 # If the parent directory does not exist, Git clone on Windows will not
1169 # create it, so we need to do it manually.
1170 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001171 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001172
Joanna Wang1a977bd2022-06-02 21:51:17 +00001173 if in_cog_workspace:
1174 clone_cmd = ['citc', 'clone-repo', url, self.checkout_path]
1175 clone_cmd.append(
1176 gclient_utils.ExtractRefName(self.remote, revision) or revision)
1177 try:
1178 self._Run(clone_cmd,
1179 options,
1180 cwd=self._root_dir,
1181 retry=True,
1182 print_stdout=print_stdout,
1183 filter_fn=filter_fn)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001184 except:
1185 traceback.print_exc(file=self.out_fh)
1186 raise
1187 self._SetFetchConfig(options)
Travis Lane738b48a2022-11-28 20:28:51 +00001188 elif hasattr(options, 'no_history') and options.no_history:
1189 self._Run(['init', self.checkout_path], options, cwd=self._root_dir)
1190 self._Run(['remote', 'add', 'origin', url], options)
1191 revision = self._AutoFetchRef(options, revision, depth=1)
1192 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1193 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001194 else:
1195 cfg = gclient_utils.DefaultIndexPackConfig(url)
1196 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
1197 if self.cache_dir:
1198 clone_cmd.append('--shared')
1199 if options.verbose:
1200 clone_cmd.append('--verbose')
1201 clone_cmd.append(url)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001202 tmp_dir = tempfile.mkdtemp(prefix='_gclient_%s_' %
1203 os.path.basename(self.checkout_path),
1204 dir=parent_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001205 clone_cmd.append(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001206
1207 try:
1208 self._Run(clone_cmd,
1209 options,
1210 cwd=self._root_dir,
1211 retry=True,
1212 print_stdout=print_stdout,
1213 filter_fn=filter_fn)
Joanna Wang47fd5672022-08-05 20:53:31 +00001214 logging.debug('Cloned into temporary dir, moving to checkout_path')
Joanna Wang1a977bd2022-06-02 21:51:17 +00001215 gclient_utils.safe_makedirs(self.checkout_path)
1216 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1217 os.path.join(self.checkout_path, '.git'))
1218 except:
1219 traceback.print_exc(file=self.out_fh)
1220 raise
1221 finally:
1222 if os.listdir(tmp_dir):
1223 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
Josip Sokcevicebccac72022-06-24 21:44:49 +00001224 gclient_utils.rmtree(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001225
1226 self._SetFetchConfig(options)
1227 self._Fetch(options, prune=options.force)
1228 revision = self._AutoFetchRef(options, revision)
1229 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1230 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
1231
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001232 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001233 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001234 self.Print(
Joanna Wang1a977bd2022-06-02 21:51:17 +00001235 ('Checked out %s to a detached HEAD. Before making any commits\n'
1236 'in this repo, you should use \'git checkout <branch>\' to switch \n'
1237 'to an existing branch or use \'git checkout %s -b <branch>\' to\n'
1238 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001239
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001240 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001241 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001242 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001243 raise gclient_utils.Error("Background task requires input. Rerun "
1244 "gclient with --jobs=1 so that\n"
1245 "interaction is possible.")
Edward Lesmesae3586b2020-03-23 21:21:14 +00001246 return gclient_utils.AskForData(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001247
1248
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001249 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001250 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001251 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001252 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001253 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001254 revision = upstream
1255 if newbase:
1256 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001257 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001258 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001259 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001260 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001261 printed_path = True
1262 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001263 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001264
1265 if merge:
1266 merge_output = self._Capture(['merge', revision])
1267 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001268 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001269 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001270
1271 # Build the rebase command here using the args
1272 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1273 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001274 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001275 rebase_cmd.append('--verbose')
1276 if newbase:
1277 rebase_cmd.extend(['--onto', newbase])
1278 rebase_cmd.append(upstream)
1279 if branch:
1280 rebase_cmd.append(branch)
1281
1282 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001283 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001284 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001285 if (re.match(br'cannot rebase: you have unstaged changes', e.stderr) or
1286 re.match(br'cannot rebase: your index contains uncommitted changes',
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001287 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001288 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001289 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001290 'Cannot rebase because of unstaged changes.\n'
1291 '\'git reset --hard HEAD\' ?\n'
1292 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001293 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001294 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001295 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001296 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001297 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001298 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001299
1300 if re.match(r'quit|q', rebase_action, re.I):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001301 raise gclient_utils.Error("Please merge or rebase manually\n"
1302 "cd %s && git " % self.checkout_path
1303 + "%s" % ' '.join(rebase_cmd))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001304
1305 if re.match(r'show|s', rebase_action, re.I):
Edward Lemur979fa782019-08-13 22:44:05 +00001306 self.Print('%s' % e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001307 continue
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001308
1309 gclient_utils.Error("Input not recognized")
1310 continue
Edward Lemur979fa782019-08-13 22:44:05 +00001311 elif re.search(br'^CONFLICT', e.stdout, re.M):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001312 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1313 "Fix the conflict and run gclient again.\n"
1314 "See 'man git-rebase' for details.\n")
1315 else:
Edward Lemur979fa782019-08-13 22:44:05 +00001316 self.Print(e.stdout.decode('utf-8').strip())
1317 self.Print('Rebase produced error output:\n%s' %
1318 e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001319 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1320 "manually.\ncd %s && git " %
1321 self.checkout_path
1322 + "%s" % ' '.join(rebase_cmd))
1323
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001324 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001325 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001326 # Make the output a little prettier. It's nice to have some
1327 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001328 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001329
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001330 @staticmethod
1331 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001332 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1333 if not ok:
1334 raise gclient_utils.Error('git version %s < minimum required %s' %
1335 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001336
John Budorick882c91e2018-07-12 22:11:41 +00001337 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001338 # Special case handling if all 3 conditions are met:
1339 # * the mirros have recently changed, but deps destination remains same,
1340 # * the git histories of mirrors are conflicting.
1341 # * git cache is used
1342 # This manifests itself in current checkout having invalid HEAD commit on
1343 # most git operations. Since git cache is used, just deleted the .git
1344 # folder, and re-create it by cloning.
1345 try:
1346 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1347 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001348 if (b'fatal: bad object HEAD' in e.stderr
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001349 and self.cache_dir and self.cache_dir in url):
1350 self.Print((
1351 'Likely due to DEPS change with git cache_dir, '
1352 'the current commit points to no longer existing object.\n'
1353 '%s' % e)
1354 )
1355 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001356 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001357 else:
1358 raise
1359
msb@chromium.org786fb682010-06-02 15:16:23 +00001360 def _IsRebasing(self):
1361 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1362 # have a plumbing command to determine whether a rebase is in progress, so
1363 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1364 g = os.path.join(self.checkout_path, '.git')
1365 return (
1366 os.path.isdir(os.path.join(g, "rebase-merge")) or
1367 os.path.isdir(os.path.join(g, "rebase-apply")))
1368
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001369 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001370 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1371 if os.path.exists(lockfile):
1372 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001373 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001374 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1375 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001376 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001377
msb@chromium.org786fb682010-06-02 15:16:23 +00001378 # Make sure the tree is clean; see git-rebase.sh for reference
1379 try:
1380 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001381 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001382 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001383 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001384 '\tYou have unstaged changes.\n'
1385 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001386 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001387 try:
1388 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001389 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001390 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001391 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001392 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001393 '\tYour index contains uncommitted changes\n'
1394 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001395 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001396
agable83faed02016-10-24 14:37:10 -07001397 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001398 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1399 # reference by a commit). If not, error out -- most likely a rebase is
1400 # in progress, try to detect so we can give a better error.
1401 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001402 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1403 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001404 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001405 # Commit is not contained by any rev. See if the user is rebasing:
1406 if self._IsRebasing():
1407 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001408 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001409 '\tAlready in a conflict, i.e. (no branch).\n'
1410 '\tFix the conflict and run gclient again.\n'
1411 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1412 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001413 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001414 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001415 name = ('saved-by-gclient-' +
1416 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001417 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001418 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001419 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001420
msb@chromium.org5bde4852009-12-14 16:47:12 +00001421 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001422 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001423 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001424 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001425 return None
1426 return branch
1427
borenet@google.comc3e09d22014-04-10 13:58:18 +00001428 def _Capture(self, args, **kwargs):
Mike Frysinger286fb162019-09-30 03:14:10 +00001429 set_git_dir = 'cwd' not in kwargs
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001430 kwargs.setdefault('cwd', self.checkout_path)
1431 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001432 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001433 env = scm.GIT.ApplyEnvVars(kwargs)
Mike Frysinger286fb162019-09-30 03:14:10 +00001434 # If an explicit cwd isn't set, then default to the .git/ subdir so we get
1435 # stricter behavior. This can be useful in cases of slight corruption --
1436 # we don't accidentally go corrupting parent git checks too. See
1437 # https://crbug.com/1000825 for an example.
1438 if set_git_dir:
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001439 git_dir = os.path.abspath(os.path.join(self.checkout_path, '.git'))
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001440 # Depending on how the .gclient file was defined, self.checkout_path
1441 # might be set to a unicode string, not a regular string; on Windows
1442 # Python2, we can't set env vars to be unicode strings, so we
1443 # forcibly cast the value to a string before setting it.
1444 env.setdefault('GIT_DIR', str(git_dir))
Raul Tambrecd862e32019-05-10 21:19:00 +00001445 ret = subprocess2.check_output(
1446 ['git'] + args, env=env, **kwargs).decode('utf-8')
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001447 if strip:
1448 ret = ret.strip()
Erik Chene16ffff2019-10-14 20:35:53 +00001449 self.Print('Finished running: %s %s' % ('git', ' '.join(args)))
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001450 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001451
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001452 def _Checkout(self, options, ref, force=False, quiet=None):
1453 """Performs a 'git-checkout' operation.
1454
1455 Args:
1456 options: The configured option set
1457 ref: (str) The branch/commit to checkout
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001458 quiet: (bool/None) Whether or not the checkout should pass '--quiet'; if
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001459 'None', the behavior is inferred from 'options.verbose'.
1460 Returns: (str) The output of the checkout operation
1461 """
1462 if quiet is None:
1463 quiet = (not options.verbose)
1464 checkout_args = ['checkout']
1465 if force:
1466 checkout_args.append('--force')
1467 if quiet:
1468 checkout_args.append('--quiet')
1469 checkout_args.append(ref)
1470 return self._Capture(checkout_args)
1471
Travis Lane738b48a2022-11-28 20:28:51 +00001472 def _Fetch(self,
1473 options,
1474 remote=None,
1475 prune=False,
1476 quiet=False,
1477 refspec=None,
1478 depth=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001479 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001480 # When updating, the ref is modified to be a remote ref .
1481 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1482 # Try to reverse that mapping.
1483 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1484 if original_ref:
1485 refspec = original_ref + ':' + refspec
1486 # When a mirror is configured, it only fetches
1487 # refs/{heads,branch-heads,tags}/*.
1488 # If asked to fetch other refs, we must fetch those directly from the
1489 # repository, and not from the mirror.
1490 if not original_ref.startswith(
1491 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1492 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001493 fetch_cmd = cfg + [
1494 'fetch',
1495 remote or self.remote,
1496 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001497 if refspec:
1498 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001499
1500 if prune:
1501 fetch_cmd.append('--prune')
1502 if options.verbose:
1503 fetch_cmd.append('--verbose')
danakjd5c0b562019-11-08 17:27:47 +00001504 if not hasattr(options, 'with_tags') or not options.with_tags:
1505 fetch_cmd.append('--no-tags')
dnj@chromium.org680f2172014-06-25 00:39:32 +00001506 elif quiet:
1507 fetch_cmd.append('--quiet')
Travis Lane738b48a2022-11-28 20:28:51 +00001508 if depth:
1509 fetch_cmd.append('--depth=' + str(depth))
tandrii64103db2016-10-11 05:30:05 -07001510 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001511
Edward Lemur579c9862018-07-13 23:17:51 +00001512 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001513 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1514 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001515 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001516 try:
1517 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1518 options)
1519 self._Run(['config', 'remote.%s.fetch' % self.remote,
1520 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1521 except subprocess2.CalledProcessError as e:
1522 # If exit code was 5, it means we attempted to unset a config that
1523 # didn't exist. Ignore it.
1524 if e.returncode != 5:
1525 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001526 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001527 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001528 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1529 '^\\+refs/branch-heads/\\*:.*$']
1530 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001531 if hasattr(options, 'with_tags') and options.with_tags:
1532 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1533 '+refs/tags/*:refs/tags/*',
1534 '^\\+refs/tags/\\*:.*$']
1535 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001536
Travis Lane738b48a2022-11-28 20:28:51 +00001537 def _AutoFetchRef(self, options, revision, depth=None):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001538 """Attempts to fetch |revision| if not available in local repo.
1539
1540 Returns possibly updated revision."""
Edward Lemure0ba7b82020-03-11 20:31:32 +00001541 if not scm.GIT.IsValidRevision(self.checkout_path, revision):
Travis Lane738b48a2022-11-28 20:28:51 +00001542 self._Fetch(options, refspec=revision, depth=depth)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001543 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1544 return revision
1545
Edward Lemur24146be2019-08-01 21:44:52 +00001546 def _Run(self, args, options, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001547 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001548 kwargs.setdefault('cwd', self.checkout_path)
Edward Lemur24146be2019-08-01 21:44:52 +00001549 kwargs.setdefault('filter_fn', self.filter)
1550 kwargs.setdefault('show_header', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001551 env = scm.GIT.ApplyEnvVars(kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +00001552
agable@chromium.org772efaf2014-04-01 02:35:44 +00001553 cmd = ['git'] + args
Edward Lemur24146be2019-08-01 21:44:52 +00001554 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001555
1556
1557class CipdPackage(object):
1558 """A representation of a single CIPD package."""
1559
John Budorickd3ba72b2018-03-20 12:27:42 -07001560 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001561 self._authority_for_subdir = authority_for_subdir
1562 self._name = name
1563 self._version = version
1564
1565 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001566 def authority_for_subdir(self):
1567 """Whether this package has authority to act on behalf of its subdir.
1568
1569 Some operations should only be performed once per subdirectory. A package
1570 that has authority for its subdirectory is the only package that should
1571 perform such operations.
1572
1573 Returns:
1574 bool; whether this package has subdir authority.
1575 """
1576 return self._authority_for_subdir
1577
1578 @property
1579 def name(self):
1580 return self._name
1581
1582 @property
1583 def version(self):
1584 return self._version
1585
1586
1587class CipdRoot(object):
1588 """A representation of a single CIPD root."""
1589 def __init__(self, root_dir, service_url):
1590 self._all_packages = set()
1591 self._mutator_lock = threading.Lock()
1592 self._packages_by_subdir = collections.defaultdict(list)
1593 self._root_dir = root_dir
1594 self._service_url = service_url
Dan Le Febvre456d0852023-05-24 23:43:40 +00001595 self._resolved_packages = None
John Budorick0f7b2002018-01-19 15:46:17 -08001596
1597 def add_package(self, subdir, package, version):
1598 """Adds a package to this CIPD root.
1599
1600 As far as clients are concerned, this grants both root and subdir authority
1601 to packages arbitrarily. (The implementation grants root authority to the
1602 first package added and subdir authority to the first package added for that
1603 subdir, but clients should not depend on or expect that behavior.)
1604
1605 Args:
1606 subdir: str; relative path to where the package should be installed from
1607 the cipd root directory.
1608 package: str; the cipd package name.
1609 version: str; the cipd package version.
1610 Returns:
1611 CipdPackage; the package that was created and added to this root.
1612 """
1613 with self._mutator_lock:
1614 cipd_package = CipdPackage(
1615 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001616 not self._packages_by_subdir[subdir])
1617 self._all_packages.add(cipd_package)
1618 self._packages_by_subdir[subdir].append(cipd_package)
1619 return cipd_package
1620
1621 def packages(self, subdir):
1622 """Get the list of configured packages for the given subdir."""
1623 return list(self._packages_by_subdir[subdir])
1624
Dan Le Febvre456d0852023-05-24 23:43:40 +00001625 def resolved_packages(self):
1626 if not self._resolved_packages:
1627 self._resolved_packages = self.ensure_file_resolve()
1628 return self._resolved_packages
1629
John Budorick0f7b2002018-01-19 15:46:17 -08001630 def clobber(self):
1631 """Remove the .cipd directory.
1632
1633 This is useful for forcing ensure to redownload and reinitialize all
1634 packages.
1635 """
1636 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001637 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001638 try:
1639 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1640 except OSError:
1641 if os.path.exists(cipd_cache_dir):
1642 raise
1643
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001644 def expand_package_name(self, package_name_string, **kwargs):
1645 """Run `cipd expand-package-name`.
1646
1647 CIPD package names can be declared with placeholder variables
1648 such as '${platform}', this cmd will return the package name
1649 with the variables resolved. The resolution is based on the host
1650 the command is executing on.
1651 """
1652
1653 kwargs.setdefault('stderr', subprocess2.PIPE)
1654 cmd = ['cipd', 'expand-package-name', package_name_string]
1655 ret = subprocess2.check_output(cmd, **kwargs).decode('utf-8')
1656 return ret.strip()
1657
John Budorick0f7b2002018-01-19 15:46:17 -08001658 @contextlib.contextmanager
1659 def _create_ensure_file(self):
1660 try:
Stephanie Kim700aee72022-06-01 19:58:30 +00001661 contents = '$ParanoidMode CheckPresence\n'
1662 # TODO(crbug/1329641): Remove once cipd packages have been updated
1663 # to always be created in copy mode.
1664 contents += '$OverrideInstallMode copy\n\n'
Edward Lesmes05934952019-12-19 20:38:09 +00001665 for subdir, packages in sorted(self._packages_by_subdir.items()):
1666 contents += '@Subdir %s\n' % subdir
1667 for package in sorted(packages, key=lambda p: p.name):
1668 contents += '%s %s\n' % (package.name, package.version)
1669 contents += '\n'
John Budorick0f7b2002018-01-19 15:46:17 -08001670 ensure_file = None
1671 with tempfile.NamedTemporaryFile(
Edward Lesmes05934952019-12-19 20:38:09 +00001672 suffix='.ensure', delete=False, mode='wb') as ensure_file:
1673 ensure_file.write(contents.encode('utf-8', 'replace'))
John Budorick0f7b2002018-01-19 15:46:17 -08001674 yield ensure_file.name
1675 finally:
1676 if ensure_file is not None and os.path.exists(ensure_file.name):
1677 os.remove(ensure_file.name)
1678
1679 def ensure(self):
1680 """Run `cipd ensure`."""
1681 with self._mutator_lock:
1682 with self._create_ensure_file() as ensure_file:
1683 cmd = [
1684 'cipd', 'ensure',
1685 '-log-level', 'error',
1686 '-root', self.root_dir,
1687 '-ensure-file', ensure_file,
1688 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001689 gclient_utils.CheckCallAndFilter(
1690 cmd, print_stdout=True, show_header=True)
John Budorick0f7b2002018-01-19 15:46:17 -08001691
Dan Le Febvre456d0852023-05-24 23:43:40 +00001692 @contextlib.contextmanager
1693 def _create_ensure_file_for_resolve(self):
1694 try:
1695 contents = '$ResolvedVersions %s\n' % os.devnull
1696 for subdir, packages in sorted(self._packages_by_subdir.items()):
1697 contents += '@Subdir %s\n' % subdir
1698 for package in sorted(packages, key=lambda p: p.name):
1699 contents += '%s %s\n' % (package.name, package.version)
1700 contents += '\n'
1701 ensure_file = None
1702 with tempfile.NamedTemporaryFile(suffix='.ensure',
1703 delete=False,
1704 mode='wb') as ensure_file:
1705 ensure_file.write(contents.encode('utf-8', 'replace'))
1706 yield ensure_file.name
1707 finally:
1708 if ensure_file is not None and os.path.exists(ensure_file.name):
1709 os.remove(ensure_file.name)
1710
1711 def _create_resolved_file(self):
1712 return tempfile.NamedTemporaryFile(suffix='.resolved',
1713 delete=False,
1714 mode='wb')
1715
1716 def ensure_file_resolve(self):
1717 """Run `cipd ensure-file-resolve`."""
1718 with self._mutator_lock:
1719 with self._create_resolved_file() as output_file:
1720 with self._create_ensure_file_for_resolve() as ensure_file:
1721 cmd = [
1722 'cipd',
1723 'ensure-file-resolve',
1724 '-log-level',
1725 'error',
1726 '-ensure-file',
1727 ensure_file,
1728 '-json-output',
1729 output_file.name,
1730 ]
1731 gclient_utils.CheckCallAndFilter(cmd,
1732 print_stdout=False,
1733 show_header=False)
1734 with open(output_file.name) as f:
1735 output_json = json.load(f)
1736 return output_json.get('result', {})
1737
John Budorickd3ba72b2018-03-20 12:27:42 -07001738 def run(self, command):
1739 if command == 'update':
1740 self.ensure()
1741 elif command == 'revert':
1742 self.clobber()
1743 self.ensure()
1744
John Budorick0f7b2002018-01-19 15:46:17 -08001745 def created_package(self, package):
1746 """Checks whether this root created the given package.
1747
1748 Args:
1749 package: CipdPackage; the package to check.
1750 Returns:
1751 bool; whether this root created the given package.
1752 """
1753 return package in self._all_packages
1754
1755 @property
1756 def root_dir(self):
1757 return self._root_dir
1758
1759 @property
1760 def service_url(self):
1761 return self._service_url
1762
1763
1764class CipdWrapper(SCMWrapper):
1765 """Wrapper for CIPD.
1766
1767 Currently only supports chrome-infra-packages.appspot.com.
1768 """
John Budorick3929e9e2018-02-04 18:18:07 -08001769 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001770
1771 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1772 out_cb=None, root=None, package=None):
1773 super(CipdWrapper, self).__init__(
1774 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1775 out_cb=out_cb)
1776 assert root.created_package(package)
1777 self._package = package
1778 self._root = root
1779
1780 #override
1781 def GetCacheMirror(self):
1782 return None
1783
1784 #override
1785 def GetActualRemoteURL(self, options):
1786 return self._root.service_url
1787
1788 #override
1789 def DoesRemoteURLMatch(self, options):
1790 del options
1791 return True
1792
1793 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001794 """Does nothing.
1795
1796 CIPD packages should be reverted at the root by running
1797 `CipdRoot.run('revert')`.
1798 """
John Budorick0f7b2002018-01-19 15:46:17 -08001799
1800 def diff(self, options, args, file_list):
1801 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001802
1803 def pack(self, options, args, file_list):
1804 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001805
1806 def revinfo(self, options, args, file_list):
1807 """Grab the instance ID."""
1808 try:
1809 tmpdir = tempfile.mkdtemp()
Dan Le Febvre456d0852023-05-24 23:43:40 +00001810 # Attempt to get instance_id from the root resolved cache.
1811 # Resolved cache will not match on any CIPD packages with
1812 # variables such as ${platform}, they will fall back to
1813 # the slower method below.
1814 resolved = self._root.resolved_packages()
1815 if resolved:
1816 # CIPD uses POSIX separators across all platforms, so
1817 # replace any Windows separators.
1818 path_split = self.relpath.replace(os.sep, "/").split(":")
1819 if len(path_split) > 1:
1820 src_path, package = path_split
1821 if src_path in resolved:
1822 for resolved_package in resolved[src_path]:
1823 if package == resolved_package.get('pin', {}).get('package'):
1824 return resolved_package.get('pin', {}).get('instance_id')
1825
John Budorick0f7b2002018-01-19 15:46:17 -08001826 describe_json_path = os.path.join(tmpdir, 'describe.json')
1827 cmd = [
1828 'cipd', 'describe',
1829 self._package.name,
1830 '-log-level', 'error',
1831 '-version', self._package.version,
1832 '-json-output', describe_json_path
1833 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001834 gclient_utils.CheckCallAndFilter(cmd)
John Budorick0f7b2002018-01-19 15:46:17 -08001835 with open(describe_json_path) as f:
1836 describe_json = json.load(f)
1837 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1838 finally:
1839 gclient_utils.rmtree(tmpdir)
1840
1841 def status(self, options, args, file_list):
1842 pass
1843
1844 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001845 """Does nothing.
1846
1847 CIPD packages should be updated at the root by running
1848 `CipdRoot.run('update')`.
1849 """