blob: 6a75af567c85a20cab84c1b95bb6c25f8b3a98ba [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
maruel@chromium.org31cb48a2011-04-04 18:01:36 +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
Robert Iannuccia19649b2018-06-29 16:31:45 +0000211 @property
212 def cache_dir(self):
213 try:
214 return git_cache.Mirror.GetCachePath()
215 except RuntimeError:
216 return None
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000217
John Budorick0f7b2002018-01-19 15:46:17 -0800218 def __init__(self, url=None, *args, **kwargs):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000219 """Removes 'git+' fake prefix from git URL."""
Henrique Ferreiroe72279d2019-04-17 12:01:50 +0000220 if url and (url.startswith('git+http://') or
221 url.startswith('git+https://')):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000222 url = url[4:]
John Budorick0f7b2002018-01-19 15:46:17 -0800223 SCMWrapper.__init__(self, url, *args, **kwargs)
szager@chromium.org848fd492014-04-09 19:06:44 +0000224 filter_kwargs = { 'time_throttle': 1, 'out_fh': self.out_fh }
225 if self.out_cb:
226 filter_kwargs['predicate'] = self.out_cb
227 self.filter = gclient_utils.GitFilter(**filter_kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +0000228 self._running_under_rosetta = None
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000229
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000230 def GetCheckoutRoot(self):
231 return scm.GIT.GetCheckoutRoot(self.checkout_path)
232
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000233 def GetRevisionDate(self, _revision):
floitsch@google.comeaab7842011-04-28 09:07:58 +0000234 """Returns the given revision's date in ISO-8601 format (which contains the
235 time zone)."""
236 # TODO(floitsch): get the time-stamp of the given revision and not just the
237 # time-stamp of the currently checked out revision.
238 return self._Capture(['log', '-n', '1', '--format=%ai'])
239
Aaron Gablef4068aa2017-12-12 15:14:09 -0800240 def _GetDiffFilenames(self, base):
241 """Returns the names of files modified since base."""
242 return self._Capture(
Raul Tambrecd862e32019-05-10 21:19:00 +0000243 # Filter to remove base if it is None.
244 list(filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only',
245 base])
246 )).split()
Aaron Gablef4068aa2017-12-12 15:14:09 -0800247
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000248 def diff(self, options, _args, _file_list):
Aaron Gable1853f662018-02-12 15:45:56 -0800249 _, revision = gclient_utils.SplitUrlRevision(self.url)
250 if not revision:
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000251 revision = 'refs/remotes/%s/main' % self.remote
Aaron Gable1853f662018-02-12 15:45:56 -0800252 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000253
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000254 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000255 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000256 repository.
257
258 The patch file is generated from a diff of the merge base of HEAD and
259 its upstream branch.
260 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700261 try:
262 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
263 except subprocess2.CalledProcessError:
264 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000265 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700266 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000267 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000268 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000269
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800270 def _Scrub(self, target, options):
271 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000272 quiet = []
273 if not options.verbose:
274 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800275 self._Run(['reset', '--hard', target] + quiet, options)
276 if options.force and options.delete_unversioned_trees:
277 # where `target` is a commit that contains both upper and lower case
278 # versions of the same file on a case insensitive filesystem, we are
279 # actually in a broken state here. The index will have both 'a' and 'A',
280 # but only one of them will exist on the disk. To progress, we delete
281 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800282 output = self._Capture([
283 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800284 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800285 # --porcelain (v1) looks like:
286 # XY filename
287 try:
288 filename = line[3:]
289 self.Print('_____ Deleting residual after reset: %r.' % filename)
290 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800291 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800292 except OSError:
293 pass
294
John Budorick882c91e2018-07-12 22:11:41 +0000295 def _FetchAndReset(self, revision, file_list, options):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800296 """Equivalent to git fetch; git reset."""
Edward Lemur579c9862018-07-13 23:17:51 +0000297 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000298
dnj@chromium.org680f2172014-06-25 00:39:32 +0000299 self._Fetch(options, prune=True, quiet=options.verbose)
John Budorick882c91e2018-07-12 22:11:41 +0000300 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000301 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800302 files = self._Capture(
303 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambreb946b232019-03-26 14:48:46 +0000304 file_list.extend(
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000305 [os.path.join(self.checkout_path, f) for f in files])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000306
szager@chromium.org8a139702014-06-20 15:55:01 +0000307 def _DisableHooks(self):
308 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
309 if not os.path.isdir(hook_dir):
310 return
311 for f in os.listdir(hook_dir):
312 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000313 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
314 if os.path.exists(disabled_hook_path):
315 os.remove(disabled_hook_path)
316 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000317
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000318 def _maybe_break_locks(self, options):
319 """This removes all .lock files from this repo's .git directory, if the
320 user passed the --break_repo_locks command line flag.
321
322 In particular, this will cleanup index.lock files, as well as ref lock
323 files.
324 """
325 if options.break_repo_locks:
326 git_dir = os.path.join(self.checkout_path, '.git')
327 for path, _, filenames in os.walk(git_dir):
328 for filename in filenames:
329 if filename.endswith('.lock'):
330 to_break = os.path.join(path, filename)
331 self.Print('breaking lock: %s' % (to_break,))
332 try:
333 os.remove(to_break)
334 except OSError as ex:
335 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
336 raise
337
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000338 def _download_topics(self, patch_rev, googlesource_url):
339 """This method returns new patch_revs to process that have the same topic.
340
341 It does the following:
342 1. Finds the topic of the Gerrit change specified in the patch_rev.
343 2. Find all changes with that topic.
344 3. Append patch_rev of the changes with the same topic to the patch_revs
345 to process.
346 4. Returns the new patch_revs to process.
347 """
348 patch_revs_to_process = []
349 # Parse the patch_rev to extract the CL and patchset.
350 patch_rev_tokens = patch_rev.split('/')
351 change = patch_rev_tokens[-2]
352 # Parse the googlesource_url.
353 tokens = re.search(
354 '//(.+).googlesource.com/(.+?)(?:\.git)?$', googlesource_url)
355 if not tokens or len(tokens.groups()) != 2:
356 # googlesource_url is not in the expected format.
357 return patch_revs_to_process
358
359 # parse the gerrit host and repo out of googlesource_url.
360 host, repo = tokens.groups()[:2]
361 gerrit_host_url = '%s-review.googlesource.com' % host
362
363 # 1. Find the topic of the Gerrit change specified in the patch_rev.
364 change_object = gerrit_util.GetChange(gerrit_host_url, change)
365 topic = change_object.get('topic')
366 if not topic:
367 # This change has no topic set.
368 return patch_revs_to_process
369
370 # 2. Find all changes with that topic.
371 changes_with_same_topic = gerrit_util.QueryChanges(
372 gerrit_host_url,
373 [('topic', topic), ('status', 'open'), ('repo', repo)],
374 o_params=['ALL_REVISIONS'])
375 for c in changes_with_same_topic:
376 if str(c['_number']) == change:
377 # This change is already in the patch_rev.
378 continue
379 self.Print('Found CL %d with the topic name %s' % (
380 c['_number'], topic))
381 # 3. Append patch_rev of the changes with the same topic to the
382 # patch_revs to process.
383 curr_rev = c['current_revision']
384 new_patch_rev = c['revisions'][curr_rev]['ref']
385 patch_revs_to_process.append(new_patch_rev)
386
387 # 4. Return the new patch_revs to process.
388 return patch_revs_to_process
389
Edward Lemur3acbc742019-05-30 17:57:35 +0000390 def apply_patch_ref(self, patch_repo, patch_rev, target_rev, options,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000391 file_list):
392 """Apply a patch on top of the revision we're synced at.
393
Edward Lemur3acbc742019-05-30 17:57:35 +0000394 The patch ref is given by |patch_repo|@|patch_rev|.
395 |target_rev| is usually the branch that the |patch_rev| was uploaded against
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000396 (e.g. 'refs/heads/main'), but this is not required.
Edward Lemur3acbc742019-05-30 17:57:35 +0000397
398 We cherry-pick all commits reachable from |patch_rev| on top of the curret
399 HEAD, excluding those reachable from |target_rev|
400 (i.e. git cherry-pick target_rev..patch_rev).
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000401
402 Graphically, it looks like this:
403
Edward Lemur3acbc742019-05-30 17:57:35 +0000404 ... -> o -> [possibly already landed commits] -> target_rev
405 \
406 -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000407
Edward Lemur3acbc742019-05-30 17:57:35 +0000408 The final checkout state is then:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000409
Edward Lemur3acbc742019-05-30 17:57:35 +0000410 ... -> HEAD -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000411
412 After application, if |options.reset_patch_ref| is specified, we soft reset
Edward Lemur3acbc742019-05-30 17:57:35 +0000413 the cherry-picked changes, keeping them in git index only.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000414
415 Args:
Edward Lemur3acbc742019-05-30 17:57:35 +0000416 patch_repo: The patch origin.
417 e.g. 'https://foo.googlesource.com/bar'
418 patch_rev: The revision to patch.
419 e.g. 'refs/changes/1234/34/1'.
420 target_rev: The revision to use when finding the merge base.
421 Typically, the branch that the patch was uploaded against.
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000422 e.g. 'refs/heads/main' or 'refs/heads/infra/config'.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000423 options: The options passed to gclient.
424 file_list: A list where modified files will be appended.
425 """
426
Edward Lemurca7d8812018-07-24 17:42:45 +0000427 # Abort any cherry-picks in progress.
428 try:
429 self._Capture(['cherry-pick', '--abort'])
430 except subprocess2.CalledProcessError:
431 pass
432
Edward Lesmesc621b212018-03-21 20:26:56 -0400433 base_rev = self._Capture(['rev-parse', 'HEAD'])
Edward Lemura0ffbe42019-05-01 16:52:18 +0000434
Edward Lemur3acbc742019-05-30 17:57:35 +0000435 if not target_rev:
Edward Lemur4c5c8ab2019-06-07 15:58:13 +0000436 raise gclient_utils.Error('A target revision for the patch must be given')
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000437
438 if target_rev.startswith(('refs/heads/', 'refs/branch-heads')):
Edward Lesmesf627d9f2020-07-23 19:50:50 +0000439 # If |target_rev| is in refs/heads/** or refs/branch-heads/**, try first
440 # to find the corresponding remote ref for it, since |target_rev| might
441 # point to a local ref which is not up to date with the corresponding
442 # remote ref.
Edward Lemur3acbc742019-05-30 17:57:35 +0000443 remote_ref = ''.join(scm.GIT.RefToRemoteRef(target_rev, self.remote))
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000444 self.Print('Trying the corresponding remote ref for %r: %r\n' % (
Edward Lemur3acbc742019-05-30 17:57:35 +0000445 target_rev, remote_ref))
446 if scm.GIT.IsValidRevision(self.checkout_path, remote_ref):
Josip Sokcevic3d7cbce2021-10-05 20:48:04 +0000447 # refs/remotes may need to be updated to cleanly cherry-pick changes.
448 # See https://crbug.com/1255178.
449 self._Capture(['fetch', '--no-tags', self.remote, target_rev])
Edward Lemur3acbc742019-05-30 17:57:35 +0000450 target_rev = remote_ref
451 elif not scm.GIT.IsValidRevision(self.checkout_path, target_rev):
452 # Fetch |target_rev| if it's not already available.
453 url, _ = gclient_utils.SplitUrlRevision(self.url)
Edward Lesmes07a68342021-04-20 23:39:30 +0000454 mirror = self._GetMirror(url, options, target_rev, target_rev)
Edward Lemur3acbc742019-05-30 17:57:35 +0000455 if mirror:
456 rev_type = 'branch' if target_rev.startswith('refs/') else 'hash'
457 self._UpdateMirrorIfNotContains(mirror, options, rev_type, target_rev)
458 self._Fetch(options, refspec=target_rev)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000459
Ravi Mistryecda7822022-02-28 16:22:20 +0000460 patch_revs_to_process = [patch_rev]
461
462 if hasattr(options, 'download_topics') and options.download_topics:
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000463 patch_revs_to_process_from_topics = self._download_topics(
464 patch_rev, self.url)
465 patch_revs_to_process.extend(patch_revs_to_process_from_topics)
Ravi Mistryecda7822022-02-28 16:22:20 +0000466
Edward Lesmesc621b212018-03-21 20:26:56 -0400467 self._Capture(['reset', '--hard'])
Ravi Mistryecda7822022-02-28 16:22:20 +0000468 for pr in patch_revs_to_process:
469 self.Print('===Applying patch===')
470 self.Print('Revision to patch is %r @ %r.' % (patch_repo, pr))
471 self.Print('Current dir is %r' % self.checkout_path)
472 self._Capture(['fetch', '--no-tags', patch_repo, pr])
473 pr = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400474
Ravi Mistryecda7822022-02-28 16:22:20 +0000475 if not options.rebase_patch_ref:
476 self._Capture(['checkout', pr])
477 # Adjust base_rev to be the first parent of our checked out patch ref;
478 # This will allow us to correctly extend `file_list`, and will show the
479 # correct file-list to programs which do `git diff --cached` expecting
480 # to see the patch diff.
481 base_rev = self._Capture(['rev-parse', pr+'~'])
482 else:
483 self.Print('Will cherrypick %r .. %r on top of %r.' % (
484 target_rev, pr, base_rev))
Edward Lemur3acbc742019-05-30 17:57:35 +0000485 try:
Ravi Mistryecda7822022-02-28 16:22:20 +0000486 if scm.GIT.IsAncestor(self.checkout_path, pr, target_rev):
487 if len(patch_revs_to_process) > 1:
488 # If there are multiple patch_revs_to_process then we do not want
489 # want to invalidate a previous patch so throw an error.
490 raise gclient_utils.Error(
491 'patch_rev %s is an ancestor of target_rev %s. This '
492 'situation is unsupported when we need to apply multiple '
493 'patch_revs: %s' % (pr, target_rev, patch_revs_to_process))
494 # If |patch_rev| is an ancestor of |target_rev|, check it out.
495 self._Capture(['checkout', pr])
496 else:
497 # If a change was uploaded on top of another change, which has
498 # already landed, one of the commits in the cherry-pick range will
499 # be redundant, since it has already landed and its changes
500 # incorporated in the tree.
501 # We pass '--keep-redundant-commits' to ignore those changes.
502 self._Capture(['cherry-pick', target_rev + '..' + pr,
503 '--keep-redundant-commits'])
Edward Lemurca7d8812018-07-24 17:42:45 +0000504
Ravi Mistryecda7822022-02-28 16:22:20 +0000505 except subprocess2.CalledProcessError as e:
506 self.Print('Failed to apply patch.')
507 self.Print('Revision to patch was %r @ %r.' % (patch_repo, pr))
508 self.Print('Tried to cherrypick %r .. %r on top of %r.' % (
509 target_rev, pr, base_rev))
510 self.Print('Current dir is %r' % self.checkout_path)
511 self.Print('git returned non-zero exit status %s:\n%s' % (
512 e.returncode, e.stderr.decode('utf-8')))
513 # Print the current status so that developers know what changes caused
514 # the patch failure, since git cherry-pick doesn't show that
515 # information.
516 self.Print(self._Capture(['status']))
517 try:
518 self._Capture(['cherry-pick', '--abort'])
519 except subprocess2.CalledProcessError:
520 pass
521 raise
522
523 if file_list is not None:
524 file_list.extend(self._GetDiffFilenames(base_rev))
Edward Lemurca7d8812018-07-24 17:42:45 +0000525
Edward Lesmesc621b212018-03-21 20:26:56 -0400526 if options.reset_patch_ref:
527 self._Capture(['reset', '--soft', base_rev])
528
msb@chromium.orge28e4982009-09-25 20:51:45 +0000529 def update(self, options, args, file_list):
530 """Runs git to update or transparently checkout the working copy.
531
532 All updated files will be appended to file_list.
533
534 Raises:
535 Error: if can't get URL for relative path.
536 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000537 if args:
538 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
539
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000540 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000541
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000542 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000543 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000544 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000545 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000546 # Override the revision number.
547 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000548 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000549 # Check again for a revision in case an initial ref was specified
550 # in the url, for example bla.git@refs/heads/custombranch
551 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000552 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000553 if not revision:
Josip Sokcevic091f5ac2021-01-14 23:14:21 +0000554 # If a dependency is not pinned, track the default remote branch.
555 revision = scm.GIT.GetRemoteHeadRef(self.checkout_path, self.url,
556 self.remote)
Edward Lesmes4ea67bb2021-04-20 17:33:52 +0000557 if revision.startswith('origin/'):
558 revision = 'refs/remotes/' + revision
msb@chromium.orge28e4982009-09-25 20:51:45 +0000559
Tomasz Wiszkowskid4e66882021-08-19 21:35:09 +0000560 if managed and platform.system() == 'Windows':
szager@chromium.org8a139702014-06-20 15:55:01 +0000561 self._DisableHooks()
562
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000563 printed_path = False
564 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000565 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700566 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000567 verbose = ['--verbose']
568 printed_path = True
569
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000570 revision_ref = revision
571 if ':' in revision:
572 revision_ref, _, revision = revision.partition(':')
573
Edward Lesmes8073a502020-04-15 02:11:14 +0000574 if revision_ref.startswith('refs/branch-heads'):
575 options.with_branch_heads = True
576
Edward Lesmes07a68342021-04-20 23:39:30 +0000577 mirror = self._GetMirror(url, options, revision, revision_ref)
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000578 if mirror:
579 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000580
John Budorick882c91e2018-07-12 22:11:41 +0000581 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
582 if remote_ref:
583 # Rewrite remote refs to their local equivalents.
584 revision = ''.join(remote_ref)
585 rev_type = "branch"
586 elif revision.startswith('refs/'):
587 # Local branch? We probably don't want to support, since DEPS should
588 # always specify branches as they are in the upstream repo.
589 rev_type = "branch"
590 else:
591 # hash is also a tag, only make a distinction at checkout
592 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000593
primiano@chromium.org1c127382015-02-17 11:15:40 +0000594 # If we are going to introduce a new project, there is a possibility that
595 # we are syncing back to a state where the project was originally a
596 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
597 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
598 # In such case, we might have a backup of the former .git folder, which can
599 # be used to avoid re-fetching the entire repo again (useful for bisects).
600 backup_dir = self.GetGitBackupDirPath()
601 target_dir = os.path.join(self.checkout_path, '.git')
602 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
603 gclient_utils.safe_makedirs(self.checkout_path)
604 os.rename(backup_dir, target_dir)
605 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800606 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000607
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000608 if (not os.path.exists(self.checkout_path) or
609 (os.path.isdir(self.checkout_path) and
610 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000611 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000612 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000613 try:
John Budorick882c91e2018-07-12 22:11:41 +0000614 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000615 except subprocess2.CalledProcessError:
616 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000617 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000618 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800619 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000620 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000621 file_list.extend(
Edward Lemur979fa782019-08-13 22:44:05 +0000622 [os.path.join(self.checkout_path, f) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000623 if mirror:
624 self._Capture(
625 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000626 if not verbose:
627 # Make the output a little prettier. It's nice to have some whitespace
628 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000629 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000630 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000631
John Budorick21a51b32018-09-19 19:39:20 +0000632 if mirror:
633 self._Capture(
634 ['remote', 'set-url', '--push', 'origin', mirror.url])
635
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000636 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000637 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000638 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
639 return self._Capture(['rev-parse', '--verify', 'HEAD'])
640
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000641 self._maybe_break_locks(options)
642
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000643 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000644 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000645
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000646 # See if the url has changed (the unittests use git://foo for the url, let
647 # that through).
648 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
649 return_early = False
650 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
651 # unit test pass. (and update the comment above)
652 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
653 # This allows devs to use experimental repos which have a different url
654 # but whose branch(s) are the same as official repos.
Raul Tambrecd862e32019-05-10 21:19:00 +0000655 if (current_url.rstrip('/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000656 subprocess2.capture(
Aaron Gableac9b0f32019-04-18 17:38:37 +0000657 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000658 cwd=self.checkout_path).strip() != 'False'):
Anthony Polito486f1812020-08-04 23:40:33 +0000659 self.Print('_____ switching %s from %s to new upstream %s' % (
660 self.relpath, current_url, url))
iannucci@chromium.org78514212014-08-20 23:08:00 +0000661 if not (options.force or options.reset):
662 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700663 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000664 # Switch over to the new upstream
665 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000666 if mirror:
667 with open(os.path.join(
668 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
669 'w') as fh:
670 fh.write(os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000671 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
672 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000673
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000674 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000675 else:
John Budorick882c91e2018-07-12 22:11:41 +0000676 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000677
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000678 if return_early:
679 return self._Capture(['rev-parse', '--verify', 'HEAD'])
680
msb@chromium.org5bde4852009-12-14 16:47:12 +0000681 cur_branch = self._GetCurrentBranch()
682
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000683 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000684 # 0) HEAD is detached. Probably from our initial clone.
685 # - make sure HEAD is contained by a named ref, then update.
686 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700687 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000688 # - try to rebase onto the new hash or branch
689 # 2) current branch is tracking a remote branch with local committed
690 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000691 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000692 # 3) current branch is tracking a remote branch w/or w/out changes, and
693 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000694 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000695 # 4) current branch is tracking a remote branch, but DEPS switches to a
696 # different remote branch, and
697 # a) current branch has no local changes, and --force:
698 # - checkout new branch
699 # b) current branch has local changes, and --force and --reset:
700 # - checkout new branch
701 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000702
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000703 # GetUpstreamBranch returns something like 'refs/remotes/origin/main' for
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000704 # a tracking branch
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000705 # or 'main' if not a tracking branch (it's based on a specific rev/hash)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000706 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000707 if cur_branch is None:
708 upstream_branch = None
709 current_type = "detached"
710 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000711 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000712 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
713 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
714 current_type = "hash"
715 logging.debug("Current branch is not tracking an upstream (remote)"
716 " branch.")
717 elif upstream_branch.startswith('refs/remotes'):
718 current_type = "branch"
719 else:
720 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000721
Edward Lemur579c9862018-07-13 23:17:51 +0000722 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000723
Michael Spang73fac912019-03-08 18:44:19 +0000724 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000725 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000726 self._Fetch(options, prune=options.force)
727
728 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
729 sha_only=True):
730 # Update the remotes first so we have all the refs.
731 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
732 cwd=self.checkout_path)
733 if verbose:
734 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000735
John Budorick882c91e2018-07-12 22:11:41 +0000736 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200737
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000738 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000739 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000740 target = 'HEAD'
741 if options.upstream and upstream_branch:
742 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800743 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000744
msb@chromium.org786fb682010-06-02 15:16:23 +0000745 if current_type == 'detached':
746 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800747 # We just did a Scrub, this is as clean as it's going to get. In
748 # particular if HEAD is a commit that contains two versions of the same
749 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
750 # to actually "Clean" the checkout; that commit is uncheckoutable on this
751 # system. The best we can do is carry forward to the checkout step.
752 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000753 self._CheckClean(revision)
754 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000755 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000756 self.Print('Up-to-date; skipping checkout.')
757 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000758 # 'git checkout' may need to overwrite existing untracked files. Allow
759 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000760 self._Checkout(
761 options,
John Budorick882c91e2018-07-12 22:11:41 +0000762 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000763 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000764 quiet=True,
765 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000766 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000767 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000768 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000769 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700770 # Can't find a merge-base since we don't know our upstream. That makes
771 # this command VERY likely to produce a rebase failure. For now we
772 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000773 upstream_branch = self.remote
774 if options.revision or deps_revision:
775 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700776 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700777 printed_path=printed_path, merge=options.merge)
778 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000779 elif rev_type == 'hash':
780 # case 2
781 self._AttemptRebase(upstream_branch, file_list, options,
782 newbase=revision, printed_path=printed_path,
783 merge=options.merge)
784 printed_path = True
785 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000786 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000787 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000788 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000789 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000790 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000791 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000792 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000793 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
794 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000795 force_switch = False
796 if options.force:
797 try:
John Budorick882c91e2018-07-12 22:11:41 +0000798 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000799 # case 4a
800 force_switch = True
801 except gclient_utils.Error as e:
802 if options.reset:
803 # case 4b
804 force_switch = True
805 else:
806 switch_error = '%s\n%s' % (e.message, switch_error)
807 if force_switch:
808 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000809 (upstream_branch, new_base))
810 switch_branch = 'gclient_' + remote_ref[1]
811 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000812 self._Checkout(options, switch_branch, force=True, quiet=True)
813 else:
814 # case 4c
815 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000816 else:
817 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800818 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000819 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000820 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000821 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000822 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000823 if options.merge:
824 merge_args.append('--ff')
825 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000826 merge_args.append('--ff-only')
827 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000828 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000829 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700830 rebase_files = []
Edward Lemur979fa782019-08-13 22:44:05 +0000831 if re.match(b'fatal: Not possible to fast-forward, aborting.',
832 e.stderr):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000833 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000834 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700835 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000836 printed_path = True
837 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000838 if not options.auto_rebase:
839 try:
840 action = self._AskForData(
841 'Cannot %s, attempt to rebase? '
842 '(y)es / (q)uit / (s)kip : ' %
843 ('merge' if options.merge else 'fast-forward merge'),
844 options)
845 except ValueError:
846 raise gclient_utils.Error('Invalid Character')
847 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700848 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000849 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000850 printed_path = True
851 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000852
853 if re.match(r'quit|q', action, re.I):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000854 raise gclient_utils.Error("Can't fast-forward, please merge or "
855 "rebase manually.\n"
856 "cd %s && git " % self.checkout_path
857 + "rebase %s" % upstream_branch)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000858
859 if re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000860 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000861 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000862
863 self.Print('Input not recognized')
Edward Lemur979fa782019-08-13 22:44:05 +0000864 elif re.match(b"error: Your local changes to '.*' would be "
865 b"overwritten by merge. Aborting.\nPlease, commit your "
866 b"changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000867 e.stderr):
868 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000869 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700870 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000871 printed_path = True
Edward Lemur979fa782019-08-13 22:44:05 +0000872 raise gclient_utils.Error(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000873 else:
874 # Some other problem happened with the merge
875 logging.error("Error during fast-forward merge in %s!" % self.relpath)
Edward Lemur979fa782019-08-13 22:44:05 +0000876 self.Print(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000877 raise
878 else:
879 # Fast-forward merge was successful
880 if not re.match('Already up-to-date.', merge_output) or verbose:
881 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700882 self.Print('_____ %s at %s' % (self.relpath, revision),
883 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000884 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000885 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000886 if not verbose:
887 # Make the output a little prettier. It's nice to have some
888 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000889 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000890
agablec3937b92016-10-25 10:13:03 -0700891 if file_list is not None:
892 file_list.extend(
893 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000894
895 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000896 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700897 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000898 '\nConflict while rebasing this branch.\n'
899 'Fix the conflict and run gclient again.\n'
900 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700901 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000902
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000903 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000904 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
905 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000906
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000907 # If --reset and --delete_unversioned_trees are specified, remove any
908 # untracked directories.
909 if options.reset and options.delete_unversioned_trees:
910 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
911 # merge-base by default), so doesn't include untracked files. So we use
912 # 'git ls-files --directory --others --exclude-standard' here directly.
913 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800914 ['-c', 'core.quotePath=false', 'ls-files',
915 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000916 self.checkout_path)
917 for path in (p for p in paths.splitlines() if p.endswith('/')):
918 full_path = os.path.join(self.checkout_path, path)
919 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000920 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000921 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000922
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000923 return self._Capture(['rev-parse', '--verify', 'HEAD'])
924
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000925 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000926 """Reverts local modifications.
927
928 All reverted files will be appended to file_list.
929 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000930 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000931 # revert won't work if the directory doesn't exist. It needs to
932 # checkout instead.
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000933 self.Print('_____ %s is missing, syncing instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000934 # Don't reuse the args.
935 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000936
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000937 default_rev = "refs/heads/main"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000938 if options.upstream:
939 if self._GetCurrentBranch():
940 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
941 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000942 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000943 if not deps_revision:
944 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000945 if deps_revision.startswith('refs/heads/'):
946 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700947 try:
948 deps_revision = self.GetUsableRev(deps_revision, options)
949 except NoUsableRevError as e:
950 # If the DEPS entry's url and hash changed, try to update the origin.
951 # See also http://crbug.com/520067.
John Budorickd94f8ea2020-03-27 15:55:24 +0000952 logging.warning(
953 "Couldn't find usable revision, will retrying to update instead: %s",
smutae7ea312016-07-18 11:59:41 -0700954 e.message)
955 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000956
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000957 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800958 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000959
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800960 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000961 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000962
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000963 if file_list is not None:
964 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
965
966 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000967 """Returns revision"""
968 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000969
msb@chromium.orge28e4982009-09-25 20:51:45 +0000970 def runhooks(self, options, args, file_list):
971 self.status(options, args, file_list)
972
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000973 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000974 """Display status information."""
975 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000976 self.Print('________ couldn\'t run status in %s:\n'
977 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000978 else:
Anthony Politobb457342019-11-15 22:26:01 +0000979 merge_base = []
980 if self.url:
981 _, base_rev = gclient_utils.SplitUrlRevision(self.url)
982 if base_rev:
983 merge_base = [base_rev]
Aaron Gablef4068aa2017-12-12 15:14:09 -0800984 self._Run(
985 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
Edward Lemur24146be2019-08-01 21:44:52 +0000986 options, always_show_header=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000987 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800988 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000989 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000990
smutae7ea312016-07-18 11:59:41 -0700991 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700992 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700993 sha1 = None
994 if not os.path.isdir(self.checkout_path):
995 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800996 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700997
998 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
999 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001000 else:
agable41e3a6c2016-10-20 11:36:56 -07001001 # May exist in origin, but we don't have it yet, so fetch and look
1002 # again.
1003 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -07001004 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1005 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001006
1007 if not sha1:
1008 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -08001009 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -07001010
1011 return sha1
1012
primiano@chromium.org1c127382015-02-17 11:15:40 +00001013 def GetGitBackupDirPath(self):
1014 """Returns the path where the .git folder for the current project can be
1015 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
1016 return os.path.join(self._root_dir,
1017 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
1018
Edward Lesmes07a68342021-04-20 23:39:30 +00001019 def _GetMirror(self, url, options, revision=None, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001020 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +00001021 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001022 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +00001023 mirror_kwargs = {
1024 'print_func': self.filter,
Edward Lesmes07a68342021-04-20 23:39:30 +00001025 'refs': [],
1026 'commits': [],
hinoka@google.comb1b54572014-04-16 22:29:23 +00001027 }
hinoka@google.comb1b54572014-04-16 22:29:23 +00001028 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1029 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001030 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
1031 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001032 if hasattr(options, 'with_tags') and options.with_tags:
1033 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001034 elif revision_ref and revision_ref.startswith('refs/tags/'):
1035 mirror_kwargs['refs'].append(revision_ref)
Edward Lesmes07a68342021-04-20 23:39:30 +00001036 if revision and not revision.startswith('refs/'):
1037 mirror_kwargs['commits'].append(revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001038 return git_cache.Mirror(url, **mirror_kwargs)
1039
John Budorick882c91e2018-07-12 22:11:41 +00001040 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001041 """Update a git mirror by fetching the latest commits from the remote,
1042 unless mirror already contains revision whose type is sha1 hash.
1043 """
John Budorick882c91e2018-07-12 22:11:41 +00001044 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001045 if options.verbose:
1046 self.Print('skipping mirror update, it has rev=%s already' % revision,
1047 timestamp=False)
1048 return
1049
szager@chromium.org3ec84f62014-08-22 21:00:22 +00001050 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +00001051 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001052 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +00001053 depth = 10
1054 else:
1055 depth = 10000
1056 else:
1057 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +00001058 mirror.populate(verbose=options.verbose,
1059 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +00001060 depth=depth,
Vadim Shtayura08049e22017-10-11 00:14:52 +00001061 lock_timeout=getattr(options, 'lock_timeout', 0))
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001062
John Budorick882c91e2018-07-12 22:11:41 +00001063 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001064 """Clone a git repository from the given URL.
1065
msb@chromium.org786fb682010-06-02 15:16:23 +00001066 Once we've cloned the repo, we checkout a working branch if the specified
1067 revision is a branch head. If it is a tag or a specific commit, then we
1068 leave HEAD detached as it makes future updates simpler -- in this case the
1069 user should first create a new branch or switch to an existing branch before
1070 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001071 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001072 # git clone doesn't seem to insert a newline properly before printing
1073 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001074 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +00001075 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +00001076 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001077 if self.cache_dir:
1078 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001079 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001080 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001081 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001082 # If the parent directory does not exist, Git clone on Windows will not
1083 # create it, so we need to do it manually.
1084 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001085 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001086
1087 template_dir = None
1088 if hasattr(options, 'no_history') and options.no_history:
John Budorick882c91e2018-07-12 22:11:41 +00001089 if gclient_utils.IsGitSha(revision):
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001090 # In the case of a subproject, the pinned sha is not necessarily the
1091 # head of the remote branch (so we can't just use --depth=N). Instead,
1092 # we tell git to fetch all the remote objects from SHA..HEAD by means of
1093 # a template git dir which has a 'shallow' file pointing to the sha.
1094 template_dir = tempfile.mkdtemp(
1095 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
1096 dir=parent_dir)
1097 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
1098 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
1099 template_file.write(revision)
1100 clone_cmd.append('--template=' + template_dir)
1101 else:
1102 # Otherwise, we're just interested in the HEAD. Just use --depth.
1103 clone_cmd.append('--depth=1')
1104
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001105 tmp_dir = tempfile.mkdtemp(
1106 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
1107 dir=parent_dir)
1108 try:
1109 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +01001110 if self.print_outbuf:
1111 print_stdout = True
Edward Lemur24146be2019-08-01 21:44:52 +00001112 filter_fn = None
Edward Lemur231f5ea2018-01-31 19:02:36 +01001113 else:
1114 print_stdout = False
Edward Lemur24146be2019-08-01 21:44:52 +00001115 filter_fn = self.filter
Edward Lemur231f5ea2018-01-31 19:02:36 +01001116 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
Edward Lemur24146be2019-08-01 21:44:52 +00001117 print_stdout=print_stdout, filter_fn=filter_fn)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001118 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +00001119 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1120 os.path.join(self.checkout_path, '.git'))
Edward Lesmesd4e20f22020-07-15 21:11:08 +00001121 # TODO(https://github.com/git-for-windows/git/issues/2569): Remove once
1122 # fixed.
1123 if sys.platform.startswith('win'):
1124 try:
1125 self._Run(['config', '--unset', 'core.worktree'], options,
1126 cwd=self.checkout_path)
1127 except subprocess2.CalledProcessError:
1128 pass
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001129 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001130 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001131 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001132 finally:
1133 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001134 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001135 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001136 if template_dir:
1137 gclient_utils.rmtree(template_dir)
Edward Lemur579c9862018-07-13 23:17:51 +00001138 self._SetFetchConfig(options)
1139 self._Fetch(options, prune=options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001140 revision = self._AutoFetchRef(options, revision)
1141 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1142 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001143 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001144 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001145 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +00001146 ('Checked out %s to a detached HEAD. Before making any commits\n'
1147 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
1148 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
John Budorick882c91e2018-07-12 22:11:41 +00001149 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001150
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001151 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001152 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001153 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001154 raise gclient_utils.Error("Background task requires input. Rerun "
1155 "gclient with --jobs=1 so that\n"
1156 "interaction is possible.")
Edward Lesmesae3586b2020-03-23 21:21:14 +00001157 return gclient_utils.AskForData(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001158
1159
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001160 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001161 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001162 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001163 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001164 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001165 revision = upstream
1166 if newbase:
1167 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001168 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001169 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001170 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001171 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001172 printed_path = True
1173 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001174 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001175
1176 if merge:
1177 merge_output = self._Capture(['merge', revision])
1178 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001179 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001180 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001181
1182 # Build the rebase command here using the args
1183 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1184 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001185 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001186 rebase_cmd.append('--verbose')
1187 if newbase:
1188 rebase_cmd.extend(['--onto', newbase])
1189 rebase_cmd.append(upstream)
1190 if branch:
1191 rebase_cmd.append(branch)
1192
1193 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001194 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001195 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001196 if (re.match(br'cannot rebase: you have unstaged changes', e.stderr) or
1197 re.match(br'cannot rebase: your index contains uncommitted changes',
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001198 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001199 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001200 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001201 'Cannot rebase because of unstaged changes.\n'
1202 '\'git reset --hard HEAD\' ?\n'
1203 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001204 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001205 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001206 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001207 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001208 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001209 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001210
1211 if re.match(r'quit|q', rebase_action, re.I):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001212 raise gclient_utils.Error("Please merge or rebase manually\n"
1213 "cd %s && git " % self.checkout_path
1214 + "%s" % ' '.join(rebase_cmd))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001215
1216 if re.match(r'show|s', rebase_action, re.I):
Edward Lemur979fa782019-08-13 22:44:05 +00001217 self.Print('%s' % e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001218 continue
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001219
1220 gclient_utils.Error("Input not recognized")
1221 continue
Edward Lemur979fa782019-08-13 22:44:05 +00001222 elif re.search(br'^CONFLICT', e.stdout, re.M):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001223 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1224 "Fix the conflict and run gclient again.\n"
1225 "See 'man git-rebase' for details.\n")
1226 else:
Edward Lemur979fa782019-08-13 22:44:05 +00001227 self.Print(e.stdout.decode('utf-8').strip())
1228 self.Print('Rebase produced error output:\n%s' %
1229 e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001230 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1231 "manually.\ncd %s && git " %
1232 self.checkout_path
1233 + "%s" % ' '.join(rebase_cmd))
1234
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001235 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001236 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001237 # Make the output a little prettier. It's nice to have some
1238 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001239 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001240
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001241 @staticmethod
1242 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001243 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1244 if not ok:
1245 raise gclient_utils.Error('git version %s < minimum required %s' %
1246 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001247
John Budorick882c91e2018-07-12 22:11:41 +00001248 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001249 # Special case handling if all 3 conditions are met:
1250 # * the mirros have recently changed, but deps destination remains same,
1251 # * the git histories of mirrors are conflicting.
1252 # * git cache is used
1253 # This manifests itself in current checkout having invalid HEAD commit on
1254 # most git operations. Since git cache is used, just deleted the .git
1255 # folder, and re-create it by cloning.
1256 try:
1257 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1258 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001259 if (b'fatal: bad object HEAD' in e.stderr
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001260 and self.cache_dir and self.cache_dir in url):
1261 self.Print((
1262 'Likely due to DEPS change with git cache_dir, '
1263 'the current commit points to no longer existing object.\n'
1264 '%s' % e)
1265 )
1266 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001267 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001268 else:
1269 raise
1270
msb@chromium.org786fb682010-06-02 15:16:23 +00001271 def _IsRebasing(self):
1272 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1273 # have a plumbing command to determine whether a rebase is in progress, so
1274 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1275 g = os.path.join(self.checkout_path, '.git')
1276 return (
1277 os.path.isdir(os.path.join(g, "rebase-merge")) or
1278 os.path.isdir(os.path.join(g, "rebase-apply")))
1279
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001280 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001281 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1282 if os.path.exists(lockfile):
1283 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001284 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001285 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1286 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001287 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001288
msb@chromium.org786fb682010-06-02 15:16:23 +00001289 # Make sure the tree is clean; see git-rebase.sh for reference
1290 try:
1291 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001292 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001293 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001294 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001295 '\tYou have unstaged changes.\n'
1296 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001297 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001298 try:
1299 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001300 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001301 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001302 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001303 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001304 '\tYour index contains uncommitted changes\n'
1305 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001306 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001307
agable83faed02016-10-24 14:37:10 -07001308 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001309 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1310 # reference by a commit). If not, error out -- most likely a rebase is
1311 # in progress, try to detect so we can give a better error.
1312 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001313 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1314 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001315 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001316 # Commit is not contained by any rev. See if the user is rebasing:
1317 if self._IsRebasing():
1318 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001319 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001320 '\tAlready in a conflict, i.e. (no branch).\n'
1321 '\tFix the conflict and run gclient again.\n'
1322 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1323 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001324 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001325 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001326 name = ('saved-by-gclient-' +
1327 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001328 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001329 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001330 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001331
msb@chromium.org5bde4852009-12-14 16:47:12 +00001332 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001333 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001334 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001335 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001336 return None
1337 return branch
1338
borenet@google.comc3e09d22014-04-10 13:58:18 +00001339 def _Capture(self, args, **kwargs):
Mike Frysinger286fb162019-09-30 03:14:10 +00001340 set_git_dir = 'cwd' not in kwargs
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001341 kwargs.setdefault('cwd', self.checkout_path)
1342 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001343 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001344 env = scm.GIT.ApplyEnvVars(kwargs)
Mike Frysinger286fb162019-09-30 03:14:10 +00001345 # If an explicit cwd isn't set, then default to the .git/ subdir so we get
1346 # stricter behavior. This can be useful in cases of slight corruption --
1347 # we don't accidentally go corrupting parent git checks too. See
1348 # https://crbug.com/1000825 for an example.
1349 if set_git_dir:
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001350 git_dir = os.path.abspath(os.path.join(self.checkout_path, '.git'))
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001351 # Depending on how the .gclient file was defined, self.checkout_path
1352 # might be set to a unicode string, not a regular string; on Windows
1353 # Python2, we can't set env vars to be unicode strings, so we
1354 # forcibly cast the value to a string before setting it.
1355 env.setdefault('GIT_DIR', str(git_dir))
Raul Tambrecd862e32019-05-10 21:19:00 +00001356 ret = subprocess2.check_output(
1357 ['git'] + args, env=env, **kwargs).decode('utf-8')
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001358 if strip:
1359 ret = ret.strip()
Erik Chene16ffff2019-10-14 20:35:53 +00001360 self.Print('Finished running: %s %s' % ('git', ' '.join(args)))
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001361 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001362
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001363 def _Checkout(self, options, ref, force=False, quiet=None):
1364 """Performs a 'git-checkout' operation.
1365
1366 Args:
1367 options: The configured option set
1368 ref: (str) The branch/commit to checkout
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001369 quiet: (bool/None) Whether or not the checkout should pass '--quiet'; if
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001370 'None', the behavior is inferred from 'options.verbose'.
1371 Returns: (str) The output of the checkout operation
1372 """
1373 if quiet is None:
1374 quiet = (not options.verbose)
1375 checkout_args = ['checkout']
1376 if force:
1377 checkout_args.append('--force')
1378 if quiet:
1379 checkout_args.append('--quiet')
1380 checkout_args.append(ref)
1381 return self._Capture(checkout_args)
1382
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001383 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1384 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001385 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001386 # When updating, the ref is modified to be a remote ref .
1387 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1388 # Try to reverse that mapping.
1389 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1390 if original_ref:
1391 refspec = original_ref + ':' + refspec
1392 # When a mirror is configured, it only fetches
1393 # refs/{heads,branch-heads,tags}/*.
1394 # If asked to fetch other refs, we must fetch those directly from the
1395 # repository, and not from the mirror.
1396 if not original_ref.startswith(
1397 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1398 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001399 fetch_cmd = cfg + [
1400 'fetch',
1401 remote or self.remote,
1402 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001403 if refspec:
1404 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001405
1406 if prune:
1407 fetch_cmd.append('--prune')
1408 if options.verbose:
1409 fetch_cmd.append('--verbose')
danakjd5c0b562019-11-08 17:27:47 +00001410 if not hasattr(options, 'with_tags') or not options.with_tags:
1411 fetch_cmd.append('--no-tags')
dnj@chromium.org680f2172014-06-25 00:39:32 +00001412 elif quiet:
1413 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001414 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001415
Edward Lemur579c9862018-07-13 23:17:51 +00001416 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001417 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1418 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001419 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001420 try:
1421 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1422 options)
1423 self._Run(['config', 'remote.%s.fetch' % self.remote,
1424 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1425 except subprocess2.CalledProcessError as e:
1426 # If exit code was 5, it means we attempted to unset a config that
1427 # didn't exist. Ignore it.
1428 if e.returncode != 5:
1429 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001430 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001431 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001432 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1433 '^\\+refs/branch-heads/\\*:.*$']
1434 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001435 if hasattr(options, 'with_tags') and options.with_tags:
1436 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1437 '+refs/tags/*:refs/tags/*',
1438 '^\\+refs/tags/\\*:.*$']
1439 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001440
John Budorick882c91e2018-07-12 22:11:41 +00001441 def _AutoFetchRef(self, options, revision):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001442 """Attempts to fetch |revision| if not available in local repo.
1443
1444 Returns possibly updated revision."""
Edward Lemure0ba7b82020-03-11 20:31:32 +00001445 if not scm.GIT.IsValidRevision(self.checkout_path, revision):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001446 self._Fetch(options, refspec=revision)
1447 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1448 return revision
1449
Edward Lemur24146be2019-08-01 21:44:52 +00001450 def _Run(self, args, options, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001451 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001452 kwargs.setdefault('cwd', self.checkout_path)
Edward Lemur24146be2019-08-01 21:44:52 +00001453 kwargs.setdefault('filter_fn', self.filter)
1454 kwargs.setdefault('show_header', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001455 env = scm.GIT.ApplyEnvVars(kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +00001456
agable@chromium.org772efaf2014-04-01 02:35:44 +00001457 cmd = ['git'] + args
Edward Lemur24146be2019-08-01 21:44:52 +00001458 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001459
1460
1461class CipdPackage(object):
1462 """A representation of a single CIPD package."""
1463
John Budorickd3ba72b2018-03-20 12:27:42 -07001464 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001465 self._authority_for_subdir = authority_for_subdir
1466 self._name = name
1467 self._version = version
1468
1469 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001470 def authority_for_subdir(self):
1471 """Whether this package has authority to act on behalf of its subdir.
1472
1473 Some operations should only be performed once per subdirectory. A package
1474 that has authority for its subdirectory is the only package that should
1475 perform such operations.
1476
1477 Returns:
1478 bool; whether this package has subdir authority.
1479 """
1480 return self._authority_for_subdir
1481
1482 @property
1483 def name(self):
1484 return self._name
1485
1486 @property
1487 def version(self):
1488 return self._version
1489
1490
1491class CipdRoot(object):
1492 """A representation of a single CIPD root."""
1493 def __init__(self, root_dir, service_url):
1494 self._all_packages = set()
1495 self._mutator_lock = threading.Lock()
1496 self._packages_by_subdir = collections.defaultdict(list)
1497 self._root_dir = root_dir
1498 self._service_url = service_url
1499
1500 def add_package(self, subdir, package, version):
1501 """Adds a package to this CIPD root.
1502
1503 As far as clients are concerned, this grants both root and subdir authority
1504 to packages arbitrarily. (The implementation grants root authority to the
1505 first package added and subdir authority to the first package added for that
1506 subdir, but clients should not depend on or expect that behavior.)
1507
1508 Args:
1509 subdir: str; relative path to where the package should be installed from
1510 the cipd root directory.
1511 package: str; the cipd package name.
1512 version: str; the cipd package version.
1513 Returns:
1514 CipdPackage; the package that was created and added to this root.
1515 """
1516 with self._mutator_lock:
1517 cipd_package = CipdPackage(
1518 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001519 not self._packages_by_subdir[subdir])
1520 self._all_packages.add(cipd_package)
1521 self._packages_by_subdir[subdir].append(cipd_package)
1522 return cipd_package
1523
1524 def packages(self, subdir):
1525 """Get the list of configured packages for the given subdir."""
1526 return list(self._packages_by_subdir[subdir])
1527
1528 def clobber(self):
1529 """Remove the .cipd directory.
1530
1531 This is useful for forcing ensure to redownload and reinitialize all
1532 packages.
1533 """
1534 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001535 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001536 try:
1537 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1538 except OSError:
1539 if os.path.exists(cipd_cache_dir):
1540 raise
1541
1542 @contextlib.contextmanager
1543 def _create_ensure_file(self):
1544 try:
Edward Lesmes05934952019-12-19 20:38:09 +00001545 contents = '$ParanoidMode CheckPresence\n\n'
1546 for subdir, packages in sorted(self._packages_by_subdir.items()):
1547 contents += '@Subdir %s\n' % subdir
1548 for package in sorted(packages, key=lambda p: p.name):
1549 contents += '%s %s\n' % (package.name, package.version)
1550 contents += '\n'
John Budorick0f7b2002018-01-19 15:46:17 -08001551 ensure_file = None
1552 with tempfile.NamedTemporaryFile(
Edward Lesmes05934952019-12-19 20:38:09 +00001553 suffix='.ensure', delete=False, mode='wb') as ensure_file:
1554 ensure_file.write(contents.encode('utf-8', 'replace'))
John Budorick0f7b2002018-01-19 15:46:17 -08001555 yield ensure_file.name
1556 finally:
1557 if ensure_file is not None and os.path.exists(ensure_file.name):
1558 os.remove(ensure_file.name)
1559
1560 def ensure(self):
1561 """Run `cipd ensure`."""
1562 with self._mutator_lock:
1563 with self._create_ensure_file() as ensure_file:
1564 cmd = [
1565 'cipd', 'ensure',
1566 '-log-level', 'error',
1567 '-root', self.root_dir,
1568 '-ensure-file', ensure_file,
1569 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001570 gclient_utils.CheckCallAndFilter(
1571 cmd, print_stdout=True, show_header=True)
John Budorick0f7b2002018-01-19 15:46:17 -08001572
John Budorickd3ba72b2018-03-20 12:27:42 -07001573 def run(self, command):
1574 if command == 'update':
1575 self.ensure()
1576 elif command == 'revert':
1577 self.clobber()
1578 self.ensure()
1579
John Budorick0f7b2002018-01-19 15:46:17 -08001580 def created_package(self, package):
1581 """Checks whether this root created the given package.
1582
1583 Args:
1584 package: CipdPackage; the package to check.
1585 Returns:
1586 bool; whether this root created the given package.
1587 """
1588 return package in self._all_packages
1589
1590 @property
1591 def root_dir(self):
1592 return self._root_dir
1593
1594 @property
1595 def service_url(self):
1596 return self._service_url
1597
1598
1599class CipdWrapper(SCMWrapper):
1600 """Wrapper for CIPD.
1601
1602 Currently only supports chrome-infra-packages.appspot.com.
1603 """
John Budorick3929e9e2018-02-04 18:18:07 -08001604 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001605
1606 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1607 out_cb=None, root=None, package=None):
1608 super(CipdWrapper, self).__init__(
1609 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1610 out_cb=out_cb)
1611 assert root.created_package(package)
1612 self._package = package
1613 self._root = root
1614
1615 #override
1616 def GetCacheMirror(self):
1617 return None
1618
1619 #override
1620 def GetActualRemoteURL(self, options):
1621 return self._root.service_url
1622
1623 #override
1624 def DoesRemoteURLMatch(self, options):
1625 del options
1626 return True
1627
1628 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001629 """Does nothing.
1630
1631 CIPD packages should be reverted at the root by running
1632 `CipdRoot.run('revert')`.
1633 """
John Budorick0f7b2002018-01-19 15:46:17 -08001634
1635 def diff(self, options, args, file_list):
1636 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001637
1638 def pack(self, options, args, file_list):
1639 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001640
1641 def revinfo(self, options, args, file_list):
1642 """Grab the instance ID."""
1643 try:
1644 tmpdir = tempfile.mkdtemp()
1645 describe_json_path = os.path.join(tmpdir, 'describe.json')
1646 cmd = [
1647 'cipd', 'describe',
1648 self._package.name,
1649 '-log-level', 'error',
1650 '-version', self._package.version,
1651 '-json-output', describe_json_path
1652 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001653 gclient_utils.CheckCallAndFilter(cmd)
John Budorick0f7b2002018-01-19 15:46:17 -08001654 with open(describe_json_path) as f:
1655 describe_json = json.load(f)
1656 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1657 finally:
1658 gclient_utils.rmtree(tmpdir)
1659
1660 def status(self, options, args, file_list):
1661 pass
1662
1663 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001664 """Does nothing.
1665
1666 CIPD packages should be updated at the root by running
1667 `CipdRoot.run('update')`.
1668 """