blob: 574da8c3de777f963afe2b014078c044d16db877 [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.orgd5800f12009-11-12 20:03:43 +00004"""Gclient-specific SCM-specific operations."""
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00005
John Budorick0f7b2002018-01-19 15:46:17 -08006import collections
7import contextlib
borenet@google.comb2256212014-05-07 20:57:28 +00008import errno
John Budorick0f7b2002018-01-19 15:46:17 -08009import json
maruel@chromium.org754960e2009-09-21 12:31:05 +000010import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000011import os
Tomasz Wiszkowskid4e66882021-08-19 21:35:09 +000012import platform
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000013import posixpath
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import re
Gavin Mak65c49b12023-08-24 18:06:42 +000015import shutil
maruel@chromium.org90541732011-04-01 17:54:18 +000016import sys
ilevy@chromium.org3534aa52013-07-20 01:58:08 +000017import tempfile
John Budorick0f7b2002018-01-19 15:46:17 -080018import threading
zty@chromium.org6279e8a2014-02-13 01:45:25 +000019import traceback
Raul Tambreb946b232019-03-26 14:48:46 +000020
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000021import gclient_utils
Ravi Mistryecda7822022-02-28 16:22:20 +000022import gerrit_util
szager@chromium.org848fd492014-04-09 19:06:44 +000023import git_cache
Josip Sokcevic7958e302023-03-01 23:02:21 +000024import scm
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000025import subprocess2
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000026
Mike Frysinger124bb8e2023-09-06 05:48:55 +000027# TODO: Should fix these warnings.
28# pylint: disable=line-too-long
29
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000030
smutae7ea312016-07-18 11:59:41 -070031class NoUsableRevError(gclient_utils.Error):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000032 """Raised if requested revision isn't found in checkout."""
smutae7ea312016-07-18 11:59:41 -070033
34
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000035class DiffFiltererWrapper(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000036 """Simple base class which tracks which file is being diffed and
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000037 replaces instances of its file name in the original and
agable41e3a6c2016-10-20 11:36:56 -070038 working copy lines of the git diff output."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +000039 index_string = None
40 original_prefix = "--- "
41 working_prefix = "+++ "
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000042
Mike Frysinger124bb8e2023-09-06 05:48:55 +000043 def __init__(self, relpath, print_func):
44 # Note that we always use '/' as the path separator to be
45 # consistent with cygwin-style output on Windows
46 self._relpath = relpath.replace("\\", "/")
47 self._current_file = None
48 self._print_func = print_func
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000049
Mike Frysinger124bb8e2023-09-06 05:48:55 +000050 def SetCurrentFile(self, current_file):
51 self._current_file = current_file
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000052
Mike Frysinger124bb8e2023-09-06 05:48:55 +000053 @property
54 def _replacement_file(self):
55 return posixpath.join(self._relpath, self._current_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000056
Mike Frysinger124bb8e2023-09-06 05:48:55 +000057 def _Replace(self, line):
58 return line.replace(self._current_file, self._replacement_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000059
Mike Frysinger124bb8e2023-09-06 05:48:55 +000060 def Filter(self, line):
61 if (line.startswith(self.index_string)):
62 self.SetCurrentFile(line[len(self.index_string):])
63 line = self._Replace(line)
64 else:
65 if (line.startswith(self.original_prefix)
66 or line.startswith(self.working_prefix)):
67 line = self._Replace(line)
68 self._print_func(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000069
70
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000071class GitDiffFilterer(DiffFiltererWrapper):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000072 index_string = "diff --git "
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000073
Mike Frysinger124bb8e2023-09-06 05:48:55 +000074 def SetCurrentFile(self, current_file):
75 # Get filename by parsing "a/<filename> b/<filename>"
76 self._current_file = current_file[:(len(current_file) / 2)][2:]
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000077
Mike Frysinger124bb8e2023-09-06 05:48:55 +000078 def _Replace(self, line):
79 return re.sub("[a|b]/" + self._current_file, self._replacement_file,
80 line)
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000081
82
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000083# SCMWrapper base class
84
Mike Frysinger124bb8e2023-09-06 05:48:55 +000085
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000086class SCMWrapper(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000087 """Add necessary glue between all the supported SCM.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000088
msb@chromium.orgd6504212010-01-13 17:34:31 +000089 This is the abstraction layer to bind to different SCM.
90 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +000091 def __init__(self,
92 url=None,
93 root_dir=None,
94 relpath=None,
95 out_fh=None,
96 out_cb=None,
97 print_outbuf=False):
98 self.url = url
99 self._root_dir = root_dir
100 if self._root_dir:
101 self._root_dir = self._root_dir.replace('/', os.sep)
102 self.relpath = relpath
103 if self.relpath:
104 self.relpath = self.relpath.replace('/', os.sep)
105 if self.relpath and self._root_dir:
106 self.checkout_path = os.path.join(self._root_dir, self.relpath)
107 if out_fh is None:
108 out_fh = sys.stdout
109 self.out_fh = out_fh
110 self.out_cb = out_cb
111 self.print_outbuf = print_outbuf
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000112
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000113 def Print(self, *args, **kwargs):
114 kwargs.setdefault('file', self.out_fh)
115 if kwargs.pop('timestamp', True):
116 self.out_fh.write('[%s] ' % gclient_utils.Elapsed())
117 print(*args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000118
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000119 def RunCommand(self, command, options, args, file_list=None):
120 commands = [
121 'update', 'updatesingle', 'revert', 'revinfo', 'status', 'diff',
122 'pack', 'runhooks'
123 ]
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000124
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000125 if not command in commands:
126 raise gclient_utils.Error('Unknown command %s' % command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000127
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000128 if not command in dir(self):
129 raise gclient_utils.Error(
130 'Command %s not implemented in %s wrapper' %
131 (command, self.__class__.__name__))
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000132
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000133 return getattr(self, command)(options, args, file_list)
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000134
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000135 @staticmethod
136 def _get_first_remote_url(checkout_path):
137 log = scm.GIT.Capture(
138 ['config', '--local', '--get-regexp', r'remote.*.url'],
139 cwd=checkout_path)
140 # Get the second token of the first line of the log.
141 return log.splitlines()[0].split(' ', 1)[1]
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000142
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000143 def GetCacheMirror(self):
144 if getattr(self, 'cache_dir', None):
145 url, _ = gclient_utils.SplitUrlRevision(self.url)
146 return git_cache.Mirror(url)
147 return None
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000148
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000149 def GetActualRemoteURL(self, options):
150 """Attempt to determine the remote URL for this SCMWrapper."""
151 # Git
152 if os.path.exists(os.path.join(self.checkout_path, '.git')):
153 actual_remote_url = self._get_first_remote_url(self.checkout_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000154
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000155 mirror = self.GetCacheMirror()
156 # If the cache is used, obtain the actual remote URL from there.
157 if (mirror and mirror.exists() and mirror.mirror_path.replace(
158 '\\', '/') == actual_remote_url.replace('\\', '/')):
159 actual_remote_url = self._get_first_remote_url(
160 mirror.mirror_path)
161 return actual_remote_url
162 return None
borenet@google.com88d10082014-03-21 17:24:48 +0000163
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000164 def DoesRemoteURLMatch(self, options):
165 """Determine whether the remote URL of this checkout is the expected URL."""
166 if not os.path.exists(self.checkout_path):
167 # A checkout which doesn't exist can't be broken.
168 return True
borenet@google.com88d10082014-03-21 17:24:48 +0000169
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000170 actual_remote_url = self.GetActualRemoteURL(options)
171 if actual_remote_url:
172 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip(
173 '/') == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/'))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000174
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000175 # This may occur if the self.checkout_path exists but does not contain a
176 # valid git checkout.
177 return False
borenet@google.com88d10082014-03-21 17:24:48 +0000178
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000179 def _DeleteOrMove(self, force):
180 """Delete the checkout directory or move it out of the way.
borenet@google.comb09097a2014-04-09 19:09:08 +0000181
182 Args:
183 force: bool; if True, delete the directory. Otherwise, just move it.
184 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000185 if force and os.environ.get('CHROME_HEADLESS') == '1':
186 self.Print('_____ Conflicting directory found in %s. Removing.' %
187 self.checkout_path)
188 gclient_utils.AddWarning('Conflicting directory %s deleted.' %
189 self.checkout_path)
190 gclient_utils.rmtree(self.checkout_path)
191 else:
192 bad_scm_dir = os.path.join(self._root_dir, '_bad_scm',
193 os.path.dirname(self.relpath))
borenet@google.comb2256212014-05-07 20:57:28 +0000194
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000195 try:
196 os.makedirs(bad_scm_dir)
197 except OSError as e:
198 if e.errno != errno.EEXIST:
199 raise
borenet@google.comb2256212014-05-07 20:57:28 +0000200
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000201 dest_path = tempfile.mkdtemp(prefix=os.path.basename(self.relpath),
202 dir=bad_scm_dir)
203 self.Print(
204 '_____ Conflicting directory found in %s. Moving to %s.' %
205 (self.checkout_path, dest_path))
206 gclient_utils.AddWarning('Conflicting directory %s moved to %s.' %
207 (self.checkout_path, dest_path))
208 shutil.move(self.checkout_path, dest_path)
borenet@google.comb09097a2014-04-09 19:09:08 +0000209
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000210
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000211class GitWrapper(SCMWrapper):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000212 """Wrapper for Git"""
213 name = 'git'
214 remote = 'origin'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000215
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000216 @property
217 def cache_dir(self):
218 try:
219 return git_cache.Mirror.GetCachePath()
220 except RuntimeError:
221 return None
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000222
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000223 def __init__(self, url=None, *args, **kwargs):
224 """Removes 'git+' fake prefix from git URL."""
225 if url and (url.startswith('git+http://')
226 or url.startswith('git+https://')):
227 url = url[4:]
228 SCMWrapper.__init__(self, url, *args, **kwargs)
229 filter_kwargs = {'time_throttle': 1, 'out_fh': self.out_fh}
230 if self.out_cb:
231 filter_kwargs['predicate'] = self.out_cb
232 self.filter = gclient_utils.GitFilter(**filter_kwargs)
233 self._running_under_rosetta = None
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000234
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000235 def GetCheckoutRoot(self):
236 return scm.GIT.GetCheckoutRoot(self.checkout_path)
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000237
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000238 def GetRevisionDate(self, _revision):
239 """Returns the given revision's date in ISO-8601 format (which contains the
floitsch@google.comeaab7842011-04-28 09:07:58 +0000240 time zone)."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000241 # TODO(floitsch): get the time-stamp of the given revision and not just
242 # the time-stamp of the currently checked out revision.
243 return self._Capture(['log', '-n', '1', '--format=%ai'])
floitsch@google.comeaab7842011-04-28 09:07:58 +0000244
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000245 def _GetDiffFilenames(self, base):
246 """Returns the names of files modified since base."""
247 return self._Capture(
248 # Filter to remove base if it is None.
249 list(
250 filter(
251 bool,
252 ['-c', 'core.quotePath=false', 'diff', '--name-only', base])
253 )).split()
Aaron Gablef4068aa2017-12-12 15:14:09 -0800254
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000255 def diff(self, options, _args, _file_list):
256 _, revision = gclient_utils.SplitUrlRevision(self.url)
257 if not revision:
258 revision = 'refs/remotes/%s/main' % self.remote
259 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000260
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000261 def pack(self, _options, _args, _file_list):
262 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000263 repository.
264
265 The patch file is generated from a diff of the merge base of HEAD and
266 its upstream branch.
267 """
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800268 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000269 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
270 except subprocess2.CalledProcessError:
271 merge_base = []
272 gclient_utils.CheckCallAndFilter(['git', 'diff'] + merge_base,
273 cwd=self.checkout_path,
274 filter_fn=GitDiffFilterer(
275 self.relpath,
276 print_func=self.Print).Filter)
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800277
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000278 def _Scrub(self, target, options):
279 """Scrubs out all changes in the local repo, back to the state of target."""
280 quiet = []
281 if not options.verbose:
282 quiet = ['--quiet']
283 self._Run(['reset', '--hard', target] + quiet, options)
284 if options.force and options.delete_unversioned_trees:
285 # where `target` is a commit that contains both upper and lower case
286 # versions of the same file on a case insensitive filesystem, we are
287 # actually in a broken state here. The index will have both 'a' and
288 # 'A', but only one of them will exist on the disk. To progress, we
289 # delete everything that status thinks is modified.
290 output = self._Capture(
291 ['-c', 'core.quotePath=false', 'status', '--porcelain'],
292 strip=False)
293 for line in output.splitlines():
294 # --porcelain (v1) looks like:
295 # XY filename
296 try:
297 filename = line[3:]
298 self.Print('_____ Deleting residual after reset: %r.' %
299 filename)
300 gclient_utils.rm_file_or_tree(
301 os.path.join(self.checkout_path, filename))
302 except OSError:
303 pass
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000304
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000305 def _FetchAndReset(self, revision, file_list, options):
306 """Equivalent to git fetch; git reset."""
307 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000308
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000309 self._Fetch(options, prune=True, quiet=options.verbose)
310 self._Scrub(revision, options)
311 if file_list is not None:
312 files = self._Capture(['-c', 'core.quotePath=false',
313 'ls-files']).splitlines()
314 file_list.extend(
315 [os.path.join(self.checkout_path, f) for f in files])
szager@chromium.org8a139702014-06-20 15:55:01 +0000316
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000317 def _DisableHooks(self):
318 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
319 if not os.path.isdir(hook_dir):
320 return
321 for f in os.listdir(hook_dir):
322 if not f.endswith('.sample') and not f.endswith('.disabled'):
323 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
324 if os.path.exists(disabled_hook_path):
325 os.remove(disabled_hook_path)
326 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
327
328 def _maybe_break_locks(self, options):
329 """This removes all .lock files from this repo's .git directory, if the
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000330 user passed the --break_repo_locks command line flag.
331
332 In particular, this will cleanup index.lock files, as well as ref lock
333 files.
334 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000335 if options.break_repo_locks:
336 git_dir = os.path.join(self.checkout_path, '.git')
337 for path, _, filenames in os.walk(git_dir):
338 for filename in filenames:
339 if filename.endswith('.lock'):
340 to_break = os.path.join(path, filename)
341 self.Print('breaking lock: %s' % (to_break, ))
342 try:
343 os.remove(to_break)
344 except OSError as ex:
345 self.Print('FAILED to break lock: %s: %s' %
346 (to_break, ex))
347 raise
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000348
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000349 def _download_topics(self, patch_rev, googlesource_url):
350 """This method returns new patch_revs to process that have the same topic.
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000351
352 It does the following:
353 1. Finds the topic of the Gerrit change specified in the patch_rev.
354 2. Find all changes with that topic.
355 3. Append patch_rev of the changes with the same topic to the patch_revs
356 to process.
357 4. Returns the new patch_revs to process.
358 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000359 patch_revs_to_process = []
360 # Parse the patch_rev to extract the CL and patchset.
361 patch_rev_tokens = patch_rev.split('/')
362 change = patch_rev_tokens[-2]
363 # Parse the googlesource_url.
364 tokens = re.search('//(.+).googlesource.com/(.+?)(?:\.git)?$',
365 googlesource_url)
366 if not tokens or len(tokens.groups()) != 2:
367 # googlesource_url is not in the expected format.
368 return patch_revs_to_process
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000369
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000370 # parse the gerrit host and repo out of googlesource_url.
371 host, repo = tokens.groups()[:2]
372 gerrit_host_url = '%s-review.googlesource.com' % host
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000373
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000374 # 1. Find the topic of the Gerrit change specified in the patch_rev.
375 change_object = gerrit_util.GetChange(gerrit_host_url, change)
376 topic = change_object.get('topic')
377 if not topic:
378 # This change has no topic set.
379 return patch_revs_to_process
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000380
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000381 # 2. Find all changes with that topic.
382 changes_with_same_topic = gerrit_util.QueryChanges(
383 gerrit_host_url, [('topic', topic), ('status', 'open'),
384 ('repo', repo)],
385 o_params=['ALL_REVISIONS'])
386 for c in changes_with_same_topic:
387 if str(c['_number']) == change:
388 # This change is already in the patch_rev.
389 continue
390 self.Print('Found CL %d with the topic name %s' %
391 (c['_number'], topic))
392 # 3. Append patch_rev of the changes with the same topic to the
393 # patch_revs to process.
394 curr_rev = c['current_revision']
395 new_patch_rev = c['revisions'][curr_rev]['ref']
396 patch_revs_to_process.append(new_patch_rev)
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000397
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000398 # 4. Return the new patch_revs to process.
399 return patch_revs_to_process
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000400
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000401 def _ref_to_remote_ref(self, target_rev):
402 """Helper function for scm.GIT.RefToRemoteRef with error checking.
Kenneth Russell02e70b42023-08-07 22:09:29 +0000403
404 Joins the results of scm.GIT.RefToRemoteRef into a string, but raises a
405 comprehensible error if RefToRemoteRef fails.
406
407 Args:
408 target_rev: a ref somewhere under refs/.
409 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000410 tmp_ref = scm.GIT.RefToRemoteRef(target_rev, self.remote)
411 if not tmp_ref:
412 raise gclient_utils.Error(
413 'Failed to turn target revision %r in repo %r into remote ref' %
414 (target_rev, self.checkout_path))
415 return ''.join(tmp_ref)
Kenneth Russell02e70b42023-08-07 22:09:29 +0000416
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000417 def apply_patch_ref(self, patch_repo, patch_rev, target_rev, options,
418 file_list):
419 # type: (str, str, str, optparse.Values, Collection[str]) -> str
420 """Apply a patch on top of the revision we're synced at.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000421
Edward Lemur3acbc742019-05-30 17:57:35 +0000422 The patch ref is given by |patch_repo|@|patch_rev|.
423 |target_rev| is usually the branch that the |patch_rev| was uploaded against
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000424 (e.g. 'refs/heads/main'), but this is not required.
Edward Lemur3acbc742019-05-30 17:57:35 +0000425
426 We cherry-pick all commits reachable from |patch_rev| on top of the curret
427 HEAD, excluding those reachable from |target_rev|
428 (i.e. git cherry-pick target_rev..patch_rev).
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000429
430 Graphically, it looks like this:
431
Edward Lemur3acbc742019-05-30 17:57:35 +0000432 ... -> o -> [possibly already landed commits] -> target_rev
433 \
434 -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000435
Edward Lemur3acbc742019-05-30 17:57:35 +0000436 The final checkout state is then:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000437
Edward Lemur3acbc742019-05-30 17:57:35 +0000438 ... -> HEAD -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000439
440 After application, if |options.reset_patch_ref| is specified, we soft reset
Edward Lemur3acbc742019-05-30 17:57:35 +0000441 the cherry-picked changes, keeping them in git index only.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000442
443 Args:
Edward Lemur3acbc742019-05-30 17:57:35 +0000444 patch_repo: The patch origin.
445 e.g. 'https://foo.googlesource.com/bar'
446 patch_rev: The revision to patch.
447 e.g. 'refs/changes/1234/34/1'.
448 target_rev: The revision to use when finding the merge base.
449 Typically, the branch that the patch was uploaded against.
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000450 e.g. 'refs/heads/main' or 'refs/heads/infra/config'.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000451 options: The options passed to gclient.
452 file_list: A list where modified files will be appended.
453 """
454
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000455 # Abort any cherry-picks in progress.
Edward Lemur3acbc742019-05-30 17:57:35 +0000456 try:
Ravi Mistryecda7822022-02-28 16:22:20 +0000457 self._Capture(['cherry-pick', '--abort'])
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000458 except subprocess2.CalledProcessError:
Ravi Mistryecda7822022-02-28 16:22:20 +0000459 pass
Ravi Mistryecda7822022-02-28 16:22:20 +0000460
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000461 base_rev = self.revinfo(None, None, None)
Edward Lemurca7d8812018-07-24 17:42:45 +0000462
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000463 if not target_rev:
464 raise gclient_utils.Error(
465 'A target revision for the patch must be given')
Edward Lesmesc621b212018-03-21 20:26:56 -0400466
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000467 if target_rev.startswith(('refs/heads/', 'refs/branch-heads')):
468 # If |target_rev| is in refs/heads/** or refs/branch-heads/**, try
469 # first to find the corresponding remote ref for it, since
470 # |target_rev| might point to a local ref which is not up to date
471 # with the corresponding remote ref.
472 remote_ref = self._ref_to_remote_ref(target_rev)
473 self.Print('Trying the corresponding remote ref for %r: %r\n' %
474 (target_rev, remote_ref))
475 if scm.GIT.IsValidRevision(self.checkout_path, remote_ref):
476 # refs/remotes may need to be updated to cleanly cherry-pick
477 # changes. See https://crbug.com/1255178.
478 self._Capture(['fetch', '--no-tags', self.remote, target_rev])
479 target_rev = remote_ref
480 elif not scm.GIT.IsValidRevision(self.checkout_path, target_rev):
481 # Fetch |target_rev| if it's not already available.
482 url, _ = gclient_utils.SplitUrlRevision(self.url)
483 mirror = self._GetMirror(url, options, target_rev, target_rev)
484 if mirror:
485 rev_type = 'branch' if target_rev.startswith(
486 'refs/') else 'hash'
487 self._UpdateMirrorIfNotContains(mirror, options, rev_type,
488 target_rev)
489 self._Fetch(options, refspec=target_rev)
490
491 patch_revs_to_process = [patch_rev]
492
493 if hasattr(options, 'download_topics') and options.download_topics:
494 patch_revs_to_process_from_topics = self._download_topics(
495 patch_rev, self.url)
496 patch_revs_to_process.extend(patch_revs_to_process_from_topics)
497
498 self._Capture(['reset', '--hard'])
499 for pr in patch_revs_to_process:
500 self.Print('===Applying patch===')
501 self.Print('Revision to patch is %r @ %r.' % (patch_repo, pr))
502 self.Print('Current dir is %r' % self.checkout_path)
503 self._Capture(['fetch', '--no-tags', patch_repo, pr])
504 pr = self._Capture(['rev-parse', 'FETCH_HEAD'])
505
506 if not options.rebase_patch_ref:
507 self._Capture(['checkout', pr])
508 # Adjust base_rev to be the first parent of our checked out
509 # patch ref; This will allow us to correctly extend `file_list`,
510 # and will show the correct file-list to programs which do `git
511 # diff --cached` expecting to see the patch diff.
512 base_rev = self._Capture(['rev-parse', pr + '~'])
513 else:
514 self.Print('Will cherrypick %r .. %r on top of %r.' %
515 (target_rev, pr, base_rev))
516 try:
517 if scm.GIT.IsAncestor(pr,
518 target_rev,
519 cwd=self.checkout_path):
520 if len(patch_revs_to_process) > 1:
521 # If there are multiple patch_revs_to_process then
522 # we do not want want to invalidate a previous patch
523 # so throw an error.
524 raise gclient_utils.Error(
525 'patch_rev %s is an ancestor of target_rev %s. This '
526 'situation is unsupported when we need to apply multiple '
527 'patch_revs: %s' %
528 (pr, target_rev, patch_revs_to_process))
529 # If |patch_rev| is an ancestor of |target_rev|, check
530 # it out.
531 self._Capture(['checkout', pr])
532 else:
533 # If a change was uploaded on top of another change,
534 # which has already landed, one of the commits in the
535 # cherry-pick range will be redundant, since it has
536 # already landed and its changes incorporated in the
537 # tree. We pass '--keep-redundant-commits' to ignore
538 # those changes.
539 self._Capture([
540 'cherry-pick', target_rev + '..' + pr,
541 '--keep-redundant-commits'
542 ])
543
544 except subprocess2.CalledProcessError as e:
545 self.Print('Failed to apply patch.')
546 self.Print('Revision to patch was %r @ %r.' %
547 (patch_repo, pr))
548 self.Print('Tried to cherrypick %r .. %r on top of %r.' %
549 (target_rev, pr, base_rev))
550 self.Print('Current dir is %r' % self.checkout_path)
551 self.Print('git returned non-zero exit status %s:\n%s' %
552 (e.returncode, e.stderr.decode('utf-8')))
553 # Print the current status so that developers know what
554 # changes caused the patch failure, since git cherry-pick
555 # doesn't show that information.
556 self.Print(self._Capture(['status']))
557 try:
558 self._Capture(['cherry-pick', '--abort'])
559 except subprocess2.CalledProcessError:
560 pass
561 raise
562
563 if file_list is not None:
564 file_list.extend(self._GetDiffFilenames(base_rev))
565
566 latest_commit = self.revinfo(None, None, None)
567 if options.reset_patch_ref:
568 self._Capture(['reset', '--soft', base_rev])
569 return latest_commit
570
571 def check_diff(self, previous_commit, files=None):
572 # type: (str, Optional[List[str]]) -> bool
573 """Check if a diff exists between the current commit and `previous_commit`.
Joanna Wang5a7c8242022-07-01 19:09:00 +0000574
575 Returns True if there were diffs or if an error was encountered.
576 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000577 cmd = ['diff', previous_commit, '--quiet']
578 if files:
579 cmd += ['--'] + files
580 try:
581 self._Capture(cmd)
582 return False
583 except subprocess2.CalledProcessError as e:
584 # git diff --quiet exits with 1 if there were diffs.
585 if e.returncode != 1:
586 self.Print('git returned non-zero exit status %s:\n%s' %
587 (e.returncode, e.stderr.decode('utf-8')))
588 return True
Joanna Wang5a7c8242022-07-01 19:09:00 +0000589
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000590 def set_config(f):
591 def wrapper(*args):
592 return_val = f(*args)
593 if os.path.exists(os.path.join(args[0].checkout_path, '.git')):
594 # If diff.ignoreSubmodules is not already set, set it to `all`.
595 config = subprocess2.capture(['git', 'config', '-l'],
596 cwd=args[0].checkout_path).decode(
597 'utf-8').strip().splitlines()
598 if 'diff.ignoresubmodules=dirty' not in config:
599 subprocess2.capture(
600 ['git', 'config', 'diff.ignoreSubmodules', 'dirty'],
601 cwd=args[0].checkout_path)
602 if 'fetch.recursesubmodules=off' not in config:
603 subprocess2.capture(
604 ['git', 'config', 'fetch.recurseSubmodules', 'off'],
605 cwd=args[0].checkout_path)
Josip Sokcevic3b9212b2023-09-18 19:26:26 +0000606 if 'push.recursesubmodules=off' not in config:
607 # The default is off, but if user sets submodules.recurse to
608 # on, this becomes on too. We never want to push submodules
609 # for gclient managed repositories. Push, even if a no-op,
610 # will increase `git cl upload` latency.
611 subprocess2.capture(
612 ['git', 'config', 'push.recurseSubmodules', 'off'],
613 cwd=args[0].checkout_path)
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000614 return return_val
Joanna Wange1753f62023-06-26 14:32:43 +0000615
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000616 return wrapper
Joanna Wange1753f62023-06-26 14:32:43 +0000617
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000618 @set_config
619 def update(self, options, args, file_list):
620 """Runs git to update or transparently checkout the working copy.
msb@chromium.orge28e4982009-09-25 20:51:45 +0000621
622 All updated files will be appended to file_list.
623
624 Raises:
625 Error: if can't get URL for relative path.
626 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000627 if args:
628 raise gclient_utils.Error("Unsupported argument(s): %s" %
629 ",".join(args))
msb@chromium.orge28e4982009-09-25 20:51:45 +0000630
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000631 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
632 revision = deps_revision
633 managed = True
634 if options.revision:
635 # Override the revision number.
636 revision = str(options.revision)
637 if revision == 'unmanaged':
638 # Check again for a revision in case an initial ref was specified
639 # in the url, for example bla.git@refs/heads/custombranch
640 revision = deps_revision
641 managed = False
642 if not revision:
643 # If a dependency is not pinned, track the default remote branch.
644 revision = scm.GIT.GetRemoteHeadRef(self.checkout_path, self.url,
645 self.remote)
646 if revision.startswith('origin/'):
647 revision = 'refs/remotes/' + revision
msb@chromium.orge28e4982009-09-25 20:51:45 +0000648
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000649 if managed and platform.system() == 'Windows':
650 self._DisableHooks()
szager@chromium.org8a139702014-06-20 15:55:01 +0000651
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000652 printed_path = False
653 verbose = []
654 if options.verbose:
655 self.Print('_____ %s at %s' % (self.relpath, revision),
656 timestamp=False)
657 verbose = ['--verbose']
658 printed_path = True
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000659
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000660 revision_ref = revision
661 if ':' in revision:
662 revision_ref, _, revision = revision.partition(':')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000663
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000664 if revision_ref.startswith('refs/branch-heads'):
665 options.with_branch_heads = True
Edward Lesmes8073a502020-04-15 02:11:14 +0000666
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000667 mirror = self._GetMirror(url, options, revision, revision_ref)
668 if mirror:
669 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000670
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000671 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
672 if remote_ref:
673 # Rewrite remote refs to their local equivalents.
674 revision = ''.join(remote_ref)
675 rev_type = "branch"
676 elif revision.startswith('refs/'):
677 # Local branch? We probably don't want to support, since DEPS should
678 # always specify branches as they are in the upstream repo.
679 rev_type = "branch"
Aravind Vasudevancf465852023-03-29 16:47:12 +0000680 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000681 # hash is also a tag, only make a distinction at checkout
682 rev_type = "hash"
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000683
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000684 # If we are going to introduce a new project, there is a possibility
685 # that we are syncing back to a state where the project was originally a
686 # sub-project rolled by DEPS (realistic case: crossing the Blink merge
687 # point syncing backwards, when Blink was a DEPS entry and not part of
688 # src.git). In such case, we might have a backup of the former .git
689 # folder, which can be used to avoid re-fetching the entire repo again
690 # (useful for bisects).
691 backup_dir = self.GetGitBackupDirPath()
692 target_dir = os.path.join(self.checkout_path, '.git')
693 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
694 gclient_utils.safe_makedirs(self.checkout_path)
695 os.rename(backup_dir, target_dir)
696 # Reset to a clean state
697 self._Scrub('HEAD', options)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000698
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000699 if (not os.path.exists(self.checkout_path) or
700 (os.path.isdir(self.checkout_path)
701 and not os.path.exists(os.path.join(self.checkout_path, '.git')))):
702 if mirror:
703 self._UpdateMirrorIfNotContains(mirror, options, rev_type,
704 revision)
705 try:
706 self._Clone(revision, url, options)
707 except subprocess2.CalledProcessError as e:
708 logging.warning('Clone failed due to: %s', e)
709 self._DeleteOrMove(options.force)
710 self._Clone(revision, url, options)
711 if file_list is not None:
712 files = self._Capture(
713 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
714 file_list.extend(
715 [os.path.join(self.checkout_path, f) for f in files])
716 if mirror:
717 self._Capture(
718 ['remote', 'set-url', '--push', 'origin', mirror.url])
719 if not verbose:
720 # Make the output a little prettier. It's nice to have some
721 # whitespace between projects when cloning.
722 self.Print('')
723 return self._Capture(['rev-parse', '--verify', 'HEAD'])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000724
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000725 if mirror:
726 self._Capture(['remote', 'set-url', '--push', 'origin', mirror.url])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000727
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000728 if not managed:
729 self._SetFetchConfig(options)
730 self.Print('________ unmanaged solution; skipping %s' %
731 self.relpath)
732 return self._Capture(['rev-parse', '--verify', 'HEAD'])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000733
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000734 self._maybe_break_locks(options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000735
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000736 if mirror:
737 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
Edward Lemur579c9862018-07-13 23:17:51 +0000738
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000739 # See if the url has changed (the unittests use git://foo for the url,
740 # let that through).
741 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
742 return_early = False
743 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
744 # unit test pass. (and update the comment above)
745 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
746 # This allows devs to use experimental repos which have a different url
747 # but whose branch(s) are the same as official repos.
Joanna Wang4af28182023-09-22 18:16:27 +0000748 strp_url = url[:-4] if url.endswith('.git') else url
749 strp_current_url = current_url[:-4] if current_url.endswith(
750 '.git') else current_url
751 if (strp_current_url.rstrip('/') != strp_url.rstrip('/')
752 and url != 'git://foo' and
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000753 subprocess2.capture([
754 'git', 'config',
755 'remote.%s.gclient-auto-fix-url' % self.remote
756 ],
757 cwd=self.checkout_path).strip() != 'False'):
758 self.Print('_____ switching %s from %s to new upstream %s' %
759 (self.relpath, current_url, url))
760 if not (options.force or options.reset):
761 # Make sure it's clean
762 self._CheckClean(revision)
763 # Switch over to the new upstream
764 self._Run(['remote', 'set-url', self.remote, url], options)
765 if mirror:
766 if git_cache.Mirror.CacheDirToUrl(current_url.rstrip(
767 '/')) == git_cache.Mirror.CacheDirToUrl(
768 url.rstrip('/')):
769 # Reset alternates when the cache dir is updated.
770 with open(
771 os.path.join(self.checkout_path, '.git', 'objects',
772 'info', 'alternates'), 'w') as fh:
773 fh.write(os.path.join(url, 'objects'))
774 else:
775 # Because we use Git alternatives, our existing repository
776 # is not self-contained. It's possible that new git
777 # alternative doesn't have all necessary objects that the
778 # current repository needs. Instead of blindly hoping that
779 # new alternative contains all necessary objects, keep the
780 # old alternative and just append a new one on top of it.
781 with open(
782 os.path.join(self.checkout_path, '.git', 'objects',
783 'info', 'alternates'), 'a') as fh:
784 fh.write("\n" + os.path.join(url, 'objects'))
785 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
786 self._FetchAndReset(revision, file_list, options)
Michael Spang73fac912019-03-08 18:44:19 +0000787
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000788 return_early = True
789 else:
790 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
791
792 if return_early:
793 return self._Capture(['rev-parse', '--verify', 'HEAD'])
794
795 cur_branch = self._GetCurrentBranch()
796
797 # Cases:
798 # 0) HEAD is detached. Probably from our initial clone.
799 # - make sure HEAD is contained by a named ref, then update.
800 # Cases 1-4. HEAD is a branch.
801 # 1) current branch is not tracking a remote branch
802 # - try to rebase onto the new hash or branch
803 # 2) current branch is tracking a remote branch with local committed
804 # changes, but the DEPS file switched to point to a hash
805 # - rebase those changes on top of the hash
806 # 3) current branch is tracking a remote branch w/or w/out changes, and
807 # no DEPS switch
808 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
809 # 4) current branch is tracking a remote branch, but DEPS switches to a
810 # different remote branch, and a) current branch has no local changes,
811 # and --force: - checkout new branch b) current branch has local
812 # changes, and --force and --reset: - checkout new branch c) otherwise
813 # exit
814
815 # GetUpstreamBranch returns something like 'refs/remotes/origin/main'
816 # for a tracking branch or 'main' if not a tracking branch (it's based
817 # on a specific rev/hash) or it returns None if it couldn't find an
818 # upstream
819 if cur_branch is None:
820 upstream_branch = None
821 current_type = "detached"
822 logging.debug("Detached HEAD")
823 else:
824 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
825 if not upstream_branch or not upstream_branch.startswith(
826 'refs/remotes'):
827 current_type = "hash"
828 logging.debug(
829 "Current branch is not tracking an upstream (remote)"
830 " branch.")
831 elif upstream_branch.startswith('refs/remotes'):
832 current_type = "branch"
833 else:
834 raise gclient_utils.Error('Invalid Upstream: %s' %
835 upstream_branch)
836
837 self._SetFetchConfig(options)
838
839 # Fetch upstream if we don't already have |revision|.
840 if not scm.GIT.IsValidRevision(
841 self.checkout_path, revision, sha_only=True):
842 self._Fetch(options, prune=options.force)
843
844 if not scm.GIT.IsValidRevision(
845 self.checkout_path, revision, sha_only=True):
846 # Update the remotes first so we have all the refs.
847 remote_output = scm.GIT.Capture(['remote'] + verbose +
848 ['update'],
849 cwd=self.checkout_path)
850 if verbose:
851 self.Print(remote_output)
852
853 revision = self._AutoFetchRef(options, revision)
854
855 # This is a big hammer, debatable if it should even be here...
856 if options.force or options.reset:
857 target = 'HEAD'
858 if options.upstream and upstream_branch:
859 target = upstream_branch
860 self._Scrub(target, options)
861
862 if current_type == 'detached':
863 # case 0
864 # We just did a Scrub, this is as clean as it's going to get. In
865 # particular if HEAD is a commit that contains two versions of the
866 # same file on a case-insensitive filesystem (e.g. 'a' and 'A'),
867 # there's no way to actually "Clean" the checkout; that commit is
868 # uncheckoutable on this system. The best we can do is carry forward
869 # to the checkout step.
870 if not (options.force or options.reset):
871 self._CheckClean(revision)
872 self._CheckDetachedHead(revision, options)
873 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
874 self.Print('Up-to-date; skipping checkout.')
875 else:
876 # 'git checkout' may need to overwrite existing untracked files.
877 # Allow it only when nuclear options are enabled.
878 self._Checkout(
879 options,
880 revision,
881 force=(options.force and options.delete_unversioned_trees),
882 quiet=True,
883 )
884 if not printed_path:
885 self.Print('_____ %s at %s' % (self.relpath, revision),
886 timestamp=False)
887 elif current_type == 'hash':
888 # case 1
889 # Can't find a merge-base since we don't know our upstream. That
890 # makes this command VERY likely to produce a rebase failure. For
891 # now we assume origin is our upstream since that's what the old
892 # behavior was.
893 upstream_branch = self.remote
894 if options.revision or deps_revision:
895 upstream_branch = revision
896 self._AttemptRebase(upstream_branch,
897 file_list,
898 options,
899 printed_path=printed_path,
900 merge=options.merge)
901 printed_path = True
902 elif rev_type == 'hash':
903 # case 2
904 self._AttemptRebase(upstream_branch,
905 file_list,
906 options,
907 newbase=revision,
908 printed_path=printed_path,
909 merge=options.merge)
910 printed_path = True
911 elif remote_ref and ''.join(remote_ref) != upstream_branch:
912 # case 4
913 new_base = ''.join(remote_ref)
914 if not printed_path:
915 self.Print('_____ %s at %s' % (self.relpath, revision),
916 timestamp=False)
917 switch_error = (
918 "Could not switch upstream branch from %s to %s\n" %
919 (upstream_branch, new_base) +
920 "Please use --force or merge or rebase manually:\n" +
921 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
922 "OR git checkout -b <some new branch> %s" % new_base)
923 force_switch = False
924 if options.force:
925 try:
926 self._CheckClean(revision)
927 # case 4a
928 force_switch = True
929 except gclient_utils.Error as e:
930 if options.reset:
931 # case 4b
932 force_switch = True
933 else:
Philipp Thielf2449cd2023-10-20 17:27:25 +0000934 switch_error = '%s\n%s' % (str(e), switch_error)
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000935 if force_switch:
936 self.Print("Switching upstream branch from %s to %s" %
937 (upstream_branch, new_base))
938 switch_branch = 'gclient_' + remote_ref[1]
939 self._Capture(['branch', '-f', switch_branch, new_base])
940 self._Checkout(options, switch_branch, force=True, quiet=True)
941 else:
942 # case 4c
943 raise gclient_utils.Error(switch_error)
944 else:
945 # case 3 - the default case
946 rebase_files = self._GetDiffFilenames(upstream_branch)
947 if verbose:
948 self.Print('Trying fast-forward merge to branch : %s' %
949 upstream_branch)
950 try:
951 merge_args = ['merge']
952 if options.merge:
953 merge_args.append('--ff')
954 else:
955 merge_args.append('--ff-only')
956 merge_args.append(upstream_branch)
957 merge_output = self._Capture(merge_args)
958 except subprocess2.CalledProcessError as e:
959 rebase_files = []
960 if re.search(b'fatal: Not possible to fast-forward, aborting.',
961 e.stderr):
962 if not printed_path:
963 self.Print('_____ %s at %s' % (self.relpath, revision),
964 timestamp=False)
965 printed_path = True
966 while True:
967 if not options.auto_rebase:
968 try:
969 action = self._AskForData(
970 'Cannot %s, attempt to rebase? '
971 '(y)es / (q)uit / (s)kip : ' %
972 ('merge' if options.merge else
973 'fast-forward merge'), options)
974 except ValueError:
975 raise gclient_utils.Error('Invalid Character')
976 if options.auto_rebase or re.match(
977 r'yes|y', action, re.I):
978 self._AttemptRebase(upstream_branch,
979 rebase_files,
980 options,
981 printed_path=printed_path,
982 merge=False)
983 printed_path = True
984 break
985
986 if re.match(r'quit|q', action, re.I):
987 raise gclient_utils.Error(
988 "Can't fast-forward, please merge or "
989 "rebase manually.\n"
990 "cd %s && git " % self.checkout_path +
991 "rebase %s" % upstream_branch)
992
993 if re.match(r'skip|s', action, re.I):
994 self.Print('Skipping %s' % self.relpath)
995 return
996
997 self.Print('Input not recognized')
998 elif re.match(
999 b"error: Your local changes to '.*' would be "
1000 b"overwritten by merge. Aborting.\nPlease, commit your "
1001 b"changes or stash them before you can merge.\n",
1002 e.stderr):
1003 if not printed_path:
1004 self.Print('_____ %s at %s' % (self.relpath, revision),
1005 timestamp=False)
1006 printed_path = True
1007 raise gclient_utils.Error(e.stderr.decode('utf-8'))
1008 else:
1009 # Some other problem happened with the merge
1010 logging.error("Error during fast-forward merge in %s!" %
1011 self.relpath)
1012 self.Print(e.stderr.decode('utf-8'))
1013 raise
1014 else:
1015 # Fast-forward merge was successful
1016 if not re.match('Already up-to-date.', merge_output) or verbose:
1017 if not printed_path:
1018 self.Print('_____ %s at %s' % (self.relpath, revision),
1019 timestamp=False)
1020 printed_path = True
1021 self.Print(merge_output.strip())
1022 if not verbose:
1023 # Make the output a little prettier. It's nice to have
1024 # some whitespace between projects when syncing.
1025 self.Print('')
1026
1027 if file_list is not None:
1028 file_list.extend(
1029 [os.path.join(self.checkout_path, f) for f in rebase_files])
1030
1031 # If the rebase generated a conflict, abort and ask user to fix
1032 if self._IsRebasing():
1033 raise gclient_utils.Error(
1034 '\n____ %s at %s\n'
1035 '\nConflict while rebasing this branch.\n'
1036 'Fix the conflict and run gclient again.\n'
1037 'See man git-rebase for details.\n' % (self.relpath, revision))
1038
Michael Spang73fac912019-03-08 18:44:19 +00001039 if verbose:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001040 self.Print('Checked out revision %s' %
1041 self.revinfo(options, (), None),
agable83faed02016-10-24 14:37:10 -07001042 timestamp=False)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001043
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001044 # If --reset and --delete_unversioned_trees are specified, remove any
1045 # untracked directories.
1046 if options.reset and options.delete_unversioned_trees:
1047 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1
1048 # (the merge-base by default), so doesn't include untracked files.
1049 # So we use 'git ls-files --directory --others --exclude-standard'
1050 # here directly.
1051 paths = scm.GIT.Capture([
1052 '-c', 'core.quotePath=false', 'ls-files', '--directory',
1053 '--others', '--exclude-standard'
1054 ], self.checkout_path)
1055 for path in (p for p in paths.splitlines() if p.endswith('/')):
1056 full_path = os.path.join(self.checkout_path, path)
1057 if not os.path.islink(full_path):
1058 self.Print('_____ removing unversioned directory %s' % path)
1059 gclient_utils.rmtree(full_path)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001060
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001061 return self._Capture(['rev-parse', '--verify', 'HEAD'])
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001062
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001063 def revert(self, options, _args, file_list):
1064 """Reverts local modifications.
msb@chromium.orge28e4982009-09-25 20:51:45 +00001065
1066 All reverted files will be appended to file_list.
1067 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001068 if not os.path.isdir(self.checkout_path):
1069 # revert won't work if the directory doesn't exist. It needs to
1070 # checkout instead.
1071 self.Print('_____ %s is missing, syncing instead' % self.relpath)
1072 # Don't reuse the args.
1073 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001074
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001075 default_rev = "refs/heads/main"
1076 if options.upstream:
1077 if self._GetCurrentBranch():
1078 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
1079 default_rev = upstream_branch or default_rev
1080 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
1081 if not deps_revision:
1082 deps_revision = default_rev
1083 if deps_revision.startswith('refs/heads/'):
1084 deps_revision = deps_revision.replace('refs/heads/',
1085 self.remote + '/')
1086 try:
1087 deps_revision = self.GetUsableRev(deps_revision, options)
1088 except NoUsableRevError as e:
1089 # If the DEPS entry's url and hash changed, try to update the
1090 # origin. See also http://crbug.com/520067.
1091 logging.warning(
1092 "Couldn't find usable revision, will retrying to update instead: %s",
Philipp Thielf2449cd2023-10-20 17:27:25 +00001093 str(e))
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001094 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001095
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001096 if file_list is not None:
1097 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001098
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001099 self._Scrub(deps_revision, options)
1100 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001101
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001102 if file_list is not None:
1103 file_list.extend(
1104 [os.path.join(self.checkout_path, f) for f in files])
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001105
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001106 def revinfo(self, _options, _args, _file_list):
1107 """Returns revision"""
1108 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +00001109
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001110 def runhooks(self, options, args, file_list):
1111 self.status(options, args, file_list)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001112
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001113 def status(self, options, _args, file_list):
1114 """Display status information."""
1115 if not os.path.isdir(self.checkout_path):
1116 self.Print('________ couldn\'t run status in %s:\n'
1117 'The directory does not exist.' % self.checkout_path)
1118 else:
1119 merge_base = []
1120 if self.url:
1121 _, base_rev = gclient_utils.SplitUrlRevision(self.url)
1122 if base_rev:
1123 if base_rev.startswith('refs/'):
1124 base_rev = self._ref_to_remote_ref(base_rev)
1125 merge_base = [base_rev]
1126 self._Run(['-c', 'core.quotePath=false', 'diff', '--name-status'] +
1127 merge_base,
1128 options,
1129 always_show_header=options.verbose)
1130 if file_list is not None:
1131 files = self._GetDiffFilenames(
1132 merge_base[0] if merge_base else None)
1133 file_list.extend(
1134 [os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +00001135
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001136 def GetUsableRev(self, rev, options):
1137 """Finds a useful revision for this repository."""
1138 sha1 = None
1139 if not os.path.isdir(self.checkout_path):
1140 raise NoUsableRevError(
1141 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -07001142
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001143 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1144 sha1 = rev
1145 else:
1146 # May exist in origin, but we don't have it yet, so fetch and look
1147 # again.
1148 self._Fetch(options)
1149 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1150 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001151
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001152 if not sha1:
1153 raise NoUsableRevError(
1154 'Hash %s does not appear to be a valid hash in this repo.' %
1155 rev)
smutae7ea312016-07-18 11:59:41 -07001156
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001157 return sha1
smutae7ea312016-07-18 11:59:41 -07001158
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001159 def GetGitBackupDirPath(self):
1160 """Returns the path where the .git folder for the current project can be
primiano@chromium.org1c127382015-02-17 11:15:40 +00001161 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001162 return os.path.join(self._root_dir,
1163 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
primiano@chromium.org1c127382015-02-17 11:15:40 +00001164
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001165 def _GetMirror(self, url, options, revision=None, revision_ref=None):
1166 """Get a git_cache.Mirror object for the argument url."""
1167 if not self.cache_dir:
1168 return None
1169 mirror_kwargs = {
1170 'print_func': self.filter,
1171 'refs': [],
1172 'commits': [],
1173 }
1174 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1175 mirror_kwargs['refs'].append('refs/branch-heads/*')
1176 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
1177 mirror_kwargs['refs'].append(revision_ref)
1178 if hasattr(options, 'with_tags') and options.with_tags:
1179 mirror_kwargs['refs'].append('refs/tags/*')
1180 elif revision_ref and revision_ref.startswith('refs/tags/'):
1181 mirror_kwargs['refs'].append(revision_ref)
1182 if revision and not revision.startswith('refs/'):
1183 mirror_kwargs['commits'].append(revision)
1184 return git_cache.Mirror(url, **mirror_kwargs)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001185
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001186 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
1187 """Update a git mirror by fetching the latest commits from the remote,
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001188 unless mirror already contains revision whose type is sha1 hash.
1189 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001190 if rev_type == 'hash' and mirror.contains_revision(revision):
1191 if options.verbose:
1192 self.Print('skipping mirror update, it has rev=%s already' %
1193 revision,
1194 timestamp=False)
1195 return
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001196
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001197 if getattr(options, 'shallow', False):
1198 depth = 10000
1199 else:
1200 depth = None
1201 mirror.populate(verbose=options.verbose,
1202 bootstrap=not getattr(options, 'no_bootstrap', False),
1203 depth=depth,
1204 lock_timeout=getattr(options, 'lock_timeout', 0))
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001205
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001206 def _Clone(self, revision, url, options):
1207 """Clone a git repository from the given URL.
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001208
Josip Sokcevic88cc0b82023-11-09 19:19:23 +00001209 Once we've cloned the repo, we checkout a working branch if the
1210 specified revision is a branch head. If it is a tag or a specific
1211 commit, then we leave HEAD detached as it makes future updates simpler
1212 -- in this case the user should first create a new branch or switch to
1213 an existing branch before making changes in the repo."""
Joanna Wang1a977bd2022-06-02 21:51:17 +00001214
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001215 if self.print_outbuf:
1216 print_stdout = True
1217 filter_fn = None
1218 else:
1219 print_stdout = False
1220 filter_fn = self.filter
Joanna Wang1a977bd2022-06-02 21:51:17 +00001221
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001222 if not options.verbose:
1223 # git clone doesn't seem to insert a newline properly before
1224 # printing to stdout
1225 self.Print('')
Joanna Wang1a977bd2022-06-02 21:51:17 +00001226
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001227 # If the parent directory does not exist, Git clone on Windows will not
1228 # create it, so we need to do it manually.
1229 parent_dir = os.path.dirname(self.checkout_path)
1230 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001231
Josip Sokcevic88cc0b82023-11-09 19:19:23 +00001232 if hasattr(options, 'no_history') and options.no_history:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001233 self._Run(['init', self.checkout_path], options, cwd=self._root_dir)
1234 self._Run(['remote', 'add', 'origin', url], options)
1235 revision = self._AutoFetchRef(options, revision, depth=1)
1236 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1237 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
1238 else:
1239 cfg = gclient_utils.DefaultIndexPackConfig(url)
1240 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
1241 if self.cache_dir:
1242 clone_cmd.append('--shared')
1243 if options.verbose:
1244 clone_cmd.append('--verbose')
1245 clone_cmd.append(url)
1246 tmp_dir = tempfile.mkdtemp(prefix='_gclient_%s_' %
1247 os.path.basename(self.checkout_path),
1248 dir=parent_dir)
1249 clone_cmd.append(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001250
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001251 try:
1252 self._Run(clone_cmd,
1253 options,
1254 cwd=self._root_dir,
1255 retry=True,
1256 print_stdout=print_stdout,
1257 filter_fn=filter_fn)
1258 logging.debug(
1259 'Cloned into temporary dir, moving to checkout_path')
1260 gclient_utils.safe_makedirs(self.checkout_path)
1261 gclient_utils.safe_rename(
1262 os.path.join(tmp_dir, '.git'),
1263 os.path.join(self.checkout_path, '.git'))
1264 except:
1265 traceback.print_exc(file=self.out_fh)
1266 raise
1267 finally:
1268 if os.listdir(tmp_dir):
1269 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
1270 gclient_utils.rmtree(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001271
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001272 self._SetFetchConfig(options)
1273 self._Fetch(options, prune=options.force)
1274 revision = self._AutoFetchRef(options, revision)
1275 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1276 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001277
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001278 if self._GetCurrentBranch() is None:
1279 # Squelch git's very verbose detached HEAD warning and use our own
1280 self.Print((
1281 'Checked out %s to a detached HEAD. Before making any commits\n'
1282 'in this repo, you should use \'git checkout <branch>\' to switch \n'
1283 'to an existing branch or use \'git checkout %s -b <branch>\' to\n'
1284 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001285
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001286 def _AskForData(self, prompt, options):
1287 if options.jobs > 1:
1288 self.Print(prompt)
1289 raise gclient_utils.Error("Background task requires input. Rerun "
1290 "gclient with --jobs=1 so that\n"
1291 "interaction is possible.")
1292 return gclient_utils.AskForData(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001293
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001294 def _AttemptRebase(self,
1295 upstream,
1296 files,
1297 options,
1298 newbase=None,
1299 branch=None,
1300 printed_path=False,
1301 merge=False):
1302 """Attempt to rebase onto either upstream or, if specified, newbase."""
1303 if files is not None:
1304 files.extend(self._GetDiffFilenames(upstream))
1305 revision = upstream
1306 if newbase:
1307 revision = newbase
1308 action = 'merge' if merge else 'rebase'
1309 if not printed_path:
1310 self.Print('_____ %s : Attempting %s onto %s...' %
1311 (self.relpath, action, revision))
1312 printed_path = True
1313 else:
1314 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001315
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001316 if merge:
1317 merge_output = self._Capture(['merge', revision])
1318 if options.verbose:
1319 self.Print(merge_output)
1320 return
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001321
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001322 # Build the rebase command here using the args
1323 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1324 rebase_cmd = ['rebase']
1325 if options.verbose:
1326 rebase_cmd.append('--verbose')
1327 if newbase:
1328 rebase_cmd.extend(['--onto', newbase])
1329 rebase_cmd.append(upstream)
1330 if branch:
1331 rebase_cmd.append(branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001332
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001333 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001334 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001335 except subprocess2.CalledProcessError as e:
1336 if (re.match(
1337 br'cannot rebase: you have unstaged changes', e.stderr
1338 ) or re.match(
1339 br'cannot rebase: your index contains uncommitted changes',
1340 e.stderr)):
1341 while True:
1342 rebase_action = self._AskForData(
1343 'Cannot rebase because of unstaged changes.\n'
1344 '\'git reset --hard HEAD\' ?\n'
1345 'WARNING: destroys any uncommitted work in your current branch!'
1346 ' (y)es / (q)uit / (s)how : ', options)
1347 if re.match(r'yes|y', rebase_action, re.I):
1348 self._Scrub('HEAD', options)
1349 # Should this be recursive?
1350 rebase_output = scm.GIT.Capture(rebase_cmd,
1351 cwd=self.checkout_path)
1352 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001353
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001354 if re.match(r'quit|q', rebase_action, re.I):
1355 raise gclient_utils.Error(
1356 "Please merge or rebase manually\n"
1357 "cd %s && git " % self.checkout_path +
1358 "%s" % ' '.join(rebase_cmd))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001359
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001360 if re.match(r'show|s', rebase_action, re.I):
1361 self.Print('%s' % e.stderr.decode('utf-8').strip())
1362 continue
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001363
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001364 gclient_utils.Error("Input not recognized")
1365 continue
1366 elif re.search(br'^CONFLICT', e.stdout, re.M):
1367 raise gclient_utils.Error(
1368 "Conflict while rebasing this branch.\n"
1369 "Fix the conflict and run gclient again.\n"
1370 "See 'man git-rebase' for details.\n")
1371 else:
1372 self.Print(e.stdout.decode('utf-8').strip())
1373 self.Print('Rebase produced error output:\n%s' %
1374 e.stderr.decode('utf-8').strip())
1375 raise gclient_utils.Error(
1376 "Unrecognized error, please merge or rebase "
1377 "manually.\ncd %s && git " % self.checkout_path +
1378 "%s" % ' '.join(rebase_cmd))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001379
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001380 self.Print(rebase_output.strip())
1381 if not options.verbose:
1382 # Make the output a little prettier. It's nice to have some
1383 # whitespace between projects when syncing.
1384 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001385
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001386 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
1387 # Special case handling if all 3 conditions are met:
1388 # * the mirros have recently changed, but deps destination remains same,
1389 # * the git histories of mirrors are conflicting. * git cache is used
1390 # This manifests itself in current checkout having invalid HEAD commit
1391 # on most git operations. Since git cache is used, just deleted the .git
1392 # folder, and re-create it by cloning.
1393 try:
1394 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1395 except subprocess2.CalledProcessError as e:
1396 if (b'fatal: bad object HEAD' in e.stderr and self.cache_dir
1397 and self.cache_dir in url):
1398 self.Print(
1399 ('Likely due to DEPS change with git cache_dir, '
1400 'the current commit points to no longer existing object.\n'
1401 '%s' % e))
1402 self._DeleteOrMove(options.force)
1403 self._Clone(revision, url, options)
1404 else:
1405 raise
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001406
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001407 def _IsRebasing(self):
1408 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git
1409 # doesn't have a plumbing command to determine whether a rebase is in
1410 # progress, so for now emualate (more-or-less) git-rebase.sh /
1411 # git-completion.bash
1412 g = os.path.join(self.checkout_path, '.git')
1413 return (os.path.isdir(os.path.join(g, "rebase-merge"))
1414 or os.path.isdir(os.path.join(g, "rebase-apply")))
msb@chromium.org786fb682010-06-02 15:16:23 +00001415
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001416 def _CheckClean(self, revision):
1417 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1418 if os.path.exists(lockfile):
1419 raise gclient_utils.Error(
1420 '\n____ %s at %s\n'
1421 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1422 '\tIf no git executable is running, then clean up %r and try again.\n'
1423 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001424
Aravind Vasudevan259774c2023-12-05 00:50:49 +00001425 # Ensure that the tree is clean.
1426 if scm.GIT.Capture([
1427 'status', '--porcelain', '--untracked-files=no',
1428 '--ignore-submodules'
1429 ],
1430 cwd=self.checkout_path):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001431 raise gclient_utils.Error(
1432 '\n____ %s at %s\n'
Aravind Vasudevan259774c2023-12-05 00:50:49 +00001433 '\tYou have uncommitted changes.\n'
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001434 '\tcd into %s, run git status to see changes,\n'
1435 '\tand commit, stash, or reset.\n' %
1436 (self.relpath, revision, self.relpath))
msb@chromium.org786fb682010-06-02 15:16:23 +00001437
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001438 def _CheckDetachedHead(self, revision, _options):
1439 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1440 # reference by a commit). If not, error out -- most likely a rebase is
1441 # in progress, try to detect so we can give a better error.
1442 try:
1443 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1444 cwd=self.checkout_path)
1445 except subprocess2.CalledProcessError:
1446 # Commit is not contained by any rev. See if the user is rebasing:
1447 if self._IsRebasing():
1448 # Punt to the user
1449 raise gclient_utils.Error(
1450 '\n____ %s at %s\n'
1451 '\tAlready in a conflict, i.e. (no branch).\n'
1452 '\tFix the conflict and run gclient again.\n'
1453 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1454 '\tSee man git-rebase for details.\n' %
1455 (self.relpath, revision))
1456 # Let's just save off the commit so we can proceed.
1457 name = ('saved-by-gclient-' +
1458 self._Capture(['rev-parse', '--short', 'HEAD']))
1459 self._Capture(['branch', '-f', name])
1460 self.Print(
1461 '_____ found an unreferenced commit and saved it as \'%s\'' %
1462 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001463
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001464 def _GetCurrentBranch(self):
1465 # Returns name of current branch or None for detached HEAD
1466 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
1467 if branch == 'HEAD':
1468 return None
1469 return branch
msb@chromium.org5bde4852009-12-14 16:47:12 +00001470
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001471 def _Capture(self, args, **kwargs):
1472 set_git_dir = 'cwd' not in kwargs
1473 kwargs.setdefault('cwd', self.checkout_path)
1474 kwargs.setdefault('stderr', subprocess2.PIPE)
1475 strip = kwargs.pop('strip', True)
1476 env = scm.GIT.ApplyEnvVars(kwargs)
1477 # If an explicit cwd isn't set, then default to the .git/ subdir so we
1478 # get stricter behavior. This can be useful in cases of slight
1479 # corruption -- we don't accidentally go corrupting parent git checks
1480 # too. See https://crbug.com/1000825 for an example.
1481 if set_git_dir:
Gavin Mak7f5b53f2023-09-07 18:13:01 +00001482 env.setdefault(
1483 'GIT_DIR',
1484 os.path.abspath(os.path.join(self.checkout_path, '.git')))
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001485 ret = subprocess2.check_output(['git'] + args, env=env,
1486 **kwargs).decode('utf-8')
1487 if strip:
1488 ret = ret.strip()
1489 self.Print('Finished running: %s %s' % ('git', ' '.join(args)))
1490 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001491
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001492 def _Checkout(self, options, ref, force=False, quiet=None):
1493 """Performs a 'git-checkout' operation.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001494
1495 Args:
1496 options: The configured option set
1497 ref: (str) The branch/commit to checkout
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001498 quiet: (bool/None) Whether or not the checkout should pass '--quiet'; if
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001499 'None', the behavior is inferred from 'options.verbose'.
1500 Returns: (str) The output of the checkout operation
1501 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001502 if quiet is None:
1503 quiet = (not options.verbose)
1504 checkout_args = ['checkout']
1505 if force:
1506 checkout_args.append('--force')
1507 if quiet:
1508 checkout_args.append('--quiet')
1509 checkout_args.append(ref)
1510 return self._Capture(checkout_args)
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001511
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001512 def _Fetch(self,
1513 options,
1514 remote=None,
1515 prune=False,
1516 quiet=False,
1517 refspec=None,
1518 depth=None):
1519 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
1520 # When updating, the ref is modified to be a remote ref .
1521 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1522 # Try to reverse that mapping.
1523 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1524 if original_ref:
1525 refspec = original_ref + ':' + refspec
1526 # When a mirror is configured, it only fetches
1527 # refs/{heads,branch-heads,tags}/*.
1528 # If asked to fetch other refs, we must fetch those directly from
1529 # the repository, and not from the mirror.
1530 if not original_ref.startswith(
1531 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1532 remote, _ = gclient_utils.SplitUrlRevision(self.url)
1533 fetch_cmd = cfg + [
1534 'fetch',
1535 remote or self.remote,
1536 ]
1537 if refspec:
1538 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001539
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001540 if prune:
1541 fetch_cmd.append('--prune')
1542 if options.verbose:
1543 fetch_cmd.append('--verbose')
1544 if not hasattr(options, 'with_tags') or not options.with_tags:
1545 fetch_cmd.append('--no-tags')
1546 elif quiet:
1547 fetch_cmd.append('--quiet')
1548 if depth:
1549 fetch_cmd.append('--depth=' + str(depth))
1550 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001551
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001552 def _SetFetchConfig(self, options):
1553 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001554 if requested."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001555 if options.force or options.reset:
1556 try:
1557 self._Run(
1558 ['config', '--unset-all',
1559 'remote.%s.fetch' % self.remote], options)
1560 self._Run([
1561 'config',
1562 'remote.%s.fetch' % self.remote,
1563 '+refs/heads/*:refs/remotes/%s/*' % self.remote
1564 ], options)
1565 except subprocess2.CalledProcessError as e:
1566 # If exit code was 5, it means we attempted to unset a config
1567 # that didn't exist. Ignore it.
1568 if e.returncode != 5:
1569 raise
1570 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1571 config_cmd = [
1572 'config',
1573 'remote.%s.fetch' % self.remote,
1574 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1575 '^\\+refs/branch-heads/\\*:.*$'
1576 ]
1577 self._Run(config_cmd, options)
1578 if hasattr(options, 'with_tags') and options.with_tags:
1579 config_cmd = [
1580 'config',
1581 'remote.%s.fetch' % self.remote, '+refs/tags/*:refs/tags/*',
1582 '^\\+refs/tags/\\*:.*$'
1583 ]
1584 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001585
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001586 def _AutoFetchRef(self, options, revision, depth=None):
1587 """Attempts to fetch |revision| if not available in local repo.
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001588
1589 Returns possibly updated revision."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001590 if not scm.GIT.IsValidRevision(self.checkout_path, revision):
1591 self._Fetch(options, refspec=revision, depth=depth)
1592 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1593 return revision
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001594
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001595 def _Run(self, args, options, **kwargs):
1596 # Disable 'unused options' warning | pylint: disable=unused-argument
1597 kwargs.setdefault('cwd', self.checkout_path)
1598 kwargs.setdefault('filter_fn', self.filter)
1599 kwargs.setdefault('show_header', True)
1600 env = scm.GIT.ApplyEnvVars(kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +00001601
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001602 cmd = ['git'] + args
1603 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001604
1605
1606class CipdPackage(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001607 """A representation of a single CIPD package."""
1608 def __init__(self, name, version, authority_for_subdir):
1609 self._authority_for_subdir = authority_for_subdir
1610 self._name = name
1611 self._version = version
John Budorick0f7b2002018-01-19 15:46:17 -08001612
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001613 @property
1614 def authority_for_subdir(self):
1615 """Whether this package has authority to act on behalf of its subdir.
John Budorick0f7b2002018-01-19 15:46:17 -08001616
1617 Some operations should only be performed once per subdirectory. A package
1618 that has authority for its subdirectory is the only package that should
1619 perform such operations.
1620
1621 Returns:
1622 bool; whether this package has subdir authority.
1623 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001624 return self._authority_for_subdir
John Budorick0f7b2002018-01-19 15:46:17 -08001625
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001626 @property
1627 def name(self):
1628 return self._name
John Budorick0f7b2002018-01-19 15:46:17 -08001629
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001630 @property
1631 def version(self):
1632 return self._version
John Budorick0f7b2002018-01-19 15:46:17 -08001633
1634
1635class CipdRoot(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001636 """A representation of a single CIPD root."""
Yiwei Zhang52353702023-09-18 15:53:52 +00001637 def __init__(self, root_dir, service_url, log_level=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001638 self._all_packages = set()
1639 self._mutator_lock = threading.Lock()
1640 self._packages_by_subdir = collections.defaultdict(list)
1641 self._root_dir = root_dir
1642 self._service_url = service_url
1643 self._resolved_packages = None
Yiwei Zhang52353702023-09-18 15:53:52 +00001644 self._log_level = log_level or 'error'
John Budorick0f7b2002018-01-19 15:46:17 -08001645
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001646 def add_package(self, subdir, package, version):
1647 """Adds a package to this CIPD root.
John Budorick0f7b2002018-01-19 15:46:17 -08001648
1649 As far as clients are concerned, this grants both root and subdir authority
1650 to packages arbitrarily. (The implementation grants root authority to the
1651 first package added and subdir authority to the first package added for that
1652 subdir, but clients should not depend on or expect that behavior.)
1653
1654 Args:
1655 subdir: str; relative path to where the package should be installed from
1656 the cipd root directory.
1657 package: str; the cipd package name.
1658 version: str; the cipd package version.
1659 Returns:
1660 CipdPackage; the package that was created and added to this root.
1661 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001662 with self._mutator_lock:
1663 cipd_package = CipdPackage(package, version,
1664 not self._packages_by_subdir[subdir])
1665 self._all_packages.add(cipd_package)
1666 self._packages_by_subdir[subdir].append(cipd_package)
1667 return cipd_package
John Budorick0f7b2002018-01-19 15:46:17 -08001668
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001669 def packages(self, subdir):
1670 """Get the list of configured packages for the given subdir."""
1671 return list(self._packages_by_subdir[subdir])
John Budorick0f7b2002018-01-19 15:46:17 -08001672
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001673 def resolved_packages(self):
1674 if not self._resolved_packages:
1675 self._resolved_packages = self.ensure_file_resolve()
1676 return self._resolved_packages
Dan Le Febvre456d0852023-05-24 23:43:40 +00001677
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001678 def clobber(self):
1679 """Remove the .cipd directory.
John Budorick0f7b2002018-01-19 15:46:17 -08001680
1681 This is useful for forcing ensure to redownload and reinitialize all
1682 packages.
1683 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001684 with self._mutator_lock:
1685 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
1686 try:
1687 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1688 except OSError:
1689 if os.path.exists(cipd_cache_dir):
1690 raise
John Budorick0f7b2002018-01-19 15:46:17 -08001691
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001692 def expand_package_name(self, package_name_string, **kwargs):
1693 """Run `cipd expand-package-name`.
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001694
1695 CIPD package names can be declared with placeholder variables
1696 such as '${platform}', this cmd will return the package name
1697 with the variables resolved. The resolution is based on the host
1698 the command is executing on.
1699 """
1700
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001701 kwargs.setdefault('stderr', subprocess2.PIPE)
1702 cmd = ['cipd', 'expand-package-name', package_name_string]
1703 ret = subprocess2.check_output(cmd, **kwargs).decode('utf-8')
1704 return ret.strip()
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001705
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001706 @contextlib.contextmanager
1707 def _create_ensure_file(self):
1708 try:
1709 contents = '$ParanoidMode CheckPresence\n'
1710 # TODO(crbug/1329641): Remove once cipd packages have been updated
1711 # to always be created in copy mode.
1712 contents += '$OverrideInstallMode copy\n\n'
1713 for subdir, packages in sorted(self._packages_by_subdir.items()):
1714 contents += '@Subdir %s\n' % subdir
1715 for package in sorted(packages, key=lambda p: p.name):
1716 contents += '%s %s\n' % (package.name, package.version)
1717 contents += '\n'
1718 ensure_file = None
1719 with tempfile.NamedTemporaryFile(suffix='.ensure',
1720 delete=False,
1721 mode='wb') as ensure_file:
1722 ensure_file.write(contents.encode('utf-8', 'replace'))
1723 yield ensure_file.name
1724 finally:
1725 if ensure_file is not None and os.path.exists(ensure_file.name):
1726 os.remove(ensure_file.name)
John Budorick0f7b2002018-01-19 15:46:17 -08001727
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001728 def ensure(self):
1729 """Run `cipd ensure`."""
1730 with self._mutator_lock:
1731 with self._create_ensure_file() as ensure_file:
1732 cmd = [
1733 'cipd',
1734 'ensure',
1735 '-log-level',
Yiwei Zhang52353702023-09-18 15:53:52 +00001736 self._log_level,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001737 '-root',
1738 self.root_dir,
1739 '-ensure-file',
1740 ensure_file,
1741 ]
1742 gclient_utils.CheckCallAndFilter(cmd,
1743 print_stdout=True,
1744 show_header=True)
John Budorick0f7b2002018-01-19 15:46:17 -08001745
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001746 @contextlib.contextmanager
1747 def _create_ensure_file_for_resolve(self):
1748 try:
1749 contents = '$ResolvedVersions %s\n' % os.devnull
1750 for subdir, packages in sorted(self._packages_by_subdir.items()):
1751 contents += '@Subdir %s\n' % subdir
1752 for package in sorted(packages, key=lambda p: p.name):
1753 contents += '%s %s\n' % (package.name, package.version)
1754 contents += '\n'
1755 ensure_file = None
1756 with tempfile.NamedTemporaryFile(suffix='.ensure',
1757 delete=False,
1758 mode='wb') as ensure_file:
1759 ensure_file.write(contents.encode('utf-8', 'replace'))
1760 yield ensure_file.name
1761 finally:
1762 if ensure_file is not None and os.path.exists(ensure_file.name):
1763 os.remove(ensure_file.name)
Dan Le Febvre456d0852023-05-24 23:43:40 +00001764
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001765 def _create_resolved_file(self):
1766 return tempfile.NamedTemporaryFile(suffix='.resolved',
1767 delete=False,
1768 mode='wb')
Dan Le Febvre456d0852023-05-24 23:43:40 +00001769
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001770 def ensure_file_resolve(self):
1771 """Run `cipd ensure-file-resolve`."""
1772 with self._mutator_lock:
1773 with self._create_resolved_file() as output_file:
1774 with self._create_ensure_file_for_resolve() as ensure_file:
1775 cmd = [
1776 'cipd',
1777 'ensure-file-resolve',
1778 '-log-level',
Yiwei Zhang52353702023-09-18 15:53:52 +00001779 self._log_level,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001780 '-ensure-file',
1781 ensure_file,
1782 '-json-output',
1783 output_file.name,
1784 ]
1785 gclient_utils.CheckCallAndFilter(cmd,
1786 print_stdout=False,
1787 show_header=False)
1788 with open(output_file.name) as f:
1789 output_json = json.load(f)
1790 return output_json.get('result', {})
Dan Le Febvre456d0852023-05-24 23:43:40 +00001791
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001792 def run(self, command):
1793 if command == 'update':
1794 self.ensure()
1795 elif command == 'revert':
1796 self.clobber()
1797 self.ensure()
John Budorickd3ba72b2018-03-20 12:27:42 -07001798
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001799 def created_package(self, package):
1800 """Checks whether this root created the given package.
John Budorick0f7b2002018-01-19 15:46:17 -08001801
1802 Args:
1803 package: CipdPackage; the package to check.
1804 Returns:
1805 bool; whether this root created the given package.
1806 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001807 return package in self._all_packages
John Budorick0f7b2002018-01-19 15:46:17 -08001808
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001809 @property
1810 def root_dir(self):
1811 return self._root_dir
John Budorick0f7b2002018-01-19 15:46:17 -08001812
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001813 @property
1814 def service_url(self):
1815 return self._service_url
John Budorick0f7b2002018-01-19 15:46:17 -08001816
1817
1818class CipdWrapper(SCMWrapper):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001819 """Wrapper for CIPD.
John Budorick0f7b2002018-01-19 15:46:17 -08001820
1821 Currently only supports chrome-infra-packages.appspot.com.
1822 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001823 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001824
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001825 def __init__(self,
1826 url=None,
1827 root_dir=None,
1828 relpath=None,
1829 out_fh=None,
1830 out_cb=None,
1831 root=None,
1832 package=None):
1833 super(CipdWrapper, self).__init__(url=url,
1834 root_dir=root_dir,
1835 relpath=relpath,
1836 out_fh=out_fh,
1837 out_cb=out_cb)
1838 assert root.created_package(package)
1839 self._package = package
1840 self._root = root
John Budorick0f7b2002018-01-19 15:46:17 -08001841
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001842 #override
1843 def GetCacheMirror(self):
1844 return None
John Budorick0f7b2002018-01-19 15:46:17 -08001845
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001846 #override
1847 def GetActualRemoteURL(self, options):
1848 return self._root.service_url
John Budorick0f7b2002018-01-19 15:46:17 -08001849
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001850 #override
1851 def DoesRemoteURLMatch(self, options):
1852 del options
1853 return True
John Budorick0f7b2002018-01-19 15:46:17 -08001854
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001855 def revert(self, options, args, file_list):
1856 """Does nothing.
John Budorickd3ba72b2018-03-20 12:27:42 -07001857
1858 CIPD packages should be reverted at the root by running
1859 `CipdRoot.run('revert')`.
1860 """
John Budorick0f7b2002018-01-19 15:46:17 -08001861
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001862 def diff(self, options, args, file_list):
1863 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001864
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001865 def pack(self, options, args, file_list):
1866 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001867
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001868 def revinfo(self, options, args, file_list):
1869 """Grab the instance ID."""
1870 try:
1871 tmpdir = tempfile.mkdtemp()
1872 # Attempt to get instance_id from the root resolved cache.
1873 # Resolved cache will not match on any CIPD packages with
1874 # variables such as ${platform}, they will fall back to
1875 # the slower method below.
1876 resolved = self._root.resolved_packages()
1877 if resolved:
1878 # CIPD uses POSIX separators across all platforms, so
1879 # replace any Windows separators.
1880 path_split = self.relpath.replace(os.sep, "/").split(":")
1881 if len(path_split) > 1:
1882 src_path, package = path_split
1883 if src_path in resolved:
1884 for resolved_package in resolved[src_path]:
1885 if package == resolved_package.get(
1886 'pin', {}).get('package'):
1887 return resolved_package.get(
1888 'pin', {}).get('instance_id')
Dan Le Febvre456d0852023-05-24 23:43:40 +00001889
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001890 describe_json_path = os.path.join(tmpdir, 'describe.json')
1891 cmd = [
1892 'cipd', 'describe', self._package.name, '-log-level', 'error',
1893 '-version', self._package.version, '-json-output',
1894 describe_json_path
1895 ]
1896 gclient_utils.CheckCallAndFilter(cmd)
1897 with open(describe_json_path) as f:
1898 describe_json = json.load(f)
1899 return describe_json.get('result', {}).get('pin',
1900 {}).get('instance_id')
1901 finally:
1902 gclient_utils.rmtree(tmpdir)
John Budorick0f7b2002018-01-19 15:46:17 -08001903
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001904 def status(self, options, args, file_list):
1905 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001906
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001907 def update(self, options, args, file_list):
1908 """Does nothing.
John Budorickd3ba72b2018-03-20 12:27:42 -07001909
1910 CIPD packages should be updated at the root by running
1911 `CipdRoot.run('update')`.
1912 """
Josip Sokcevic8fb358e2023-11-15 22:24:06 +00001913
1914
1915class CogWrapper(SCMWrapper):
1916 """Wrapper for Cog, all no-op."""
1917 name = 'cog'
1918
1919 def __init__(self):
1920 super(CogWrapper, self).__init__()
1921
1922 #override
1923 def GetCacheMirror(self):
1924 return None
1925
1926 #override
1927 def GetActualRemoteURL(self, options):
1928 return None
1929
1930 #override
1931 def DoesRemoteURLMatch(self, options):
1932 del options
1933 return True
1934
1935 def revert(self, options, args, file_list):
1936 pass
1937
1938 def diff(self, options, args, file_list):
1939 pass
1940
1941 def pack(self, options, args, file_list):
1942 pass
1943
1944 def revinfo(self, options, args, file_list):
1945 pass
1946
1947 def status(self, options, args, file_list):
1948 pass
1949
1950 def update(self, options, args, file_list):
1951 pass