blob: 602b81e3eb8e0eb119f089392e0b11311ea42bdd [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 _is_env_cog = None
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000217
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000218 @staticmethod
219 def _IsCog():
220 """Returns true if the env is cog"""
221 if not GitWrapper._is_env_cog:
222 GitWrapper._is_env_cog = any(
223 os.getcwd().startswith(x)
224 for x in ['/google/cog/cloud', '/google/src/cloud'])
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000225
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000226 return GitWrapper._is_env_cog
Aravind Vasudevan14e6d232022-06-02 20:42:16 +0000227
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000228 @property
229 def cache_dir(self):
230 try:
231 return git_cache.Mirror.GetCachePath()
232 except RuntimeError:
233 return None
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000234
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000235 def __init__(self, url=None, *args, **kwargs):
236 """Removes 'git+' fake prefix from git URL."""
237 if url and (url.startswith('git+http://')
238 or url.startswith('git+https://')):
239 url = url[4:]
240 SCMWrapper.__init__(self, url, *args, **kwargs)
241 filter_kwargs = {'time_throttle': 1, 'out_fh': self.out_fh}
242 if self.out_cb:
243 filter_kwargs['predicate'] = self.out_cb
244 self.filter = gclient_utils.GitFilter(**filter_kwargs)
245 self._running_under_rosetta = None
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000246
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000247 def GetCheckoutRoot(self):
248 return scm.GIT.GetCheckoutRoot(self.checkout_path)
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000249
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000250 def GetRevisionDate(self, _revision):
251 """Returns the given revision's date in ISO-8601 format (which contains the
floitsch@google.comeaab7842011-04-28 09:07:58 +0000252 time zone)."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000253 # TODO(floitsch): get the time-stamp of the given revision and not just
254 # the time-stamp of the currently checked out revision.
255 return self._Capture(['log', '-n', '1', '--format=%ai'])
floitsch@google.comeaab7842011-04-28 09:07:58 +0000256
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000257 def _GetDiffFilenames(self, base):
258 """Returns the names of files modified since base."""
259 return self._Capture(
260 # Filter to remove base if it is None.
261 list(
262 filter(
263 bool,
264 ['-c', 'core.quotePath=false', 'diff', '--name-only', base])
265 )).split()
Aaron Gablef4068aa2017-12-12 15:14:09 -0800266
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000267 def diff(self, options, _args, _file_list):
268 _, revision = gclient_utils.SplitUrlRevision(self.url)
269 if not revision:
270 revision = 'refs/remotes/%s/main' % self.remote
271 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000272
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000273 def pack(self, _options, _args, _file_list):
274 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000275 repository.
276
277 The patch file is generated from a diff of the merge base of HEAD and
278 its upstream branch.
279 """
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800280 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000281 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
282 except subprocess2.CalledProcessError:
283 merge_base = []
284 gclient_utils.CheckCallAndFilter(['git', 'diff'] + merge_base,
285 cwd=self.checkout_path,
286 filter_fn=GitDiffFilterer(
287 self.relpath,
288 print_func=self.Print).Filter)
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800289
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000290 def _Scrub(self, target, options):
291 """Scrubs out all changes in the local repo, back to the state of target."""
292 quiet = []
293 if not options.verbose:
294 quiet = ['--quiet']
295 self._Run(['reset', '--hard', target] + quiet, options)
296 if options.force and options.delete_unversioned_trees:
297 # where `target` is a commit that contains both upper and lower case
298 # versions of the same file on a case insensitive filesystem, we are
299 # actually in a broken state here. The index will have both 'a' and
300 # 'A', but only one of them will exist on the disk. To progress, we
301 # delete everything that status thinks is modified.
302 output = self._Capture(
303 ['-c', 'core.quotePath=false', 'status', '--porcelain'],
304 strip=False)
305 for line in output.splitlines():
306 # --porcelain (v1) looks like:
307 # XY filename
308 try:
309 filename = line[3:]
310 self.Print('_____ Deleting residual after reset: %r.' %
311 filename)
312 gclient_utils.rm_file_or_tree(
313 os.path.join(self.checkout_path, filename))
314 except OSError:
315 pass
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000316
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000317 def _FetchAndReset(self, revision, file_list, options):
318 """Equivalent to git fetch; git reset."""
319 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000320
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000321 self._Fetch(options, prune=True, quiet=options.verbose)
322 self._Scrub(revision, options)
323 if file_list is not None:
324 files = self._Capture(['-c', 'core.quotePath=false',
325 'ls-files']).splitlines()
326 file_list.extend(
327 [os.path.join(self.checkout_path, f) for f in files])
szager@chromium.org8a139702014-06-20 15:55:01 +0000328
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000329 def _DisableHooks(self):
330 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
331 if not os.path.isdir(hook_dir):
332 return
333 for f in os.listdir(hook_dir):
334 if not f.endswith('.sample') and not f.endswith('.disabled'):
335 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
336 if os.path.exists(disabled_hook_path):
337 os.remove(disabled_hook_path)
338 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
339
340 def _maybe_break_locks(self, options):
341 """This removes all .lock files from this repo's .git directory, if the
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000342 user passed the --break_repo_locks command line flag.
343
344 In particular, this will cleanup index.lock files, as well as ref lock
345 files.
346 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000347 if options.break_repo_locks:
348 git_dir = os.path.join(self.checkout_path, '.git')
349 for path, _, filenames in os.walk(git_dir):
350 for filename in filenames:
351 if filename.endswith('.lock'):
352 to_break = os.path.join(path, filename)
353 self.Print('breaking lock: %s' % (to_break, ))
354 try:
355 os.remove(to_break)
356 except OSError as ex:
357 self.Print('FAILED to break lock: %s: %s' %
358 (to_break, ex))
359 raise
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000360
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000361 def _download_topics(self, patch_rev, googlesource_url):
362 """This method returns new patch_revs to process that have the same topic.
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000363
364 It does the following:
365 1. Finds the topic of the Gerrit change specified in the patch_rev.
366 2. Find all changes with that topic.
367 3. Append patch_rev of the changes with the same topic to the patch_revs
368 to process.
369 4. Returns the new patch_revs to process.
370 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000371 patch_revs_to_process = []
372 # Parse the patch_rev to extract the CL and patchset.
373 patch_rev_tokens = patch_rev.split('/')
374 change = patch_rev_tokens[-2]
375 # Parse the googlesource_url.
376 tokens = re.search('//(.+).googlesource.com/(.+?)(?:\.git)?$',
377 googlesource_url)
378 if not tokens or len(tokens.groups()) != 2:
379 # googlesource_url is not in the expected format.
380 return patch_revs_to_process
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000381
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000382 # parse the gerrit host and repo out of googlesource_url.
383 host, repo = tokens.groups()[:2]
384 gerrit_host_url = '%s-review.googlesource.com' % host
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000385
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000386 # 1. Find the topic of the Gerrit change specified in the patch_rev.
387 change_object = gerrit_util.GetChange(gerrit_host_url, change)
388 topic = change_object.get('topic')
389 if not topic:
390 # This change has no topic set.
391 return patch_revs_to_process
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000392
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000393 # 2. Find all changes with that topic.
394 changes_with_same_topic = gerrit_util.QueryChanges(
395 gerrit_host_url, [('topic', topic), ('status', 'open'),
396 ('repo', repo)],
397 o_params=['ALL_REVISIONS'])
398 for c in changes_with_same_topic:
399 if str(c['_number']) == change:
400 # This change is already in the patch_rev.
401 continue
402 self.Print('Found CL %d with the topic name %s' %
403 (c['_number'], topic))
404 # 3. Append patch_rev of the changes with the same topic to the
405 # patch_revs to process.
406 curr_rev = c['current_revision']
407 new_patch_rev = c['revisions'][curr_rev]['ref']
408 patch_revs_to_process.append(new_patch_rev)
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000409
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000410 # 4. Return the new patch_revs to process.
411 return patch_revs_to_process
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000412
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000413 def _ref_to_remote_ref(self, target_rev):
414 """Helper function for scm.GIT.RefToRemoteRef with error checking.
Kenneth Russell02e70b42023-08-07 22:09:29 +0000415
416 Joins the results of scm.GIT.RefToRemoteRef into a string, but raises a
417 comprehensible error if RefToRemoteRef fails.
418
419 Args:
420 target_rev: a ref somewhere under refs/.
421 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000422 tmp_ref = scm.GIT.RefToRemoteRef(target_rev, self.remote)
423 if not tmp_ref:
424 raise gclient_utils.Error(
425 'Failed to turn target revision %r in repo %r into remote ref' %
426 (target_rev, self.checkout_path))
427 return ''.join(tmp_ref)
Kenneth Russell02e70b42023-08-07 22:09:29 +0000428
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000429 def apply_patch_ref(self, patch_repo, patch_rev, target_rev, options,
430 file_list):
431 # type: (str, str, str, optparse.Values, Collection[str]) -> str
432 """Apply a patch on top of the revision we're synced at.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000433
Edward Lemur3acbc742019-05-30 17:57:35 +0000434 The patch ref is given by |patch_repo|@|patch_rev|.
435 |target_rev| is usually the branch that the |patch_rev| was uploaded against
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000436 (e.g. 'refs/heads/main'), but this is not required.
Edward Lemur3acbc742019-05-30 17:57:35 +0000437
438 We cherry-pick all commits reachable from |patch_rev| on top of the curret
439 HEAD, excluding those reachable from |target_rev|
440 (i.e. git cherry-pick target_rev..patch_rev).
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000441
442 Graphically, it looks like this:
443
Edward Lemur3acbc742019-05-30 17:57:35 +0000444 ... -> o -> [possibly already landed commits] -> target_rev
445 \
446 -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000447
Edward Lemur3acbc742019-05-30 17:57:35 +0000448 The final checkout state is then:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000449
Edward Lemur3acbc742019-05-30 17:57:35 +0000450 ... -> HEAD -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000451
452 After application, if |options.reset_patch_ref| is specified, we soft reset
Edward Lemur3acbc742019-05-30 17:57:35 +0000453 the cherry-picked changes, keeping them in git index only.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000454
455 Args:
Edward Lemur3acbc742019-05-30 17:57:35 +0000456 patch_repo: The patch origin.
457 e.g. 'https://foo.googlesource.com/bar'
458 patch_rev: The revision to patch.
459 e.g. 'refs/changes/1234/34/1'.
460 target_rev: The revision to use when finding the merge base.
461 Typically, the branch that the patch was uploaded against.
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000462 e.g. 'refs/heads/main' or 'refs/heads/infra/config'.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000463 options: The options passed to gclient.
464 file_list: A list where modified files will be appended.
465 """
466
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000467 # Abort any cherry-picks in progress.
Edward Lemur3acbc742019-05-30 17:57:35 +0000468 try:
Ravi Mistryecda7822022-02-28 16:22:20 +0000469 self._Capture(['cherry-pick', '--abort'])
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000470 except subprocess2.CalledProcessError:
Ravi Mistryecda7822022-02-28 16:22:20 +0000471 pass
Ravi Mistryecda7822022-02-28 16:22:20 +0000472
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000473 base_rev = self.revinfo(None, None, None)
Edward Lemurca7d8812018-07-24 17:42:45 +0000474
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000475 if not target_rev:
476 raise gclient_utils.Error(
477 'A target revision for the patch must be given')
Edward Lesmesc621b212018-03-21 20:26:56 -0400478
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000479 if target_rev.startswith(('refs/heads/', 'refs/branch-heads')):
480 # If |target_rev| is in refs/heads/** or refs/branch-heads/**, try
481 # first to find the corresponding remote ref for it, since
482 # |target_rev| might point to a local ref which is not up to date
483 # with the corresponding remote ref.
484 remote_ref = self._ref_to_remote_ref(target_rev)
485 self.Print('Trying the corresponding remote ref for %r: %r\n' %
486 (target_rev, remote_ref))
487 if scm.GIT.IsValidRevision(self.checkout_path, remote_ref):
488 # refs/remotes may need to be updated to cleanly cherry-pick
489 # changes. See https://crbug.com/1255178.
490 self._Capture(['fetch', '--no-tags', self.remote, target_rev])
491 target_rev = remote_ref
492 elif not scm.GIT.IsValidRevision(self.checkout_path, target_rev):
493 # Fetch |target_rev| if it's not already available.
494 url, _ = gclient_utils.SplitUrlRevision(self.url)
495 mirror = self._GetMirror(url, options, target_rev, target_rev)
496 if mirror:
497 rev_type = 'branch' if target_rev.startswith(
498 'refs/') else 'hash'
499 self._UpdateMirrorIfNotContains(mirror, options, rev_type,
500 target_rev)
501 self._Fetch(options, refspec=target_rev)
502
503 patch_revs_to_process = [patch_rev]
504
505 if hasattr(options, 'download_topics') and options.download_topics:
506 patch_revs_to_process_from_topics = self._download_topics(
507 patch_rev, self.url)
508 patch_revs_to_process.extend(patch_revs_to_process_from_topics)
509
510 self._Capture(['reset', '--hard'])
511 for pr in patch_revs_to_process:
512 self.Print('===Applying patch===')
513 self.Print('Revision to patch is %r @ %r.' % (patch_repo, pr))
514 self.Print('Current dir is %r' % self.checkout_path)
515 self._Capture(['fetch', '--no-tags', patch_repo, pr])
516 pr = self._Capture(['rev-parse', 'FETCH_HEAD'])
517
518 if not options.rebase_patch_ref:
519 self._Capture(['checkout', pr])
520 # Adjust base_rev to be the first parent of our checked out
521 # patch ref; This will allow us to correctly extend `file_list`,
522 # and will show the correct file-list to programs which do `git
523 # diff --cached` expecting to see the patch diff.
524 base_rev = self._Capture(['rev-parse', pr + '~'])
525 else:
526 self.Print('Will cherrypick %r .. %r on top of %r.' %
527 (target_rev, pr, base_rev))
528 try:
529 if scm.GIT.IsAncestor(pr,
530 target_rev,
531 cwd=self.checkout_path):
532 if len(patch_revs_to_process) > 1:
533 # If there are multiple patch_revs_to_process then
534 # we do not want want to invalidate a previous patch
535 # so throw an error.
536 raise gclient_utils.Error(
537 'patch_rev %s is an ancestor of target_rev %s. This '
538 'situation is unsupported when we need to apply multiple '
539 'patch_revs: %s' %
540 (pr, target_rev, patch_revs_to_process))
541 # If |patch_rev| is an ancestor of |target_rev|, check
542 # it out.
543 self._Capture(['checkout', pr])
544 else:
545 # If a change was uploaded on top of another change,
546 # which has already landed, one of the commits in the
547 # cherry-pick range will be redundant, since it has
548 # already landed and its changes incorporated in the
549 # tree. We pass '--keep-redundant-commits' to ignore
550 # those changes.
551 self._Capture([
552 'cherry-pick', target_rev + '..' + pr,
553 '--keep-redundant-commits'
554 ])
555
556 except subprocess2.CalledProcessError as e:
557 self.Print('Failed to apply patch.')
558 self.Print('Revision to patch was %r @ %r.' %
559 (patch_repo, pr))
560 self.Print('Tried to cherrypick %r .. %r on top of %r.' %
561 (target_rev, pr, base_rev))
562 self.Print('Current dir is %r' % self.checkout_path)
563 self.Print('git returned non-zero exit status %s:\n%s' %
564 (e.returncode, e.stderr.decode('utf-8')))
565 # Print the current status so that developers know what
566 # changes caused the patch failure, since git cherry-pick
567 # doesn't show that information.
568 self.Print(self._Capture(['status']))
569 try:
570 self._Capture(['cherry-pick', '--abort'])
571 except subprocess2.CalledProcessError:
572 pass
573 raise
574
575 if file_list is not None:
576 file_list.extend(self._GetDiffFilenames(base_rev))
577
578 latest_commit = self.revinfo(None, None, None)
579 if options.reset_patch_ref:
580 self._Capture(['reset', '--soft', base_rev])
581 return latest_commit
582
583 def check_diff(self, previous_commit, files=None):
584 # type: (str, Optional[List[str]]) -> bool
585 """Check if a diff exists between the current commit and `previous_commit`.
Joanna Wang5a7c8242022-07-01 19:09:00 +0000586
587 Returns True if there were diffs or if an error was encountered.
588 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000589 cmd = ['diff', previous_commit, '--quiet']
590 if files:
591 cmd += ['--'] + files
592 try:
593 self._Capture(cmd)
594 return False
595 except subprocess2.CalledProcessError as e:
596 # git diff --quiet exits with 1 if there were diffs.
597 if e.returncode != 1:
598 self.Print('git returned non-zero exit status %s:\n%s' %
599 (e.returncode, e.stderr.decode('utf-8')))
600 return True
Joanna Wang5a7c8242022-07-01 19:09:00 +0000601
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000602 def set_config(f):
603 def wrapper(*args):
604 return_val = f(*args)
605 if os.path.exists(os.path.join(args[0].checkout_path, '.git')):
606 # If diff.ignoreSubmodules is not already set, set it to `all`.
607 config = subprocess2.capture(['git', 'config', '-l'],
608 cwd=args[0].checkout_path).decode(
609 'utf-8').strip().splitlines()
610 if 'diff.ignoresubmodules=dirty' not in config:
611 subprocess2.capture(
612 ['git', 'config', 'diff.ignoreSubmodules', 'dirty'],
613 cwd=args[0].checkout_path)
614 if 'fetch.recursesubmodules=off' not in config:
615 subprocess2.capture(
616 ['git', 'config', 'fetch.recurseSubmodules', 'off'],
617 cwd=args[0].checkout_path)
Josip Sokcevic3b9212b2023-09-18 19:26:26 +0000618 if 'push.recursesubmodules=off' not in config:
619 # The default is off, but if user sets submodules.recurse to
620 # on, this becomes on too. We never want to push submodules
621 # for gclient managed repositories. Push, even if a no-op,
622 # will increase `git cl upload` latency.
623 subprocess2.capture(
624 ['git', 'config', 'push.recurseSubmodules', 'off'],
625 cwd=args[0].checkout_path)
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000626 return return_val
Joanna Wange1753f62023-06-26 14:32:43 +0000627
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000628 return wrapper
Joanna Wange1753f62023-06-26 14:32:43 +0000629
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000630 @set_config
631 def update(self, options, args, file_list):
632 """Runs git to update or transparently checkout the working copy.
msb@chromium.orge28e4982009-09-25 20:51:45 +0000633
634 All updated files will be appended to file_list.
635
636 Raises:
637 Error: if can't get URL for relative path.
638 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000639 if args:
640 raise gclient_utils.Error("Unsupported argument(s): %s" %
641 ",".join(args))
msb@chromium.orge28e4982009-09-25 20:51:45 +0000642
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000643 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000644
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000645 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
646 revision = deps_revision
647 managed = True
648 if options.revision:
649 # Override the revision number.
650 revision = str(options.revision)
651 if revision == 'unmanaged':
652 # Check again for a revision in case an initial ref was specified
653 # in the url, for example bla.git@refs/heads/custombranch
654 revision = deps_revision
655 managed = False
656 if not revision:
657 # If a dependency is not pinned, track the default remote branch.
658 revision = scm.GIT.GetRemoteHeadRef(self.checkout_path, self.url,
659 self.remote)
660 if revision.startswith('origin/'):
661 revision = 'refs/remotes/' + revision
msb@chromium.orge28e4982009-09-25 20:51:45 +0000662
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000663 if managed and platform.system() == 'Windows':
664 self._DisableHooks()
szager@chromium.org8a139702014-06-20 15:55:01 +0000665
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000666 printed_path = False
667 verbose = []
668 if options.verbose:
669 self.Print('_____ %s at %s' % (self.relpath, revision),
670 timestamp=False)
671 verbose = ['--verbose']
672 printed_path = True
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000673
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000674 revision_ref = revision
675 if ':' in revision:
676 revision_ref, _, revision = revision.partition(':')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000677
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000678 if revision_ref.startswith('refs/branch-heads'):
679 options.with_branch_heads = True
Edward Lesmes8073a502020-04-15 02:11:14 +0000680
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000681 mirror = self._GetMirror(url, options, revision, revision_ref)
682 if mirror:
683 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000684
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000685 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
686 if remote_ref:
687 # Rewrite remote refs to their local equivalents.
688 revision = ''.join(remote_ref)
689 rev_type = "branch"
690 elif revision.startswith('refs/'):
691 # Local branch? We probably don't want to support, since DEPS should
692 # always specify branches as they are in the upstream repo.
693 rev_type = "branch"
Aravind Vasudevancf465852023-03-29 16:47:12 +0000694 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000695 # hash is also a tag, only make a distinction at checkout
696 rev_type = "hash"
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000697
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000698 # If we are going to introduce a new project, there is a possibility
699 # that we are syncing back to a state where the project was originally a
700 # sub-project rolled by DEPS (realistic case: crossing the Blink merge
701 # point syncing backwards, when Blink was a DEPS entry and not part of
702 # src.git). In such case, we might have a backup of the former .git
703 # folder, which can be used to avoid re-fetching the entire repo again
704 # (useful for bisects).
705 backup_dir = self.GetGitBackupDirPath()
706 target_dir = os.path.join(self.checkout_path, '.git')
707 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
708 gclient_utils.safe_makedirs(self.checkout_path)
709 os.rename(backup_dir, target_dir)
710 # Reset to a clean state
711 self._Scrub('HEAD', options)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000712
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000713 if (not os.path.exists(self.checkout_path) or
714 (os.path.isdir(self.checkout_path)
715 and not os.path.exists(os.path.join(self.checkout_path, '.git')))):
716 if mirror:
717 self._UpdateMirrorIfNotContains(mirror, options, rev_type,
718 revision)
719 try:
720 self._Clone(revision, url, options)
721 except subprocess2.CalledProcessError as e:
722 logging.warning('Clone failed due to: %s', e)
723 self._DeleteOrMove(options.force)
724 self._Clone(revision, url, options)
725 if file_list is not None:
726 files = self._Capture(
727 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
728 file_list.extend(
729 [os.path.join(self.checkout_path, f) for f in files])
730 if mirror:
731 self._Capture(
732 ['remote', 'set-url', '--push', 'origin', mirror.url])
733 if not verbose:
734 # Make the output a little prettier. It's nice to have some
735 # whitespace between projects when cloning.
736 self.Print('')
737 return self._Capture(['rev-parse', '--verify', 'HEAD'])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000738
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000739 if mirror:
740 self._Capture(['remote', 'set-url', '--push', 'origin', mirror.url])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000741
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000742 if not managed:
743 self._SetFetchConfig(options)
744 self.Print('________ unmanaged solution; skipping %s' %
745 self.relpath)
746 return self._Capture(['rev-parse', '--verify', 'HEAD'])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000747
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000748 self._maybe_break_locks(options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000749
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000750 if mirror:
751 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
Edward Lemur579c9862018-07-13 23:17:51 +0000752
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000753 # See if the url has changed (the unittests use git://foo for the url,
754 # let that through).
755 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
756 return_early = False
757 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
758 # unit test pass. (and update the comment above)
759 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
760 # This allows devs to use experimental repos which have a different url
761 # but whose branch(s) are the same as official repos.
762 if (current_url.rstrip('/') != url.rstrip('/') and url != 'git://foo'
763 and
764 subprocess2.capture([
765 'git', 'config',
766 'remote.%s.gclient-auto-fix-url' % self.remote
767 ],
768 cwd=self.checkout_path).strip() != 'False'):
769 self.Print('_____ switching %s from %s to new upstream %s' %
770 (self.relpath, current_url, url))
771 if not (options.force or options.reset):
772 # Make sure it's clean
773 self._CheckClean(revision)
774 # Switch over to the new upstream
775 self._Run(['remote', 'set-url', self.remote, url], options)
776 if mirror:
777 if git_cache.Mirror.CacheDirToUrl(current_url.rstrip(
778 '/')) == git_cache.Mirror.CacheDirToUrl(
779 url.rstrip('/')):
780 # Reset alternates when the cache dir is updated.
781 with open(
782 os.path.join(self.checkout_path, '.git', 'objects',
783 'info', 'alternates'), 'w') as fh:
784 fh.write(os.path.join(url, 'objects'))
785 else:
786 # Because we use Git alternatives, our existing repository
787 # is not self-contained. It's possible that new git
788 # alternative doesn't have all necessary objects that the
789 # current repository needs. Instead of blindly hoping that
790 # new alternative contains all necessary objects, keep the
791 # old alternative and just append a new one on top of it.
792 with open(
793 os.path.join(self.checkout_path, '.git', 'objects',
794 'info', 'alternates'), 'a') as fh:
795 fh.write("\n" + os.path.join(url, 'objects'))
796 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
797 self._FetchAndReset(revision, file_list, options)
Michael Spang73fac912019-03-08 18:44:19 +0000798
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000799 return_early = True
800 else:
801 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
802
803 if return_early:
804 return self._Capture(['rev-parse', '--verify', 'HEAD'])
805
806 cur_branch = self._GetCurrentBranch()
807
808 # Cases:
809 # 0) HEAD is detached. Probably from our initial clone.
810 # - make sure HEAD is contained by a named ref, then update.
811 # Cases 1-4. HEAD is a branch.
812 # 1) current branch is not tracking a remote branch
813 # - try to rebase onto the new hash or branch
814 # 2) current branch is tracking a remote branch with local committed
815 # changes, but the DEPS file switched to point to a hash
816 # - rebase those changes on top of the hash
817 # 3) current branch is tracking a remote branch w/or w/out changes, and
818 # no DEPS switch
819 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
820 # 4) current branch is tracking a remote branch, but DEPS switches to a
821 # different remote branch, and a) current branch has no local changes,
822 # and --force: - checkout new branch b) current branch has local
823 # changes, and --force and --reset: - checkout new branch c) otherwise
824 # exit
825
826 # GetUpstreamBranch returns something like 'refs/remotes/origin/main'
827 # for a tracking branch or 'main' if not a tracking branch (it's based
828 # on a specific rev/hash) or it returns None if it couldn't find an
829 # upstream
830 if cur_branch is None:
831 upstream_branch = None
832 current_type = "detached"
833 logging.debug("Detached HEAD")
834 else:
835 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
836 if not upstream_branch or not upstream_branch.startswith(
837 'refs/remotes'):
838 current_type = "hash"
839 logging.debug(
840 "Current branch is not tracking an upstream (remote)"
841 " branch.")
842 elif upstream_branch.startswith('refs/remotes'):
843 current_type = "branch"
844 else:
845 raise gclient_utils.Error('Invalid Upstream: %s' %
846 upstream_branch)
847
848 self._SetFetchConfig(options)
849
850 # Fetch upstream if we don't already have |revision|.
851 if not scm.GIT.IsValidRevision(
852 self.checkout_path, revision, sha_only=True):
853 self._Fetch(options, prune=options.force)
854
855 if not scm.GIT.IsValidRevision(
856 self.checkout_path, revision, sha_only=True):
857 # Update the remotes first so we have all the refs.
858 remote_output = scm.GIT.Capture(['remote'] + verbose +
859 ['update'],
860 cwd=self.checkout_path)
861 if verbose:
862 self.Print(remote_output)
863
864 revision = self._AutoFetchRef(options, revision)
865
866 # This is a big hammer, debatable if it should even be here...
867 if options.force or options.reset:
868 target = 'HEAD'
869 if options.upstream and upstream_branch:
870 target = upstream_branch
871 self._Scrub(target, options)
872
873 if current_type == 'detached':
874 # case 0
875 # We just did a Scrub, this is as clean as it's going to get. In
876 # particular if HEAD is a commit that contains two versions of the
877 # same file on a case-insensitive filesystem (e.g. 'a' and 'A'),
878 # there's no way to actually "Clean" the checkout; that commit is
879 # uncheckoutable on this system. The best we can do is carry forward
880 # to the checkout step.
881 if not (options.force or options.reset):
882 self._CheckClean(revision)
883 self._CheckDetachedHead(revision, options)
884 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
885 self.Print('Up-to-date; skipping checkout.')
886 else:
887 # 'git checkout' may need to overwrite existing untracked files.
888 # Allow it only when nuclear options are enabled.
889 self._Checkout(
890 options,
891 revision,
892 force=(options.force and options.delete_unversioned_trees),
893 quiet=True,
894 )
895 if not printed_path:
896 self.Print('_____ %s at %s' % (self.relpath, revision),
897 timestamp=False)
898 elif current_type == 'hash':
899 # case 1
900 # Can't find a merge-base since we don't know our upstream. That
901 # makes this command VERY likely to produce a rebase failure. For
902 # now we assume origin is our upstream since that's what the old
903 # behavior was.
904 upstream_branch = self.remote
905 if options.revision or deps_revision:
906 upstream_branch = revision
907 self._AttemptRebase(upstream_branch,
908 file_list,
909 options,
910 printed_path=printed_path,
911 merge=options.merge)
912 printed_path = True
913 elif rev_type == 'hash':
914 # case 2
915 self._AttemptRebase(upstream_branch,
916 file_list,
917 options,
918 newbase=revision,
919 printed_path=printed_path,
920 merge=options.merge)
921 printed_path = True
922 elif remote_ref and ''.join(remote_ref) != upstream_branch:
923 # case 4
924 new_base = ''.join(remote_ref)
925 if not printed_path:
926 self.Print('_____ %s at %s' % (self.relpath, revision),
927 timestamp=False)
928 switch_error = (
929 "Could not switch upstream branch from %s to %s\n" %
930 (upstream_branch, new_base) +
931 "Please use --force or merge or rebase manually:\n" +
932 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
933 "OR git checkout -b <some new branch> %s" % new_base)
934 force_switch = False
935 if options.force:
936 try:
937 self._CheckClean(revision)
938 # case 4a
939 force_switch = True
940 except gclient_utils.Error as e:
941 if options.reset:
942 # case 4b
943 force_switch = True
944 else:
945 switch_error = '%s\n%s' % (e.message, switch_error)
946 if force_switch:
947 self.Print("Switching upstream branch from %s to %s" %
948 (upstream_branch, new_base))
949 switch_branch = 'gclient_' + remote_ref[1]
950 self._Capture(['branch', '-f', switch_branch, new_base])
951 self._Checkout(options, switch_branch, force=True, quiet=True)
952 else:
953 # case 4c
954 raise gclient_utils.Error(switch_error)
955 else:
956 # case 3 - the default case
957 rebase_files = self._GetDiffFilenames(upstream_branch)
958 if verbose:
959 self.Print('Trying fast-forward merge to branch : %s' %
960 upstream_branch)
961 try:
962 merge_args = ['merge']
963 if options.merge:
964 merge_args.append('--ff')
965 else:
966 merge_args.append('--ff-only')
967 merge_args.append(upstream_branch)
968 merge_output = self._Capture(merge_args)
969 except subprocess2.CalledProcessError as e:
970 rebase_files = []
971 if re.search(b'fatal: Not possible to fast-forward, aborting.',
972 e.stderr):
973 if not printed_path:
974 self.Print('_____ %s at %s' % (self.relpath, revision),
975 timestamp=False)
976 printed_path = True
977 while True:
978 if not options.auto_rebase:
979 try:
980 action = self._AskForData(
981 'Cannot %s, attempt to rebase? '
982 '(y)es / (q)uit / (s)kip : ' %
983 ('merge' if options.merge else
984 'fast-forward merge'), options)
985 except ValueError:
986 raise gclient_utils.Error('Invalid Character')
987 if options.auto_rebase or re.match(
988 r'yes|y', action, re.I):
989 self._AttemptRebase(upstream_branch,
990 rebase_files,
991 options,
992 printed_path=printed_path,
993 merge=False)
994 printed_path = True
995 break
996
997 if re.match(r'quit|q', action, re.I):
998 raise gclient_utils.Error(
999 "Can't fast-forward, please merge or "
1000 "rebase manually.\n"
1001 "cd %s && git " % self.checkout_path +
1002 "rebase %s" % upstream_branch)
1003
1004 if re.match(r'skip|s', action, re.I):
1005 self.Print('Skipping %s' % self.relpath)
1006 return
1007
1008 self.Print('Input not recognized')
1009 elif re.match(
1010 b"error: Your local changes to '.*' would be "
1011 b"overwritten by merge. Aborting.\nPlease, commit your "
1012 b"changes or stash them before you can merge.\n",
1013 e.stderr):
1014 if not printed_path:
1015 self.Print('_____ %s at %s' % (self.relpath, revision),
1016 timestamp=False)
1017 printed_path = True
1018 raise gclient_utils.Error(e.stderr.decode('utf-8'))
1019 else:
1020 # Some other problem happened with the merge
1021 logging.error("Error during fast-forward merge in %s!" %
1022 self.relpath)
1023 self.Print(e.stderr.decode('utf-8'))
1024 raise
1025 else:
1026 # Fast-forward merge was successful
1027 if not re.match('Already up-to-date.', merge_output) or verbose:
1028 if not printed_path:
1029 self.Print('_____ %s at %s' % (self.relpath, revision),
1030 timestamp=False)
1031 printed_path = True
1032 self.Print(merge_output.strip())
1033 if not verbose:
1034 # Make the output a little prettier. It's nice to have
1035 # some whitespace between projects when syncing.
1036 self.Print('')
1037
1038 if file_list is not None:
1039 file_list.extend(
1040 [os.path.join(self.checkout_path, f) for f in rebase_files])
1041
1042 # If the rebase generated a conflict, abort and ask user to fix
1043 if self._IsRebasing():
1044 raise gclient_utils.Error(
1045 '\n____ %s at %s\n'
1046 '\nConflict while rebasing this branch.\n'
1047 'Fix the conflict and run gclient again.\n'
1048 'See man git-rebase for details.\n' % (self.relpath, revision))
1049
Michael Spang73fac912019-03-08 18:44:19 +00001050 if verbose:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001051 self.Print('Checked out revision %s' %
1052 self.revinfo(options, (), None),
agable83faed02016-10-24 14:37:10 -07001053 timestamp=False)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001054
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001055 # If --reset and --delete_unversioned_trees are specified, remove any
1056 # untracked directories.
1057 if options.reset and options.delete_unversioned_trees:
1058 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1
1059 # (the merge-base by default), so doesn't include untracked files.
1060 # So we use 'git ls-files --directory --others --exclude-standard'
1061 # here directly.
1062 paths = scm.GIT.Capture([
1063 '-c', 'core.quotePath=false', 'ls-files', '--directory',
1064 '--others', '--exclude-standard'
1065 ], self.checkout_path)
1066 for path in (p for p in paths.splitlines() if p.endswith('/')):
1067 full_path = os.path.join(self.checkout_path, path)
1068 if not os.path.islink(full_path):
1069 self.Print('_____ removing unversioned directory %s' % path)
1070 gclient_utils.rmtree(full_path)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001071
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001072 return self._Capture(['rev-parse', '--verify', 'HEAD'])
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001073
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001074 def revert(self, options, _args, file_list):
1075 """Reverts local modifications.
msb@chromium.orge28e4982009-09-25 20:51:45 +00001076
1077 All reverted files will be appended to file_list.
1078 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001079 if not os.path.isdir(self.checkout_path):
1080 # revert won't work if the directory doesn't exist. It needs to
1081 # checkout instead.
1082 self.Print('_____ %s is missing, syncing instead' % self.relpath)
1083 # Don't reuse the args.
1084 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001085
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001086 default_rev = "refs/heads/main"
1087 if options.upstream:
1088 if self._GetCurrentBranch():
1089 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
1090 default_rev = upstream_branch or default_rev
1091 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
1092 if not deps_revision:
1093 deps_revision = default_rev
1094 if deps_revision.startswith('refs/heads/'):
1095 deps_revision = deps_revision.replace('refs/heads/',
1096 self.remote + '/')
1097 try:
1098 deps_revision = self.GetUsableRev(deps_revision, options)
1099 except NoUsableRevError as e:
1100 # If the DEPS entry's url and hash changed, try to update the
1101 # origin. See also http://crbug.com/520067.
1102 logging.warning(
1103 "Couldn't find usable revision, will retrying to update instead: %s",
1104 e.message)
1105 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001106
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001107 if file_list is not None:
1108 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001109
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001110 self._Scrub(deps_revision, options)
1111 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001112
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001113 if file_list is not None:
1114 file_list.extend(
1115 [os.path.join(self.checkout_path, f) for f in files])
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001116
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001117 def revinfo(self, _options, _args, _file_list):
1118 """Returns revision"""
1119 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +00001120
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001121 def runhooks(self, options, args, file_list):
1122 self.status(options, args, file_list)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001123
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001124 def status(self, options, _args, file_list):
1125 """Display status information."""
1126 if not os.path.isdir(self.checkout_path):
1127 self.Print('________ couldn\'t run status in %s:\n'
1128 'The directory does not exist.' % self.checkout_path)
1129 else:
1130 merge_base = []
1131 if self.url:
1132 _, base_rev = gclient_utils.SplitUrlRevision(self.url)
1133 if base_rev:
1134 if base_rev.startswith('refs/'):
1135 base_rev = self._ref_to_remote_ref(base_rev)
1136 merge_base = [base_rev]
1137 self._Run(['-c', 'core.quotePath=false', 'diff', '--name-status'] +
1138 merge_base,
1139 options,
1140 always_show_header=options.verbose)
1141 if file_list is not None:
1142 files = self._GetDiffFilenames(
1143 merge_base[0] if merge_base else None)
1144 file_list.extend(
1145 [os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +00001146
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001147 def GetUsableRev(self, rev, options):
1148 """Finds a useful revision for this repository."""
1149 sha1 = None
1150 if not os.path.isdir(self.checkout_path):
1151 raise NoUsableRevError(
1152 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -07001153
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001154 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1155 sha1 = rev
1156 else:
1157 # May exist in origin, but we don't have it yet, so fetch and look
1158 # again.
1159 self._Fetch(options)
1160 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1161 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001162
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001163 if not sha1:
1164 raise NoUsableRevError(
1165 'Hash %s does not appear to be a valid hash in this repo.' %
1166 rev)
smutae7ea312016-07-18 11:59:41 -07001167
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001168 return sha1
smutae7ea312016-07-18 11:59:41 -07001169
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001170 def GetGitBackupDirPath(self):
1171 """Returns the path where the .git folder for the current project can be
primiano@chromium.org1c127382015-02-17 11:15:40 +00001172 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001173 return os.path.join(self._root_dir,
1174 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
primiano@chromium.org1c127382015-02-17 11:15:40 +00001175
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001176 def _GetMirror(self, url, options, revision=None, revision_ref=None):
1177 """Get a git_cache.Mirror object for the argument url."""
1178 if not self.cache_dir:
1179 return None
1180 mirror_kwargs = {
1181 'print_func': self.filter,
1182 'refs': [],
1183 'commits': [],
1184 }
1185 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1186 mirror_kwargs['refs'].append('refs/branch-heads/*')
1187 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
1188 mirror_kwargs['refs'].append(revision_ref)
1189 if hasattr(options, 'with_tags') and options.with_tags:
1190 mirror_kwargs['refs'].append('refs/tags/*')
1191 elif revision_ref and revision_ref.startswith('refs/tags/'):
1192 mirror_kwargs['refs'].append(revision_ref)
1193 if revision and not revision.startswith('refs/'):
1194 mirror_kwargs['commits'].append(revision)
1195 return git_cache.Mirror(url, **mirror_kwargs)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001196
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001197 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
1198 """Update a git mirror by fetching the latest commits from the remote,
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001199 unless mirror already contains revision whose type is sha1 hash.
1200 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001201 if rev_type == 'hash' and mirror.contains_revision(revision):
1202 if options.verbose:
1203 self.Print('skipping mirror update, it has rev=%s already' %
1204 revision,
1205 timestamp=False)
1206 return
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001207
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001208 if getattr(options, 'shallow', False):
1209 depth = 10000
1210 else:
1211 depth = None
1212 mirror.populate(verbose=options.verbose,
1213 bootstrap=not getattr(options, 'no_bootstrap', False),
1214 depth=depth,
1215 lock_timeout=getattr(options, 'lock_timeout', 0))
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001216
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001217 def _Clone(self, revision, url, options):
1218 """Clone a git repository from the given URL.
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001219
msb@chromium.org786fb682010-06-02 15:16:23 +00001220 Once we've cloned the repo, we checkout a working branch if the specified
1221 revision is a branch head. If it is a tag or a specific commit, then we
1222 leave HEAD detached as it makes future updates simpler -- in this case the
1223 user should first create a new branch or switch to an existing branch before
1224 making changes in the repo."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001225 in_cog_workspace = self._IsCog()
Joanna Wang1a977bd2022-06-02 21:51:17 +00001226
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001227 if self.print_outbuf:
1228 print_stdout = True
1229 filter_fn = None
1230 else:
1231 print_stdout = False
1232 filter_fn = self.filter
Joanna Wang1a977bd2022-06-02 21:51:17 +00001233
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001234 if not options.verbose:
1235 # git clone doesn't seem to insert a newline properly before
1236 # printing to stdout
1237 self.Print('')
Joanna Wang1a977bd2022-06-02 21:51:17 +00001238
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001239 # If the parent directory does not exist, Git clone on Windows will not
1240 # create it, so we need to do it manually.
1241 parent_dir = os.path.dirname(self.checkout_path)
1242 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001243
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001244 if in_cog_workspace:
1245 clone_cmd = ['citc', 'clone-repo', url, self.checkout_path]
1246 clone_cmd.append(
1247 gclient_utils.ExtractRefName(self.remote, revision) or revision)
1248 try:
1249 self._Run(clone_cmd,
1250 options,
1251 cwd=self._root_dir,
1252 retry=True,
1253 print_stdout=print_stdout,
1254 filter_fn=filter_fn)
1255 except:
1256 traceback.print_exc(file=self.out_fh)
1257 raise
1258 self._SetFetchConfig(options)
1259 elif hasattr(options, 'no_history') and options.no_history:
1260 self._Run(['init', self.checkout_path], options, cwd=self._root_dir)
1261 self._Run(['remote', 'add', 'origin', url], options)
1262 revision = self._AutoFetchRef(options, revision, depth=1)
1263 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1264 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
1265 else:
1266 cfg = gclient_utils.DefaultIndexPackConfig(url)
1267 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
1268 if self.cache_dir:
1269 clone_cmd.append('--shared')
1270 if options.verbose:
1271 clone_cmd.append('--verbose')
1272 clone_cmd.append(url)
1273 tmp_dir = tempfile.mkdtemp(prefix='_gclient_%s_' %
1274 os.path.basename(self.checkout_path),
1275 dir=parent_dir)
1276 clone_cmd.append(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001277
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001278 try:
1279 self._Run(clone_cmd,
1280 options,
1281 cwd=self._root_dir,
1282 retry=True,
1283 print_stdout=print_stdout,
1284 filter_fn=filter_fn)
1285 logging.debug(
1286 'Cloned into temporary dir, moving to checkout_path')
1287 gclient_utils.safe_makedirs(self.checkout_path)
1288 gclient_utils.safe_rename(
1289 os.path.join(tmp_dir, '.git'),
1290 os.path.join(self.checkout_path, '.git'))
1291 except:
1292 traceback.print_exc(file=self.out_fh)
1293 raise
1294 finally:
1295 if os.listdir(tmp_dir):
1296 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
1297 gclient_utils.rmtree(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001298
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001299 self._SetFetchConfig(options)
1300 self._Fetch(options, prune=options.force)
1301 revision = self._AutoFetchRef(options, revision)
1302 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1303 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001304
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001305 if self._GetCurrentBranch() is None:
1306 # Squelch git's very verbose detached HEAD warning and use our own
1307 self.Print((
1308 'Checked out %s to a detached HEAD. Before making any commits\n'
1309 'in this repo, you should use \'git checkout <branch>\' to switch \n'
1310 'to an existing branch or use \'git checkout %s -b <branch>\' to\n'
1311 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001312
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001313 def _AskForData(self, prompt, options):
1314 if options.jobs > 1:
1315 self.Print(prompt)
1316 raise gclient_utils.Error("Background task requires input. Rerun "
1317 "gclient with --jobs=1 so that\n"
1318 "interaction is possible.")
1319 return gclient_utils.AskForData(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001320
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001321 def _AttemptRebase(self,
1322 upstream,
1323 files,
1324 options,
1325 newbase=None,
1326 branch=None,
1327 printed_path=False,
1328 merge=False):
1329 """Attempt to rebase onto either upstream or, if specified, newbase."""
1330 if files is not None:
1331 files.extend(self._GetDiffFilenames(upstream))
1332 revision = upstream
1333 if newbase:
1334 revision = newbase
1335 action = 'merge' if merge else 'rebase'
1336 if not printed_path:
1337 self.Print('_____ %s : Attempting %s onto %s...' %
1338 (self.relpath, action, revision))
1339 printed_path = True
1340 else:
1341 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001342
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001343 if merge:
1344 merge_output = self._Capture(['merge', revision])
1345 if options.verbose:
1346 self.Print(merge_output)
1347 return
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001348
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001349 # Build the rebase command here using the args
1350 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1351 rebase_cmd = ['rebase']
1352 if options.verbose:
1353 rebase_cmd.append('--verbose')
1354 if newbase:
1355 rebase_cmd.extend(['--onto', newbase])
1356 rebase_cmd.append(upstream)
1357 if branch:
1358 rebase_cmd.append(branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001359
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001360 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001361 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001362 except subprocess2.CalledProcessError as e:
1363 if (re.match(
1364 br'cannot rebase: you have unstaged changes', e.stderr
1365 ) or re.match(
1366 br'cannot rebase: your index contains uncommitted changes',
1367 e.stderr)):
1368 while True:
1369 rebase_action = self._AskForData(
1370 'Cannot rebase because of unstaged changes.\n'
1371 '\'git reset --hard HEAD\' ?\n'
1372 'WARNING: destroys any uncommitted work in your current branch!'
1373 ' (y)es / (q)uit / (s)how : ', options)
1374 if re.match(r'yes|y', rebase_action, re.I):
1375 self._Scrub('HEAD', options)
1376 # Should this be recursive?
1377 rebase_output = scm.GIT.Capture(rebase_cmd,
1378 cwd=self.checkout_path)
1379 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001380
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001381 if re.match(r'quit|q', rebase_action, re.I):
1382 raise gclient_utils.Error(
1383 "Please merge or rebase manually\n"
1384 "cd %s && git " % self.checkout_path +
1385 "%s" % ' '.join(rebase_cmd))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001386
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001387 if re.match(r'show|s', rebase_action, re.I):
1388 self.Print('%s' % e.stderr.decode('utf-8').strip())
1389 continue
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001390
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001391 gclient_utils.Error("Input not recognized")
1392 continue
1393 elif re.search(br'^CONFLICT', e.stdout, re.M):
1394 raise gclient_utils.Error(
1395 "Conflict while rebasing this branch.\n"
1396 "Fix the conflict and run gclient again.\n"
1397 "See 'man git-rebase' for details.\n")
1398 else:
1399 self.Print(e.stdout.decode('utf-8').strip())
1400 self.Print('Rebase produced error output:\n%s' %
1401 e.stderr.decode('utf-8').strip())
1402 raise gclient_utils.Error(
1403 "Unrecognized error, please merge or rebase "
1404 "manually.\ncd %s && git " % self.checkout_path +
1405 "%s" % ' '.join(rebase_cmd))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001406
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001407 self.Print(rebase_output.strip())
1408 if not options.verbose:
1409 # Make the output a little prettier. It's nice to have some
1410 # whitespace between projects when syncing.
1411 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001412
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001413 @staticmethod
1414 def _CheckMinVersion(min_version):
1415 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1416 if not ok:
1417 raise gclient_utils.Error('git version %s < minimum required %s' %
1418 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001419
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001420 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
1421 # Special case handling if all 3 conditions are met:
1422 # * the mirros have recently changed, but deps destination remains same,
1423 # * the git histories of mirrors are conflicting. * git cache is used
1424 # This manifests itself in current checkout having invalid HEAD commit
1425 # on most git operations. Since git cache is used, just deleted the .git
1426 # folder, and re-create it by cloning.
1427 try:
1428 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1429 except subprocess2.CalledProcessError as e:
1430 if (b'fatal: bad object HEAD' in e.stderr and self.cache_dir
1431 and self.cache_dir in url):
1432 self.Print(
1433 ('Likely due to DEPS change with git cache_dir, '
1434 'the current commit points to no longer existing object.\n'
1435 '%s' % e))
1436 self._DeleteOrMove(options.force)
1437 self._Clone(revision, url, options)
1438 else:
1439 raise
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001440
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001441 def _IsRebasing(self):
1442 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git
1443 # doesn't have a plumbing command to determine whether a rebase is in
1444 # progress, so for now emualate (more-or-less) git-rebase.sh /
1445 # git-completion.bash
1446 g = os.path.join(self.checkout_path, '.git')
1447 return (os.path.isdir(os.path.join(g, "rebase-merge"))
1448 or os.path.isdir(os.path.join(g, "rebase-apply")))
msb@chromium.org786fb682010-06-02 15:16:23 +00001449
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001450 def _CheckClean(self, revision):
1451 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1452 if os.path.exists(lockfile):
1453 raise gclient_utils.Error(
1454 '\n____ %s at %s\n'
1455 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1456 '\tIf no git executable is running, then clean up %r and try again.\n'
1457 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001458
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001459 # Make sure the tree is clean; see git-rebase.sh for reference
1460 try:
1461 scm.GIT.Capture(
1462 ['update-index', '--ignore-submodules', '--refresh'],
1463 cwd=self.checkout_path)
1464 except subprocess2.CalledProcessError:
1465 raise gclient_utils.Error(
1466 '\n____ %s at %s\n'
1467 '\tYou have unstaged changes.\n'
1468 '\tcd into %s, run git status to see changes,\n'
1469 '\tand commit, stash, or reset.\n' %
1470 (self.relpath, revision, self.relpath))
1471 try:
1472 scm.GIT.Capture([
1473 'diff-index', '--cached', '--name-status', '-r',
1474 '--ignore-submodules', 'HEAD', '--'
1475 ],
1476 cwd=self.checkout_path)
1477 except subprocess2.CalledProcessError:
1478 raise gclient_utils.Error(
1479 '\n____ %s at %s\n'
1480 '\tYour index contains uncommitted changes\n'
1481 '\tcd into %s, run git status to see changes,\n'
1482 '\tand commit, stash, or reset.\n' %
1483 (self.relpath, revision, self.relpath))
msb@chromium.org786fb682010-06-02 15:16:23 +00001484
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001485 def _CheckDetachedHead(self, revision, _options):
1486 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1487 # reference by a commit). If not, error out -- most likely a rebase is
1488 # in progress, try to detect so we can give a better error.
1489 try:
1490 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1491 cwd=self.checkout_path)
1492 except subprocess2.CalledProcessError:
1493 # Commit is not contained by any rev. See if the user is rebasing:
1494 if self._IsRebasing():
1495 # Punt to the user
1496 raise gclient_utils.Error(
1497 '\n____ %s at %s\n'
1498 '\tAlready in a conflict, i.e. (no branch).\n'
1499 '\tFix the conflict and run gclient again.\n'
1500 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1501 '\tSee man git-rebase for details.\n' %
1502 (self.relpath, revision))
1503 # Let's just save off the commit so we can proceed.
1504 name = ('saved-by-gclient-' +
1505 self._Capture(['rev-parse', '--short', 'HEAD']))
1506 self._Capture(['branch', '-f', name])
1507 self.Print(
1508 '_____ found an unreferenced commit and saved it as \'%s\'' %
1509 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001510
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001511 def _GetCurrentBranch(self):
1512 # Returns name of current branch or None for detached HEAD
1513 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
1514 if branch == 'HEAD':
1515 return None
1516 return branch
msb@chromium.org5bde4852009-12-14 16:47:12 +00001517
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001518 def _Capture(self, args, **kwargs):
1519 set_git_dir = 'cwd' not in kwargs
1520 kwargs.setdefault('cwd', self.checkout_path)
1521 kwargs.setdefault('stderr', subprocess2.PIPE)
1522 strip = kwargs.pop('strip', True)
1523 env = scm.GIT.ApplyEnvVars(kwargs)
1524 # If an explicit cwd isn't set, then default to the .git/ subdir so we
1525 # get stricter behavior. This can be useful in cases of slight
1526 # corruption -- we don't accidentally go corrupting parent git checks
1527 # too. See https://crbug.com/1000825 for an example.
1528 if set_git_dir:
Gavin Mak7f5b53f2023-09-07 18:13:01 +00001529 env.setdefault(
1530 'GIT_DIR',
1531 os.path.abspath(os.path.join(self.checkout_path, '.git')))
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001532 ret = subprocess2.check_output(['git'] + args, env=env,
1533 **kwargs).decode('utf-8')
1534 if strip:
1535 ret = ret.strip()
1536 self.Print('Finished running: %s %s' % ('git', ' '.join(args)))
1537 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001538
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001539 def _Checkout(self, options, ref, force=False, quiet=None):
1540 """Performs a 'git-checkout' operation.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001541
1542 Args:
1543 options: The configured option set
1544 ref: (str) The branch/commit to checkout
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001545 quiet: (bool/None) Whether or not the checkout should pass '--quiet'; if
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001546 'None', the behavior is inferred from 'options.verbose'.
1547 Returns: (str) The output of the checkout operation
1548 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001549 if quiet is None:
1550 quiet = (not options.verbose)
1551 checkout_args = ['checkout']
1552 if force:
1553 checkout_args.append('--force')
1554 if quiet:
1555 checkout_args.append('--quiet')
1556 checkout_args.append(ref)
1557 return self._Capture(checkout_args)
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001558
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001559 def _Fetch(self,
1560 options,
1561 remote=None,
1562 prune=False,
1563 quiet=False,
1564 refspec=None,
1565 depth=None):
1566 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
1567 # When updating, the ref is modified to be a remote ref .
1568 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1569 # Try to reverse that mapping.
1570 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1571 if original_ref:
1572 refspec = original_ref + ':' + refspec
1573 # When a mirror is configured, it only fetches
1574 # refs/{heads,branch-heads,tags}/*.
1575 # If asked to fetch other refs, we must fetch those directly from
1576 # the repository, and not from the mirror.
1577 if not original_ref.startswith(
1578 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1579 remote, _ = gclient_utils.SplitUrlRevision(self.url)
1580 fetch_cmd = cfg + [
1581 'fetch',
1582 remote or self.remote,
1583 ]
1584 if refspec:
1585 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001586
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001587 if prune:
1588 fetch_cmd.append('--prune')
1589 if options.verbose:
1590 fetch_cmd.append('--verbose')
1591 if not hasattr(options, 'with_tags') or not options.with_tags:
1592 fetch_cmd.append('--no-tags')
1593 elif quiet:
1594 fetch_cmd.append('--quiet')
1595 if depth:
1596 fetch_cmd.append('--depth=' + str(depth))
1597 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001598
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001599 def _SetFetchConfig(self, options):
1600 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001601 if requested."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001602 if options.force or options.reset:
1603 try:
1604 self._Run(
1605 ['config', '--unset-all',
1606 'remote.%s.fetch' % self.remote], options)
1607 self._Run([
1608 'config',
1609 'remote.%s.fetch' % self.remote,
1610 '+refs/heads/*:refs/remotes/%s/*' % self.remote
1611 ], options)
1612 except subprocess2.CalledProcessError as e:
1613 # If exit code was 5, it means we attempted to unset a config
1614 # that didn't exist. Ignore it.
1615 if e.returncode != 5:
1616 raise
1617 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1618 config_cmd = [
1619 'config',
1620 'remote.%s.fetch' % self.remote,
1621 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1622 '^\\+refs/branch-heads/\\*:.*$'
1623 ]
1624 self._Run(config_cmd, options)
1625 if hasattr(options, 'with_tags') and options.with_tags:
1626 config_cmd = [
1627 'config',
1628 'remote.%s.fetch' % self.remote, '+refs/tags/*:refs/tags/*',
1629 '^\\+refs/tags/\\*:.*$'
1630 ]
1631 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001632
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001633 def _AutoFetchRef(self, options, revision, depth=None):
1634 """Attempts to fetch |revision| if not available in local repo.
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001635
1636 Returns possibly updated revision."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001637 if not scm.GIT.IsValidRevision(self.checkout_path, revision):
1638 self._Fetch(options, refspec=revision, depth=depth)
1639 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1640 return revision
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001641
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001642 def _Run(self, args, options, **kwargs):
1643 # Disable 'unused options' warning | pylint: disable=unused-argument
1644 kwargs.setdefault('cwd', self.checkout_path)
1645 kwargs.setdefault('filter_fn', self.filter)
1646 kwargs.setdefault('show_header', True)
1647 env = scm.GIT.ApplyEnvVars(kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +00001648
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001649 cmd = ['git'] + args
1650 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001651
1652
1653class CipdPackage(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001654 """A representation of a single CIPD package."""
1655 def __init__(self, name, version, authority_for_subdir):
1656 self._authority_for_subdir = authority_for_subdir
1657 self._name = name
1658 self._version = version
John Budorick0f7b2002018-01-19 15:46:17 -08001659
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001660 @property
1661 def authority_for_subdir(self):
1662 """Whether this package has authority to act on behalf of its subdir.
John Budorick0f7b2002018-01-19 15:46:17 -08001663
1664 Some operations should only be performed once per subdirectory. A package
1665 that has authority for its subdirectory is the only package that should
1666 perform such operations.
1667
1668 Returns:
1669 bool; whether this package has subdir authority.
1670 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001671 return self._authority_for_subdir
John Budorick0f7b2002018-01-19 15:46:17 -08001672
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001673 @property
1674 def name(self):
1675 return self._name
John Budorick0f7b2002018-01-19 15:46:17 -08001676
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001677 @property
1678 def version(self):
1679 return self._version
John Budorick0f7b2002018-01-19 15:46:17 -08001680
1681
1682class CipdRoot(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001683 """A representation of a single CIPD root."""
Yiwei Zhang52353702023-09-18 15:53:52 +00001684 def __init__(self, root_dir, service_url, log_level=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001685 self._all_packages = set()
1686 self._mutator_lock = threading.Lock()
1687 self._packages_by_subdir = collections.defaultdict(list)
1688 self._root_dir = root_dir
1689 self._service_url = service_url
1690 self._resolved_packages = None
Yiwei Zhang52353702023-09-18 15:53:52 +00001691 self._log_level = log_level or 'error'
John Budorick0f7b2002018-01-19 15:46:17 -08001692
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001693 def add_package(self, subdir, package, version):
1694 """Adds a package to this CIPD root.
John Budorick0f7b2002018-01-19 15:46:17 -08001695
1696 As far as clients are concerned, this grants both root and subdir authority
1697 to packages arbitrarily. (The implementation grants root authority to the
1698 first package added and subdir authority to the first package added for that
1699 subdir, but clients should not depend on or expect that behavior.)
1700
1701 Args:
1702 subdir: str; relative path to where the package should be installed from
1703 the cipd root directory.
1704 package: str; the cipd package name.
1705 version: str; the cipd package version.
1706 Returns:
1707 CipdPackage; the package that was created and added to this root.
1708 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001709 with self._mutator_lock:
1710 cipd_package = CipdPackage(package, version,
1711 not self._packages_by_subdir[subdir])
1712 self._all_packages.add(cipd_package)
1713 self._packages_by_subdir[subdir].append(cipd_package)
1714 return cipd_package
John Budorick0f7b2002018-01-19 15:46:17 -08001715
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001716 def packages(self, subdir):
1717 """Get the list of configured packages for the given subdir."""
1718 return list(self._packages_by_subdir[subdir])
John Budorick0f7b2002018-01-19 15:46:17 -08001719
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001720 def resolved_packages(self):
1721 if not self._resolved_packages:
1722 self._resolved_packages = self.ensure_file_resolve()
1723 return self._resolved_packages
Dan Le Febvre456d0852023-05-24 23:43:40 +00001724
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001725 def clobber(self):
1726 """Remove the .cipd directory.
John Budorick0f7b2002018-01-19 15:46:17 -08001727
1728 This is useful for forcing ensure to redownload and reinitialize all
1729 packages.
1730 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001731 with self._mutator_lock:
1732 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
1733 try:
1734 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1735 except OSError:
1736 if os.path.exists(cipd_cache_dir):
1737 raise
John Budorick0f7b2002018-01-19 15:46:17 -08001738
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001739 def expand_package_name(self, package_name_string, **kwargs):
1740 """Run `cipd expand-package-name`.
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001741
1742 CIPD package names can be declared with placeholder variables
1743 such as '${platform}', this cmd will return the package name
1744 with the variables resolved. The resolution is based on the host
1745 the command is executing on.
1746 """
1747
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001748 kwargs.setdefault('stderr', subprocess2.PIPE)
1749 cmd = ['cipd', 'expand-package-name', package_name_string]
1750 ret = subprocess2.check_output(cmd, **kwargs).decode('utf-8')
1751 return ret.strip()
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001752
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001753 @contextlib.contextmanager
1754 def _create_ensure_file(self):
1755 try:
1756 contents = '$ParanoidMode CheckPresence\n'
1757 # TODO(crbug/1329641): Remove once cipd packages have been updated
1758 # to always be created in copy mode.
1759 contents += '$OverrideInstallMode copy\n\n'
1760 for subdir, packages in sorted(self._packages_by_subdir.items()):
1761 contents += '@Subdir %s\n' % subdir
1762 for package in sorted(packages, key=lambda p: p.name):
1763 contents += '%s %s\n' % (package.name, package.version)
1764 contents += '\n'
1765 ensure_file = None
1766 with tempfile.NamedTemporaryFile(suffix='.ensure',
1767 delete=False,
1768 mode='wb') as ensure_file:
1769 ensure_file.write(contents.encode('utf-8', 'replace'))
1770 yield ensure_file.name
1771 finally:
1772 if ensure_file is not None and os.path.exists(ensure_file.name):
1773 os.remove(ensure_file.name)
John Budorick0f7b2002018-01-19 15:46:17 -08001774
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001775 def ensure(self):
1776 """Run `cipd ensure`."""
1777 with self._mutator_lock:
1778 with self._create_ensure_file() as ensure_file:
1779 cmd = [
1780 'cipd',
1781 'ensure',
1782 '-log-level',
Yiwei Zhang52353702023-09-18 15:53:52 +00001783 self._log_level,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001784 '-root',
1785 self.root_dir,
1786 '-ensure-file',
1787 ensure_file,
1788 ]
1789 gclient_utils.CheckCallAndFilter(cmd,
1790 print_stdout=True,
1791 show_header=True)
John Budorick0f7b2002018-01-19 15:46:17 -08001792
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001793 @contextlib.contextmanager
1794 def _create_ensure_file_for_resolve(self):
1795 try:
1796 contents = '$ResolvedVersions %s\n' % os.devnull
1797 for subdir, packages in sorted(self._packages_by_subdir.items()):
1798 contents += '@Subdir %s\n' % subdir
1799 for package in sorted(packages, key=lambda p: p.name):
1800 contents += '%s %s\n' % (package.name, package.version)
1801 contents += '\n'
1802 ensure_file = None
1803 with tempfile.NamedTemporaryFile(suffix='.ensure',
1804 delete=False,
1805 mode='wb') as ensure_file:
1806 ensure_file.write(contents.encode('utf-8', 'replace'))
1807 yield ensure_file.name
1808 finally:
1809 if ensure_file is not None and os.path.exists(ensure_file.name):
1810 os.remove(ensure_file.name)
Dan Le Febvre456d0852023-05-24 23:43:40 +00001811
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001812 def _create_resolved_file(self):
1813 return tempfile.NamedTemporaryFile(suffix='.resolved',
1814 delete=False,
1815 mode='wb')
Dan Le Febvre456d0852023-05-24 23:43:40 +00001816
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001817 def ensure_file_resolve(self):
1818 """Run `cipd ensure-file-resolve`."""
1819 with self._mutator_lock:
1820 with self._create_resolved_file() as output_file:
1821 with self._create_ensure_file_for_resolve() as ensure_file:
1822 cmd = [
1823 'cipd',
1824 'ensure-file-resolve',
1825 '-log-level',
Yiwei Zhang52353702023-09-18 15:53:52 +00001826 self._log_level,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001827 '-ensure-file',
1828 ensure_file,
1829 '-json-output',
1830 output_file.name,
1831 ]
1832 gclient_utils.CheckCallAndFilter(cmd,
1833 print_stdout=False,
1834 show_header=False)
1835 with open(output_file.name) as f:
1836 output_json = json.load(f)
1837 return output_json.get('result', {})
Dan Le Febvre456d0852023-05-24 23:43:40 +00001838
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001839 def run(self, command):
1840 if command == 'update':
1841 self.ensure()
1842 elif command == 'revert':
1843 self.clobber()
1844 self.ensure()
John Budorickd3ba72b2018-03-20 12:27:42 -07001845
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001846 def created_package(self, package):
1847 """Checks whether this root created the given package.
John Budorick0f7b2002018-01-19 15:46:17 -08001848
1849 Args:
1850 package: CipdPackage; the package to check.
1851 Returns:
1852 bool; whether this root created the given package.
1853 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001854 return package in self._all_packages
John Budorick0f7b2002018-01-19 15:46:17 -08001855
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001856 @property
1857 def root_dir(self):
1858 return self._root_dir
John Budorick0f7b2002018-01-19 15:46:17 -08001859
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001860 @property
1861 def service_url(self):
1862 return self._service_url
John Budorick0f7b2002018-01-19 15:46:17 -08001863
1864
1865class CipdWrapper(SCMWrapper):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001866 """Wrapper for CIPD.
John Budorick0f7b2002018-01-19 15:46:17 -08001867
1868 Currently only supports chrome-infra-packages.appspot.com.
1869 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001870 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001871
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001872 def __init__(self,
1873 url=None,
1874 root_dir=None,
1875 relpath=None,
1876 out_fh=None,
1877 out_cb=None,
1878 root=None,
1879 package=None):
1880 super(CipdWrapper, self).__init__(url=url,
1881 root_dir=root_dir,
1882 relpath=relpath,
1883 out_fh=out_fh,
1884 out_cb=out_cb)
1885 assert root.created_package(package)
1886 self._package = package
1887 self._root = root
John Budorick0f7b2002018-01-19 15:46:17 -08001888
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001889 #override
1890 def GetCacheMirror(self):
1891 return None
John Budorick0f7b2002018-01-19 15:46:17 -08001892
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001893 #override
1894 def GetActualRemoteURL(self, options):
1895 return self._root.service_url
John Budorick0f7b2002018-01-19 15:46:17 -08001896
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001897 #override
1898 def DoesRemoteURLMatch(self, options):
1899 del options
1900 return True
John Budorick0f7b2002018-01-19 15:46:17 -08001901
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001902 def revert(self, options, args, file_list):
1903 """Does nothing.
John Budorickd3ba72b2018-03-20 12:27:42 -07001904
1905 CIPD packages should be reverted at the root by running
1906 `CipdRoot.run('revert')`.
1907 """
John Budorick0f7b2002018-01-19 15:46:17 -08001908
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001909 def diff(self, options, args, file_list):
1910 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001911
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001912 def pack(self, options, args, file_list):
1913 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001914
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001915 def revinfo(self, options, args, file_list):
1916 """Grab the instance ID."""
1917 try:
1918 tmpdir = tempfile.mkdtemp()
1919 # Attempt to get instance_id from the root resolved cache.
1920 # Resolved cache will not match on any CIPD packages with
1921 # variables such as ${platform}, they will fall back to
1922 # the slower method below.
1923 resolved = self._root.resolved_packages()
1924 if resolved:
1925 # CIPD uses POSIX separators across all platforms, so
1926 # replace any Windows separators.
1927 path_split = self.relpath.replace(os.sep, "/").split(":")
1928 if len(path_split) > 1:
1929 src_path, package = path_split
1930 if src_path in resolved:
1931 for resolved_package in resolved[src_path]:
1932 if package == resolved_package.get(
1933 'pin', {}).get('package'):
1934 return resolved_package.get(
1935 'pin', {}).get('instance_id')
Dan Le Febvre456d0852023-05-24 23:43:40 +00001936
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001937 describe_json_path = os.path.join(tmpdir, 'describe.json')
1938 cmd = [
1939 'cipd', 'describe', self._package.name, '-log-level', 'error',
1940 '-version', self._package.version, '-json-output',
1941 describe_json_path
1942 ]
1943 gclient_utils.CheckCallAndFilter(cmd)
1944 with open(describe_json_path) as f:
1945 describe_json = json.load(f)
1946 return describe_json.get('result', {}).get('pin',
1947 {}).get('instance_id')
1948 finally:
1949 gclient_utils.rmtree(tmpdir)
John Budorick0f7b2002018-01-19 15:46:17 -08001950
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001951 def status(self, options, args, file_list):
1952 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001953
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001954 def update(self, options, args, file_list):
1955 """Does nothing.
John Budorickd3ba72b2018-03-20 12:27:42 -07001956
1957 CIPD packages should be updated at the root by running
1958 `CipdRoot.run('update')`.
1959 """