blob: 7e14197a6e700224b3751d5a30a60875716bf8e7 [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)
618 return return_val
Joanna Wange1753f62023-06-26 14:32:43 +0000619
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000620 return wrapper
Joanna Wange1753f62023-06-26 14:32:43 +0000621
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000622 @set_config
623 def update(self, options, args, file_list):
624 """Runs git to update or transparently checkout the working copy.
msb@chromium.orge28e4982009-09-25 20:51:45 +0000625
626 All updated files will be appended to file_list.
627
628 Raises:
629 Error: if can't get URL for relative path.
630 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000631 if args:
632 raise gclient_utils.Error("Unsupported argument(s): %s" %
633 ",".join(args))
msb@chromium.orge28e4982009-09-25 20:51:45 +0000634
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000635 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000636
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000637 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
638 revision = deps_revision
639 managed = True
640 if options.revision:
641 # Override the revision number.
642 revision = str(options.revision)
643 if revision == 'unmanaged':
644 # Check again for a revision in case an initial ref was specified
645 # in the url, for example bla.git@refs/heads/custombranch
646 revision = deps_revision
647 managed = False
648 if not revision:
649 # If a dependency is not pinned, track the default remote branch.
650 revision = scm.GIT.GetRemoteHeadRef(self.checkout_path, self.url,
651 self.remote)
652 if revision.startswith('origin/'):
653 revision = 'refs/remotes/' + revision
msb@chromium.orge28e4982009-09-25 20:51:45 +0000654
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000655 if managed and platform.system() == 'Windows':
656 self._DisableHooks()
szager@chromium.org8a139702014-06-20 15:55:01 +0000657
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000658 printed_path = False
659 verbose = []
660 if options.verbose:
661 self.Print('_____ %s at %s' % (self.relpath, revision),
662 timestamp=False)
663 verbose = ['--verbose']
664 printed_path = True
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000665
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000666 revision_ref = revision
667 if ':' in revision:
668 revision_ref, _, revision = revision.partition(':')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000669
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000670 if revision_ref.startswith('refs/branch-heads'):
671 options.with_branch_heads = True
Edward Lesmes8073a502020-04-15 02:11:14 +0000672
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000673 mirror = self._GetMirror(url, options, revision, revision_ref)
674 if mirror:
675 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000676
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000677 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
678 if remote_ref:
679 # Rewrite remote refs to their local equivalents.
680 revision = ''.join(remote_ref)
681 rev_type = "branch"
682 elif revision.startswith('refs/'):
683 # Local branch? We probably don't want to support, since DEPS should
684 # always specify branches as they are in the upstream repo.
685 rev_type = "branch"
Aravind Vasudevancf465852023-03-29 16:47:12 +0000686 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000687 # hash is also a tag, only make a distinction at checkout
688 rev_type = "hash"
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000689
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000690 # If we are going to introduce a new project, there is a possibility
691 # that we are syncing back to a state where the project was originally a
692 # sub-project rolled by DEPS (realistic case: crossing the Blink merge
693 # point syncing backwards, when Blink was a DEPS entry and not part of
694 # src.git). In such case, we might have a backup of the former .git
695 # folder, which can be used to avoid re-fetching the entire repo again
696 # (useful for bisects).
697 backup_dir = self.GetGitBackupDirPath()
698 target_dir = os.path.join(self.checkout_path, '.git')
699 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
700 gclient_utils.safe_makedirs(self.checkout_path)
701 os.rename(backup_dir, target_dir)
702 # Reset to a clean state
703 self._Scrub('HEAD', options)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000704
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000705 if (not os.path.exists(self.checkout_path) or
706 (os.path.isdir(self.checkout_path)
707 and not os.path.exists(os.path.join(self.checkout_path, '.git')))):
708 if mirror:
709 self._UpdateMirrorIfNotContains(mirror, options, rev_type,
710 revision)
711 try:
712 self._Clone(revision, url, options)
713 except subprocess2.CalledProcessError as e:
714 logging.warning('Clone failed due to: %s', e)
715 self._DeleteOrMove(options.force)
716 self._Clone(revision, url, options)
717 if file_list is not None:
718 files = self._Capture(
719 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
720 file_list.extend(
721 [os.path.join(self.checkout_path, f) for f in files])
722 if mirror:
723 self._Capture(
724 ['remote', 'set-url', '--push', 'origin', mirror.url])
725 if not verbose:
726 # Make the output a little prettier. It's nice to have some
727 # whitespace between projects when cloning.
728 self.Print('')
729 return self._Capture(['rev-parse', '--verify', 'HEAD'])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000730
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000731 if mirror:
732 self._Capture(['remote', 'set-url', '--push', 'origin', mirror.url])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000733
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000734 if not managed:
735 self._SetFetchConfig(options)
736 self.Print('________ unmanaged solution; skipping %s' %
737 self.relpath)
738 return self._Capture(['rev-parse', '--verify', 'HEAD'])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000739
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000740 self._maybe_break_locks(options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000741
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000742 if mirror:
743 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
Edward Lemur579c9862018-07-13 23:17:51 +0000744
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000745 # See if the url has changed (the unittests use git://foo for the url,
746 # let that through).
747 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
748 return_early = False
749 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
750 # unit test pass. (and update the comment above)
751 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
752 # This allows devs to use experimental repos which have a different url
753 # but whose branch(s) are the same as official repos.
754 if (current_url.rstrip('/') != url.rstrip('/') and url != 'git://foo'
755 and
756 subprocess2.capture([
757 'git', 'config',
758 'remote.%s.gclient-auto-fix-url' % self.remote
759 ],
760 cwd=self.checkout_path).strip() != 'False'):
761 self.Print('_____ switching %s from %s to new upstream %s' %
762 (self.relpath, current_url, url))
763 if not (options.force or options.reset):
764 # Make sure it's clean
765 self._CheckClean(revision)
766 # Switch over to the new upstream
767 self._Run(['remote', 'set-url', self.remote, url], options)
768 if mirror:
769 if git_cache.Mirror.CacheDirToUrl(current_url.rstrip(
770 '/')) == git_cache.Mirror.CacheDirToUrl(
771 url.rstrip('/')):
772 # Reset alternates when the cache dir is updated.
773 with open(
774 os.path.join(self.checkout_path, '.git', 'objects',
775 'info', 'alternates'), 'w') as fh:
776 fh.write(os.path.join(url, 'objects'))
777 else:
778 # Because we use Git alternatives, our existing repository
779 # is not self-contained. It's possible that new git
780 # alternative doesn't have all necessary objects that the
781 # current repository needs. Instead of blindly hoping that
782 # new alternative contains all necessary objects, keep the
783 # old alternative and just append a new one on top of it.
784 with open(
785 os.path.join(self.checkout_path, '.git', 'objects',
786 'info', 'alternates'), 'a') as fh:
787 fh.write("\n" + os.path.join(url, 'objects'))
788 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
789 self._FetchAndReset(revision, file_list, options)
Michael Spang73fac912019-03-08 18:44:19 +0000790
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000791 return_early = True
792 else:
793 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
794
795 if return_early:
796 return self._Capture(['rev-parse', '--verify', 'HEAD'])
797
798 cur_branch = self._GetCurrentBranch()
799
800 # Cases:
801 # 0) HEAD is detached. Probably from our initial clone.
802 # - make sure HEAD is contained by a named ref, then update.
803 # Cases 1-4. HEAD is a branch.
804 # 1) current branch is not tracking a remote branch
805 # - try to rebase onto the new hash or branch
806 # 2) current branch is tracking a remote branch with local committed
807 # changes, but the DEPS file switched to point to a hash
808 # - rebase those changes on top of the hash
809 # 3) current branch is tracking a remote branch w/or w/out changes, and
810 # no DEPS switch
811 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
812 # 4) current branch is tracking a remote branch, but DEPS switches to a
813 # different remote branch, and a) current branch has no local changes,
814 # and --force: - checkout new branch b) current branch has local
815 # changes, and --force and --reset: - checkout new branch c) otherwise
816 # exit
817
818 # GetUpstreamBranch returns something like 'refs/remotes/origin/main'
819 # for a tracking branch or 'main' if not a tracking branch (it's based
820 # on a specific rev/hash) or it returns None if it couldn't find an
821 # upstream
822 if cur_branch is None:
823 upstream_branch = None
824 current_type = "detached"
825 logging.debug("Detached HEAD")
826 else:
827 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
828 if not upstream_branch or not upstream_branch.startswith(
829 'refs/remotes'):
830 current_type = "hash"
831 logging.debug(
832 "Current branch is not tracking an upstream (remote)"
833 " branch.")
834 elif upstream_branch.startswith('refs/remotes'):
835 current_type = "branch"
836 else:
837 raise gclient_utils.Error('Invalid Upstream: %s' %
838 upstream_branch)
839
840 self._SetFetchConfig(options)
841
842 # Fetch upstream if we don't already have |revision|.
843 if not scm.GIT.IsValidRevision(
844 self.checkout_path, revision, sha_only=True):
845 self._Fetch(options, prune=options.force)
846
847 if not scm.GIT.IsValidRevision(
848 self.checkout_path, revision, sha_only=True):
849 # Update the remotes first so we have all the refs.
850 remote_output = scm.GIT.Capture(['remote'] + verbose +
851 ['update'],
852 cwd=self.checkout_path)
853 if verbose:
854 self.Print(remote_output)
855
856 revision = self._AutoFetchRef(options, revision)
857
858 # This is a big hammer, debatable if it should even be here...
859 if options.force or options.reset:
860 target = 'HEAD'
861 if options.upstream and upstream_branch:
862 target = upstream_branch
863 self._Scrub(target, options)
864
865 if current_type == 'detached':
866 # case 0
867 # We just did a Scrub, this is as clean as it's going to get. In
868 # particular if HEAD is a commit that contains two versions of the
869 # same file on a case-insensitive filesystem (e.g. 'a' and 'A'),
870 # there's no way to actually "Clean" the checkout; that commit is
871 # uncheckoutable on this system. The best we can do is carry forward
872 # to the checkout step.
873 if not (options.force or options.reset):
874 self._CheckClean(revision)
875 self._CheckDetachedHead(revision, options)
876 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
877 self.Print('Up-to-date; skipping checkout.')
878 else:
879 # 'git checkout' may need to overwrite existing untracked files.
880 # Allow it only when nuclear options are enabled.
881 self._Checkout(
882 options,
883 revision,
884 force=(options.force and options.delete_unversioned_trees),
885 quiet=True,
886 )
887 if not printed_path:
888 self.Print('_____ %s at %s' % (self.relpath, revision),
889 timestamp=False)
890 elif current_type == 'hash':
891 # case 1
892 # Can't find a merge-base since we don't know our upstream. That
893 # makes this command VERY likely to produce a rebase failure. For
894 # now we assume origin is our upstream since that's what the old
895 # behavior was.
896 upstream_branch = self.remote
897 if options.revision or deps_revision:
898 upstream_branch = revision
899 self._AttemptRebase(upstream_branch,
900 file_list,
901 options,
902 printed_path=printed_path,
903 merge=options.merge)
904 printed_path = True
905 elif rev_type == 'hash':
906 # case 2
907 self._AttemptRebase(upstream_branch,
908 file_list,
909 options,
910 newbase=revision,
911 printed_path=printed_path,
912 merge=options.merge)
913 printed_path = True
914 elif remote_ref and ''.join(remote_ref) != upstream_branch:
915 # case 4
916 new_base = ''.join(remote_ref)
917 if not printed_path:
918 self.Print('_____ %s at %s' % (self.relpath, revision),
919 timestamp=False)
920 switch_error = (
921 "Could not switch upstream branch from %s to %s\n" %
922 (upstream_branch, new_base) +
923 "Please use --force or merge or rebase manually:\n" +
924 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
925 "OR git checkout -b <some new branch> %s" % new_base)
926 force_switch = False
927 if options.force:
928 try:
929 self._CheckClean(revision)
930 # case 4a
931 force_switch = True
932 except gclient_utils.Error as e:
933 if options.reset:
934 # case 4b
935 force_switch = True
936 else:
937 switch_error = '%s\n%s' % (e.message, switch_error)
938 if force_switch:
939 self.Print("Switching upstream branch from %s to %s" %
940 (upstream_branch, new_base))
941 switch_branch = 'gclient_' + remote_ref[1]
942 self._Capture(['branch', '-f', switch_branch, new_base])
943 self._Checkout(options, switch_branch, force=True, quiet=True)
944 else:
945 # case 4c
946 raise gclient_utils.Error(switch_error)
947 else:
948 # case 3 - the default case
949 rebase_files = self._GetDiffFilenames(upstream_branch)
950 if verbose:
951 self.Print('Trying fast-forward merge to branch : %s' %
952 upstream_branch)
953 try:
954 merge_args = ['merge']
955 if options.merge:
956 merge_args.append('--ff')
957 else:
958 merge_args.append('--ff-only')
959 merge_args.append(upstream_branch)
960 merge_output = self._Capture(merge_args)
961 except subprocess2.CalledProcessError as e:
962 rebase_files = []
963 if re.search(b'fatal: Not possible to fast-forward, aborting.',
964 e.stderr):
965 if not printed_path:
966 self.Print('_____ %s at %s' % (self.relpath, revision),
967 timestamp=False)
968 printed_path = True
969 while True:
970 if not options.auto_rebase:
971 try:
972 action = self._AskForData(
973 'Cannot %s, attempt to rebase? '
974 '(y)es / (q)uit / (s)kip : ' %
975 ('merge' if options.merge else
976 'fast-forward merge'), options)
977 except ValueError:
978 raise gclient_utils.Error('Invalid Character')
979 if options.auto_rebase or re.match(
980 r'yes|y', action, re.I):
981 self._AttemptRebase(upstream_branch,
982 rebase_files,
983 options,
984 printed_path=printed_path,
985 merge=False)
986 printed_path = True
987 break
988
989 if re.match(r'quit|q', action, re.I):
990 raise gclient_utils.Error(
991 "Can't fast-forward, please merge or "
992 "rebase manually.\n"
993 "cd %s && git " % self.checkout_path +
994 "rebase %s" % upstream_branch)
995
996 if re.match(r'skip|s', action, re.I):
997 self.Print('Skipping %s' % self.relpath)
998 return
999
1000 self.Print('Input not recognized')
1001 elif re.match(
1002 b"error: Your local changes to '.*' would be "
1003 b"overwritten by merge. Aborting.\nPlease, commit your "
1004 b"changes or stash them before you can merge.\n",
1005 e.stderr):
1006 if not printed_path:
1007 self.Print('_____ %s at %s' % (self.relpath, revision),
1008 timestamp=False)
1009 printed_path = True
1010 raise gclient_utils.Error(e.stderr.decode('utf-8'))
1011 else:
1012 # Some other problem happened with the merge
1013 logging.error("Error during fast-forward merge in %s!" %
1014 self.relpath)
1015 self.Print(e.stderr.decode('utf-8'))
1016 raise
1017 else:
1018 # Fast-forward merge was successful
1019 if not re.match('Already up-to-date.', merge_output) or verbose:
1020 if not printed_path:
1021 self.Print('_____ %s at %s' % (self.relpath, revision),
1022 timestamp=False)
1023 printed_path = True
1024 self.Print(merge_output.strip())
1025 if not verbose:
1026 # Make the output a little prettier. It's nice to have
1027 # some whitespace between projects when syncing.
1028 self.Print('')
1029
1030 if file_list is not None:
1031 file_list.extend(
1032 [os.path.join(self.checkout_path, f) for f in rebase_files])
1033
1034 # If the rebase generated a conflict, abort and ask user to fix
1035 if self._IsRebasing():
1036 raise gclient_utils.Error(
1037 '\n____ %s at %s\n'
1038 '\nConflict while rebasing this branch.\n'
1039 'Fix the conflict and run gclient again.\n'
1040 'See man git-rebase for details.\n' % (self.relpath, revision))
1041
Michael Spang73fac912019-03-08 18:44:19 +00001042 if verbose:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001043 self.Print('Checked out revision %s' %
1044 self.revinfo(options, (), None),
agable83faed02016-10-24 14:37:10 -07001045 timestamp=False)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001046
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001047 # If --reset and --delete_unversioned_trees are specified, remove any
1048 # untracked directories.
1049 if options.reset and options.delete_unversioned_trees:
1050 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1
1051 # (the merge-base by default), so doesn't include untracked files.
1052 # So we use 'git ls-files --directory --others --exclude-standard'
1053 # here directly.
1054 paths = scm.GIT.Capture([
1055 '-c', 'core.quotePath=false', 'ls-files', '--directory',
1056 '--others', '--exclude-standard'
1057 ], self.checkout_path)
1058 for path in (p for p in paths.splitlines() if p.endswith('/')):
1059 full_path = os.path.join(self.checkout_path, path)
1060 if not os.path.islink(full_path):
1061 self.Print('_____ removing unversioned directory %s' % path)
1062 gclient_utils.rmtree(full_path)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001063
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001064 return self._Capture(['rev-parse', '--verify', 'HEAD'])
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001065
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001066 def revert(self, options, _args, file_list):
1067 """Reverts local modifications.
msb@chromium.orge28e4982009-09-25 20:51:45 +00001068
1069 All reverted files will be appended to file_list.
1070 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001071 if not os.path.isdir(self.checkout_path):
1072 # revert won't work if the directory doesn't exist. It needs to
1073 # checkout instead.
1074 self.Print('_____ %s is missing, syncing instead' % self.relpath)
1075 # Don't reuse the args.
1076 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001077
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001078 default_rev = "refs/heads/main"
1079 if options.upstream:
1080 if self._GetCurrentBranch():
1081 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
1082 default_rev = upstream_branch or default_rev
1083 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
1084 if not deps_revision:
1085 deps_revision = default_rev
1086 if deps_revision.startswith('refs/heads/'):
1087 deps_revision = deps_revision.replace('refs/heads/',
1088 self.remote + '/')
1089 try:
1090 deps_revision = self.GetUsableRev(deps_revision, options)
1091 except NoUsableRevError as e:
1092 # If the DEPS entry's url and hash changed, try to update the
1093 # origin. See also http://crbug.com/520067.
1094 logging.warning(
1095 "Couldn't find usable revision, will retrying to update instead: %s",
1096 e.message)
1097 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001098
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001099 if file_list is not None:
1100 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001101
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001102 self._Scrub(deps_revision, options)
1103 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001104
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001105 if file_list is not None:
1106 file_list.extend(
1107 [os.path.join(self.checkout_path, f) for f in files])
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001108
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001109 def revinfo(self, _options, _args, _file_list):
1110 """Returns revision"""
1111 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +00001112
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001113 def runhooks(self, options, args, file_list):
1114 self.status(options, args, file_list)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001115
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001116 def status(self, options, _args, file_list):
1117 """Display status information."""
1118 if not os.path.isdir(self.checkout_path):
1119 self.Print('________ couldn\'t run status in %s:\n'
1120 'The directory does not exist.' % self.checkout_path)
1121 else:
1122 merge_base = []
1123 if self.url:
1124 _, base_rev = gclient_utils.SplitUrlRevision(self.url)
1125 if base_rev:
1126 if base_rev.startswith('refs/'):
1127 base_rev = self._ref_to_remote_ref(base_rev)
1128 merge_base = [base_rev]
1129 self._Run(['-c', 'core.quotePath=false', 'diff', '--name-status'] +
1130 merge_base,
1131 options,
1132 always_show_header=options.verbose)
1133 if file_list is not None:
1134 files = self._GetDiffFilenames(
1135 merge_base[0] if merge_base else None)
1136 file_list.extend(
1137 [os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +00001138
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001139 def GetUsableRev(self, rev, options):
1140 """Finds a useful revision for this repository."""
1141 sha1 = None
1142 if not os.path.isdir(self.checkout_path):
1143 raise NoUsableRevError(
1144 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -07001145
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001146 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1147 sha1 = rev
1148 else:
1149 # May exist in origin, but we don't have it yet, so fetch and look
1150 # again.
1151 self._Fetch(options)
1152 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1153 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001154
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001155 if not sha1:
1156 raise NoUsableRevError(
1157 'Hash %s does not appear to be a valid hash in this repo.' %
1158 rev)
smutae7ea312016-07-18 11:59:41 -07001159
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001160 return sha1
smutae7ea312016-07-18 11:59:41 -07001161
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001162 def GetGitBackupDirPath(self):
1163 """Returns the path where the .git folder for the current project can be
primiano@chromium.org1c127382015-02-17 11:15:40 +00001164 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001165 return os.path.join(self._root_dir,
1166 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
primiano@chromium.org1c127382015-02-17 11:15:40 +00001167
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001168 def _GetMirror(self, url, options, revision=None, revision_ref=None):
1169 """Get a git_cache.Mirror object for the argument url."""
1170 if not self.cache_dir:
1171 return None
1172 mirror_kwargs = {
1173 'print_func': self.filter,
1174 'refs': [],
1175 'commits': [],
1176 }
1177 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1178 mirror_kwargs['refs'].append('refs/branch-heads/*')
1179 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
1180 mirror_kwargs['refs'].append(revision_ref)
1181 if hasattr(options, 'with_tags') and options.with_tags:
1182 mirror_kwargs['refs'].append('refs/tags/*')
1183 elif revision_ref and revision_ref.startswith('refs/tags/'):
1184 mirror_kwargs['refs'].append(revision_ref)
1185 if revision and not revision.startswith('refs/'):
1186 mirror_kwargs['commits'].append(revision)
1187 return git_cache.Mirror(url, **mirror_kwargs)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001188
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001189 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
1190 """Update a git mirror by fetching the latest commits from the remote,
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001191 unless mirror already contains revision whose type is sha1 hash.
1192 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001193 if rev_type == 'hash' and mirror.contains_revision(revision):
1194 if options.verbose:
1195 self.Print('skipping mirror update, it has rev=%s already' %
1196 revision,
1197 timestamp=False)
1198 return
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001199
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001200 if getattr(options, 'shallow', False):
1201 depth = 10000
1202 else:
1203 depth = None
1204 mirror.populate(verbose=options.verbose,
1205 bootstrap=not getattr(options, 'no_bootstrap', False),
1206 depth=depth,
1207 lock_timeout=getattr(options, 'lock_timeout', 0))
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001208
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001209 def _Clone(self, revision, url, options):
1210 """Clone a git repository from the given URL.
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001211
msb@chromium.org786fb682010-06-02 15:16:23 +00001212 Once we've cloned the repo, we checkout a working branch if the specified
1213 revision is a branch head. If it is a tag or a specific commit, then we
1214 leave HEAD detached as it makes future updates simpler -- in this case the
1215 user should first create a new branch or switch to an existing branch before
1216 making changes in the repo."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001217 in_cog_workspace = self._IsCog()
Joanna Wang1a977bd2022-06-02 21:51:17 +00001218
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001219 if self.print_outbuf:
1220 print_stdout = True
1221 filter_fn = None
1222 else:
1223 print_stdout = False
1224 filter_fn = self.filter
Joanna Wang1a977bd2022-06-02 21:51:17 +00001225
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001226 if not options.verbose:
1227 # git clone doesn't seem to insert a newline properly before
1228 # printing to stdout
1229 self.Print('')
Joanna Wang1a977bd2022-06-02 21:51:17 +00001230
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001231 # If the parent directory does not exist, Git clone on Windows will not
1232 # create it, so we need to do it manually.
1233 parent_dir = os.path.dirname(self.checkout_path)
1234 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001235
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001236 if in_cog_workspace:
1237 clone_cmd = ['citc', 'clone-repo', url, self.checkout_path]
1238 clone_cmd.append(
1239 gclient_utils.ExtractRefName(self.remote, revision) or revision)
1240 try:
1241 self._Run(clone_cmd,
1242 options,
1243 cwd=self._root_dir,
1244 retry=True,
1245 print_stdout=print_stdout,
1246 filter_fn=filter_fn)
1247 except:
1248 traceback.print_exc(file=self.out_fh)
1249 raise
1250 self._SetFetchConfig(options)
1251 elif hasattr(options, 'no_history') and options.no_history:
1252 self._Run(['init', self.checkout_path], options, cwd=self._root_dir)
1253 self._Run(['remote', 'add', 'origin', url], options)
1254 revision = self._AutoFetchRef(options, revision, depth=1)
1255 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1256 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
1257 else:
1258 cfg = gclient_utils.DefaultIndexPackConfig(url)
1259 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
1260 if self.cache_dir:
1261 clone_cmd.append('--shared')
1262 if options.verbose:
1263 clone_cmd.append('--verbose')
1264 clone_cmd.append(url)
1265 tmp_dir = tempfile.mkdtemp(prefix='_gclient_%s_' %
1266 os.path.basename(self.checkout_path),
1267 dir=parent_dir)
1268 clone_cmd.append(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001269
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001270 try:
1271 self._Run(clone_cmd,
1272 options,
1273 cwd=self._root_dir,
1274 retry=True,
1275 print_stdout=print_stdout,
1276 filter_fn=filter_fn)
1277 logging.debug(
1278 'Cloned into temporary dir, moving to checkout_path')
1279 gclient_utils.safe_makedirs(self.checkout_path)
1280 gclient_utils.safe_rename(
1281 os.path.join(tmp_dir, '.git'),
1282 os.path.join(self.checkout_path, '.git'))
1283 except:
1284 traceback.print_exc(file=self.out_fh)
1285 raise
1286 finally:
1287 if os.listdir(tmp_dir):
1288 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
1289 gclient_utils.rmtree(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001290
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001291 self._SetFetchConfig(options)
1292 self._Fetch(options, prune=options.force)
1293 revision = self._AutoFetchRef(options, revision)
1294 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1295 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001296
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001297 if self._GetCurrentBranch() is None:
1298 # Squelch git's very verbose detached HEAD warning and use our own
1299 self.Print((
1300 'Checked out %s to a detached HEAD. Before making any commits\n'
1301 'in this repo, you should use \'git checkout <branch>\' to switch \n'
1302 'to an existing branch or use \'git checkout %s -b <branch>\' to\n'
1303 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001304
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001305 def _AskForData(self, prompt, options):
1306 if options.jobs > 1:
1307 self.Print(prompt)
1308 raise gclient_utils.Error("Background task requires input. Rerun "
1309 "gclient with --jobs=1 so that\n"
1310 "interaction is possible.")
1311 return gclient_utils.AskForData(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001312
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001313 def _AttemptRebase(self,
1314 upstream,
1315 files,
1316 options,
1317 newbase=None,
1318 branch=None,
1319 printed_path=False,
1320 merge=False):
1321 """Attempt to rebase onto either upstream or, if specified, newbase."""
1322 if files is not None:
1323 files.extend(self._GetDiffFilenames(upstream))
1324 revision = upstream
1325 if newbase:
1326 revision = newbase
1327 action = 'merge' if merge else 'rebase'
1328 if not printed_path:
1329 self.Print('_____ %s : Attempting %s onto %s...' %
1330 (self.relpath, action, revision))
1331 printed_path = True
1332 else:
1333 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001334
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001335 if merge:
1336 merge_output = self._Capture(['merge', revision])
1337 if options.verbose:
1338 self.Print(merge_output)
1339 return
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001340
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001341 # Build the rebase command here using the args
1342 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1343 rebase_cmd = ['rebase']
1344 if options.verbose:
1345 rebase_cmd.append('--verbose')
1346 if newbase:
1347 rebase_cmd.extend(['--onto', newbase])
1348 rebase_cmd.append(upstream)
1349 if branch:
1350 rebase_cmd.append(branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001351
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001352 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001353 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001354 except subprocess2.CalledProcessError as e:
1355 if (re.match(
1356 br'cannot rebase: you have unstaged changes', e.stderr
1357 ) or re.match(
1358 br'cannot rebase: your index contains uncommitted changes',
1359 e.stderr)):
1360 while True:
1361 rebase_action = self._AskForData(
1362 'Cannot rebase because of unstaged changes.\n'
1363 '\'git reset --hard HEAD\' ?\n'
1364 'WARNING: destroys any uncommitted work in your current branch!'
1365 ' (y)es / (q)uit / (s)how : ', options)
1366 if re.match(r'yes|y', rebase_action, re.I):
1367 self._Scrub('HEAD', options)
1368 # Should this be recursive?
1369 rebase_output = scm.GIT.Capture(rebase_cmd,
1370 cwd=self.checkout_path)
1371 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001372
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001373 if re.match(r'quit|q', rebase_action, re.I):
1374 raise gclient_utils.Error(
1375 "Please merge or rebase manually\n"
1376 "cd %s && git " % self.checkout_path +
1377 "%s" % ' '.join(rebase_cmd))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001378
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001379 if re.match(r'show|s', rebase_action, re.I):
1380 self.Print('%s' % e.stderr.decode('utf-8').strip())
1381 continue
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001382
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001383 gclient_utils.Error("Input not recognized")
1384 continue
1385 elif re.search(br'^CONFLICT', e.stdout, re.M):
1386 raise gclient_utils.Error(
1387 "Conflict while rebasing this branch.\n"
1388 "Fix the conflict and run gclient again.\n"
1389 "See 'man git-rebase' for details.\n")
1390 else:
1391 self.Print(e.stdout.decode('utf-8').strip())
1392 self.Print('Rebase produced error output:\n%s' %
1393 e.stderr.decode('utf-8').strip())
1394 raise gclient_utils.Error(
1395 "Unrecognized error, please merge or rebase "
1396 "manually.\ncd %s && git " % self.checkout_path +
1397 "%s" % ' '.join(rebase_cmd))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001398
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001399 self.Print(rebase_output.strip())
1400 if not options.verbose:
1401 # Make the output a little prettier. It's nice to have some
1402 # whitespace between projects when syncing.
1403 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001404
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001405 @staticmethod
1406 def _CheckMinVersion(min_version):
1407 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1408 if not ok:
1409 raise gclient_utils.Error('git version %s < minimum required %s' %
1410 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001411
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001412 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
1413 # Special case handling if all 3 conditions are met:
1414 # * the mirros have recently changed, but deps destination remains same,
1415 # * the git histories of mirrors are conflicting. * git cache is used
1416 # This manifests itself in current checkout having invalid HEAD commit
1417 # on most git operations. Since git cache is used, just deleted the .git
1418 # folder, and re-create it by cloning.
1419 try:
1420 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1421 except subprocess2.CalledProcessError as e:
1422 if (b'fatal: bad object HEAD' in e.stderr and self.cache_dir
1423 and self.cache_dir in url):
1424 self.Print(
1425 ('Likely due to DEPS change with git cache_dir, '
1426 'the current commit points to no longer existing object.\n'
1427 '%s' % e))
1428 self._DeleteOrMove(options.force)
1429 self._Clone(revision, url, options)
1430 else:
1431 raise
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001432
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001433 def _IsRebasing(self):
1434 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git
1435 # doesn't have a plumbing command to determine whether a rebase is in
1436 # progress, so for now emualate (more-or-less) git-rebase.sh /
1437 # git-completion.bash
1438 g = os.path.join(self.checkout_path, '.git')
1439 return (os.path.isdir(os.path.join(g, "rebase-merge"))
1440 or os.path.isdir(os.path.join(g, "rebase-apply")))
msb@chromium.org786fb682010-06-02 15:16:23 +00001441
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001442 def _CheckClean(self, revision):
1443 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1444 if os.path.exists(lockfile):
1445 raise gclient_utils.Error(
1446 '\n____ %s at %s\n'
1447 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1448 '\tIf no git executable is running, then clean up %r and try again.\n'
1449 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001450
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001451 # Make sure the tree is clean; see git-rebase.sh for reference
1452 try:
1453 scm.GIT.Capture(
1454 ['update-index', '--ignore-submodules', '--refresh'],
1455 cwd=self.checkout_path)
1456 except subprocess2.CalledProcessError:
1457 raise gclient_utils.Error(
1458 '\n____ %s at %s\n'
1459 '\tYou have unstaged changes.\n'
1460 '\tcd into %s, run git status to see changes,\n'
1461 '\tand commit, stash, or reset.\n' %
1462 (self.relpath, revision, self.relpath))
1463 try:
1464 scm.GIT.Capture([
1465 'diff-index', '--cached', '--name-status', '-r',
1466 '--ignore-submodules', 'HEAD', '--'
1467 ],
1468 cwd=self.checkout_path)
1469 except subprocess2.CalledProcessError:
1470 raise gclient_utils.Error(
1471 '\n____ %s at %s\n'
1472 '\tYour index contains uncommitted changes\n'
1473 '\tcd into %s, run git status to see changes,\n'
1474 '\tand commit, stash, or reset.\n' %
1475 (self.relpath, revision, self.relpath))
msb@chromium.org786fb682010-06-02 15:16:23 +00001476
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001477 def _CheckDetachedHead(self, revision, _options):
1478 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1479 # reference by a commit). If not, error out -- most likely a rebase is
1480 # in progress, try to detect so we can give a better error.
1481 try:
1482 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1483 cwd=self.checkout_path)
1484 except subprocess2.CalledProcessError:
1485 # Commit is not contained by any rev. See if the user is rebasing:
1486 if self._IsRebasing():
1487 # Punt to the user
1488 raise gclient_utils.Error(
1489 '\n____ %s at %s\n'
1490 '\tAlready in a conflict, i.e. (no branch).\n'
1491 '\tFix the conflict and run gclient again.\n'
1492 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1493 '\tSee man git-rebase for details.\n' %
1494 (self.relpath, revision))
1495 # Let's just save off the commit so we can proceed.
1496 name = ('saved-by-gclient-' +
1497 self._Capture(['rev-parse', '--short', 'HEAD']))
1498 self._Capture(['branch', '-f', name])
1499 self.Print(
1500 '_____ found an unreferenced commit and saved it as \'%s\'' %
1501 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001502
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001503 def _GetCurrentBranch(self):
1504 # Returns name of current branch or None for detached HEAD
1505 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
1506 if branch == 'HEAD':
1507 return None
1508 return branch
msb@chromium.org5bde4852009-12-14 16:47:12 +00001509
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001510 def _Capture(self, args, **kwargs):
1511 set_git_dir = 'cwd' not in kwargs
1512 kwargs.setdefault('cwd', self.checkout_path)
1513 kwargs.setdefault('stderr', subprocess2.PIPE)
1514 strip = kwargs.pop('strip', True)
1515 env = scm.GIT.ApplyEnvVars(kwargs)
1516 # If an explicit cwd isn't set, then default to the .git/ subdir so we
1517 # get stricter behavior. This can be useful in cases of slight
1518 # corruption -- we don't accidentally go corrupting parent git checks
1519 # too. See https://crbug.com/1000825 for an example.
1520 if set_git_dir:
1521 git_dir = os.path.abspath(os.path.join(self.checkout_path, '.git'))
1522 # Depending on how the .gclient file was defined, self.checkout_path
1523 # might be set to a unicode string, not a regular string; on Windows
1524 # Python2, we can't set env vars to be unicode strings, so we
1525 # forcibly cast the value to a string before setting it.
1526 env.setdefault('GIT_DIR', str(git_dir))
1527 ret = subprocess2.check_output(['git'] + args, env=env,
1528 **kwargs).decode('utf-8')
1529 if strip:
1530 ret = ret.strip()
1531 self.Print('Finished running: %s %s' % ('git', ' '.join(args)))
1532 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001533
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001534 def _Checkout(self, options, ref, force=False, quiet=None):
1535 """Performs a 'git-checkout' operation.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001536
1537 Args:
1538 options: The configured option set
1539 ref: (str) The branch/commit to checkout
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001540 quiet: (bool/None) Whether or not the checkout should pass '--quiet'; if
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001541 'None', the behavior is inferred from 'options.verbose'.
1542 Returns: (str) The output of the checkout operation
1543 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001544 if quiet is None:
1545 quiet = (not options.verbose)
1546 checkout_args = ['checkout']
1547 if force:
1548 checkout_args.append('--force')
1549 if quiet:
1550 checkout_args.append('--quiet')
1551 checkout_args.append(ref)
1552 return self._Capture(checkout_args)
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001553
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001554 def _Fetch(self,
1555 options,
1556 remote=None,
1557 prune=False,
1558 quiet=False,
1559 refspec=None,
1560 depth=None):
1561 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
1562 # When updating, the ref is modified to be a remote ref .
1563 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1564 # Try to reverse that mapping.
1565 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1566 if original_ref:
1567 refspec = original_ref + ':' + refspec
1568 # When a mirror is configured, it only fetches
1569 # refs/{heads,branch-heads,tags}/*.
1570 # If asked to fetch other refs, we must fetch those directly from
1571 # the repository, and not from the mirror.
1572 if not original_ref.startswith(
1573 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1574 remote, _ = gclient_utils.SplitUrlRevision(self.url)
1575 fetch_cmd = cfg + [
1576 'fetch',
1577 remote or self.remote,
1578 ]
1579 if refspec:
1580 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001581
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001582 if prune:
1583 fetch_cmd.append('--prune')
1584 if options.verbose:
1585 fetch_cmd.append('--verbose')
1586 if not hasattr(options, 'with_tags') or not options.with_tags:
1587 fetch_cmd.append('--no-tags')
1588 elif quiet:
1589 fetch_cmd.append('--quiet')
1590 if depth:
1591 fetch_cmd.append('--depth=' + str(depth))
1592 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001593
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001594 def _SetFetchConfig(self, options):
1595 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001596 if requested."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001597 if options.force or options.reset:
1598 try:
1599 self._Run(
1600 ['config', '--unset-all',
1601 'remote.%s.fetch' % self.remote], options)
1602 self._Run([
1603 'config',
1604 'remote.%s.fetch' % self.remote,
1605 '+refs/heads/*:refs/remotes/%s/*' % self.remote
1606 ], options)
1607 except subprocess2.CalledProcessError as e:
1608 # If exit code was 5, it means we attempted to unset a config
1609 # that didn't exist. Ignore it.
1610 if e.returncode != 5:
1611 raise
1612 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1613 config_cmd = [
1614 'config',
1615 'remote.%s.fetch' % self.remote,
1616 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1617 '^\\+refs/branch-heads/\\*:.*$'
1618 ]
1619 self._Run(config_cmd, options)
1620 if hasattr(options, 'with_tags') and options.with_tags:
1621 config_cmd = [
1622 'config',
1623 'remote.%s.fetch' % self.remote, '+refs/tags/*:refs/tags/*',
1624 '^\\+refs/tags/\\*:.*$'
1625 ]
1626 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001627
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001628 def _AutoFetchRef(self, options, revision, depth=None):
1629 """Attempts to fetch |revision| if not available in local repo.
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001630
1631 Returns possibly updated revision."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001632 if not scm.GIT.IsValidRevision(self.checkout_path, revision):
1633 self._Fetch(options, refspec=revision, depth=depth)
1634 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1635 return revision
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001636
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001637 def _Run(self, args, options, **kwargs):
1638 # Disable 'unused options' warning | pylint: disable=unused-argument
1639 kwargs.setdefault('cwd', self.checkout_path)
1640 kwargs.setdefault('filter_fn', self.filter)
1641 kwargs.setdefault('show_header', True)
1642 env = scm.GIT.ApplyEnvVars(kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +00001643
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001644 cmd = ['git'] + args
1645 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001646
1647
1648class CipdPackage(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001649 """A representation of a single CIPD package."""
1650 def __init__(self, name, version, authority_for_subdir):
1651 self._authority_for_subdir = authority_for_subdir
1652 self._name = name
1653 self._version = version
John Budorick0f7b2002018-01-19 15:46:17 -08001654
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001655 @property
1656 def authority_for_subdir(self):
1657 """Whether this package has authority to act on behalf of its subdir.
John Budorick0f7b2002018-01-19 15:46:17 -08001658
1659 Some operations should only be performed once per subdirectory. A package
1660 that has authority for its subdirectory is the only package that should
1661 perform such operations.
1662
1663 Returns:
1664 bool; whether this package has subdir authority.
1665 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001666 return self._authority_for_subdir
John Budorick0f7b2002018-01-19 15:46:17 -08001667
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001668 @property
1669 def name(self):
1670 return self._name
John Budorick0f7b2002018-01-19 15:46:17 -08001671
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001672 @property
1673 def version(self):
1674 return self._version
John Budorick0f7b2002018-01-19 15:46:17 -08001675
1676
1677class CipdRoot(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001678 """A representation of a single CIPD root."""
1679 def __init__(self, root_dir, service_url):
1680 self._all_packages = set()
1681 self._mutator_lock = threading.Lock()
1682 self._packages_by_subdir = collections.defaultdict(list)
1683 self._root_dir = root_dir
1684 self._service_url = service_url
1685 self._resolved_packages = None
John Budorick0f7b2002018-01-19 15:46:17 -08001686
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001687 def add_package(self, subdir, package, version):
1688 """Adds a package to this CIPD root.
John Budorick0f7b2002018-01-19 15:46:17 -08001689
1690 As far as clients are concerned, this grants both root and subdir authority
1691 to packages arbitrarily. (The implementation grants root authority to the
1692 first package added and subdir authority to the first package added for that
1693 subdir, but clients should not depend on or expect that behavior.)
1694
1695 Args:
1696 subdir: str; relative path to where the package should be installed from
1697 the cipd root directory.
1698 package: str; the cipd package name.
1699 version: str; the cipd package version.
1700 Returns:
1701 CipdPackage; the package that was created and added to this root.
1702 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001703 with self._mutator_lock:
1704 cipd_package = CipdPackage(package, version,
1705 not self._packages_by_subdir[subdir])
1706 self._all_packages.add(cipd_package)
1707 self._packages_by_subdir[subdir].append(cipd_package)
1708 return cipd_package
John Budorick0f7b2002018-01-19 15:46:17 -08001709
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001710 def packages(self, subdir):
1711 """Get the list of configured packages for the given subdir."""
1712 return list(self._packages_by_subdir[subdir])
John Budorick0f7b2002018-01-19 15:46:17 -08001713
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001714 def resolved_packages(self):
1715 if not self._resolved_packages:
1716 self._resolved_packages = self.ensure_file_resolve()
1717 return self._resolved_packages
Dan Le Febvre456d0852023-05-24 23:43:40 +00001718
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001719 def clobber(self):
1720 """Remove the .cipd directory.
John Budorick0f7b2002018-01-19 15:46:17 -08001721
1722 This is useful for forcing ensure to redownload and reinitialize all
1723 packages.
1724 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001725 with self._mutator_lock:
1726 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
1727 try:
1728 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1729 except OSError:
1730 if os.path.exists(cipd_cache_dir):
1731 raise
John Budorick0f7b2002018-01-19 15:46:17 -08001732
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001733 def expand_package_name(self, package_name_string, **kwargs):
1734 """Run `cipd expand-package-name`.
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001735
1736 CIPD package names can be declared with placeholder variables
1737 such as '${platform}', this cmd will return the package name
1738 with the variables resolved. The resolution is based on the host
1739 the command is executing on.
1740 """
1741
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001742 kwargs.setdefault('stderr', subprocess2.PIPE)
1743 cmd = ['cipd', 'expand-package-name', package_name_string]
1744 ret = subprocess2.check_output(cmd, **kwargs).decode('utf-8')
1745 return ret.strip()
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001746
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001747 @contextlib.contextmanager
1748 def _create_ensure_file(self):
1749 try:
1750 contents = '$ParanoidMode CheckPresence\n'
1751 # TODO(crbug/1329641): Remove once cipd packages have been updated
1752 # to always be created in copy mode.
1753 contents += '$OverrideInstallMode copy\n\n'
1754 for subdir, packages in sorted(self._packages_by_subdir.items()):
1755 contents += '@Subdir %s\n' % subdir
1756 for package in sorted(packages, key=lambda p: p.name):
1757 contents += '%s %s\n' % (package.name, package.version)
1758 contents += '\n'
1759 ensure_file = None
1760 with tempfile.NamedTemporaryFile(suffix='.ensure',
1761 delete=False,
1762 mode='wb') as ensure_file:
1763 ensure_file.write(contents.encode('utf-8', 'replace'))
1764 yield ensure_file.name
1765 finally:
1766 if ensure_file is not None and os.path.exists(ensure_file.name):
1767 os.remove(ensure_file.name)
John Budorick0f7b2002018-01-19 15:46:17 -08001768
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001769 def ensure(self):
1770 """Run `cipd ensure`."""
1771 with self._mutator_lock:
1772 with self._create_ensure_file() as ensure_file:
1773 cmd = [
1774 'cipd',
1775 'ensure',
1776 '-log-level',
Yiwei Zhang28116102023-09-06 17:33:20 +00001777 'info',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001778 '-root',
1779 self.root_dir,
1780 '-ensure-file',
1781 ensure_file,
1782 ]
1783 gclient_utils.CheckCallAndFilter(cmd,
1784 print_stdout=True,
1785 show_header=True)
John Budorick0f7b2002018-01-19 15:46:17 -08001786
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001787 @contextlib.contextmanager
1788 def _create_ensure_file_for_resolve(self):
1789 try:
1790 contents = '$ResolvedVersions %s\n' % os.devnull
1791 for subdir, packages in sorted(self._packages_by_subdir.items()):
1792 contents += '@Subdir %s\n' % subdir
1793 for package in sorted(packages, key=lambda p: p.name):
1794 contents += '%s %s\n' % (package.name, package.version)
1795 contents += '\n'
1796 ensure_file = None
1797 with tempfile.NamedTemporaryFile(suffix='.ensure',
1798 delete=False,
1799 mode='wb') as ensure_file:
1800 ensure_file.write(contents.encode('utf-8', 'replace'))
1801 yield ensure_file.name
1802 finally:
1803 if ensure_file is not None and os.path.exists(ensure_file.name):
1804 os.remove(ensure_file.name)
Dan Le Febvre456d0852023-05-24 23:43:40 +00001805
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001806 def _create_resolved_file(self):
1807 return tempfile.NamedTemporaryFile(suffix='.resolved',
1808 delete=False,
1809 mode='wb')
Dan Le Febvre456d0852023-05-24 23:43:40 +00001810
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001811 def ensure_file_resolve(self):
1812 """Run `cipd ensure-file-resolve`."""
1813 with self._mutator_lock:
1814 with self._create_resolved_file() as output_file:
1815 with self._create_ensure_file_for_resolve() as ensure_file:
1816 cmd = [
1817 'cipd',
1818 'ensure-file-resolve',
1819 '-log-level',
Yiwei Zhang28116102023-09-06 17:33:20 +00001820 'info',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001821 '-ensure-file',
1822 ensure_file,
1823 '-json-output',
1824 output_file.name,
1825 ]
1826 gclient_utils.CheckCallAndFilter(cmd,
1827 print_stdout=False,
1828 show_header=False)
1829 with open(output_file.name) as f:
1830 output_json = json.load(f)
1831 return output_json.get('result', {})
Dan Le Febvre456d0852023-05-24 23:43:40 +00001832
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001833 def run(self, command):
1834 if command == 'update':
1835 self.ensure()
1836 elif command == 'revert':
1837 self.clobber()
1838 self.ensure()
John Budorickd3ba72b2018-03-20 12:27:42 -07001839
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001840 def created_package(self, package):
1841 """Checks whether this root created the given package.
John Budorick0f7b2002018-01-19 15:46:17 -08001842
1843 Args:
1844 package: CipdPackage; the package to check.
1845 Returns:
1846 bool; whether this root created the given package.
1847 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001848 return package in self._all_packages
John Budorick0f7b2002018-01-19 15:46:17 -08001849
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001850 @property
1851 def root_dir(self):
1852 return self._root_dir
John Budorick0f7b2002018-01-19 15:46:17 -08001853
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001854 @property
1855 def service_url(self):
1856 return self._service_url
John Budorick0f7b2002018-01-19 15:46:17 -08001857
1858
1859class CipdWrapper(SCMWrapper):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001860 """Wrapper for CIPD.
John Budorick0f7b2002018-01-19 15:46:17 -08001861
1862 Currently only supports chrome-infra-packages.appspot.com.
1863 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001864 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001865
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001866 def __init__(self,
1867 url=None,
1868 root_dir=None,
1869 relpath=None,
1870 out_fh=None,
1871 out_cb=None,
1872 root=None,
1873 package=None):
1874 super(CipdWrapper, self).__init__(url=url,
1875 root_dir=root_dir,
1876 relpath=relpath,
1877 out_fh=out_fh,
1878 out_cb=out_cb)
1879 assert root.created_package(package)
1880 self._package = package
1881 self._root = root
John Budorick0f7b2002018-01-19 15:46:17 -08001882
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001883 #override
1884 def GetCacheMirror(self):
1885 return None
John Budorick0f7b2002018-01-19 15:46:17 -08001886
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001887 #override
1888 def GetActualRemoteURL(self, options):
1889 return self._root.service_url
John Budorick0f7b2002018-01-19 15:46:17 -08001890
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001891 #override
1892 def DoesRemoteURLMatch(self, options):
1893 del options
1894 return True
John Budorick0f7b2002018-01-19 15:46:17 -08001895
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001896 def revert(self, options, args, file_list):
1897 """Does nothing.
John Budorickd3ba72b2018-03-20 12:27:42 -07001898
1899 CIPD packages should be reverted at the root by running
1900 `CipdRoot.run('revert')`.
1901 """
John Budorick0f7b2002018-01-19 15:46:17 -08001902
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001903 def diff(self, options, args, file_list):
1904 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001905
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001906 def pack(self, options, args, file_list):
1907 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001908
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001909 def revinfo(self, options, args, file_list):
1910 """Grab the instance ID."""
1911 try:
1912 tmpdir = tempfile.mkdtemp()
1913 # Attempt to get instance_id from the root resolved cache.
1914 # Resolved cache will not match on any CIPD packages with
1915 # variables such as ${platform}, they will fall back to
1916 # the slower method below.
1917 resolved = self._root.resolved_packages()
1918 if resolved:
1919 # CIPD uses POSIX separators across all platforms, so
1920 # replace any Windows separators.
1921 path_split = self.relpath.replace(os.sep, "/").split(":")
1922 if len(path_split) > 1:
1923 src_path, package = path_split
1924 if src_path in resolved:
1925 for resolved_package in resolved[src_path]:
1926 if package == resolved_package.get(
1927 'pin', {}).get('package'):
1928 return resolved_package.get(
1929 'pin', {}).get('instance_id')
Dan Le Febvre456d0852023-05-24 23:43:40 +00001930
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001931 describe_json_path = os.path.join(tmpdir, 'describe.json')
1932 cmd = [
1933 'cipd', 'describe', self._package.name, '-log-level', 'error',
1934 '-version', self._package.version, '-json-output',
1935 describe_json_path
1936 ]
1937 gclient_utils.CheckCallAndFilter(cmd)
1938 with open(describe_json_path) as f:
1939 describe_json = json.load(f)
1940 return describe_json.get('result', {}).get('pin',
1941 {}).get('instance_id')
1942 finally:
1943 gclient_utils.rmtree(tmpdir)
John Budorick0f7b2002018-01-19 15:46:17 -08001944
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001945 def status(self, options, args, file_list):
1946 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001947
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001948 def update(self, options, args, file_list):
1949 """Does nothing.
John Budorickd3ba72b2018-03-20 12:27:42 -07001950
1951 CIPD packages should be updated at the root by running
1952 `CipdRoot.run('update')`.
1953 """