blob: e0b7b259db666a6829f50f803050a07f32b925a2 [file] [log] [blame]
steveblock@chromium.org93567042012-02-15 01:02:26 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00004"""Gclient-specific SCM-specific operations."""
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00005
John Budorick0f7b2002018-01-19 15:46:17 -08006import collections
7import contextlib
borenet@google.comb2256212014-05-07 20:57:28 +00008import errno
John Budorick0f7b2002018-01-19 15:46:17 -08009import json
maruel@chromium.org754960e2009-09-21 12:31:05 +000010import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000011import os
Tomasz Wiszkowskid4e66882021-08-19 21:35:09 +000012import platform
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000013import posixpath
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import re
Gavin Mak65c49b12023-08-24 18:06:42 +000015import shutil
maruel@chromium.org90541732011-04-01 17:54:18 +000016import sys
ilevy@chromium.org3534aa52013-07-20 01:58:08 +000017import tempfile
John Budorick0f7b2002018-01-19 15:46:17 -080018import threading
zty@chromium.org6279e8a2014-02-13 01:45:25 +000019import traceback
Raul Tambreb946b232019-03-26 14:48:46 +000020
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000021import gclient_utils
Ravi Mistryecda7822022-02-28 16:22:20 +000022import gerrit_util
szager@chromium.org848fd492014-04-09 19:06:44 +000023import git_cache
Josip Sokcevic7958e302023-03-01 23:02:21 +000024import scm
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000025import subprocess2
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000026
Mike Frysinger124bb8e2023-09-06 05:48:55 +000027# TODO: Should fix these warnings.
28# pylint: disable=line-too-long
29
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000030
smutae7ea312016-07-18 11:59:41 -070031class NoUsableRevError(gclient_utils.Error):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000032 """Raised if requested revision isn't found in checkout."""
smutae7ea312016-07-18 11:59:41 -070033
34
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000035class DiffFiltererWrapper(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000036 """Simple base class which tracks which file is being diffed and
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000037 replaces instances of its file name in the original and
agable41e3a6c2016-10-20 11:36:56 -070038 working copy lines of the git diff output."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +000039 index_string = None
40 original_prefix = "--- "
41 working_prefix = "+++ "
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000042
Mike Frysinger124bb8e2023-09-06 05:48:55 +000043 def __init__(self, relpath, print_func):
44 # Note that we always use '/' as the path separator to be
45 # consistent with cygwin-style output on Windows
46 self._relpath = relpath.replace("\\", "/")
47 self._current_file = None
48 self._print_func = print_func
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000049
Mike Frysinger124bb8e2023-09-06 05:48:55 +000050 def SetCurrentFile(self, current_file):
51 self._current_file = current_file
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000052
Mike Frysinger124bb8e2023-09-06 05:48:55 +000053 @property
54 def _replacement_file(self):
55 return posixpath.join(self._relpath, self._current_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000056
Mike Frysinger124bb8e2023-09-06 05:48:55 +000057 def _Replace(self, line):
58 return line.replace(self._current_file, self._replacement_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000059
Mike Frysinger124bb8e2023-09-06 05:48:55 +000060 def Filter(self, line):
61 if (line.startswith(self.index_string)):
62 self.SetCurrentFile(line[len(self.index_string):])
63 line = self._Replace(line)
64 else:
65 if (line.startswith(self.original_prefix)
66 or line.startswith(self.working_prefix)):
67 line = self._Replace(line)
68 self._print_func(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000069
70
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000071class GitDiffFilterer(DiffFiltererWrapper):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000072 index_string = "diff --git "
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000073
Mike Frysinger124bb8e2023-09-06 05:48:55 +000074 def SetCurrentFile(self, current_file):
75 # Get filename by parsing "a/<filename> b/<filename>"
76 self._current_file = current_file[:(len(current_file) / 2)][2:]
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000077
Mike Frysinger124bb8e2023-09-06 05:48:55 +000078 def _Replace(self, line):
79 return re.sub("[a|b]/" + self._current_file, self._replacement_file,
80 line)
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000081
82
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000083# SCMWrapper base class
84
Mike Frysinger124bb8e2023-09-06 05:48:55 +000085
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000086class SCMWrapper(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000087 """Add necessary glue between all the supported SCM.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000088
msb@chromium.orgd6504212010-01-13 17:34:31 +000089 This is the abstraction layer to bind to different SCM.
90 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +000091 def __init__(self,
92 url=None,
93 root_dir=None,
94 relpath=None,
95 out_fh=None,
96 out_cb=None,
97 print_outbuf=False):
98 self.url = url
99 self._root_dir = root_dir
100 if self._root_dir:
101 self._root_dir = self._root_dir.replace('/', os.sep)
102 self.relpath = relpath
103 if self.relpath:
104 self.relpath = self.relpath.replace('/', os.sep)
105 if self.relpath and self._root_dir:
106 self.checkout_path = os.path.join(self._root_dir, self.relpath)
107 if out_fh is None:
108 out_fh = sys.stdout
109 self.out_fh = out_fh
110 self.out_cb = out_cb
111 self.print_outbuf = print_outbuf
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000112
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000113 def Print(self, *args, **kwargs):
114 kwargs.setdefault('file', self.out_fh)
115 if kwargs.pop('timestamp', True):
116 self.out_fh.write('[%s] ' % gclient_utils.Elapsed())
117 print(*args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000118
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000119 def RunCommand(self, command, options, args, file_list=None):
120 commands = [
121 'update', 'updatesingle', 'revert', 'revinfo', 'status', 'diff',
122 'pack', 'runhooks'
123 ]
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000124
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000125 if not command in commands:
126 raise gclient_utils.Error('Unknown command %s' % command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000127
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000128 if not command in dir(self):
129 raise gclient_utils.Error(
130 'Command %s not implemented in %s wrapper' %
131 (command, self.__class__.__name__))
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000132
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000133 return getattr(self, command)(options, args, file_list)
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000134
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000135 @staticmethod
136 def _get_first_remote_url(checkout_path):
137 log = scm.GIT.Capture(
138 ['config', '--local', '--get-regexp', r'remote.*.url'],
139 cwd=checkout_path)
140 # Get the second token of the first line of the log.
141 return log.splitlines()[0].split(' ', 1)[1]
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000142
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000143 def GetCacheMirror(self):
144 if getattr(self, 'cache_dir', None):
145 url, _ = gclient_utils.SplitUrlRevision(self.url)
146 return git_cache.Mirror(url)
147 return None
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000148
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000149 def GetActualRemoteURL(self, options):
150 """Attempt to determine the remote URL for this SCMWrapper."""
151 # Git
152 if os.path.exists(os.path.join(self.checkout_path, '.git')):
153 actual_remote_url = self._get_first_remote_url(self.checkout_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000154
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000155 mirror = self.GetCacheMirror()
156 # If the cache is used, obtain the actual remote URL from there.
157 if (mirror and mirror.exists() and mirror.mirror_path.replace(
158 '\\', '/') == actual_remote_url.replace('\\', '/')):
159 actual_remote_url = self._get_first_remote_url(
160 mirror.mirror_path)
161 return actual_remote_url
162 return None
borenet@google.com88d10082014-03-21 17:24:48 +0000163
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000164 def DoesRemoteURLMatch(self, options):
165 """Determine whether the remote URL of this checkout is the expected URL."""
166 if not os.path.exists(self.checkout_path):
167 # A checkout which doesn't exist can't be broken.
168 return True
borenet@google.com88d10082014-03-21 17:24:48 +0000169
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000170 actual_remote_url = self.GetActualRemoteURL(options)
171 if actual_remote_url:
172 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip(
173 '/') == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/'))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000174
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000175 # This may occur if the self.checkout_path exists but does not contain a
176 # valid git checkout.
177 return False
borenet@google.com88d10082014-03-21 17:24:48 +0000178
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000179 def _DeleteOrMove(self, force):
180 """Delete the checkout directory or move it out of the way.
borenet@google.comb09097a2014-04-09 19:09:08 +0000181
182 Args:
183 force: bool; if True, delete the directory. Otherwise, just move it.
184 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000185 if force and os.environ.get('CHROME_HEADLESS') == '1':
186 self.Print('_____ Conflicting directory found in %s. Removing.' %
187 self.checkout_path)
188 gclient_utils.AddWarning('Conflicting directory %s deleted.' %
189 self.checkout_path)
190 gclient_utils.rmtree(self.checkout_path)
191 else:
192 bad_scm_dir = os.path.join(self._root_dir, '_bad_scm',
193 os.path.dirname(self.relpath))
borenet@google.comb2256212014-05-07 20:57:28 +0000194
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000195 try:
196 os.makedirs(bad_scm_dir)
197 except OSError as e:
198 if e.errno != errno.EEXIST:
199 raise
borenet@google.comb2256212014-05-07 20:57:28 +0000200
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000201 dest_path = tempfile.mkdtemp(prefix=os.path.basename(self.relpath),
202 dir=bad_scm_dir)
203 self.Print(
204 '_____ Conflicting directory found in %s. Moving to %s.' %
205 (self.checkout_path, dest_path))
206 gclient_utils.AddWarning('Conflicting directory %s moved to %s.' %
207 (self.checkout_path, dest_path))
208 shutil.move(self.checkout_path, dest_path)
borenet@google.comb09097a2014-04-09 19:09:08 +0000209
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000210
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000211class GitWrapper(SCMWrapper):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000212 """Wrapper for Git"""
213 name = 'git'
214 remote = 'origin'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000215
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000216 @property
217 def cache_dir(self):
218 try:
219 return git_cache.Mirror.GetCachePath()
220 except RuntimeError:
221 return None
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000222
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000223 def __init__(self, url=None, *args, **kwargs):
224 """Removes 'git+' fake prefix from git URL."""
225 if url and (url.startswith('git+http://')
226 or url.startswith('git+https://')):
227 url = url[4:]
228 SCMWrapper.__init__(self, url, *args, **kwargs)
229 filter_kwargs = {'time_throttle': 1, 'out_fh': self.out_fh}
230 if self.out_cb:
231 filter_kwargs['predicate'] = self.out_cb
232 self.filter = gclient_utils.GitFilter(**filter_kwargs)
233 self._running_under_rosetta = None
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000234
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000235 def GetCheckoutRoot(self):
236 return scm.GIT.GetCheckoutRoot(self.checkout_path)
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000237
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000238 def GetRevisionDate(self, _revision):
239 """Returns the given revision's date in ISO-8601 format (which contains the
floitsch@google.comeaab7842011-04-28 09:07:58 +0000240 time zone)."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000241 # TODO(floitsch): get the time-stamp of the given revision and not just
242 # the time-stamp of the currently checked out revision.
243 return self._Capture(['log', '-n', '1', '--format=%ai'])
floitsch@google.comeaab7842011-04-28 09:07:58 +0000244
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000245 def _GetDiffFilenames(self, base):
246 """Returns the names of files modified since base."""
247 return self._Capture(
248 # Filter to remove base if it is None.
249 list(
250 filter(
251 bool,
252 ['-c', 'core.quotePath=false', 'diff', '--name-only', base])
253 )).split()
Aaron Gablef4068aa2017-12-12 15:14:09 -0800254
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000255 def diff(self, options, _args, _file_list):
256 _, revision = gclient_utils.SplitUrlRevision(self.url)
257 if not revision:
258 revision = 'refs/remotes/%s/main' % self.remote
259 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000260
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000261 def pack(self, _options, _args, _file_list):
262 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000263 repository.
264
265 The patch file is generated from a diff of the merge base of HEAD and
266 its upstream branch.
267 """
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800268 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000269 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
270 except subprocess2.CalledProcessError:
271 merge_base = []
272 gclient_utils.CheckCallAndFilter(['git', 'diff'] + merge_base,
273 cwd=self.checkout_path,
274 filter_fn=GitDiffFilterer(
275 self.relpath,
276 print_func=self.Print).Filter)
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800277
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000278 def _Scrub(self, target, options):
279 """Scrubs out all changes in the local repo, back to the state of target."""
280 quiet = []
281 if not options.verbose:
282 quiet = ['--quiet']
283 self._Run(['reset', '--hard', target] + quiet, options)
284 if options.force and options.delete_unversioned_trees:
285 # where `target` is a commit that contains both upper and lower case
286 # versions of the same file on a case insensitive filesystem, we are
287 # actually in a broken state here. The index will have both 'a' and
288 # 'A', but only one of them will exist on the disk. To progress, we
289 # delete everything that status thinks is modified.
290 output = self._Capture(
291 ['-c', 'core.quotePath=false', 'status', '--porcelain'],
292 strip=False)
293 for line in output.splitlines():
294 # --porcelain (v1) looks like:
295 # XY filename
296 try:
297 filename = line[3:]
298 self.Print('_____ Deleting residual after reset: %r.' %
299 filename)
300 gclient_utils.rm_file_or_tree(
301 os.path.join(self.checkout_path, filename))
302 except OSError:
303 pass
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000304
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000305 def _FetchAndReset(self, revision, file_list, options):
306 """Equivalent to git fetch; git reset."""
307 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000308
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000309 self._Fetch(options, prune=True, quiet=options.verbose)
310 self._Scrub(revision, options)
311 if file_list is not None:
312 files = self._Capture(['-c', 'core.quotePath=false',
313 'ls-files']).splitlines()
314 file_list.extend(
315 [os.path.join(self.checkout_path, f) for f in files])
szager@chromium.org8a139702014-06-20 15:55:01 +0000316
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000317 def _DisableHooks(self):
318 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
319 if not os.path.isdir(hook_dir):
320 return
321 for f in os.listdir(hook_dir):
322 if not f.endswith('.sample') and not f.endswith('.disabled'):
323 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
324 if os.path.exists(disabled_hook_path):
325 os.remove(disabled_hook_path)
326 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
327
328 def _maybe_break_locks(self, options):
329 """This removes all .lock files from this repo's .git directory, if the
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000330 user passed the --break_repo_locks command line flag.
331
332 In particular, this will cleanup index.lock files, as well as ref lock
333 files.
334 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000335 if options.break_repo_locks:
336 git_dir = os.path.join(self.checkout_path, '.git')
337 for path, _, filenames in os.walk(git_dir):
338 for filename in filenames:
339 if filename.endswith('.lock'):
340 to_break = os.path.join(path, filename)
341 self.Print('breaking lock: %s' % (to_break, ))
342 try:
343 os.remove(to_break)
344 except OSError as ex:
345 self.Print('FAILED to break lock: %s: %s' %
346 (to_break, ex))
347 raise
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000348
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000349 def _download_topics(self, patch_rev, googlesource_url):
350 """This method returns new patch_revs to process that have the same topic.
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000351
352 It does the following:
353 1. Finds the topic of the Gerrit change specified in the patch_rev.
354 2. Find all changes with that topic.
355 3. Append patch_rev of the changes with the same topic to the patch_revs
356 to process.
357 4. Returns the new patch_revs to process.
358 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000359 patch_revs_to_process = []
360 # Parse the patch_rev to extract the CL and patchset.
361 patch_rev_tokens = patch_rev.split('/')
362 change = patch_rev_tokens[-2]
363 # Parse the googlesource_url.
364 tokens = re.search('//(.+).googlesource.com/(.+?)(?:\.git)?$',
365 googlesource_url)
366 if not tokens or len(tokens.groups()) != 2:
367 # googlesource_url is not in the expected format.
368 return patch_revs_to_process
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000369
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000370 # parse the gerrit host and repo out of googlesource_url.
371 host, repo = tokens.groups()[:2]
372 gerrit_host_url = '%s-review.googlesource.com' % host
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000373
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000374 # 1. Find the topic of the Gerrit change specified in the patch_rev.
375 change_object = gerrit_util.GetChange(gerrit_host_url, change)
376 topic = change_object.get('topic')
377 if not topic:
378 # This change has no topic set.
379 return patch_revs_to_process
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000380
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000381 # 2. Find all changes with that topic.
382 changes_with_same_topic = gerrit_util.QueryChanges(
383 gerrit_host_url, [('topic', topic), ('status', 'open'),
384 ('repo', repo)],
385 o_params=['ALL_REVISIONS'])
386 for c in changes_with_same_topic:
387 if str(c['_number']) == change:
388 # This change is already in the patch_rev.
389 continue
390 self.Print('Found CL %d with the topic name %s' %
391 (c['_number'], topic))
392 # 3. Append patch_rev of the changes with the same topic to the
393 # patch_revs to process.
394 curr_rev = c['current_revision']
395 new_patch_rev = c['revisions'][curr_rev]['ref']
396 patch_revs_to_process.append(new_patch_rev)
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000397
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000398 # 4. Return the new patch_revs to process.
399 return patch_revs_to_process
Ravi Mistryc848a4e2022-03-10 18:19:59 +0000400
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000401 def _ref_to_remote_ref(self, target_rev):
402 """Helper function for scm.GIT.RefToRemoteRef with error checking.
Kenneth Russell02e70b42023-08-07 22:09:29 +0000403
404 Joins the results of scm.GIT.RefToRemoteRef into a string, but raises a
405 comprehensible error if RefToRemoteRef fails.
406
407 Args:
408 target_rev: a ref somewhere under refs/.
409 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000410 tmp_ref = scm.GIT.RefToRemoteRef(target_rev, self.remote)
411 if not tmp_ref:
412 raise gclient_utils.Error(
413 'Failed to turn target revision %r in repo %r into remote ref' %
414 (target_rev, self.checkout_path))
415 return ''.join(tmp_ref)
Kenneth Russell02e70b42023-08-07 22:09:29 +0000416
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000417 def apply_patch_ref(self, patch_repo, patch_rev, target_rev, options,
418 file_list):
419 # type: (str, str, str, optparse.Values, Collection[str]) -> str
420 """Apply a patch on top of the revision we're synced at.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000421
Edward Lemur3acbc742019-05-30 17:57:35 +0000422 The patch ref is given by |patch_repo|@|patch_rev|.
423 |target_rev| is usually the branch that the |patch_rev| was uploaded against
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000424 (e.g. 'refs/heads/main'), but this is not required.
Edward Lemur3acbc742019-05-30 17:57:35 +0000425
426 We cherry-pick all commits reachable from |patch_rev| on top of the curret
427 HEAD, excluding those reachable from |target_rev|
428 (i.e. git cherry-pick target_rev..patch_rev).
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000429
430 Graphically, it looks like this:
431
Edward Lemur3acbc742019-05-30 17:57:35 +0000432 ... -> o -> [possibly already landed commits] -> target_rev
433 \
434 -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000435
Edward Lemur3acbc742019-05-30 17:57:35 +0000436 The final checkout state is then:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000437
Edward Lemur3acbc742019-05-30 17:57:35 +0000438 ... -> HEAD -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000439
440 After application, if |options.reset_patch_ref| is specified, we soft reset
Edward Lemur3acbc742019-05-30 17:57:35 +0000441 the cherry-picked changes, keeping them in git index only.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000442
443 Args:
Edward Lemur3acbc742019-05-30 17:57:35 +0000444 patch_repo: The patch origin.
445 e.g. 'https://foo.googlesource.com/bar'
446 patch_rev: The revision to patch.
447 e.g. 'refs/changes/1234/34/1'.
448 target_rev: The revision to use when finding the merge base.
449 Typically, the branch that the patch was uploaded against.
Josip Sokcevic9c0dc302020-11-20 18:41:25 +0000450 e.g. 'refs/heads/main' or 'refs/heads/infra/config'.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000451 options: The options passed to gclient.
452 file_list: A list where modified files will be appended.
453 """
454
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000455 # Abort any cherry-picks in progress.
Edward Lemur3acbc742019-05-30 17:57:35 +0000456 try:
Ravi Mistryecda7822022-02-28 16:22:20 +0000457 self._Capture(['cherry-pick', '--abort'])
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000458 except subprocess2.CalledProcessError:
Ravi Mistryecda7822022-02-28 16:22:20 +0000459 pass
Ravi Mistryecda7822022-02-28 16:22:20 +0000460
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000461 base_rev = self.revinfo(None, None, None)
Edward Lemurca7d8812018-07-24 17:42:45 +0000462
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000463 if not target_rev:
464 raise gclient_utils.Error(
465 'A target revision for the patch must be given')
Edward Lesmesc621b212018-03-21 20:26:56 -0400466
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000467 if target_rev.startswith(('refs/heads/', 'refs/branch-heads')):
468 # If |target_rev| is in refs/heads/** or refs/branch-heads/**, try
469 # first to find the corresponding remote ref for it, since
470 # |target_rev| might point to a local ref which is not up to date
471 # with the corresponding remote ref.
472 remote_ref = self._ref_to_remote_ref(target_rev)
473 self.Print('Trying the corresponding remote ref for %r: %r\n' %
474 (target_rev, remote_ref))
475 if scm.GIT.IsValidRevision(self.checkout_path, remote_ref):
476 # refs/remotes may need to be updated to cleanly cherry-pick
477 # changes. See https://crbug.com/1255178.
478 self._Capture(['fetch', '--no-tags', self.remote, target_rev])
479 target_rev = remote_ref
480 elif not scm.GIT.IsValidRevision(self.checkout_path, target_rev):
481 # Fetch |target_rev| if it's not already available.
482 url, _ = gclient_utils.SplitUrlRevision(self.url)
483 mirror = self._GetMirror(url, options, target_rev, target_rev)
484 if mirror:
485 rev_type = 'branch' if target_rev.startswith(
486 'refs/') else 'hash'
487 self._UpdateMirrorIfNotContains(mirror, options, rev_type,
488 target_rev)
489 self._Fetch(options, refspec=target_rev)
490
491 patch_revs_to_process = [patch_rev]
492
493 if hasattr(options, 'download_topics') and options.download_topics:
494 patch_revs_to_process_from_topics = self._download_topics(
495 patch_rev, self.url)
496 patch_revs_to_process.extend(patch_revs_to_process_from_topics)
497
498 self._Capture(['reset', '--hard'])
499 for pr in patch_revs_to_process:
500 self.Print('===Applying patch===')
501 self.Print('Revision to patch is %r @ %r.' % (patch_repo, pr))
502 self.Print('Current dir is %r' % self.checkout_path)
503 self._Capture(['fetch', '--no-tags', patch_repo, pr])
504 pr = self._Capture(['rev-parse', 'FETCH_HEAD'])
505
506 if not options.rebase_patch_ref:
507 self._Capture(['checkout', pr])
508 # Adjust base_rev to be the first parent of our checked out
509 # patch ref; This will allow us to correctly extend `file_list`,
510 # and will show the correct file-list to programs which do `git
511 # diff --cached` expecting to see the patch diff.
512 base_rev = self._Capture(['rev-parse', pr + '~'])
513 else:
514 self.Print('Will cherrypick %r .. %r on top of %r.' %
515 (target_rev, pr, base_rev))
516 try:
517 if scm.GIT.IsAncestor(pr,
518 target_rev,
519 cwd=self.checkout_path):
520 if len(patch_revs_to_process) > 1:
521 # If there are multiple patch_revs_to_process then
522 # we do not want want to invalidate a previous patch
523 # so throw an error.
524 raise gclient_utils.Error(
525 'patch_rev %s is an ancestor of target_rev %s. This '
526 'situation is unsupported when we need to apply multiple '
527 'patch_revs: %s' %
528 (pr, target_rev, patch_revs_to_process))
529 # If |patch_rev| is an ancestor of |target_rev|, check
530 # it out.
531 self._Capture(['checkout', pr])
532 else:
533 # If a change was uploaded on top of another change,
534 # which has already landed, one of the commits in the
535 # cherry-pick range will be redundant, since it has
536 # already landed and its changes incorporated in the
537 # tree. We pass '--keep-redundant-commits' to ignore
538 # those changes.
539 self._Capture([
540 'cherry-pick', target_rev + '..' + pr,
541 '--keep-redundant-commits'
542 ])
543
544 except subprocess2.CalledProcessError as e:
545 self.Print('Failed to apply patch.')
546 self.Print('Revision to patch was %r @ %r.' %
547 (patch_repo, pr))
548 self.Print('Tried to cherrypick %r .. %r on top of %r.' %
549 (target_rev, pr, base_rev))
550 self.Print('Current dir is %r' % self.checkout_path)
551 self.Print('git returned non-zero exit status %s:\n%s' %
552 (e.returncode, e.stderr.decode('utf-8')))
553 # Print the current status so that developers know what
554 # changes caused the patch failure, since git cherry-pick
555 # doesn't show that information.
556 self.Print(self._Capture(['status']))
557 try:
558 self._Capture(['cherry-pick', '--abort'])
559 except subprocess2.CalledProcessError:
560 pass
561 raise
562
563 if file_list is not None:
564 file_list.extend(self._GetDiffFilenames(base_rev))
565
566 latest_commit = self.revinfo(None, None, None)
567 if options.reset_patch_ref:
568 self._Capture(['reset', '--soft', base_rev])
569 return latest_commit
570
571 def check_diff(self, previous_commit, files=None):
572 # type: (str, Optional[List[str]]) -> bool
573 """Check if a diff exists between the current commit and `previous_commit`.
Joanna Wang5a7c8242022-07-01 19:09:00 +0000574
575 Returns True if there were diffs or if an error was encountered.
576 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000577 cmd = ['diff', previous_commit, '--quiet']
578 if files:
579 cmd += ['--'] + files
580 try:
581 self._Capture(cmd)
582 return False
583 except subprocess2.CalledProcessError as e:
584 # git diff --quiet exits with 1 if there were diffs.
585 if e.returncode != 1:
586 self.Print('git returned non-zero exit status %s:\n%s' %
587 (e.returncode, e.stderr.decode('utf-8')))
588 return True
Joanna Wang5a7c8242022-07-01 19:09:00 +0000589
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000590 def set_config(f):
591 def wrapper(*args):
592 return_val = f(*args)
593 if os.path.exists(os.path.join(args[0].checkout_path, '.git')):
594 # If diff.ignoreSubmodules is not already set, set it to `all`.
595 config = subprocess2.capture(['git', 'config', '-l'],
596 cwd=args[0].checkout_path).decode(
597 'utf-8').strip().splitlines()
598 if 'diff.ignoresubmodules=dirty' not in config:
599 subprocess2.capture(
600 ['git', 'config', 'diff.ignoreSubmodules', 'dirty'],
601 cwd=args[0].checkout_path)
602 if 'fetch.recursesubmodules=off' not in config:
603 subprocess2.capture(
604 ['git', 'config', 'fetch.recurseSubmodules', 'off'],
605 cwd=args[0].checkout_path)
Josip Sokcevic3b9212b2023-09-18 19:26:26 +0000606 if 'push.recursesubmodules=off' not in config:
607 # The default is off, but if user sets submodules.recurse to
608 # on, this becomes on too. We never want to push submodules
609 # for gclient managed repositories. Push, even if a no-op,
610 # will increase `git cl upload` latency.
611 subprocess2.capture(
612 ['git', 'config', 'push.recurseSubmodules', 'off'],
613 cwd=args[0].checkout_path)
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000614 return return_val
Joanna Wange1753f62023-06-26 14:32:43 +0000615
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000616 return wrapper
Joanna Wange1753f62023-06-26 14:32:43 +0000617
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000618 @set_config
619 def update(self, options, args, file_list):
620 """Runs git to update or transparently checkout the working copy.
msb@chromium.orge28e4982009-09-25 20:51:45 +0000621
622 All updated files will be appended to file_list.
623
624 Raises:
625 Error: if can't get URL for relative path.
626 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000627 if args:
628 raise gclient_utils.Error("Unsupported argument(s): %s" %
629 ",".join(args))
msb@chromium.orge28e4982009-09-25 20:51:45 +0000630
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000631 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000632
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000633 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
634 revision = deps_revision
635 managed = True
636 if options.revision:
637 # Override the revision number.
638 revision = str(options.revision)
639 if revision == 'unmanaged':
640 # Check again for a revision in case an initial ref was specified
641 # in the url, for example bla.git@refs/heads/custombranch
642 revision = deps_revision
643 managed = False
644 if not revision:
645 # If a dependency is not pinned, track the default remote branch.
646 revision = scm.GIT.GetRemoteHeadRef(self.checkout_path, self.url,
647 self.remote)
648 if revision.startswith('origin/'):
649 revision = 'refs/remotes/' + revision
msb@chromium.orge28e4982009-09-25 20:51:45 +0000650
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000651 if managed and platform.system() == 'Windows':
652 self._DisableHooks()
szager@chromium.org8a139702014-06-20 15:55:01 +0000653
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000654 printed_path = False
655 verbose = []
656 if options.verbose:
657 self.Print('_____ %s at %s' % (self.relpath, revision),
658 timestamp=False)
659 verbose = ['--verbose']
660 printed_path = True
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000661
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000662 revision_ref = revision
663 if ':' in revision:
664 revision_ref, _, revision = revision.partition(':')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000665
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000666 if revision_ref.startswith('refs/branch-heads'):
667 options.with_branch_heads = True
Edward Lesmes8073a502020-04-15 02:11:14 +0000668
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000669 mirror = self._GetMirror(url, options, revision, revision_ref)
670 if mirror:
671 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000672
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000673 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
674 if remote_ref:
675 # Rewrite remote refs to their local equivalents.
676 revision = ''.join(remote_ref)
677 rev_type = "branch"
678 elif revision.startswith('refs/'):
679 # Local branch? We probably don't want to support, since DEPS should
680 # always specify branches as they are in the upstream repo.
681 rev_type = "branch"
Aravind Vasudevancf465852023-03-29 16:47:12 +0000682 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000683 # hash is also a tag, only make a distinction at checkout
684 rev_type = "hash"
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000685
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000686 # If we are going to introduce a new project, there is a possibility
687 # that we are syncing back to a state where the project was originally a
688 # sub-project rolled by DEPS (realistic case: crossing the Blink merge
689 # point syncing backwards, when Blink was a DEPS entry and not part of
690 # src.git). In such case, we might have a backup of the former .git
691 # folder, which can be used to avoid re-fetching the entire repo again
692 # (useful for bisects).
693 backup_dir = self.GetGitBackupDirPath()
694 target_dir = os.path.join(self.checkout_path, '.git')
695 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
696 gclient_utils.safe_makedirs(self.checkout_path)
697 os.rename(backup_dir, target_dir)
698 # Reset to a clean state
699 self._Scrub('HEAD', options)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000700
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000701 if (not os.path.exists(self.checkout_path) or
702 (os.path.isdir(self.checkout_path)
703 and not os.path.exists(os.path.join(self.checkout_path, '.git')))):
704 if mirror:
705 self._UpdateMirrorIfNotContains(mirror, options, rev_type,
706 revision)
707 try:
708 self._Clone(revision, url, options)
709 except subprocess2.CalledProcessError as e:
710 logging.warning('Clone failed due to: %s', e)
711 self._DeleteOrMove(options.force)
712 self._Clone(revision, url, options)
713 if file_list is not None:
714 files = self._Capture(
715 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
716 file_list.extend(
717 [os.path.join(self.checkout_path, f) for f in files])
718 if mirror:
719 self._Capture(
720 ['remote', 'set-url', '--push', 'origin', mirror.url])
721 if not verbose:
722 # Make the output a little prettier. It's nice to have some
723 # whitespace between projects when cloning.
724 self.Print('')
725 return self._Capture(['rev-parse', '--verify', 'HEAD'])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000726
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000727 if mirror:
728 self._Capture(['remote', 'set-url', '--push', 'origin', mirror.url])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000729
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000730 if not managed:
731 self._SetFetchConfig(options)
732 self.Print('________ unmanaged solution; skipping %s' %
733 self.relpath)
734 return self._Capture(['rev-parse', '--verify', 'HEAD'])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000735
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000736 self._maybe_break_locks(options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000737
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000738 if mirror:
739 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
Edward Lemur579c9862018-07-13 23:17:51 +0000740
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000741 # See if the url has changed (the unittests use git://foo for the url,
742 # let that through).
743 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
744 return_early = False
745 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
746 # unit test pass. (and update the comment above)
747 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
748 # This allows devs to use experimental repos which have a different url
749 # but whose branch(s) are the same as official repos.
Joanna Wang4af28182023-09-22 18:16:27 +0000750 strp_url = url[:-4] if url.endswith('.git') else url
751 strp_current_url = current_url[:-4] if current_url.endswith(
752 '.git') else current_url
753 if (strp_current_url.rstrip('/') != strp_url.rstrip('/')
754 and url != 'git://foo' and
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000755 subprocess2.capture([
756 'git', 'config',
757 'remote.%s.gclient-auto-fix-url' % self.remote
758 ],
759 cwd=self.checkout_path).strip() != 'False'):
760 self.Print('_____ switching %s from %s to new upstream %s' %
761 (self.relpath, current_url, url))
762 if not (options.force or options.reset):
763 # Make sure it's clean
764 self._CheckClean(revision)
765 # Switch over to the new upstream
766 self._Run(['remote', 'set-url', self.remote, url], options)
767 if mirror:
768 if git_cache.Mirror.CacheDirToUrl(current_url.rstrip(
769 '/')) == git_cache.Mirror.CacheDirToUrl(
770 url.rstrip('/')):
771 # Reset alternates when the cache dir is updated.
772 with open(
773 os.path.join(self.checkout_path, '.git', 'objects',
774 'info', 'alternates'), 'w') as fh:
775 fh.write(os.path.join(url, 'objects'))
776 else:
777 # Because we use Git alternatives, our existing repository
778 # is not self-contained. It's possible that new git
779 # alternative doesn't have all necessary objects that the
780 # current repository needs. Instead of blindly hoping that
781 # new alternative contains all necessary objects, keep the
782 # old alternative and just append a new one on top of it.
783 with open(
784 os.path.join(self.checkout_path, '.git', 'objects',
785 'info', 'alternates'), 'a') as fh:
786 fh.write("\n" + os.path.join(url, 'objects'))
787 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
788 self._FetchAndReset(revision, file_list, options)
Michael Spang73fac912019-03-08 18:44:19 +0000789
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000790 return_early = True
791 else:
792 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
793
794 if return_early:
795 return self._Capture(['rev-parse', '--verify', 'HEAD'])
796
797 cur_branch = self._GetCurrentBranch()
798
799 # Cases:
800 # 0) HEAD is detached. Probably from our initial clone.
801 # - make sure HEAD is contained by a named ref, then update.
802 # Cases 1-4. HEAD is a branch.
803 # 1) current branch is not tracking a remote branch
804 # - try to rebase onto the new hash or branch
805 # 2) current branch is tracking a remote branch with local committed
806 # changes, but the DEPS file switched to point to a hash
807 # - rebase those changes on top of the hash
808 # 3) current branch is tracking a remote branch w/or w/out changes, and
809 # no DEPS switch
810 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
811 # 4) current branch is tracking a remote branch, but DEPS switches to a
812 # different remote branch, and a) current branch has no local changes,
813 # and --force: - checkout new branch b) current branch has local
814 # changes, and --force and --reset: - checkout new branch c) otherwise
815 # exit
816
817 # GetUpstreamBranch returns something like 'refs/remotes/origin/main'
818 # for a tracking branch or 'main' if not a tracking branch (it's based
819 # on a specific rev/hash) or it returns None if it couldn't find an
820 # upstream
821 if cur_branch is None:
822 upstream_branch = None
823 current_type = "detached"
824 logging.debug("Detached HEAD")
825 else:
826 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
827 if not upstream_branch or not upstream_branch.startswith(
828 'refs/remotes'):
829 current_type = "hash"
830 logging.debug(
831 "Current branch is not tracking an upstream (remote)"
832 " branch.")
833 elif upstream_branch.startswith('refs/remotes'):
834 current_type = "branch"
835 else:
836 raise gclient_utils.Error('Invalid Upstream: %s' %
837 upstream_branch)
838
839 self._SetFetchConfig(options)
840
841 # Fetch upstream if we don't already have |revision|.
842 if not scm.GIT.IsValidRevision(
843 self.checkout_path, revision, sha_only=True):
844 self._Fetch(options, prune=options.force)
845
846 if not scm.GIT.IsValidRevision(
847 self.checkout_path, revision, sha_only=True):
848 # Update the remotes first so we have all the refs.
849 remote_output = scm.GIT.Capture(['remote'] + verbose +
850 ['update'],
851 cwd=self.checkout_path)
852 if verbose:
853 self.Print(remote_output)
854
855 revision = self._AutoFetchRef(options, revision)
856
857 # This is a big hammer, debatable if it should even be here...
858 if options.force or options.reset:
859 target = 'HEAD'
860 if options.upstream and upstream_branch:
861 target = upstream_branch
862 self._Scrub(target, options)
863
864 if current_type == 'detached':
865 # case 0
866 # We just did a Scrub, this is as clean as it's going to get. In
867 # particular if HEAD is a commit that contains two versions of the
868 # same file on a case-insensitive filesystem (e.g. 'a' and 'A'),
869 # there's no way to actually "Clean" the checkout; that commit is
870 # uncheckoutable on this system. The best we can do is carry forward
871 # to the checkout step.
872 if not (options.force or options.reset):
873 self._CheckClean(revision)
874 self._CheckDetachedHead(revision, options)
875 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
876 self.Print('Up-to-date; skipping checkout.')
877 else:
878 # 'git checkout' may need to overwrite existing untracked files.
879 # Allow it only when nuclear options are enabled.
880 self._Checkout(
881 options,
882 revision,
883 force=(options.force and options.delete_unversioned_trees),
884 quiet=True,
885 )
886 if not printed_path:
887 self.Print('_____ %s at %s' % (self.relpath, revision),
888 timestamp=False)
889 elif current_type == 'hash':
890 # case 1
891 # Can't find a merge-base since we don't know our upstream. That
892 # makes this command VERY likely to produce a rebase failure. For
893 # now we assume origin is our upstream since that's what the old
894 # behavior was.
895 upstream_branch = self.remote
896 if options.revision or deps_revision:
897 upstream_branch = revision
898 self._AttemptRebase(upstream_branch,
899 file_list,
900 options,
901 printed_path=printed_path,
902 merge=options.merge)
903 printed_path = True
904 elif rev_type == 'hash':
905 # case 2
906 self._AttemptRebase(upstream_branch,
907 file_list,
908 options,
909 newbase=revision,
910 printed_path=printed_path,
911 merge=options.merge)
912 printed_path = True
913 elif remote_ref and ''.join(remote_ref) != upstream_branch:
914 # case 4
915 new_base = ''.join(remote_ref)
916 if not printed_path:
917 self.Print('_____ %s at %s' % (self.relpath, revision),
918 timestamp=False)
919 switch_error = (
920 "Could not switch upstream branch from %s to %s\n" %
921 (upstream_branch, new_base) +
922 "Please use --force or merge or rebase manually:\n" +
923 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
924 "OR git checkout -b <some new branch> %s" % new_base)
925 force_switch = False
926 if options.force:
927 try:
928 self._CheckClean(revision)
929 # case 4a
930 force_switch = True
931 except gclient_utils.Error as e:
932 if options.reset:
933 # case 4b
934 force_switch = True
935 else:
Philipp Thielf2449cd2023-10-20 17:27:25 +0000936 switch_error = '%s\n%s' % (str(e), switch_error)
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000937 if force_switch:
938 self.Print("Switching upstream branch from %s to %s" %
939 (upstream_branch, new_base))
940 switch_branch = 'gclient_' + remote_ref[1]
941 self._Capture(['branch', '-f', switch_branch, new_base])
942 self._Checkout(options, switch_branch, force=True, quiet=True)
943 else:
944 # case 4c
945 raise gclient_utils.Error(switch_error)
946 else:
947 # case 3 - the default case
948 rebase_files = self._GetDiffFilenames(upstream_branch)
949 if verbose:
950 self.Print('Trying fast-forward merge to branch : %s' %
951 upstream_branch)
952 try:
953 merge_args = ['merge']
954 if options.merge:
955 merge_args.append('--ff')
956 else:
957 merge_args.append('--ff-only')
958 merge_args.append(upstream_branch)
959 merge_output = self._Capture(merge_args)
960 except subprocess2.CalledProcessError as e:
961 rebase_files = []
962 if re.search(b'fatal: Not possible to fast-forward, aborting.',
963 e.stderr):
964 if not printed_path:
965 self.Print('_____ %s at %s' % (self.relpath, revision),
966 timestamp=False)
967 printed_path = True
968 while True:
969 if not options.auto_rebase:
970 try:
971 action = self._AskForData(
972 'Cannot %s, attempt to rebase? '
973 '(y)es / (q)uit / (s)kip : ' %
974 ('merge' if options.merge else
975 'fast-forward merge'), options)
976 except ValueError:
977 raise gclient_utils.Error('Invalid Character')
978 if options.auto_rebase or re.match(
979 r'yes|y', action, re.I):
980 self._AttemptRebase(upstream_branch,
981 rebase_files,
982 options,
983 printed_path=printed_path,
984 merge=False)
985 printed_path = True
986 break
987
988 if re.match(r'quit|q', action, re.I):
989 raise gclient_utils.Error(
990 "Can't fast-forward, please merge or "
991 "rebase manually.\n"
992 "cd %s && git " % self.checkout_path +
993 "rebase %s" % upstream_branch)
994
995 if re.match(r'skip|s', action, re.I):
996 self.Print('Skipping %s' % self.relpath)
997 return
998
999 self.Print('Input not recognized')
1000 elif re.match(
1001 b"error: Your local changes to '.*' would be "
1002 b"overwritten by merge. Aborting.\nPlease, commit your "
1003 b"changes or stash them before you can merge.\n",
1004 e.stderr):
1005 if not printed_path:
1006 self.Print('_____ %s at %s' % (self.relpath, revision),
1007 timestamp=False)
1008 printed_path = True
1009 raise gclient_utils.Error(e.stderr.decode('utf-8'))
1010 else:
1011 # Some other problem happened with the merge
1012 logging.error("Error during fast-forward merge in %s!" %
1013 self.relpath)
1014 self.Print(e.stderr.decode('utf-8'))
1015 raise
1016 else:
1017 # Fast-forward merge was successful
1018 if not re.match('Already up-to-date.', merge_output) or verbose:
1019 if not printed_path:
1020 self.Print('_____ %s at %s' % (self.relpath, revision),
1021 timestamp=False)
1022 printed_path = True
1023 self.Print(merge_output.strip())
1024 if not verbose:
1025 # Make the output a little prettier. It's nice to have
1026 # some whitespace between projects when syncing.
1027 self.Print('')
1028
1029 if file_list is not None:
1030 file_list.extend(
1031 [os.path.join(self.checkout_path, f) for f in rebase_files])
1032
1033 # If the rebase generated a conflict, abort and ask user to fix
1034 if self._IsRebasing():
1035 raise gclient_utils.Error(
1036 '\n____ %s at %s\n'
1037 '\nConflict while rebasing this branch.\n'
1038 'Fix the conflict and run gclient again.\n'
1039 'See man git-rebase for details.\n' % (self.relpath, revision))
1040
Michael Spang73fac912019-03-08 18:44:19 +00001041 if verbose:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001042 self.Print('Checked out revision %s' %
1043 self.revinfo(options, (), None),
agable83faed02016-10-24 14:37:10 -07001044 timestamp=False)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001045
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001046 # If --reset and --delete_unversioned_trees are specified, remove any
1047 # untracked directories.
1048 if options.reset and options.delete_unversioned_trees:
1049 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1
1050 # (the merge-base by default), so doesn't include untracked files.
1051 # So we use 'git ls-files --directory --others --exclude-standard'
1052 # here directly.
1053 paths = scm.GIT.Capture([
1054 '-c', 'core.quotePath=false', 'ls-files', '--directory',
1055 '--others', '--exclude-standard'
1056 ], self.checkout_path)
1057 for path in (p for p in paths.splitlines() if p.endswith('/')):
1058 full_path = os.path.join(self.checkout_path, path)
1059 if not os.path.islink(full_path):
1060 self.Print('_____ removing unversioned directory %s' % path)
1061 gclient_utils.rmtree(full_path)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001062
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001063 return self._Capture(['rev-parse', '--verify', 'HEAD'])
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001064
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001065 def revert(self, options, _args, file_list):
1066 """Reverts local modifications.
msb@chromium.orge28e4982009-09-25 20:51:45 +00001067
1068 All reverted files will be appended to file_list.
1069 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001070 if not os.path.isdir(self.checkout_path):
1071 # revert won't work if the directory doesn't exist. It needs to
1072 # checkout instead.
1073 self.Print('_____ %s is missing, syncing instead' % self.relpath)
1074 # Don't reuse the args.
1075 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001076
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001077 default_rev = "refs/heads/main"
1078 if options.upstream:
1079 if self._GetCurrentBranch():
1080 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
1081 default_rev = upstream_branch or default_rev
1082 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
1083 if not deps_revision:
1084 deps_revision = default_rev
1085 if deps_revision.startswith('refs/heads/'):
1086 deps_revision = deps_revision.replace('refs/heads/',
1087 self.remote + '/')
1088 try:
1089 deps_revision = self.GetUsableRev(deps_revision, options)
1090 except NoUsableRevError as e:
1091 # If the DEPS entry's url and hash changed, try to update the
1092 # origin. See also http://crbug.com/520067.
1093 logging.warning(
1094 "Couldn't find usable revision, will retrying to update instead: %s",
Philipp Thielf2449cd2023-10-20 17:27:25 +00001095 str(e))
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001096 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +00001097
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001098 if file_list is not None:
1099 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001100
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001101 self._Scrub(deps_revision, options)
1102 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001103
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001104 if file_list is not None:
1105 file_list.extend(
1106 [os.path.join(self.checkout_path, f) for f in files])
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001107
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001108 def revinfo(self, _options, _args, _file_list):
1109 """Returns revision"""
1110 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +00001111
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001112 def runhooks(self, options, args, file_list):
1113 self.status(options, args, file_list)
msb@chromium.orge28e4982009-09-25 20:51:45 +00001114
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001115 def status(self, options, _args, file_list):
1116 """Display status information."""
1117 if not os.path.isdir(self.checkout_path):
1118 self.Print('________ couldn\'t run status in %s:\n'
1119 'The directory does not exist.' % self.checkout_path)
1120 else:
1121 merge_base = []
1122 if self.url:
1123 _, base_rev = gclient_utils.SplitUrlRevision(self.url)
1124 if base_rev:
1125 if base_rev.startswith('refs/'):
1126 base_rev = self._ref_to_remote_ref(base_rev)
1127 merge_base = [base_rev]
1128 self._Run(['-c', 'core.quotePath=false', 'diff', '--name-status'] +
1129 merge_base,
1130 options,
1131 always_show_header=options.verbose)
1132 if file_list is not None:
1133 files = self._GetDiffFilenames(
1134 merge_base[0] if merge_base else None)
1135 file_list.extend(
1136 [os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +00001137
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001138 def GetUsableRev(self, rev, options):
1139 """Finds a useful revision for this repository."""
1140 sha1 = None
1141 if not os.path.isdir(self.checkout_path):
1142 raise NoUsableRevError(
1143 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -07001144
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001145 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1146 sha1 = rev
1147 else:
1148 # May exist in origin, but we don't have it yet, so fetch and look
1149 # again.
1150 self._Fetch(options)
1151 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
1152 sha1 = rev
smutae7ea312016-07-18 11:59:41 -07001153
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001154 if not sha1:
1155 raise NoUsableRevError(
1156 'Hash %s does not appear to be a valid hash in this repo.' %
1157 rev)
smutae7ea312016-07-18 11:59:41 -07001158
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001159 return sha1
smutae7ea312016-07-18 11:59:41 -07001160
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001161 def GetGitBackupDirPath(self):
1162 """Returns the path where the .git folder for the current project can be
primiano@chromium.org1c127382015-02-17 11:15:40 +00001163 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001164 return os.path.join(self._root_dir,
1165 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
primiano@chromium.org1c127382015-02-17 11:15:40 +00001166
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001167 def _GetMirror(self, url, options, revision=None, revision_ref=None):
1168 """Get a git_cache.Mirror object for the argument url."""
1169 if not self.cache_dir:
1170 return None
1171 mirror_kwargs = {
1172 'print_func': self.filter,
1173 'refs': [],
1174 'commits': [],
1175 }
1176 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1177 mirror_kwargs['refs'].append('refs/branch-heads/*')
1178 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
1179 mirror_kwargs['refs'].append(revision_ref)
1180 if hasattr(options, 'with_tags') and options.with_tags:
1181 mirror_kwargs['refs'].append('refs/tags/*')
1182 elif revision_ref and revision_ref.startswith('refs/tags/'):
1183 mirror_kwargs['refs'].append(revision_ref)
1184 if revision and not revision.startswith('refs/'):
1185 mirror_kwargs['commits'].append(revision)
1186 return git_cache.Mirror(url, **mirror_kwargs)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001187
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001188 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
1189 """Update a git mirror by fetching the latest commits from the remote,
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001190 unless mirror already contains revision whose type is sha1 hash.
1191 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001192 if rev_type == 'hash' and mirror.contains_revision(revision):
1193 if options.verbose:
1194 self.Print('skipping mirror update, it has rev=%s already' %
1195 revision,
1196 timestamp=False)
1197 return
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001198
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001199 if getattr(options, 'shallow', False):
1200 depth = 10000
1201 else:
1202 depth = None
1203 mirror.populate(verbose=options.verbose,
1204 bootstrap=not getattr(options, 'no_bootstrap', False),
1205 depth=depth,
1206 lock_timeout=getattr(options, 'lock_timeout', 0))
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001207
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001208 def _Clone(self, revision, url, options):
1209 """Clone a git repository from the given URL.
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001210
Josip Sokcevic88cc0b82023-11-09 19:19:23 +00001211 Once we've cloned the repo, we checkout a working branch if the
1212 specified revision is a branch head. If it is a tag or a specific
1213 commit, then we leave HEAD detached as it makes future updates simpler
1214 -- in this case the user should first create a new branch or switch to
1215 an existing branch before making changes in the repo."""
Joanna Wang1a977bd2022-06-02 21:51:17 +00001216
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001217 if self.print_outbuf:
1218 print_stdout = True
1219 filter_fn = None
1220 else:
1221 print_stdout = False
1222 filter_fn = self.filter
Joanna Wang1a977bd2022-06-02 21:51:17 +00001223
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001224 if not options.verbose:
1225 # git clone doesn't seem to insert a newline properly before
1226 # printing to stdout
1227 self.Print('')
Joanna Wang1a977bd2022-06-02 21:51:17 +00001228
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001229 # If the parent directory does not exist, Git clone on Windows will not
1230 # create it, so we need to do it manually.
1231 parent_dir = os.path.dirname(self.checkout_path)
1232 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001233
Josip Sokcevic88cc0b82023-11-09 19:19:23 +00001234 if hasattr(options, 'no_history') and options.no_history:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001235 self._Run(['init', self.checkout_path], options, cwd=self._root_dir)
1236 self._Run(['remote', 'add', 'origin', url], options)
1237 revision = self._AutoFetchRef(options, revision, depth=1)
1238 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1239 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
1240 else:
1241 cfg = gclient_utils.DefaultIndexPackConfig(url)
1242 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
1243 if self.cache_dir:
1244 clone_cmd.append('--shared')
1245 if options.verbose:
1246 clone_cmd.append('--verbose')
1247 clone_cmd.append(url)
1248 tmp_dir = tempfile.mkdtemp(prefix='_gclient_%s_' %
1249 os.path.basename(self.checkout_path),
1250 dir=parent_dir)
1251 clone_cmd.append(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001252
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001253 try:
1254 self._Run(clone_cmd,
1255 options,
1256 cwd=self._root_dir,
1257 retry=True,
1258 print_stdout=print_stdout,
1259 filter_fn=filter_fn)
1260 logging.debug(
1261 'Cloned into temporary dir, moving to checkout_path')
1262 gclient_utils.safe_makedirs(self.checkout_path)
1263 gclient_utils.safe_rename(
1264 os.path.join(tmp_dir, '.git'),
1265 os.path.join(self.checkout_path, '.git'))
1266 except:
1267 traceback.print_exc(file=self.out_fh)
1268 raise
1269 finally:
1270 if os.listdir(tmp_dir):
1271 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
1272 gclient_utils.rmtree(tmp_dir)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001273
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001274 self._SetFetchConfig(options)
1275 self._Fetch(options, prune=options.force)
1276 revision = self._AutoFetchRef(options, revision)
1277 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1278 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
Joanna Wang1a977bd2022-06-02 21:51:17 +00001279
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001280 if self._GetCurrentBranch() is None:
1281 # Squelch git's very verbose detached HEAD warning and use our own
1282 self.Print((
1283 'Checked out %s to a detached HEAD. Before making any commits\n'
1284 'in this repo, you should use \'git checkout <branch>\' to switch \n'
1285 'to an existing branch or use \'git checkout %s -b <branch>\' to\n'
1286 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001287
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001288 def _AskForData(self, prompt, options):
1289 if options.jobs > 1:
1290 self.Print(prompt)
1291 raise gclient_utils.Error("Background task requires input. Rerun "
1292 "gclient with --jobs=1 so that\n"
1293 "interaction is possible.")
1294 return gclient_utils.AskForData(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001295
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001296 def _AttemptRebase(self,
1297 upstream,
1298 files,
1299 options,
1300 newbase=None,
1301 branch=None,
1302 printed_path=False,
1303 merge=False):
1304 """Attempt to rebase onto either upstream or, if specified, newbase."""
1305 if files is not None:
1306 files.extend(self._GetDiffFilenames(upstream))
1307 revision = upstream
1308 if newbase:
1309 revision = newbase
1310 action = 'merge' if merge else 'rebase'
1311 if not printed_path:
1312 self.Print('_____ %s : Attempting %s onto %s...' %
1313 (self.relpath, action, revision))
1314 printed_path = True
1315 else:
1316 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001317
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001318 if merge:
1319 merge_output = self._Capture(['merge', revision])
1320 if options.verbose:
1321 self.Print(merge_output)
1322 return
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001323
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001324 # Build the rebase command here using the args
1325 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1326 rebase_cmd = ['rebase']
1327 if options.verbose:
1328 rebase_cmd.append('--verbose')
1329 if newbase:
1330 rebase_cmd.extend(['--onto', newbase])
1331 rebase_cmd.append(upstream)
1332 if branch:
1333 rebase_cmd.append(branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001334
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001335 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001336 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001337 except subprocess2.CalledProcessError as e:
1338 if (re.match(
1339 br'cannot rebase: you have unstaged changes', e.stderr
1340 ) or re.match(
1341 br'cannot rebase: your index contains uncommitted changes',
1342 e.stderr)):
1343 while True:
1344 rebase_action = self._AskForData(
1345 'Cannot rebase because of unstaged changes.\n'
1346 '\'git reset --hard HEAD\' ?\n'
1347 'WARNING: destroys any uncommitted work in your current branch!'
1348 ' (y)es / (q)uit / (s)how : ', options)
1349 if re.match(r'yes|y', rebase_action, re.I):
1350 self._Scrub('HEAD', options)
1351 # Should this be recursive?
1352 rebase_output = scm.GIT.Capture(rebase_cmd,
1353 cwd=self.checkout_path)
1354 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001355
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001356 if re.match(r'quit|q', rebase_action, re.I):
1357 raise gclient_utils.Error(
1358 "Please merge or rebase manually\n"
1359 "cd %s && git " % self.checkout_path +
1360 "%s" % ' '.join(rebase_cmd))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001361
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001362 if re.match(r'show|s', rebase_action, re.I):
1363 self.Print('%s' % e.stderr.decode('utf-8').strip())
1364 continue
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001365
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001366 gclient_utils.Error("Input not recognized")
1367 continue
1368 elif re.search(br'^CONFLICT', e.stdout, re.M):
1369 raise gclient_utils.Error(
1370 "Conflict while rebasing this branch.\n"
1371 "Fix the conflict and run gclient again.\n"
1372 "See 'man git-rebase' for details.\n")
1373 else:
1374 self.Print(e.stdout.decode('utf-8').strip())
1375 self.Print('Rebase produced error output:\n%s' %
1376 e.stderr.decode('utf-8').strip())
1377 raise gclient_utils.Error(
1378 "Unrecognized error, please merge or rebase "
1379 "manually.\ncd %s && git " % self.checkout_path +
1380 "%s" % ' '.join(rebase_cmd))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001381
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001382 self.Print(rebase_output.strip())
1383 if not options.verbose:
1384 # Make the output a little prettier. It's nice to have some
1385 # whitespace between projects when syncing.
1386 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001387
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001388 @staticmethod
1389 def _CheckMinVersion(min_version):
1390 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1391 if not ok:
1392 raise gclient_utils.Error('git version %s < minimum required %s' %
1393 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001394
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001395 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
1396 # Special case handling if all 3 conditions are met:
1397 # * the mirros have recently changed, but deps destination remains same,
1398 # * the git histories of mirrors are conflicting. * git cache is used
1399 # This manifests itself in current checkout having invalid HEAD commit
1400 # on most git operations. Since git cache is used, just deleted the .git
1401 # folder, and re-create it by cloning.
1402 try:
1403 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1404 except subprocess2.CalledProcessError as e:
1405 if (b'fatal: bad object HEAD' in e.stderr and self.cache_dir
1406 and self.cache_dir in url):
1407 self.Print(
1408 ('Likely due to DEPS change with git cache_dir, '
1409 'the current commit points to no longer existing object.\n'
1410 '%s' % e))
1411 self._DeleteOrMove(options.force)
1412 self._Clone(revision, url, options)
1413 else:
1414 raise
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001415
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001416 def _IsRebasing(self):
1417 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git
1418 # doesn't have a plumbing command to determine whether a rebase is in
1419 # progress, so for now emualate (more-or-less) git-rebase.sh /
1420 # git-completion.bash
1421 g = os.path.join(self.checkout_path, '.git')
1422 return (os.path.isdir(os.path.join(g, "rebase-merge"))
1423 or os.path.isdir(os.path.join(g, "rebase-apply")))
msb@chromium.org786fb682010-06-02 15:16:23 +00001424
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001425 def _CheckClean(self, revision):
1426 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1427 if os.path.exists(lockfile):
1428 raise gclient_utils.Error(
1429 '\n____ %s at %s\n'
1430 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1431 '\tIf no git executable is running, then clean up %r and try again.\n'
1432 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001433
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001434 # Make sure the tree is clean; see git-rebase.sh for reference
1435 try:
1436 scm.GIT.Capture(
1437 ['update-index', '--ignore-submodules', '--refresh'],
1438 cwd=self.checkout_path)
1439 except subprocess2.CalledProcessError:
1440 raise gclient_utils.Error(
1441 '\n____ %s at %s\n'
1442 '\tYou have unstaged changes.\n'
1443 '\tcd into %s, run git status to see changes,\n'
1444 '\tand commit, stash, or reset.\n' %
1445 (self.relpath, revision, self.relpath))
1446 try:
1447 scm.GIT.Capture([
1448 'diff-index', '--cached', '--name-status', '-r',
1449 '--ignore-submodules', 'HEAD', '--'
1450 ],
1451 cwd=self.checkout_path)
1452 except subprocess2.CalledProcessError:
1453 raise gclient_utils.Error(
1454 '\n____ %s at %s\n'
1455 '\tYour index contains uncommitted changes\n'
1456 '\tcd into %s, run git status to see changes,\n'
1457 '\tand commit, stash, or reset.\n' %
1458 (self.relpath, revision, self.relpath))
msb@chromium.org786fb682010-06-02 15:16:23 +00001459
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001460 def _CheckDetachedHead(self, revision, _options):
1461 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1462 # reference by a commit). If not, error out -- most likely a rebase is
1463 # in progress, try to detect so we can give a better error.
1464 try:
1465 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1466 cwd=self.checkout_path)
1467 except subprocess2.CalledProcessError:
1468 # Commit is not contained by any rev. See if the user is rebasing:
1469 if self._IsRebasing():
1470 # Punt to the user
1471 raise gclient_utils.Error(
1472 '\n____ %s at %s\n'
1473 '\tAlready in a conflict, i.e. (no branch).\n'
1474 '\tFix the conflict and run gclient again.\n'
1475 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1476 '\tSee man git-rebase for details.\n' %
1477 (self.relpath, revision))
1478 # Let's just save off the commit so we can proceed.
1479 name = ('saved-by-gclient-' +
1480 self._Capture(['rev-parse', '--short', 'HEAD']))
1481 self._Capture(['branch', '-f', name])
1482 self.Print(
1483 '_____ found an unreferenced commit and saved it as \'%s\'' %
1484 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001485
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001486 def _GetCurrentBranch(self):
1487 # Returns name of current branch or None for detached HEAD
1488 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
1489 if branch == 'HEAD':
1490 return None
1491 return branch
msb@chromium.org5bde4852009-12-14 16:47:12 +00001492
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001493 def _Capture(self, args, **kwargs):
1494 set_git_dir = 'cwd' not in kwargs
1495 kwargs.setdefault('cwd', self.checkout_path)
1496 kwargs.setdefault('stderr', subprocess2.PIPE)
1497 strip = kwargs.pop('strip', True)
1498 env = scm.GIT.ApplyEnvVars(kwargs)
1499 # If an explicit cwd isn't set, then default to the .git/ subdir so we
1500 # get stricter behavior. This can be useful in cases of slight
1501 # corruption -- we don't accidentally go corrupting parent git checks
1502 # too. See https://crbug.com/1000825 for an example.
1503 if set_git_dir:
Gavin Mak7f5b53f2023-09-07 18:13:01 +00001504 env.setdefault(
1505 'GIT_DIR',
1506 os.path.abspath(os.path.join(self.checkout_path, '.git')))
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001507 ret = subprocess2.check_output(['git'] + args, env=env,
1508 **kwargs).decode('utf-8')
1509 if strip:
1510 ret = ret.strip()
1511 self.Print('Finished running: %s %s' % ('git', ' '.join(args)))
1512 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001513
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001514 def _Checkout(self, options, ref, force=False, quiet=None):
1515 """Performs a 'git-checkout' operation.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001516
1517 Args:
1518 options: The configured option set
1519 ref: (str) The branch/commit to checkout
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001520 quiet: (bool/None) Whether or not the checkout should pass '--quiet'; if
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001521 'None', the behavior is inferred from 'options.verbose'.
1522 Returns: (str) The output of the checkout operation
1523 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001524 if quiet is None:
1525 quiet = (not options.verbose)
1526 checkout_args = ['checkout']
1527 if force:
1528 checkout_args.append('--force')
1529 if quiet:
1530 checkout_args.append('--quiet')
1531 checkout_args.append(ref)
1532 return self._Capture(checkout_args)
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001533
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001534 def _Fetch(self,
1535 options,
1536 remote=None,
1537 prune=False,
1538 quiet=False,
1539 refspec=None,
1540 depth=None):
1541 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
1542 # When updating, the ref is modified to be a remote ref .
1543 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1544 # Try to reverse that mapping.
1545 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1546 if original_ref:
1547 refspec = original_ref + ':' + refspec
1548 # When a mirror is configured, it only fetches
1549 # refs/{heads,branch-heads,tags}/*.
1550 # If asked to fetch other refs, we must fetch those directly from
1551 # the repository, and not from the mirror.
1552 if not original_ref.startswith(
1553 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1554 remote, _ = gclient_utils.SplitUrlRevision(self.url)
1555 fetch_cmd = cfg + [
1556 'fetch',
1557 remote or self.remote,
1558 ]
1559 if refspec:
1560 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001561
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001562 if prune:
1563 fetch_cmd.append('--prune')
1564 if options.verbose:
1565 fetch_cmd.append('--verbose')
1566 if not hasattr(options, 'with_tags') or not options.with_tags:
1567 fetch_cmd.append('--no-tags')
1568 elif quiet:
1569 fetch_cmd.append('--quiet')
1570 if depth:
1571 fetch_cmd.append('--depth=' + str(depth))
1572 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001573
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001574 def _SetFetchConfig(self, options):
1575 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001576 if requested."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001577 if options.force or options.reset:
1578 try:
1579 self._Run(
1580 ['config', '--unset-all',
1581 'remote.%s.fetch' % self.remote], options)
1582 self._Run([
1583 'config',
1584 'remote.%s.fetch' % self.remote,
1585 '+refs/heads/*:refs/remotes/%s/*' % self.remote
1586 ], options)
1587 except subprocess2.CalledProcessError as e:
1588 # If exit code was 5, it means we attempted to unset a config
1589 # that didn't exist. Ignore it.
1590 if e.returncode != 5:
1591 raise
1592 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1593 config_cmd = [
1594 'config',
1595 'remote.%s.fetch' % self.remote,
1596 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1597 '^\\+refs/branch-heads/\\*:.*$'
1598 ]
1599 self._Run(config_cmd, options)
1600 if hasattr(options, 'with_tags') and options.with_tags:
1601 config_cmd = [
1602 'config',
1603 'remote.%s.fetch' % self.remote, '+refs/tags/*:refs/tags/*',
1604 '^\\+refs/tags/\\*:.*$'
1605 ]
1606 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001607
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001608 def _AutoFetchRef(self, options, revision, depth=None):
1609 """Attempts to fetch |revision| if not available in local repo.
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001610
1611 Returns possibly updated revision."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001612 if not scm.GIT.IsValidRevision(self.checkout_path, revision):
1613 self._Fetch(options, refspec=revision, depth=depth)
1614 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1615 return revision
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001616
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001617 def _Run(self, args, options, **kwargs):
1618 # Disable 'unused options' warning | pylint: disable=unused-argument
1619 kwargs.setdefault('cwd', self.checkout_path)
1620 kwargs.setdefault('filter_fn', self.filter)
1621 kwargs.setdefault('show_header', True)
1622 env = scm.GIT.ApplyEnvVars(kwargs)
Nico Weberc49c88a2020-07-08 17:36:02 +00001623
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001624 cmd = ['git'] + args
1625 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001626
1627
1628class CipdPackage(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001629 """A representation of a single CIPD package."""
1630 def __init__(self, name, version, authority_for_subdir):
1631 self._authority_for_subdir = authority_for_subdir
1632 self._name = name
1633 self._version = version
John Budorick0f7b2002018-01-19 15:46:17 -08001634
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001635 @property
1636 def authority_for_subdir(self):
1637 """Whether this package has authority to act on behalf of its subdir.
John Budorick0f7b2002018-01-19 15:46:17 -08001638
1639 Some operations should only be performed once per subdirectory. A package
1640 that has authority for its subdirectory is the only package that should
1641 perform such operations.
1642
1643 Returns:
1644 bool; whether this package has subdir authority.
1645 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001646 return self._authority_for_subdir
John Budorick0f7b2002018-01-19 15:46:17 -08001647
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001648 @property
1649 def name(self):
1650 return self._name
John Budorick0f7b2002018-01-19 15:46:17 -08001651
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001652 @property
1653 def version(self):
1654 return self._version
John Budorick0f7b2002018-01-19 15:46:17 -08001655
1656
1657class CipdRoot(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001658 """A representation of a single CIPD root."""
Yiwei Zhang52353702023-09-18 15:53:52 +00001659 def __init__(self, root_dir, service_url, log_level=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001660 self._all_packages = set()
1661 self._mutator_lock = threading.Lock()
1662 self._packages_by_subdir = collections.defaultdict(list)
1663 self._root_dir = root_dir
1664 self._service_url = service_url
1665 self._resolved_packages = None
Yiwei Zhang52353702023-09-18 15:53:52 +00001666 self._log_level = log_level or 'error'
John Budorick0f7b2002018-01-19 15:46:17 -08001667
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001668 def add_package(self, subdir, package, version):
1669 """Adds a package to this CIPD root.
John Budorick0f7b2002018-01-19 15:46:17 -08001670
1671 As far as clients are concerned, this grants both root and subdir authority
1672 to packages arbitrarily. (The implementation grants root authority to the
1673 first package added and subdir authority to the first package added for that
1674 subdir, but clients should not depend on or expect that behavior.)
1675
1676 Args:
1677 subdir: str; relative path to where the package should be installed from
1678 the cipd root directory.
1679 package: str; the cipd package name.
1680 version: str; the cipd package version.
1681 Returns:
1682 CipdPackage; the package that was created and added to this root.
1683 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001684 with self._mutator_lock:
1685 cipd_package = CipdPackage(package, version,
1686 not self._packages_by_subdir[subdir])
1687 self._all_packages.add(cipd_package)
1688 self._packages_by_subdir[subdir].append(cipd_package)
1689 return cipd_package
John Budorick0f7b2002018-01-19 15:46:17 -08001690
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001691 def packages(self, subdir):
1692 """Get the list of configured packages for the given subdir."""
1693 return list(self._packages_by_subdir[subdir])
John Budorick0f7b2002018-01-19 15:46:17 -08001694
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001695 def resolved_packages(self):
1696 if not self._resolved_packages:
1697 self._resolved_packages = self.ensure_file_resolve()
1698 return self._resolved_packages
Dan Le Febvre456d0852023-05-24 23:43:40 +00001699
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001700 def clobber(self):
1701 """Remove the .cipd directory.
John Budorick0f7b2002018-01-19 15:46:17 -08001702
1703 This is useful for forcing ensure to redownload and reinitialize all
1704 packages.
1705 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001706 with self._mutator_lock:
1707 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
1708 try:
1709 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1710 except OSError:
1711 if os.path.exists(cipd_cache_dir):
1712 raise
John Budorick0f7b2002018-01-19 15:46:17 -08001713
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001714 def expand_package_name(self, package_name_string, **kwargs):
1715 """Run `cipd expand-package-name`.
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001716
1717 CIPD package names can be declared with placeholder variables
1718 such as '${platform}', this cmd will return the package name
1719 with the variables resolved. The resolution is based on the host
1720 the command is executing on.
1721 """
1722
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001723 kwargs.setdefault('stderr', subprocess2.PIPE)
1724 cmd = ['cipd', 'expand-package-name', package_name_string]
1725 ret = subprocess2.check_output(cmd, **kwargs).decode('utf-8')
1726 return ret.strip()
Dan Le Febvre6316ac22023-05-16 00:22:34 +00001727
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001728 @contextlib.contextmanager
1729 def _create_ensure_file(self):
1730 try:
1731 contents = '$ParanoidMode CheckPresence\n'
1732 # TODO(crbug/1329641): Remove once cipd packages have been updated
1733 # to always be created in copy mode.
1734 contents += '$OverrideInstallMode copy\n\n'
1735 for subdir, packages in sorted(self._packages_by_subdir.items()):
1736 contents += '@Subdir %s\n' % subdir
1737 for package in sorted(packages, key=lambda p: p.name):
1738 contents += '%s %s\n' % (package.name, package.version)
1739 contents += '\n'
1740 ensure_file = None
1741 with tempfile.NamedTemporaryFile(suffix='.ensure',
1742 delete=False,
1743 mode='wb') as ensure_file:
1744 ensure_file.write(contents.encode('utf-8', 'replace'))
1745 yield ensure_file.name
1746 finally:
1747 if ensure_file is not None and os.path.exists(ensure_file.name):
1748 os.remove(ensure_file.name)
John Budorick0f7b2002018-01-19 15:46:17 -08001749
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001750 def ensure(self):
1751 """Run `cipd ensure`."""
1752 with self._mutator_lock:
1753 with self._create_ensure_file() as ensure_file:
1754 cmd = [
1755 'cipd',
1756 'ensure',
1757 '-log-level',
Yiwei Zhang52353702023-09-18 15:53:52 +00001758 self._log_level,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001759 '-root',
1760 self.root_dir,
1761 '-ensure-file',
1762 ensure_file,
1763 ]
1764 gclient_utils.CheckCallAndFilter(cmd,
1765 print_stdout=True,
1766 show_header=True)
John Budorick0f7b2002018-01-19 15:46:17 -08001767
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001768 @contextlib.contextmanager
1769 def _create_ensure_file_for_resolve(self):
1770 try:
1771 contents = '$ResolvedVersions %s\n' % os.devnull
1772 for subdir, packages in sorted(self._packages_by_subdir.items()):
1773 contents += '@Subdir %s\n' % subdir
1774 for package in sorted(packages, key=lambda p: p.name):
1775 contents += '%s %s\n' % (package.name, package.version)
1776 contents += '\n'
1777 ensure_file = None
1778 with tempfile.NamedTemporaryFile(suffix='.ensure',
1779 delete=False,
1780 mode='wb') as ensure_file:
1781 ensure_file.write(contents.encode('utf-8', 'replace'))
1782 yield ensure_file.name
1783 finally:
1784 if ensure_file is not None and os.path.exists(ensure_file.name):
1785 os.remove(ensure_file.name)
Dan Le Febvre456d0852023-05-24 23:43:40 +00001786
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001787 def _create_resolved_file(self):
1788 return tempfile.NamedTemporaryFile(suffix='.resolved',
1789 delete=False,
1790 mode='wb')
Dan Le Febvre456d0852023-05-24 23:43:40 +00001791
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001792 def ensure_file_resolve(self):
1793 """Run `cipd ensure-file-resolve`."""
1794 with self._mutator_lock:
1795 with self._create_resolved_file() as output_file:
1796 with self._create_ensure_file_for_resolve() as ensure_file:
1797 cmd = [
1798 'cipd',
1799 'ensure-file-resolve',
1800 '-log-level',
Yiwei Zhang52353702023-09-18 15:53:52 +00001801 self._log_level,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001802 '-ensure-file',
1803 ensure_file,
1804 '-json-output',
1805 output_file.name,
1806 ]
1807 gclient_utils.CheckCallAndFilter(cmd,
1808 print_stdout=False,
1809 show_header=False)
1810 with open(output_file.name) as f:
1811 output_json = json.load(f)
1812 return output_json.get('result', {})
Dan Le Febvre456d0852023-05-24 23:43:40 +00001813
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001814 def run(self, command):
1815 if command == 'update':
1816 self.ensure()
1817 elif command == 'revert':
1818 self.clobber()
1819 self.ensure()
John Budorickd3ba72b2018-03-20 12:27:42 -07001820
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001821 def created_package(self, package):
1822 """Checks whether this root created the given package.
John Budorick0f7b2002018-01-19 15:46:17 -08001823
1824 Args:
1825 package: CipdPackage; the package to check.
1826 Returns:
1827 bool; whether this root created the given package.
1828 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001829 return package in self._all_packages
John Budorick0f7b2002018-01-19 15:46:17 -08001830
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001831 @property
1832 def root_dir(self):
1833 return self._root_dir
John Budorick0f7b2002018-01-19 15:46:17 -08001834
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001835 @property
1836 def service_url(self):
1837 return self._service_url
John Budorick0f7b2002018-01-19 15:46:17 -08001838
1839
1840class CipdWrapper(SCMWrapper):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001841 """Wrapper for CIPD.
John Budorick0f7b2002018-01-19 15:46:17 -08001842
1843 Currently only supports chrome-infra-packages.appspot.com.
1844 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001845 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001846
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001847 def __init__(self,
1848 url=None,
1849 root_dir=None,
1850 relpath=None,
1851 out_fh=None,
1852 out_cb=None,
1853 root=None,
1854 package=None):
1855 super(CipdWrapper, self).__init__(url=url,
1856 root_dir=root_dir,
1857 relpath=relpath,
1858 out_fh=out_fh,
1859 out_cb=out_cb)
1860 assert root.created_package(package)
1861 self._package = package
1862 self._root = root
John Budorick0f7b2002018-01-19 15:46:17 -08001863
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001864 #override
1865 def GetCacheMirror(self):
1866 return None
John Budorick0f7b2002018-01-19 15:46:17 -08001867
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001868 #override
1869 def GetActualRemoteURL(self, options):
1870 return self._root.service_url
John Budorick0f7b2002018-01-19 15:46:17 -08001871
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001872 #override
1873 def DoesRemoteURLMatch(self, options):
1874 del options
1875 return True
John Budorick0f7b2002018-01-19 15:46:17 -08001876
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001877 def revert(self, options, args, file_list):
1878 """Does nothing.
John Budorickd3ba72b2018-03-20 12:27:42 -07001879
1880 CIPD packages should be reverted at the root by running
1881 `CipdRoot.run('revert')`.
1882 """
John Budorick0f7b2002018-01-19 15:46:17 -08001883
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001884 def diff(self, options, args, file_list):
1885 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001886
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001887 def pack(self, options, args, file_list):
1888 """CIPD has no notion of diffing."""
John Budorick0f7b2002018-01-19 15:46:17 -08001889
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001890 def revinfo(self, options, args, file_list):
1891 """Grab the instance ID."""
1892 try:
1893 tmpdir = tempfile.mkdtemp()
1894 # Attempt to get instance_id from the root resolved cache.
1895 # Resolved cache will not match on any CIPD packages with
1896 # variables such as ${platform}, they will fall back to
1897 # the slower method below.
1898 resolved = self._root.resolved_packages()
1899 if resolved:
1900 # CIPD uses POSIX separators across all platforms, so
1901 # replace any Windows separators.
1902 path_split = self.relpath.replace(os.sep, "/").split(":")
1903 if len(path_split) > 1:
1904 src_path, package = path_split
1905 if src_path in resolved:
1906 for resolved_package in resolved[src_path]:
1907 if package == resolved_package.get(
1908 'pin', {}).get('package'):
1909 return resolved_package.get(
1910 'pin', {}).get('instance_id')
Dan Le Febvre456d0852023-05-24 23:43:40 +00001911
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001912 describe_json_path = os.path.join(tmpdir, 'describe.json')
1913 cmd = [
1914 'cipd', 'describe', self._package.name, '-log-level', 'error',
1915 '-version', self._package.version, '-json-output',
1916 describe_json_path
1917 ]
1918 gclient_utils.CheckCallAndFilter(cmd)
1919 with open(describe_json_path) as f:
1920 describe_json = json.load(f)
1921 return describe_json.get('result', {}).get('pin',
1922 {}).get('instance_id')
1923 finally:
1924 gclient_utils.rmtree(tmpdir)
John Budorick0f7b2002018-01-19 15:46:17 -08001925
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001926 def status(self, options, args, file_list):
1927 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001928
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001929 def update(self, options, args, file_list):
1930 """Does nothing.
John Budorickd3ba72b2018-03-20 12:27:42 -07001931
1932 CIPD packages should be updated at the root by running
1933 `CipdRoot.run('update')`.
1934 """
Josip Sokcevic8fb358e2023-11-15 22:24:06 +00001935
1936
1937class CogWrapper(SCMWrapper):
1938 """Wrapper for Cog, all no-op."""
1939 name = 'cog'
1940
1941 def __init__(self):
1942 super(CogWrapper, self).__init__()
1943
1944 #override
1945 def GetCacheMirror(self):
1946 return None
1947
1948 #override
1949 def GetActualRemoteURL(self, options):
1950 return None
1951
1952 #override
1953 def DoesRemoteURLMatch(self, options):
1954 del options
1955 return True
1956
1957 def revert(self, options, args, file_list):
1958 pass
1959
1960 def diff(self, options, args, file_list):
1961 pass
1962
1963 def pack(self, options, args, file_list):
1964 pass
1965
1966 def revinfo(self, options, args, file_list):
1967 pass
1968
1969 def status(self, options, args, file_list):
1970 pass
1971
1972 def update(self, options, args, file_list):
1973 pass