blob: a9f665ec36e9d18a81ef0b522116fe3bb7977f16 [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.
Joanna Wang4af28182023-09-22 18:16:27 +0000762 strp_url = url[:-4] if url.endswith('.git') else url
763 strp_current_url = current_url[:-4] if current_url.endswith(
764 '.git') else current_url
765 if (strp_current_url.rstrip('/') != strp_url.rstrip('/')
766 and url != 'git://foo' and
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000767 subprocess2.capture([
768 'git', 'config',
769 'remote.%s.gclient-auto-fix-url' % self.remote
770 ],
771 cwd=self.checkout_path).strip() != 'False'):
772 self.Print('_____ switching %s from %s to new upstream %s' %
773 (self.relpath, current_url, url))
774 if not (options.force or options.reset):
775 # Make sure it's clean
776 self._CheckClean(revision)
777 # Switch over to the new upstream
778 self._Run(['remote', 'set-url', self.remote, url], options)
779 if mirror:
780 if git_cache.Mirror.CacheDirToUrl(current_url.rstrip(
781 '/')) == git_cache.Mirror.CacheDirToUrl(
782 url.rstrip('/')):
783 # Reset alternates when the cache dir is updated.
784 with open(
785 os.path.join(self.checkout_path, '.git', 'objects',
786 'info', 'alternates'), 'w') as fh:
787 fh.write(os.path.join(url, 'objects'))
788 else:
789 # Because we use Git alternatives, our existing repository
790 # is not self-contained. It's possible that new git
791 # alternative doesn't have all necessary objects that the
792 # current repository needs. Instead of blindly hoping that
793 # new alternative contains all necessary objects, keep the
794 # old alternative and just append a new one on top of it.
795 with open(
796 os.path.join(self.checkout_path, '.git', 'objects',
797 'info', 'alternates'), 'a') as fh:
798 fh.write("\n" + os.path.join(url, 'objects'))
799 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
800 self._FetchAndReset(revision, file_list, options)
Michael Spang73fac912019-03-08 18:44:19 +0000801
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000802 return_early = True
803 else:
804 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
805
806 if return_early:
807 return self._Capture(['rev-parse', '--verify', 'HEAD'])
808
809 cur_branch = self._GetCurrentBranch()
810
811 # Cases:
812 # 0) HEAD is detached. Probably from our initial clone.
813 # - make sure HEAD is contained by a named ref, then update.
814 # Cases 1-4. HEAD is a branch.
815 # 1) current branch is not tracking a remote branch
816 # - try to rebase onto the new hash or branch
817 # 2) current branch is tracking a remote branch with local committed
818 # changes, but the DEPS file switched to point to a hash
819 # - rebase those changes on top of the hash
820 # 3) current branch is tracking a remote branch w/or w/out changes, and
821 # no DEPS switch
822 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
823 # 4) current branch is tracking a remote branch, but DEPS switches to a
824 # different remote branch, and a) current branch has no local changes,
825 # and --force: - checkout new branch b) current branch has local
826 # changes, and --force and --reset: - checkout new branch c) otherwise
827 # exit
828
829 # GetUpstreamBranch returns something like 'refs/remotes/origin/main'
830 # for a tracking branch or 'main' if not a tracking branch (it's based
831 # on a specific rev/hash) or it returns None if it couldn't find an
832 # upstream
833 if cur_branch is None:
834 upstream_branch = None
835 current_type = "detached"
836 logging.debug("Detached HEAD")
837 else:
838 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
839 if not upstream_branch or not upstream_branch.startswith(
840 'refs/remotes'):
841 current_type = "hash"
842 logging.debug(
843 "Current branch is not tracking an upstream (remote)"
844 " branch.")
845 elif upstream_branch.startswith('refs/remotes'):
846 current_type = "branch"
847 else:
848 raise gclient_utils.Error('Invalid Upstream: %s' %
849 upstream_branch)
850
851 self._SetFetchConfig(options)
852
853 # Fetch upstream if we don't already have |revision|.
854 if not scm.GIT.IsValidRevision(
855 self.checkout_path, revision, sha_only=True):
856 self._Fetch(options, prune=options.force)
857
858 if not scm.GIT.IsValidRevision(
859 self.checkout_path, revision, sha_only=True):
860 # Update the remotes first so we have all the refs.
861 remote_output = scm.GIT.Capture(['remote'] + verbose +
862 ['update'],
863 cwd=self.checkout_path)
864 if verbose:
865 self.Print(remote_output)
866
867 revision = self._AutoFetchRef(options, revision)
868
869 # This is a big hammer, debatable if it should even be here...
870 if options.force or options.reset:
871 target = 'HEAD'
872 if options.upstream and upstream_branch:
873 target = upstream_branch
874 self._Scrub(target, options)
875
876 if current_type == 'detached':
877 # case 0
878 # We just did a Scrub, this is as clean as it's going to get. In
879 # particular if HEAD is a commit that contains two versions of the
880 # same file on a case-insensitive filesystem (e.g. 'a' and 'A'),
881 # there's no way to actually "Clean" the checkout; that commit is
882 # uncheckoutable on this system. The best we can do is carry forward
883 # to the checkout step.
884 if not (options.force or options.reset):
885 self._CheckClean(revision)
886 self._CheckDetachedHead(revision, options)
887 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
888 self.Print('Up-to-date; skipping checkout.')
889 else:
890 # 'git checkout' may need to overwrite existing untracked files.
891 # Allow it only when nuclear options are enabled.
892 self._Checkout(
893 options,
894 revision,
895 force=(options.force and options.delete_unversioned_trees),
896 quiet=True,
897 )
898 if not printed_path:
899 self.Print('_____ %s at %s' % (self.relpath, revision),
900 timestamp=False)
901 elif current_type == 'hash':
902 # case 1
903 # Can't find a merge-base since we don't know our upstream. That
904 # makes this command VERY likely to produce a rebase failure. For
905 # now we assume origin is our upstream since that's what the old
906 # behavior was.
907 upstream_branch = self.remote
908 if options.revision or deps_revision:
909 upstream_branch = revision
910 self._AttemptRebase(upstream_branch,
911 file_list,
912 options,
913 printed_path=printed_path,
914 merge=options.merge)
915 printed_path = True
916 elif rev_type == 'hash':
917 # case 2
918 self._AttemptRebase(upstream_branch,
919 file_list,
920 options,
921 newbase=revision,
922 printed_path=printed_path,
923 merge=options.merge)
924 printed_path = True
925 elif remote_ref and ''.join(remote_ref) != upstream_branch:
926 # case 4
927 new_base = ''.join(remote_ref)
928 if not printed_path:
929 self.Print('_____ %s at %s' % (self.relpath, revision),
930 timestamp=False)
931 switch_error = (
932 "Could not switch upstream branch from %s to %s\n" %
933 (upstream_branch, new_base) +
934 "Please use --force or merge or rebase manually:\n" +
935 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
936 "OR git checkout -b <some new branch> %s" % new_base)
937 force_switch = False
938 if options.force:
939 try:
940 self._CheckClean(revision)
941 # case 4a
942 force_switch = True
943 except gclient_utils.Error as e:
944 if options.reset:
945 # case 4b
946 force_switch = True
947 else:
Philipp Thielf2449cd2023-10-20 17:27:25 +0000948 switch_error = '%s\n%s' % (str(e), switch_error)
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000949 if force_switch:
950 self.Print("Switching upstream branch from %s to %s" %
951 (upstream_branch, new_base))
952 switch_branch = 'gclient_' + remote_ref[1]
953 self._Capture(['branch', '-f', switch_branch, new_base])
954 self._Checkout(options, switch_branch, force=True, quiet=True)
955 else:
956 # case 4c
957 raise gclient_utils.Error(switch_error)
958 else:
959 # case 3 - the default case
960 rebase_files = self._GetDiffFilenames(upstream_branch)
961 if verbose:
962 self.Print('Trying fast-forward merge to branch : %s' %
963 upstream_branch)
964 try:
965 merge_args = ['merge']
966 if options.merge:
967 merge_args.append('--ff')
968 else:
969 merge_args.append('--ff-only')
970 merge_args.append(upstream_branch)
971 merge_output = self._Capture(merge_args)
972 except subprocess2.CalledProcessError as e:
973 rebase_files = []
974 if re.search(b'fatal: Not possible to fast-forward, aborting.',
975 e.stderr):
976 if not printed_path:
977 self.Print('_____ %s at %s' % (self.relpath, revision),
978 timestamp=False)
979 printed_path = True
980 while True:
981 if not options.auto_rebase:
982 try:
983 action = self._AskForData(
984 'Cannot %s, attempt to rebase? '
985 '(y)es / (q)uit / (s)kip : ' %
986 ('merge' if options.merge else
987 'fast-forward merge'), options)
988 except ValueError:
989 raise gclient_utils.Error('Invalid Character')
990 if options.auto_rebase or re.match(
991 r'yes|y', action, re.I):
992 self._AttemptRebase(upstream_branch,
993 rebase_files,
994 options,
995 printed_path=printed_path,
996 merge=False)
997 printed_path = True
998 break
999
1000 if re.match(r'quit|q', action, re.I):
1001 raise gclient_utils.Error(
1002 "Can't fast-forward, please merge or "
1003 "rebase manually.\n"
1004 "cd %s && git " % self.checkout_path +
1005 "rebase %s" % upstream_branch)
1006
1007 if re.match(r'skip|s', action, re.I):
1008 self.Print('Skipping %s' % self.relpath)
1009 return
1010
1011 self.Print('Input not recognized')
1012 elif re.match(
1013 b"error: Your local changes to '.*' would be "
1014 b"overwritten by merge. Aborting.\nPlease, commit your "
1015 b"changes or stash them before you can merge.\n",
1016 e.stderr):
1017 if not printed_path:
1018 self.Print('_____ %s at %s' % (self.relpath, revision),
1019 timestamp=False)
1020 printed_path = True
1021 raise gclient_utils.Error(e.stderr.decode('utf-8'))
1022 else:
1023 # Some other problem happened with the merge
1024 logging.error("Error during fast-forward merge in %s!" %
1025 self.relpath)
1026 self.Print(e.stderr.decode('utf-8'))
1027 raise
1028 else:
1029 # Fast-forward merge was successful
1030 if not re.match('Already up-to-date.', merge_output) or verbose:
1031 if not printed_path:
1032 self.Print('_____ %s at %s' % (self.relpath, revision),
1033 timestamp=False)
1034 printed_path = True
1035 self.Print(merge_output.strip())
1036 if not verbose:
1037 # Make the output a little prettier. It's nice to have
1038 # some whitespace between projects when syncing.
1039 self.Print('')
1040
1041 if file_list is not None:
1042 file_list.extend(
1043 [os.path.join(self.checkout_path, f) for f in rebase_files])
1044
1045 # If the rebase generated a conflict, abort and ask user to fix
1046 if self._IsRebasing():
1047 raise gclient_utils.Error(
1048 '\n____ %s at %s\n'
1049 '\nConflict while rebasing this branch.\n'
1050 'Fix the conflict and run gclient again.\n'
1051 'See man git-rebase for details.\n' % (self.relpath, revision))
1052
Michael Spang73fac912019-03-08 18:44:19 +00001053 if verbose:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001054 self.Print('Checked out revision %s' %
1055 self.revinfo(options, (), None),
agable83faed02016-10-24 14:37:10 -07001056 timestamp=False)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001057
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001058 # If --reset and --delete_unversioned_trees are specified, remove any
1059 # untracked directories.
1060 if options.reset and options.delete_unversioned_trees:
1061 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1
1062 # (the merge-base by default), so doesn't include untracked files.
1063 # So we use 'git ls-files --directory --others --exclude-standard'
1064 # here directly.
1065 paths = scm.GIT.Capture([
1066 '-c', 'core.quotePath=false', 'ls-files', '--directory',
1067 '--others', '--exclude-standard'
1068 ], self.checkout_path)
1069 for path in (p for p in paths.splitlines() if p.endswith('/')):
1070 full_path = os.path.join(self.checkout_path, path)
1071 if not os.path.islink(full_path):
1072 self.Print('_____ removing unversioned directory %s' % path)
1073 gclient_utils.rmtree(full_path)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001074
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001075 return self._Capture(['rev-parse', '--verify', 'HEAD'])
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001076
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001077 def revert(self, options, _args, file_list):
1078 """Reverts local modifications.
msb@chromium.orge28e4982009-09-25 20:51:45 +00001079
1080 All reverted files will be appended to file_list.
1081 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001082 if not os.path.isdir(self.checkout_path):
1083 # revert won't work if the directory doesn't exist. It needs to
1084 # checkout instead.
1085 self.Print('_____ %s is missing, syncing instead' % self.relpath)
1086 # Don't reuse the args.
1087 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001088
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001089 default_rev = "refs/heads/main"
1090 if options.upstream:
1091 if self._GetCurrentBranch():
1092 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
1093 default_rev = upstream_branch or default_rev
1094 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
1095 if not deps_revision:
1096 deps_revision = default_rev
1097 if deps_revision.startswith('refs/heads/'):
1098 deps_revision = deps_revision.replace('refs/heads/',
1099 self.remote + '/')
1100 try:
1101 deps_revision = self.GetUsableRev(deps_revision, options)
1102 except NoUsableRevError as e:
1103 # If the DEPS entry's url and hash changed, try to update the
1104 # origin. See also http://crbug.com/520067.
1105 logging.warning(
1106 "Couldn't find usable revision, will retrying to update instead: %s",
Philipp Thielf2449cd2023-10-20 17:27:25 +00001107 str(e))
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001108 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001109
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001110 if file_list is not None:
1111 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001112
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001113 self._Scrub(deps_revision, options)
1114 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001115
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001116 if file_list is not None:
1117 file_list.extend(
1118 [os.path.join(self.checkout_path, f) for f in files])
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001119
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001120 def revinfo(self, _options, _args, _file_list):
1121 """Returns revision"""
1122 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +00001123
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001124 def runhooks(self, options, args, file_list):
1125 self.status(options, args, file_list)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001126
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001127 def status(self, options, _args, file_list):
1128 """Display status information."""
1129 if not os.path.isdir(self.checkout_path):
1130 self.Print('________ couldn\'t run status in %s:\n'
1131 'The directory does not exist.' % self.checkout_path)
1132 else:
1133 merge_base = []
1134 if self.url:
1135 _, base_rev = gclient_utils.SplitUrlRevision(self.url)
1136 if base_rev:
1137 if base_rev.startswith('refs/'):
1138 base_rev = self._ref_to_remote_ref(base_rev)
1139 merge_base = [base_rev]
1140 self._Run(['-c', 'core.quotePath=false', 'diff', '--name-status'] +
1141 merge_base,
1142 options,
1143 always_show_header=options.verbose)
1144 if file_list is not None:
1145 files = self._GetDiffFilenames(
1146 merge_base[0] if merge_base else None)
1147 file_list.extend(
1148 [os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +00001149
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001150 def GetUsableRev(self, rev, options):
1151 """Finds a useful revision for this repository."""
1152 sha1 = None
1153 if not os.path.isdir(self.checkout_path):
1154 raise NoUsableRevError(
1155 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -07001156
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001157 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1158 sha1 = rev
1159 else:
1160 # May exist in origin, but we don't have it yet, so fetch and look
1161 # again.
1162 self._Fetch(options)
1163 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1164 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001165
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001166 if not sha1:
1167 raise NoUsableRevError(
1168 'Hash %s does not appear to be a valid hash in this repo.' %
1169 rev)
smutae7ea312016-07-18 11:59:41 -07001170
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001171 return sha1
smutae7ea312016-07-18 11:59:41 -07001172
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001173 def GetGitBackupDirPath(self):
1174 """Returns the path where the .git folder for the current project can be
primiano@chromium.org1c127382015-02-17 11:15:40 +00001175 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001176 return os.path.join(self._root_dir,
1177 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
primiano@chromium.org1c127382015-02-17 11:15:40 +00001178
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001179 def _GetMirror(self, url, options, revision=None, revision_ref=None):
1180 """Get a git_cache.Mirror object for the argument url."""
1181 if not self.cache_dir:
1182 return None
1183 mirror_kwargs = {
1184 'print_func': self.filter,
1185 'refs': [],
1186 'commits': [],
1187 }
1188 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1189 mirror_kwargs['refs'].append('refs/branch-heads/*')
1190 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
1191 mirror_kwargs['refs'].append(revision_ref)
1192 if hasattr(options, 'with_tags') and options.with_tags:
1193 mirror_kwargs['refs'].append('refs/tags/*')
1194 elif revision_ref and revision_ref.startswith('refs/tags/'):
1195 mirror_kwargs['refs'].append(revision_ref)
1196 if revision and not revision.startswith('refs/'):
1197 mirror_kwargs['commits'].append(revision)
1198 return git_cache.Mirror(url, **mirror_kwargs)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001199
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001200 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
1201 """Update a git mirror by fetching the latest commits from the remote,
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001202 unless mirror already contains revision whose type is sha1 hash.
1203 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001204 if rev_type == 'hash' and mirror.contains_revision(revision):
1205 if options.verbose:
1206 self.Print('skipping mirror update, it has rev=%s already' %
1207 revision,
1208 timestamp=False)
1209 return
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001210
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001211 if getattr(options, 'shallow', False):
1212 depth = 10000
1213 else:
1214 depth = None
1215 mirror.populate(verbose=options.verbose,
1216 bootstrap=not getattr(options, 'no_bootstrap', False),
1217 depth=depth,
1218 lock_timeout=getattr(options, 'lock_timeout', 0))
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001219
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001220 def _Clone(self, revision, url, options):
1221 """Clone a git repository from the given URL.
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001222
msb@chromium.org786fb682010-06-02 15:16:23 +00001223 Once we've cloned the repo, we checkout a working branch if the specified
1224 revision is a branch head. If it is a tag or a specific commit, then we
1225 leave HEAD detached as it makes future updates simpler -- in this case the
1226 user should first create a new branch or switch to an existing branch before
1227 making changes in the repo."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001228 in_cog_workspace = self._IsCog()
Joanna Wang1a977bd2022-06-02 21:51:17 +00001229
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001230 if self.print_outbuf:
1231 print_stdout = True
1232 filter_fn = None
1233 else:
1234 print_stdout = False
1235 filter_fn = self.filter
Joanna Wang1a977bd2022-06-02 21:51:17 +00001236
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001237 if not options.verbose:
1238 # git clone doesn't seem to insert a newline properly before
1239 # printing to stdout
1240 self.Print('')
Joanna Wang1a977bd2022-06-02 21:51:17 +00001241
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001242 # If the parent directory does not exist, Git clone on Windows will not
1243 # create it, so we need to do it manually.
1244 parent_dir = os.path.dirname(self.checkout_path)
1245 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001246
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001247 if in_cog_workspace:
1248 clone_cmd = ['citc', 'clone-repo', url, self.checkout_path]
1249 clone_cmd.append(
1250 gclient_utils.ExtractRefName(self.remote, revision) or revision)
1251 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 except:
1259 traceback.print_exc(file=self.out_fh)
1260 raise
1261 self._SetFetchConfig(options)
1262 elif hasattr(options, 'no_history') and options.no_history:
1263 self._Run(['init', self.checkout_path], options, cwd=self._root_dir)
1264 self._Run(['remote', 'add', 'origin', url], options)
1265 revision = self._AutoFetchRef(options, revision, depth=1)
1266 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1267 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
1268 else:
1269 cfg = gclient_utils.DefaultIndexPackConfig(url)
1270 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
1271 if self.cache_dir:
1272 clone_cmd.append('--shared')
1273 if options.verbose:
1274 clone_cmd.append('--verbose')
1275 clone_cmd.append(url)
1276 tmp_dir = tempfile.mkdtemp(prefix='_gclient_%s_' %
1277 os.path.basename(self.checkout_path),
1278 dir=parent_dir)
1279 clone_cmd.append(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001280
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001281 try:
1282 self._Run(clone_cmd,
1283 options,
1284 cwd=self._root_dir,
1285 retry=True,
1286 print_stdout=print_stdout,
1287 filter_fn=filter_fn)
1288 logging.debug(
1289 'Cloned into temporary dir, moving to checkout_path')
1290 gclient_utils.safe_makedirs(self.checkout_path)
1291 gclient_utils.safe_rename(
1292 os.path.join(tmp_dir, '.git'),
1293 os.path.join(self.checkout_path, '.git'))
1294 except:
1295 traceback.print_exc(file=self.out_fh)
1296 raise
1297 finally:
1298 if os.listdir(tmp_dir):
1299 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
1300 gclient_utils.rmtree(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001301
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001302 self._SetFetchConfig(options)
1303 self._Fetch(options, prune=options.force)
1304 revision = self._AutoFetchRef(options, revision)
1305 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1306 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001307
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001308 if self._GetCurrentBranch() is None:
1309 # Squelch git's very verbose detached HEAD warning and use our own
1310 self.Print((
1311 'Checked out %s to a detached HEAD. Before making any commits\n'
1312 'in this repo, you should use \'git checkout <branch>\' to switch \n'
1313 'to an existing branch or use \'git checkout %s -b <branch>\' to\n'
1314 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001315
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001316 def _AskForData(self, prompt, options):
1317 if options.jobs > 1:
1318 self.Print(prompt)
1319 raise gclient_utils.Error("Background task requires input. Rerun "
1320 "gclient with --jobs=1 so that\n"
1321 "interaction is possible.")
1322 return gclient_utils.AskForData(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001323
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001324 def _AttemptRebase(self,
1325 upstream,
1326 files,
1327 options,
1328 newbase=None,
1329 branch=None,
1330 printed_path=False,
1331 merge=False):
1332 """Attempt to rebase onto either upstream or, if specified, newbase."""
1333 if files is not None:
1334 files.extend(self._GetDiffFilenames(upstream))
1335 revision = upstream
1336 if newbase:
1337 revision = newbase
1338 action = 'merge' if merge else 'rebase'
1339 if not printed_path:
1340 self.Print('_____ %s : Attempting %s onto %s...' %
1341 (self.relpath, action, revision))
1342 printed_path = True
1343 else:
1344 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001345
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001346 if merge:
1347 merge_output = self._Capture(['merge', revision])
1348 if options.verbose:
1349 self.Print(merge_output)
1350 return
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001351
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001352 # Build the rebase command here using the args
1353 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1354 rebase_cmd = ['rebase']
1355 if options.verbose:
1356 rebase_cmd.append('--verbose')
1357 if newbase:
1358 rebase_cmd.extend(['--onto', newbase])
1359 rebase_cmd.append(upstream)
1360 if branch:
1361 rebase_cmd.append(branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001362
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001363 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001364 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001365 except subprocess2.CalledProcessError as e:
1366 if (re.match(
1367 br'cannot rebase: you have unstaged changes', e.stderr
1368 ) or re.match(
1369 br'cannot rebase: your index contains uncommitted changes',
1370 e.stderr)):
1371 while True:
1372 rebase_action = self._AskForData(
1373 'Cannot rebase because of unstaged changes.\n'
1374 '\'git reset --hard HEAD\' ?\n'
1375 'WARNING: destroys any uncommitted work in your current branch!'
1376 ' (y)es / (q)uit / (s)how : ', options)
1377 if re.match(r'yes|y', rebase_action, re.I):
1378 self._Scrub('HEAD', options)
1379 # Should this be recursive?
1380 rebase_output = scm.GIT.Capture(rebase_cmd,
1381 cwd=self.checkout_path)
1382 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001383
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001384 if re.match(r'quit|q', rebase_action, re.I):
1385 raise gclient_utils.Error(
1386 "Please merge or rebase manually\n"
1387 "cd %s && git " % self.checkout_path +
1388 "%s" % ' '.join(rebase_cmd))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001389
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001390 if re.match(r'show|s', rebase_action, re.I):
1391 self.Print('%s' % e.stderr.decode('utf-8').strip())
1392 continue
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001393
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001394 gclient_utils.Error("Input not recognized")
1395 continue
1396 elif re.search(br'^CONFLICT', e.stdout, re.M):
1397 raise gclient_utils.Error(
1398 "Conflict while rebasing this branch.\n"
1399 "Fix the conflict and run gclient again.\n"
1400 "See 'man git-rebase' for details.\n")
1401 else:
1402 self.Print(e.stdout.decode('utf-8').strip())
1403 self.Print('Rebase produced error output:\n%s' %
1404 e.stderr.decode('utf-8').strip())
1405 raise gclient_utils.Error(
1406 "Unrecognized error, please merge or rebase "
1407 "manually.\ncd %s && git " % self.checkout_path +
1408 "%s" % ' '.join(rebase_cmd))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001409
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001410 self.Print(rebase_output.strip())
1411 if not options.verbose:
1412 # Make the output a little prettier. It's nice to have some
1413 # whitespace between projects when syncing.
1414 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001415
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001416 @staticmethod
1417 def _CheckMinVersion(min_version):
1418 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1419 if not ok:
1420 raise gclient_utils.Error('git version %s < minimum required %s' %
1421 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001422
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001423 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
1424 # Special case handling if all 3 conditions are met:
1425 # * the mirros have recently changed, but deps destination remains same,
1426 # * the git histories of mirrors are conflicting. * git cache is used
1427 # This manifests itself in current checkout having invalid HEAD commit
1428 # on most git operations. Since git cache is used, just deleted the .git
1429 # folder, and re-create it by cloning.
1430 try:
1431 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1432 except subprocess2.CalledProcessError as e:
1433 if (b'fatal: bad object HEAD' in e.stderr and self.cache_dir
1434 and self.cache_dir in url):
1435 self.Print(
1436 ('Likely due to DEPS change with git cache_dir, '
1437 'the current commit points to no longer existing object.\n'
1438 '%s' % e))
1439 self._DeleteOrMove(options.force)
1440 self._Clone(revision, url, options)
1441 else:
1442 raise
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001443
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001444 def _IsRebasing(self):
1445 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git
1446 # doesn't have a plumbing command to determine whether a rebase is in
1447 # progress, so for now emualate (more-or-less) git-rebase.sh /
1448 # git-completion.bash
1449 g = os.path.join(self.checkout_path, '.git')
1450 return (os.path.isdir(os.path.join(g, "rebase-merge"))
1451 or os.path.isdir(os.path.join(g, "rebase-apply")))
msb@chromium.org786fb682010-06-02 15:16:23 +00001452
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001453 def _CheckClean(self, revision):
1454 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1455 if os.path.exists(lockfile):
1456 raise gclient_utils.Error(
1457 '\n____ %s at %s\n'
1458 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1459 '\tIf no git executable is running, then clean up %r and try again.\n'
1460 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001461
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001462 # Make sure the tree is clean; see git-rebase.sh for reference
1463 try:
1464 scm.GIT.Capture(
1465 ['update-index', '--ignore-submodules', '--refresh'],
1466 cwd=self.checkout_path)
1467 except subprocess2.CalledProcessError:
1468 raise gclient_utils.Error(
1469 '\n____ %s at %s\n'
1470 '\tYou have unstaged changes.\n'
1471 '\tcd into %s, run git status to see changes,\n'
1472 '\tand commit, stash, or reset.\n' %
1473 (self.relpath, revision, self.relpath))
1474 try:
1475 scm.GIT.Capture([
1476 'diff-index', '--cached', '--name-status', '-r',
1477 '--ignore-submodules', 'HEAD', '--'
1478 ],
1479 cwd=self.checkout_path)
1480 except subprocess2.CalledProcessError:
1481 raise gclient_utils.Error(
1482 '\n____ %s at %s\n'
1483 '\tYour index contains uncommitted changes\n'
1484 '\tcd into %s, run git status to see changes,\n'
1485 '\tand commit, stash, or reset.\n' %
1486 (self.relpath, revision, self.relpath))
msb@chromium.org786fb682010-06-02 15:16:23 +00001487
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001488 def _CheckDetachedHead(self, revision, _options):
1489 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1490 # reference by a commit). If not, error out -- most likely a rebase is
1491 # in progress, try to detect so we can give a better error.
1492 try:
1493 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1494 cwd=self.checkout_path)
1495 except subprocess2.CalledProcessError:
1496 # Commit is not contained by any rev. See if the user is rebasing:
1497 if self._IsRebasing():
1498 # Punt to the user
1499 raise gclient_utils.Error(
1500 '\n____ %s at %s\n'
1501 '\tAlready in a conflict, i.e. (no branch).\n'
1502 '\tFix the conflict and run gclient again.\n'
1503 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1504 '\tSee man git-rebase for details.\n' %
1505 (self.relpath, revision))
1506 # Let's just save off the commit so we can proceed.
1507 name = ('saved-by-gclient-' +
1508 self._Capture(['rev-parse', '--short', 'HEAD']))
1509 self._Capture(['branch', '-f', name])
1510 self.Print(
1511 '_____ found an unreferenced commit and saved it as \'%s\'' %
1512 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001513
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001514 def _GetCurrentBranch(self):
1515 # Returns name of current branch or None for detached HEAD
1516 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
1517 if branch == 'HEAD':
1518 return None
1519 return branch
msb@chromium.org5bde4852009-12-14 16:47:12 +00001520
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001521 def _Capture(self, args, **kwargs):
1522 set_git_dir = 'cwd' not in kwargs
1523 kwargs.setdefault('cwd', self.checkout_path)
1524 kwargs.setdefault('stderr', subprocess2.PIPE)
1525 strip = kwargs.pop('strip', True)
1526 env = scm.GIT.ApplyEnvVars(kwargs)
1527 # If an explicit cwd isn't set, then default to the .git/ subdir so we
1528 # get stricter behavior. This can be useful in cases of slight
1529 # corruption -- we don't accidentally go corrupting parent git checks
1530 # too. See https://crbug.com/1000825 for an example.
1531 if set_git_dir:
Gavin Mak7f5b53f2023-09-07 18:13:01 +00001532 env.setdefault(
1533 'GIT_DIR',
1534 os.path.abspath(os.path.join(self.checkout_path, '.git')))
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001535 ret = subprocess2.check_output(['git'] + args, env=env,
1536 **kwargs).decode('utf-8')
1537 if strip:
1538 ret = ret.strip()
1539 self.Print('Finished running: %s %s' % ('git', ' '.join(args)))
1540 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001541
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001542 def _Checkout(self, options, ref, force=False, quiet=None):
1543 """Performs a 'git-checkout' operation.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001544
1545 Args:
1546 options: The configured option set
1547 ref: (str) The branch/commit to checkout
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001548 quiet: (bool/None) Whether or not the checkout should pass '--quiet'; if
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001549 'None', the behavior is inferred from 'options.verbose'.
1550 Returns: (str) The output of the checkout operation
1551 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001552 if quiet is None:
1553 quiet = (not options.verbose)
1554 checkout_args = ['checkout']
1555 if force:
1556 checkout_args.append('--force')
1557 if quiet:
1558 checkout_args.append('--quiet')
1559 checkout_args.append(ref)
1560 return self._Capture(checkout_args)
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001561
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001562 def _Fetch(self,
1563 options,
1564 remote=None,
1565 prune=False,
1566 quiet=False,
1567 refspec=None,
1568 depth=None):
1569 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
1570 # When updating, the ref is modified to be a remote ref .
1571 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1572 # Try to reverse that mapping.
1573 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1574 if original_ref:
1575 refspec = original_ref + ':' + refspec
1576 # When a mirror is configured, it only fetches
1577 # refs/{heads,branch-heads,tags}/*.
1578 # If asked to fetch other refs, we must fetch those directly from
1579 # the repository, and not from the mirror.
1580 if not original_ref.startswith(
1581 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1582 remote, _ = gclient_utils.SplitUrlRevision(self.url)
1583 fetch_cmd = cfg + [
1584 'fetch',
1585 remote or self.remote,
1586 ]
1587 if refspec:
1588 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001589
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001590 if prune:
1591 fetch_cmd.append('--prune')
1592 if options.verbose:
1593 fetch_cmd.append('--verbose')
1594 if not hasattr(options, 'with_tags') or not options.with_tags:
1595 fetch_cmd.append('--no-tags')
1596 elif quiet:
1597 fetch_cmd.append('--quiet')
1598 if depth:
1599 fetch_cmd.append('--depth=' + str(depth))
1600 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001601
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001602 def _SetFetchConfig(self, options):
1603 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001604 if requested."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001605 if options.force or options.reset:
1606 try:
1607 self._Run(
1608 ['config', '--unset-all',
1609 'remote.%s.fetch' % self.remote], options)
1610 self._Run([
1611 'config',
1612 'remote.%s.fetch' % self.remote,
1613 '+refs/heads/*:refs/remotes/%s/*' % self.remote
1614 ], options)
1615 except subprocess2.CalledProcessError as e:
1616 # If exit code was 5, it means we attempted to unset a config
1617 # that didn't exist. Ignore it.
1618 if e.returncode != 5:
1619 raise
1620 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1621 config_cmd = [
1622 'config',
1623 'remote.%s.fetch' % self.remote,
1624 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1625 '^\\+refs/branch-heads/\\*:.*$'
1626 ]
1627 self._Run(config_cmd, options)
1628 if hasattr(options, 'with_tags') and options.with_tags:
1629 config_cmd = [
1630 'config',
1631 'remote.%s.fetch' % self.remote, '+refs/tags/*:refs/tags/*',
1632 '^\\+refs/tags/\\*:.*$'
1633 ]
1634 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001635
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001636 def _AutoFetchRef(self, options, revision, depth=None):
1637 """Attempts to fetch |revision| if not available in local repo.
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001638
1639 Returns possibly updated revision."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001640 if not scm.GIT.IsValidRevision(self.checkout_path, revision):
1641 self._Fetch(options, refspec=revision, depth=depth)
1642 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1643 return revision
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001644
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001645 def _Run(self, args, options, **kwargs):
1646 # Disable 'unused options' warning | pylint: disable=unused-argument
1647 kwargs.setdefault('cwd', self.checkout_path)
1648 kwargs.setdefault('filter_fn', self.filter)
1649 kwargs.setdefault('show_header', True)
1650 env = scm.GIT.ApplyEnvVars(kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +00001651
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001652 cmd = ['git'] + args
1653 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001654
1655
1656class CipdPackage(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001657 """A representation of a single CIPD package."""
1658 def __init__(self, name, version, authority_for_subdir):
1659 self._authority_for_subdir = authority_for_subdir
1660 self._name = name
1661 self._version = version
John Budorick0f7b2002018-01-19 15:46:17 -08001662
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001663 @property
1664 def authority_for_subdir(self):
1665 """Whether this package has authority to act on behalf of its subdir.
John Budorick0f7b2002018-01-19 15:46:17 -08001666
1667 Some operations should only be performed once per subdirectory. A package
1668 that has authority for its subdirectory is the only package that should
1669 perform such operations.
1670
1671 Returns:
1672 bool; whether this package has subdir authority.
1673 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001674 return self._authority_for_subdir
John Budorick0f7b2002018-01-19 15:46:17 -08001675
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001676 @property
1677 def name(self):
1678 return self._name
John Budorick0f7b2002018-01-19 15:46:17 -08001679
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001680 @property
1681 def version(self):
1682 return self._version
John Budorick0f7b2002018-01-19 15:46:17 -08001683
1684
1685class CipdRoot(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001686 """A representation of a single CIPD root."""
Yiwei Zhang52353702023-09-18 15:53:52 +00001687 def __init__(self, root_dir, service_url, log_level=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001688 self._all_packages = set()
1689 self._mutator_lock = threading.Lock()
1690 self._packages_by_subdir = collections.defaultdict(list)
1691 self._root_dir = root_dir
1692 self._service_url = service_url
1693 self._resolved_packages = None
Yiwei Zhang52353702023-09-18 15:53:52 +00001694 self._log_level = log_level or 'error'
John Budorick0f7b2002018-01-19 15:46:17 -08001695
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001696 def add_package(self, subdir, package, version):
1697 """Adds a package to this CIPD root.
John Budorick0f7b2002018-01-19 15:46:17 -08001698
1699 As far as clients are concerned, this grants both root and subdir authority
1700 to packages arbitrarily. (The implementation grants root authority to the
1701 first package added and subdir authority to the first package added for that
1702 subdir, but clients should not depend on or expect that behavior.)
1703
1704 Args:
1705 subdir: str; relative path to where the package should be installed from
1706 the cipd root directory.
1707 package: str; the cipd package name.
1708 version: str; the cipd package version.
1709 Returns:
1710 CipdPackage; the package that was created and added to this root.
1711 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001712 with self._mutator_lock:
1713 cipd_package = CipdPackage(package, version,
1714 not self._packages_by_subdir[subdir])
1715 self._all_packages.add(cipd_package)
1716 self._packages_by_subdir[subdir].append(cipd_package)
1717 return cipd_package
John Budorick0f7b2002018-01-19 15:46:17 -08001718
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001719 def packages(self, subdir):
1720 """Get the list of configured packages for the given subdir."""
1721 return list(self._packages_by_subdir[subdir])
John Budorick0f7b2002018-01-19 15:46:17 -08001722
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001723 def resolved_packages(self):
1724 if not self._resolved_packages:
1725 self._resolved_packages = self.ensure_file_resolve()
1726 return self._resolved_packages
Dan Le Febvre456d0852023-05-24 23:43:40 +00001727
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001728 def clobber(self):
1729 """Remove the .cipd directory.
John Budorick0f7b2002018-01-19 15:46:17 -08001730
1731 This is useful for forcing ensure to redownload and reinitialize all
1732 packages.
1733 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001734 with self._mutator_lock:
1735 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
1736 try:
1737 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1738 except OSError:
1739 if os.path.exists(cipd_cache_dir):
1740 raise
John Budorick0f7b2002018-01-19 15:46:17 -08001741
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001742 def expand_package_name(self, package_name_string, **kwargs):
1743 """Run `cipd expand-package-name`.
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001744
1745 CIPD package names can be declared with placeholder variables
1746 such as '${platform}', this cmd will return the package name
1747 with the variables resolved. The resolution is based on the host
1748 the command is executing on.
1749 """
1750
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001751 kwargs.setdefault('stderr', subprocess2.PIPE)
1752 cmd = ['cipd', 'expand-package-name', package_name_string]
1753 ret = subprocess2.check_output(cmd, **kwargs).decode('utf-8')
1754 return ret.strip()
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001755
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001756 @contextlib.contextmanager
1757 def _create_ensure_file(self):
1758 try:
1759 contents = '$ParanoidMode CheckPresence\n'
1760 # TODO(crbug/1329641): Remove once cipd packages have been updated
1761 # to always be created in copy mode.
1762 contents += '$OverrideInstallMode copy\n\n'
1763 for subdir, packages in sorted(self._packages_by_subdir.items()):
1764 contents += '@Subdir %s\n' % subdir
1765 for package in sorted(packages, key=lambda p: p.name):
1766 contents += '%s %s\n' % (package.name, package.version)
1767 contents += '\n'
1768 ensure_file = None
1769 with tempfile.NamedTemporaryFile(suffix='.ensure',
1770 delete=False,
1771 mode='wb') as ensure_file:
1772 ensure_file.write(contents.encode('utf-8', 'replace'))
1773 yield ensure_file.name
1774 finally:
1775 if ensure_file is not None and os.path.exists(ensure_file.name):
1776 os.remove(ensure_file.name)
John Budorick0f7b2002018-01-19 15:46:17 -08001777
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001778 def ensure(self):
1779 """Run `cipd ensure`."""
1780 with self._mutator_lock:
1781 with self._create_ensure_file() as ensure_file:
1782 cmd = [
1783 'cipd',
1784 'ensure',
1785 '-log-level',
Yiwei Zhang52353702023-09-18 15:53:52 +00001786 self._log_level,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001787 '-root',
1788 self.root_dir,
1789 '-ensure-file',
1790 ensure_file,
1791 ]
1792 gclient_utils.CheckCallAndFilter(cmd,
1793 print_stdout=True,
1794 show_header=True)
John Budorick0f7b2002018-01-19 15:46:17 -08001795
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001796 @contextlib.contextmanager
1797 def _create_ensure_file_for_resolve(self):
1798 try:
1799 contents = '$ResolvedVersions %s\n' % os.devnull
1800 for subdir, packages in sorted(self._packages_by_subdir.items()):
1801 contents += '@Subdir %s\n' % subdir
1802 for package in sorted(packages, key=lambda p: p.name):
1803 contents += '%s %s\n' % (package.name, package.version)
1804 contents += '\n'
1805 ensure_file = None
1806 with tempfile.NamedTemporaryFile(suffix='.ensure',
1807 delete=False,
1808 mode='wb') as ensure_file:
1809 ensure_file.write(contents.encode('utf-8', 'replace'))
1810 yield ensure_file.name
1811 finally:
1812 if ensure_file is not None and os.path.exists(ensure_file.name):
1813 os.remove(ensure_file.name)
Dan Le Febvre456d0852023-05-24 23:43:40 +00001814
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001815 def _create_resolved_file(self):
1816 return tempfile.NamedTemporaryFile(suffix='.resolved',
1817 delete=False,
1818 mode='wb')
Dan Le Febvre456d0852023-05-24 23:43:40 +00001819
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001820 def ensure_file_resolve(self):
1821 """Run `cipd ensure-file-resolve`."""
1822 with self._mutator_lock:
1823 with self._create_resolved_file() as output_file:
1824 with self._create_ensure_file_for_resolve() as ensure_file:
1825 cmd = [
1826 'cipd',
1827 'ensure-file-resolve',
1828 '-log-level',
Yiwei Zhang52353702023-09-18 15:53:52 +00001829 self._log_level,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001830 '-ensure-file',
1831 ensure_file,
1832 '-json-output',
1833 output_file.name,
1834 ]
1835 gclient_utils.CheckCallAndFilter(cmd,
1836 print_stdout=False,
1837 show_header=False)
1838 with open(output_file.name) as f:
1839 output_json = json.load(f)
1840 return output_json.get('result', {})
Dan Le Febvre456d0852023-05-24 23:43:40 +00001841
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001842 def run(self, command):
1843 if command == 'update':
1844 self.ensure()
1845 elif command == 'revert':
1846 self.clobber()
1847 self.ensure()
John Budorickd3ba72b2018-03-20 12:27:42 -07001848
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001849 def created_package(self, package):
1850 """Checks whether this root created the given package.
John Budorick0f7b2002018-01-19 15:46:17 -08001851
1852 Args:
1853 package: CipdPackage; the package to check.
1854 Returns:
1855 bool; whether this root created the given package.
1856 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001857 return package in self._all_packages
John Budorick0f7b2002018-01-19 15:46:17 -08001858
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001859 @property
1860 def root_dir(self):
1861 return self._root_dir
John Budorick0f7b2002018-01-19 15:46:17 -08001862
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001863 @property
1864 def service_url(self):
1865 return self._service_url
John Budorick0f7b2002018-01-19 15:46:17 -08001866
1867
1868class CipdWrapper(SCMWrapper):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001869 """Wrapper for CIPD.
John Budorick0f7b2002018-01-19 15:46:17 -08001870
1871 Currently only supports chrome-infra-packages.appspot.com.
1872 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001873 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001874
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001875 def __init__(self,
1876 url=None,
1877 root_dir=None,
1878 relpath=None,
1879 out_fh=None,
1880 out_cb=None,
1881 root=None,
1882 package=None):
1883 super(CipdWrapper, self).__init__(url=url,
1884 root_dir=root_dir,
1885 relpath=relpath,
1886 out_fh=out_fh,
1887 out_cb=out_cb)
1888 assert root.created_package(package)
1889 self._package = package
1890 self._root = root
John Budorick0f7b2002018-01-19 15:46:17 -08001891
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001892 #override
1893 def GetCacheMirror(self):
1894 return None
John Budorick0f7b2002018-01-19 15:46:17 -08001895
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001896 #override
1897 def GetActualRemoteURL(self, options):
1898 return self._root.service_url
John Budorick0f7b2002018-01-19 15:46:17 -08001899
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001900 #override
1901 def DoesRemoteURLMatch(self, options):
1902 del options
1903 return True
John Budorick0f7b2002018-01-19 15:46:17 -08001904
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001905 def revert(self, options, args, file_list):
1906 """Does nothing.
John Budorickd3ba72b2018-03-20 12:27:42 -07001907
1908 CIPD packages should be reverted at the root by running
1909 `CipdRoot.run('revert')`.
1910 """
John Budorick0f7b2002018-01-19 15:46:17 -08001911
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001912 def diff(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 pack(self, options, args, file_list):
1916 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001917
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001918 def revinfo(self, options, args, file_list):
1919 """Grab the instance ID."""
1920 try:
1921 tmpdir = tempfile.mkdtemp()
1922 # Attempt to get instance_id from the root resolved cache.
1923 # Resolved cache will not match on any CIPD packages with
1924 # variables such as ${platform}, they will fall back to
1925 # the slower method below.
1926 resolved = self._root.resolved_packages()
1927 if resolved:
1928 # CIPD uses POSIX separators across all platforms, so
1929 # replace any Windows separators.
1930 path_split = self.relpath.replace(os.sep, "/").split(":")
1931 if len(path_split) > 1:
1932 src_path, package = path_split
1933 if src_path in resolved:
1934 for resolved_package in resolved[src_path]:
1935 if package == resolved_package.get(
1936 'pin', {}).get('package'):
1937 return resolved_package.get(
1938 'pin', {}).get('instance_id')
Dan Le Febvre456d0852023-05-24 23:43:40 +00001939
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001940 describe_json_path = os.path.join(tmpdir, 'describe.json')
1941 cmd = [
1942 'cipd', 'describe', self._package.name, '-log-level', 'error',
1943 '-version', self._package.version, '-json-output',
1944 describe_json_path
1945 ]
1946 gclient_utils.CheckCallAndFilter(cmd)
1947 with open(describe_json_path) as f:
1948 describe_json = json.load(f)
1949 return describe_json.get('result', {}).get('pin',
1950 {}).get('instance_id')
1951 finally:
1952 gclient_utils.rmtree(tmpdir)
John Budorick0f7b2002018-01-19 15:46:17 -08001953
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001954 def status(self, options, args, file_list):
1955 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001956
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001957 def update(self, options, args, file_list):
1958 """Does nothing.
John Budorickd3ba72b2018-03-20 12:27:42 -07001959
1960 CIPD packages should be updated at the root by running
1961 `CipdRoot.run('update')`.
1962 """