blob: c9c26bd75b7788f46730c739666aa003c6f8655f [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.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00005"""Gclient-specific SCM-specific operations."""
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00006
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00007from __future__ import print_function
8
John Budorick0f7b2002018-01-19 15:46:17 -08009import collections
10import contextlib
borenet@google.comb2256212014-05-07 20:57:28 +000011import errno
John Budorick0f7b2002018-01-19 15:46:17 -080012import json
maruel@chromium.org754960e2009-09-21 12:31:05 +000013import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import os
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000015import posixpath
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000016import re
maruel@chromium.org90541732011-04-01 17:54:18 +000017import sys
ilevy@chromium.org3534aa52013-07-20 01:58:08 +000018import tempfile
John Budorick0f7b2002018-01-19 15:46:17 -080019import threading
zty@chromium.org6279e8a2014-02-13 01:45:25 +000020import traceback
Raul Tambreb946b232019-03-26 14:48:46 +000021
22try:
23 import urlparse
24except ImportError: # For Py3 compatibility
25 import urllib.parse as urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000026
hinoka@google.com2f2ca142014-01-07 03:59:18 +000027import download_from_google_storage
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000028import gclient_utils
szager@chromium.org848fd492014-04-09 19:06:44 +000029import git_cache
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000030import scm
borenet@google.comb2256212014-05-07 20:57:28 +000031import shutil
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000032import subprocess2
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000033
34
szager@chromium.org71cbb502013-04-19 23:30:15 +000035THIS_FILE_PATH = os.path.abspath(__file__)
36
hinoka@google.com2f2ca142014-01-07 03:59:18 +000037GSUTIL_DEFAULT_PATH = os.path.join(
hinoka@chromium.orgb091aa52014-12-20 01:47:31 +000038 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
hinoka@google.com2f2ca142014-01-07 03:59:18 +000039
maruel@chromium.org79d62372015-06-01 18:50:55 +000040
smutae7ea312016-07-18 11:59:41 -070041class NoUsableRevError(gclient_utils.Error):
42 """Raised if requested revision isn't found in checkout."""
43
44
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000045class DiffFiltererWrapper(object):
46 """Simple base class which tracks which file is being diffed and
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000047 replaces instances of its file name in the original and
agable41e3a6c2016-10-20 11:36:56 -070048 working copy lines of the git diff output."""
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000049 index_string = None
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000050 original_prefix = "--- "
51 working_prefix = "+++ "
52
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000053 def __init__(self, relpath, print_func):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000054 # Note that we always use '/' as the path separator to be
agable41e3a6c2016-10-20 11:36:56 -070055 # consistent with cygwin-style output on Windows
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000056 self._relpath = relpath.replace("\\", "/")
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000057 self._current_file = None
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000058 self._print_func = print_func
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000059
maruel@chromium.org6e29d572010-06-04 17:32:20 +000060 def SetCurrentFile(self, current_file):
61 self._current_file = current_file
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000062
iannucci@chromium.org3830a672013-02-19 20:15:14 +000063 @property
64 def _replacement_file(self):
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000065 return posixpath.join(self._relpath, self._current_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000066
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000067 def _Replace(self, line):
68 return line.replace(self._current_file, self._replacement_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000069
70 def Filter(self, line):
71 if (line.startswith(self.index_string)):
72 self.SetCurrentFile(line[len(self.index_string):])
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000073 line = self._Replace(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000074 else:
75 if (line.startswith(self.original_prefix) or
76 line.startswith(self.working_prefix)):
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000077 line = self._Replace(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000078 self._print_func(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000079
80
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000081class GitDiffFilterer(DiffFiltererWrapper):
82 index_string = "diff --git "
83
84 def SetCurrentFile(self, current_file):
85 # Get filename by parsing "a/<filename> b/<filename>"
86 self._current_file = current_file[:(len(current_file)/2)][2:]
87
88 def _Replace(self, line):
89 return re.sub("[a|b]/" + self._current_file, self._replacement_file, line)
90
91
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000092# SCMWrapper base class
93
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000094class SCMWrapper(object):
95 """Add necessary glue between all the supported SCM.
96
msb@chromium.orgd6504212010-01-13 17:34:31 +000097 This is the abstraction layer to bind to different SCM.
98 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000099 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
Edward Lemur231f5ea2018-01-31 19:02:36 +0100100 out_cb=None, print_outbuf=False):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000101 self.url = url
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +0000102 self._root_dir = root_dir
103 if self._root_dir:
104 self._root_dir = self._root_dir.replace('/', os.sep)
105 self.relpath = relpath
106 if self.relpath:
107 self.relpath = self.relpath.replace('/', os.sep)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000108 if self.relpath and self._root_dir:
109 self.checkout_path = os.path.join(self._root_dir, self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000110 if out_fh is None:
111 out_fh = sys.stdout
112 self.out_fh = out_fh
113 self.out_cb = out_cb
Edward Lemur231f5ea2018-01-31 19:02:36 +0100114 self.print_outbuf = print_outbuf
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000115
116 def Print(self, *args, **kwargs):
117 kwargs.setdefault('file', self.out_fh)
118 if kwargs.pop('timestamp', True):
119 self.out_fh.write('[%s] ' % gclient_utils.Elapsed())
120 print(*args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000121
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000122 def RunCommand(self, command, options, args, file_list=None):
agabledebf6c82016-12-21 12:50:12 -0800123 commands = ['update', 'updatesingle', 'revert',
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000124 'revinfo', 'status', 'diff', 'pack', 'runhooks']
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000125
126 if not command in commands:
127 raise gclient_utils.Error('Unknown command %s' % command)
128
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000129 if not command in dir(self):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000130 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % (
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000131 command, self.__class__.__name__))
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000132
133 return getattr(self, command)(options, args, file_list)
134
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +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]
142
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000143 def GetCacheMirror(self):
Robert Iannuccia19649b2018-06-29 16:31:45 +0000144 if getattr(self, 'cache_dir', None):
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000145 url, _ = gclient_utils.SplitUrlRevision(self.url)
146 return git_cache.Mirror(url)
147 return None
148
smut@google.comd33eab32014-07-07 19:35:18 +0000149 def GetActualRemoteURL(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000150 """Attempt to determine the remote URL for this SCMWrapper."""
smut@google.comd33eab32014-07-07 19:35:18 +0000151 # Git
borenet@google.combda475e2014-03-24 19:04:45 +0000152 if os.path.exists(os.path.join(self.checkout_path, '.git')):
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000153 actual_remote_url = self._get_first_remote_url(self.checkout_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000154
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000155 mirror = self.GetCacheMirror()
156 # If the cache is used, obtain the actual remote URL from there.
157 if (mirror and mirror.exists() and
158 mirror.mirror_path.replace('\\', '/') ==
159 actual_remote_url.replace('\\', '/')):
160 actual_remote_url = self._get_first_remote_url(mirror.mirror_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000161 return actual_remote_url
borenet@google.com88d10082014-03-21 17:24:48 +0000162 return None
163
borenet@google.com4e9be262014-04-08 19:40:30 +0000164 def DoesRemoteURLMatch(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000165 """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
169
smut@google.comd33eab32014-07-07 19:35:18 +0000170 actual_remote_url = self.GetActualRemoteURL(options)
borenet@google.com88d10082014-03-21 17:24:48 +0000171 if actual_remote_url:
borenet@google.com8156c9f2014-04-01 16:41:36 +0000172 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip('/')
173 == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/'))
borenet@google.com88d10082014-03-21 17:24:48 +0000174 else:
175 # This may occur if the self.checkout_path exists but does not contain a
agable41e3a6c2016-10-20 11:36:56 -0700176 # valid git checkout.
borenet@google.com88d10082014-03-21 17:24:48 +0000177 return False
178
borenet@google.comb09097a2014-04-09 19:09:08 +0000179 def _DeleteOrMove(self, force):
180 """Delete the checkout directory or move it out of the way.
181
182 Args:
183 force: bool; if True, delete the directory. Otherwise, just move it.
184 """
borenet@google.comb2256212014-05-07 20:57:28 +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))
194
195 try:
196 os.makedirs(bad_scm_dir)
197 except OSError as e:
198 if e.errno != errno.EEXIST:
199 raise
200
201 dest_path = tempfile.mkdtemp(
202 prefix=os.path.basename(self.relpath),
203 dir=bad_scm_dir)
204 self.Print('_____ 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):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000212 """Wrapper for Git"""
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000213 name = 'git'
maruel@chromium.org1a60dca2013-11-26 14:06:26 +0000214 remote = 'origin'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000215
Robert Iannuccia19649b2018-06-29 16:31:45 +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
John Budorick0f7b2002018-01-19 15:46:17 -0800223 def __init__(self, url=None, *args, **kwargs):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000224 """Removes 'git+' fake prefix from git URL."""
Henrique Ferreiroe72279d2019-04-17 12:01:50 +0000225 if url and (url.startswith('git+http://') or
226 url.startswith('git+https://')):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000227 url = url[4:]
John Budorick0f7b2002018-01-19 15:46:17 -0800228 SCMWrapper.__init__(self, url, *args, **kwargs)
szager@chromium.org848fd492014-04-09 19:06:44 +0000229 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)
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000233
mukai@chromium.org9e3e82c2012-04-18 12:55:43 +0000234 @staticmethod
235 def BinaryExists():
236 """Returns true if the command exists."""
237 try:
238 # We assume git is newer than 1.7. See: crbug.com/114483
239 result, version = scm.GIT.AssertVersion('1.7')
240 if not result:
241 raise gclient_utils.Error('Git version is older than 1.7: %s' % version)
242 return result
243 except OSError:
244 return False
245
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000246 def GetCheckoutRoot(self):
247 return scm.GIT.GetCheckoutRoot(self.checkout_path)
248
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000249 def GetRevisionDate(self, _revision):
floitsch@google.comeaab7842011-04-28 09:07:58 +0000250 """Returns the given revision's date in ISO-8601 format (which contains the
251 time zone)."""
252 # TODO(floitsch): get the time-stamp of the given revision and not just the
253 # time-stamp of the currently checked out revision.
254 return self._Capture(['log', '-n', '1', '--format=%ai'])
255
Aaron Gablef4068aa2017-12-12 15:14:09 -0800256 def _GetDiffFilenames(self, base):
257 """Returns the names of files modified since base."""
258 return self._Capture(
259 # Filter to remove base if it is None.
260 filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only', base])
261 ).split()
262
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000263 def diff(self, options, _args, _file_list):
Aaron Gable1853f662018-02-12 15:45:56 -0800264 _, revision = gclient_utils.SplitUrlRevision(self.url)
265 if not revision:
266 revision = 'refs/remotes/%s/master' % self.remote
267 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000268
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000269 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000270 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000271 repository.
272
273 The patch file is generated from a diff of the merge base of HEAD and
274 its upstream branch.
275 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700276 try:
277 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
278 except subprocess2.CalledProcessError:
279 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000280 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700281 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000282 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000283 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000284
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800285 def _Scrub(self, target, options):
286 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000287 quiet = []
288 if not options.verbose:
289 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800290 self._Run(['reset', '--hard', target] + quiet, options)
291 if options.force and options.delete_unversioned_trees:
292 # where `target` is a commit that contains both upper and lower case
293 # versions of the same file on a case insensitive filesystem, we are
294 # actually in a broken state here. The index will have both 'a' and 'A',
295 # but only one of them will exist on the disk. To progress, we delete
296 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800297 output = self._Capture([
298 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800299 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800300 # --porcelain (v1) looks like:
301 # XY filename
302 try:
303 filename = line[3:]
304 self.Print('_____ Deleting residual after reset: %r.' % filename)
305 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800306 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800307 except OSError:
308 pass
309
John Budorick882c91e2018-07-12 22:11:41 +0000310 def _FetchAndReset(self, revision, file_list, options):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800311 """Equivalent to git fetch; git reset."""
Edward Lemur579c9862018-07-13 23:17:51 +0000312 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000313
dnj@chromium.org680f2172014-06-25 00:39:32 +0000314 self._Fetch(options, prune=True, quiet=options.verbose)
John Budorick882c91e2018-07-12 22:11:41 +0000315 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000316 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800317 files = self._Capture(
318 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambreb946b232019-03-26 14:48:46 +0000319 file_list.extend(
320 [os.path.join(self.checkout_path, f.decode()) for f in files])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000321
szager@chromium.org8a139702014-06-20 15:55:01 +0000322 def _DisableHooks(self):
323 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
324 if not os.path.isdir(hook_dir):
325 return
326 for f in os.listdir(hook_dir):
327 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000328 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
329 if os.path.exists(disabled_hook_path):
330 os.remove(disabled_hook_path)
331 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000332
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000333 def _maybe_break_locks(self, options):
334 """This removes all .lock files from this repo's .git directory, if the
335 user passed the --break_repo_locks command line flag.
336
337 In particular, this will cleanup index.lock files, as well as ref lock
338 files.
339 """
340 if options.break_repo_locks:
341 git_dir = os.path.join(self.checkout_path, '.git')
342 for path, _, filenames in os.walk(git_dir):
343 for filename in filenames:
344 if filename.endswith('.lock'):
345 to_break = os.path.join(path, filename)
346 self.Print('breaking lock: %s' % (to_break,))
347 try:
348 os.remove(to_break)
349 except OSError as ex:
350 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
351 raise
352
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000353 # TODO(ehmaldonado): Remove after bot_update is modified to pass the patch's
354 # branch.
355 def _GetTargetBranchForCommit(self, commit):
Edward Lemurca7d8812018-07-24 17:42:45 +0000356 """Get the remote branch a commit is part of."""
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000357 _WELL_KNOWN_BRANCHES = [
358 'refs/remotes/origin/master',
359 'refs/remotes/origin/infra/config',
360 'refs/remotes/origin/lkgr',
361 ]
362 for branch in _WELL_KNOWN_BRANCHES:
363 if scm.GIT.IsAncestor(self.checkout_path, commit, branch):
364 return branch
Edward Lemurca7d8812018-07-24 17:42:45 +0000365 remote_refs = self._Capture(
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000366 ['for-each-ref', 'refs/remotes/%s' % self.remote,
Edward Lemurca7d8812018-07-24 17:42:45 +0000367 '--format=%(refname)']).splitlines()
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000368 for ref in sorted(remote_refs, reverse=True):
Edward Lemurca7d8812018-07-24 17:42:45 +0000369 if scm.GIT.IsAncestor(self.checkout_path, commit, ref):
370 return ref
371 self.Print('Failed to find a remote ref that contains %s. '
372 'Candidate refs were %s.' % (commit, remote_refs))
373 # Fallback to the commit we got.
374 # This means that apply_path_ref will try to find the merge-base between the
375 # patch and the commit (which is most likely the commit) and cherry-pick
376 # everything in between.
377 return commit
378
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000379 def apply_patch_ref(self, patch_repo, patch_ref, target_branch, options,
380 file_list):
381 """Apply a patch on top of the revision we're synced at.
382
383 The patch ref is given by |patch_repo|@|patch_ref|, and the current revision
384 is |base_rev|.
385 We also need the |target_branch| that the patch was uploaded against. We use
386 it to find a merge base between |patch_rev| and |base_rev|, so we can find
387 what commits constitute the patch:
388
389 Graphically, it looks like this:
390
391 ... -> merge_base -> [possibly already landed commits] -> target_branch
392 \
393 -> [possibly not yet landed dependent CLs] -> patch_rev
394
395 Next, we apply the commits |merge_base..patch_rev| on top of whatever is
396 currently checked out, denoted |base_rev|. Typically, it'd be a revision
397 from |target_branch|, but this is not required.
398
399 Graphically, we cherry pick |merge_base..patch_rev| on top of |base_rev|:
400
401 ... -> base_rev -> [possibly not yet landed dependent CLs] -> patch_rev
402
403 After application, if |options.reset_patch_ref| is specified, we soft reset
404 the just cherry-picked changes, keeping them in git index only.
405
406 Args:
407 patch_repo: The patch origin. e.g. 'https://foo.googlesource.com/bar'
408 patch_ref: The ref to the patch. e.g. 'refs/changes/1234/34/1'.
409 target_branch: The branch the patch was uploaded against.
410 e.g. 'refs/heads/master' or 'refs/heads/infra/config'.
411 options: The options passed to gclient.
412 file_list: A list where modified files will be appended.
413 """
414
Edward Lemurca7d8812018-07-24 17:42:45 +0000415 # Abort any cherry-picks in progress.
416 try:
417 self._Capture(['cherry-pick', '--abort'])
418 except subprocess2.CalledProcessError:
419 pass
420
Edward Lesmesc621b212018-03-21 20:26:56 -0400421 base_rev = self._Capture(['rev-parse', 'HEAD'])
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000422 target_branch = target_branch or self._GetTargetBranchForCommit(base_rev)
Edward Lesmesc621b212018-03-21 20:26:56 -0400423 self.Print('===Applying patch ref===')
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000424 self.Print('Patch ref is %r @ %r. Target branch for patch is %r. '
425 'Current HEAD is %r. Current dir is %r' % (
426 patch_repo, patch_ref, target_branch, base_rev,
427 self.checkout_path))
Edward Lesmesc621b212018-03-21 20:26:56 -0400428 self._Capture(['reset', '--hard'])
429 self._Capture(['fetch', patch_repo, patch_ref])
Edward Lemurca7d8812018-07-24 17:42:45 +0000430 patch_rev = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400431
Edward Lemurca7d8812018-07-24 17:42:45 +0000432 try:
433 if not options.rebase_patch_ref:
434 self._Capture(['checkout', patch_rev])
435 else:
436 # Find the merge-base between the branch_rev and patch_rev to find out
437 # the changes we need to cherry-pick on top of base_rev.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000438 merge_base = self._Capture(['merge-base', target_branch, patch_rev])
Edward Lemurca7d8812018-07-24 17:42:45 +0000439 self.Print('Merge base of %s and %s is %s' % (
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000440 target_branch, patch_rev, merge_base))
Edward Lemurca7d8812018-07-24 17:42:45 +0000441 if merge_base == patch_rev:
442 # If the merge-base is patch_rev, it means patch_rev is already part
443 # of the history, so just check it out.
444 self._Capture(['checkout', patch_rev])
445 else:
446 # If a change was uploaded on top of another change, which has already
447 # landed, one of the commits in the cherry-pick range will be
448 # redundant, since it has already landed and its changes incorporated
449 # in the tree.
450 # We pass '--keep-redundant-commits' to ignore those changes.
451 self._Capture(['cherry-pick', merge_base + '..' + patch_rev,
452 '--keep-redundant-commits'])
453
454 if file_list is not None:
455 file_list.extend(self._GetDiffFilenames(base_rev))
456
457 except subprocess2.CalledProcessError as e:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000458 self.Print('Failed to apply patch.')
459 self.Print('Patch ref is %r @ %r. Target branch for patch is %r. '
460 'Current HEAD is %r. Current dir is %r' % (
461 patch_repo, patch_ref, target_branch, base_rev,
462 self.checkout_path))
Edward Lemurca7d8812018-07-24 17:42:45 +0000463 self.Print('git returned non-zero exit status %s:\n%s' % (
464 e.returncode, e.stderr))
465 # Print the current status so that developers know what changes caused the
466 # patch failure, since git cherry-pick doesn't show that information.
467 self.Print(self._Capture(['status']))
John Budorick2c984a02018-07-18 23:24:13 +0000468 try:
Edward Lemurca7d8812018-07-24 17:42:45 +0000469 self._Capture(['cherry-pick', '--abort'])
470 except subprocess2.CalledProcessError:
471 pass
472 raise
473
Edward Lesmesc621b212018-03-21 20:26:56 -0400474 if options.reset_patch_ref:
475 self._Capture(['reset', '--soft', base_rev])
476
msb@chromium.orge28e4982009-09-25 20:51:45 +0000477 def update(self, options, args, file_list):
478 """Runs git to update or transparently checkout the working copy.
479
480 All updated files will be appended to file_list.
481
482 Raises:
483 Error: if can't get URL for relative path.
484 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000485 if args:
486 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
487
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000488 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000489
John Budorick882c91e2018-07-12 22:11:41 +0000490 # If a dependency is not pinned, track the default remote branch.
491 default_rev = 'refs/remotes/%s/master' % self.remote
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000492 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000493 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000494 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000495 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000496 # Override the revision number.
497 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000498 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000499 # Check again for a revision in case an initial ref was specified
500 # in the url, for example bla.git@refs/heads/custombranch
501 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000502 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000503 if not revision:
504 revision = default_rev
msb@chromium.orge28e4982009-09-25 20:51:45 +0000505
szager@chromium.org8a139702014-06-20 15:55:01 +0000506 if managed:
507 self._DisableHooks()
508
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000509 printed_path = False
510 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000511 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700512 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000513 verbose = ['--verbose']
514 printed_path = True
515
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000516 revision_ref = revision
517 if ':' in revision:
518 revision_ref, _, revision = revision.partition(':')
519
520 mirror = self._GetMirror(url, options, revision_ref)
521 if mirror:
522 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000523
John Budorick882c91e2018-07-12 22:11:41 +0000524 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
525 if remote_ref:
526 # Rewrite remote refs to their local equivalents.
527 revision = ''.join(remote_ref)
528 rev_type = "branch"
529 elif revision.startswith('refs/'):
530 # Local branch? We probably don't want to support, since DEPS should
531 # always specify branches as they are in the upstream repo.
532 rev_type = "branch"
533 else:
534 # hash is also a tag, only make a distinction at checkout
535 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000536
primiano@chromium.org1c127382015-02-17 11:15:40 +0000537 # If we are going to introduce a new project, there is a possibility that
538 # we are syncing back to a state where the project was originally a
539 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
540 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
541 # In such case, we might have a backup of the former .git folder, which can
542 # be used to avoid re-fetching the entire repo again (useful for bisects).
543 backup_dir = self.GetGitBackupDirPath()
544 target_dir = os.path.join(self.checkout_path, '.git')
545 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
546 gclient_utils.safe_makedirs(self.checkout_path)
547 os.rename(backup_dir, target_dir)
548 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800549 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000550
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000551 if (not os.path.exists(self.checkout_path) or
552 (os.path.isdir(self.checkout_path) and
553 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000554 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000555 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000556 try:
John Budorick882c91e2018-07-12 22:11:41 +0000557 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000558 except subprocess2.CalledProcessError:
559 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000560 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000561 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800562 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000563 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000564 file_list.extend(
565 [os.path.join(self.checkout_path, f.decode()) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000566 if mirror:
567 self._Capture(
568 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000569 if not verbose:
570 # Make the output a little prettier. It's nice to have some whitespace
571 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000572 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000573 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000574
John Budorick21a51b32018-09-19 19:39:20 +0000575 if mirror:
576 self._Capture(
577 ['remote', 'set-url', '--push', 'origin', mirror.url])
578
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000579 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000580 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000581 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
582 return self._Capture(['rev-parse', '--verify', 'HEAD'])
583
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000584 self._maybe_break_locks(options)
585
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000586 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000587 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000588
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000589 # See if the url has changed (the unittests use git://foo for the url, let
590 # that through).
591 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
592 return_early = False
593 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
594 # unit test pass. (and update the comment above)
595 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
596 # This allows devs to use experimental repos which have a different url
597 # but whose branch(s) are the same as official repos.
Raul Tambreb946b232019-03-26 14:48:46 +0000598 if (current_url.rstrip(b'/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000599 subprocess2.capture(
Raul Tambreb946b232019-03-26 14:48:46 +0000600 ['git', 'config',
601 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000602 cwd=self.checkout_path).strip() != 'False'):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000603 self.Print('_____ switching %s to a new upstream' % self.relpath)
iannucci@chromium.org78514212014-08-20 23:08:00 +0000604 if not (options.force or options.reset):
605 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700606 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000607 # Switch over to the new upstream
608 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000609 if mirror:
610 with open(os.path.join(
611 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
612 'w') as fh:
613 fh.write(os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000614 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
615 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000616
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000617 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000618 else:
John Budorick882c91e2018-07-12 22:11:41 +0000619 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000620
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000621 if return_early:
622 return self._Capture(['rev-parse', '--verify', 'HEAD'])
623
msb@chromium.org5bde4852009-12-14 16:47:12 +0000624 cur_branch = self._GetCurrentBranch()
625
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000626 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000627 # 0) HEAD is detached. Probably from our initial clone.
628 # - make sure HEAD is contained by a named ref, then update.
629 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700630 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000631 # - try to rebase onto the new hash or branch
632 # 2) current branch is tracking a remote branch with local committed
633 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000634 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000635 # 3) current branch is tracking a remote branch w/or w/out changes, and
636 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000637 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000638 # 4) current branch is tracking a remote branch, but DEPS switches to a
639 # different remote branch, and
640 # a) current branch has no local changes, and --force:
641 # - checkout new branch
642 # b) current branch has local changes, and --force and --reset:
643 # - checkout new branch
644 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000645
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000646 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
647 # a tracking branch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000648 # or 'master' if not a tracking branch (it's based on a specific rev/hash)
649 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000650 if cur_branch is None:
651 upstream_branch = None
652 current_type = "detached"
653 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000654 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000655 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
656 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
657 current_type = "hash"
658 logging.debug("Current branch is not tracking an upstream (remote)"
659 " branch.")
660 elif upstream_branch.startswith('refs/remotes'):
661 current_type = "branch"
662 else:
663 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000664
Edward Lemur579c9862018-07-13 23:17:51 +0000665 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000666
Michael Spang73fac912019-03-08 18:44:19 +0000667 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000668 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000669 self._Fetch(options, prune=options.force)
670
671 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
672 sha_only=True):
673 # Update the remotes first so we have all the refs.
674 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
675 cwd=self.checkout_path)
676 if verbose:
677 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000678
John Budorick882c91e2018-07-12 22:11:41 +0000679 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200680
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000681 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000682 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000683 target = 'HEAD'
684 if options.upstream and upstream_branch:
685 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800686 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000687
msb@chromium.org786fb682010-06-02 15:16:23 +0000688 if current_type == 'detached':
689 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800690 # We just did a Scrub, this is as clean as it's going to get. In
691 # particular if HEAD is a commit that contains two versions of the same
692 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
693 # to actually "Clean" the checkout; that commit is uncheckoutable on this
694 # system. The best we can do is carry forward to the checkout step.
695 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000696 self._CheckClean(revision)
697 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000698 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000699 self.Print('Up-to-date; skipping checkout.')
700 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000701 # 'git checkout' may need to overwrite existing untracked files. Allow
702 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000703 self._Checkout(
704 options,
John Budorick882c91e2018-07-12 22:11:41 +0000705 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000706 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000707 quiet=True,
708 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000709 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000710 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000711 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000712 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700713 # Can't find a merge-base since we don't know our upstream. That makes
714 # this command VERY likely to produce a rebase failure. For now we
715 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000716 upstream_branch = self.remote
717 if options.revision or deps_revision:
718 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700719 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700720 printed_path=printed_path, merge=options.merge)
721 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000722 elif rev_type == 'hash':
723 # case 2
724 self._AttemptRebase(upstream_branch, file_list, options,
725 newbase=revision, printed_path=printed_path,
726 merge=options.merge)
727 printed_path = True
728 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000729 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000730 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000731 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000732 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000733 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000734 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000735 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000736 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
737 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000738 force_switch = False
739 if options.force:
740 try:
John Budorick882c91e2018-07-12 22:11:41 +0000741 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000742 # case 4a
743 force_switch = True
744 except gclient_utils.Error as e:
745 if options.reset:
746 # case 4b
747 force_switch = True
748 else:
749 switch_error = '%s\n%s' % (e.message, switch_error)
750 if force_switch:
751 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000752 (upstream_branch, new_base))
753 switch_branch = 'gclient_' + remote_ref[1]
754 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000755 self._Checkout(options, switch_branch, force=True, quiet=True)
756 else:
757 # case 4c
758 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000759 else:
760 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800761 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000762 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000763 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000764 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000765 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000766 if options.merge:
767 merge_args.append('--ff')
768 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000769 merge_args.append('--ff-only')
770 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000771 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000772 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700773 rebase_files = []
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000774 if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr):
775 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000776 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700777 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000778 printed_path = True
779 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000780 if not options.auto_rebase:
781 try:
782 action = self._AskForData(
783 'Cannot %s, attempt to rebase? '
784 '(y)es / (q)uit / (s)kip : ' %
785 ('merge' if options.merge else 'fast-forward merge'),
786 options)
787 except ValueError:
788 raise gclient_utils.Error('Invalid Character')
789 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700790 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000791 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000792 printed_path = True
793 break
794 elif re.match(r'quit|q', action, re.I):
795 raise gclient_utils.Error("Can't fast-forward, please merge or "
796 "rebase manually.\n"
797 "cd %s && git " % self.checkout_path
798 + "rebase %s" % upstream_branch)
799 elif re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000800 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000801 return
802 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000803 self.Print('Input not recognized')
smut@google.com27c9c8a2014-09-11 19:57:55 +0000804 elif re.match("error: Your local changes to '.*' would be "
805 "overwritten by merge. Aborting.\nPlease, commit your "
806 "changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000807 e.stderr):
808 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000809 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700810 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000811 printed_path = True
812 raise gclient_utils.Error(e.stderr)
813 else:
814 # Some other problem happened with the merge
815 logging.error("Error during fast-forward merge in %s!" % self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000816 self.Print(e.stderr)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000817 raise
818 else:
819 # Fast-forward merge was successful
820 if not re.match('Already up-to-date.', merge_output) or verbose:
821 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700822 self.Print('_____ %s at %s' % (self.relpath, revision),
823 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000824 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000825 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000826 if not verbose:
827 # Make the output a little prettier. It's nice to have some
828 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000829 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000830
agablec3937b92016-10-25 10:13:03 -0700831 if file_list is not None:
832 file_list.extend(
833 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000834
835 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000836 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700837 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000838 '\nConflict while rebasing this branch.\n'
839 'Fix the conflict and run gclient again.\n'
840 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700841 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000842
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000843 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000844 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
845 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000846
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000847 # If --reset and --delete_unversioned_trees are specified, remove any
848 # untracked directories.
849 if options.reset and options.delete_unversioned_trees:
850 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
851 # merge-base by default), so doesn't include untracked files. So we use
852 # 'git ls-files --directory --others --exclude-standard' here directly.
853 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800854 ['-c', 'core.quotePath=false', 'ls-files',
855 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000856 self.checkout_path)
857 for path in (p for p in paths.splitlines() if p.endswith('/')):
858 full_path = os.path.join(self.checkout_path, path)
859 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000860 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000861 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000862
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000863 return self._Capture(['rev-parse', '--verify', 'HEAD'])
864
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000865 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000866 """Reverts local modifications.
867
868 All reverted files will be appended to file_list.
869 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000870 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000871 # revert won't work if the directory doesn't exist. It needs to
872 # checkout instead.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000873 self.Print('_____ %s is missing, synching instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000874 # Don't reuse the args.
875 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000876
877 default_rev = "refs/heads/master"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000878 if options.upstream:
879 if self._GetCurrentBranch():
880 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
881 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000882 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000883 if not deps_revision:
884 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000885 if deps_revision.startswith('refs/heads/'):
886 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700887 try:
888 deps_revision = self.GetUsableRev(deps_revision, options)
889 except NoUsableRevError as e:
890 # If the DEPS entry's url and hash changed, try to update the origin.
891 # See also http://crbug.com/520067.
892 logging.warn(
893 'Couldn\'t find usable revision, will retrying to update instead: %s',
894 e.message)
895 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000896
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000897 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800898 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000899
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800900 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000901 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000902
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000903 if file_list is not None:
904 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
905
906 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000907 """Returns revision"""
908 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000909
msb@chromium.orge28e4982009-09-25 20:51:45 +0000910 def runhooks(self, options, args, file_list):
911 self.status(options, args, file_list)
912
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000913 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000914 """Display status information."""
915 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000916 self.Print('________ couldn\'t run status in %s:\n'
917 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000918 else:
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700919 try:
920 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
921 except subprocess2.CalledProcessError:
922 merge_base = []
Aaron Gablef4068aa2017-12-12 15:14:09 -0800923 self._Run(
924 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
925 options, stdout=self.out_fh, always=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000926 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800927 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000928 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000929
smutae7ea312016-07-18 11:59:41 -0700930 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700931 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700932 sha1 = None
933 if not os.path.isdir(self.checkout_path):
934 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800935 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700936
937 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
938 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700939 else:
agable41e3a6c2016-10-20 11:36:56 -0700940 # May exist in origin, but we don't have it yet, so fetch and look
941 # again.
942 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -0700943 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
944 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700945
946 if not sha1:
947 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800948 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -0700949
950 return sha1
951
primiano@chromium.org1c127382015-02-17 11:15:40 +0000952 def GetGitBackupDirPath(self):
953 """Returns the path where the .git folder for the current project can be
954 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
955 return os.path.join(self._root_dir,
956 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
957
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000958 def _GetMirror(self, url, options, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000959 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +0000960 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000961 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000962 mirror_kwargs = {
963 'print_func': self.filter,
John Budorick882c91e2018-07-12 22:11:41 +0000964 'refs': []
hinoka@google.comb1b54572014-04-16 22:29:23 +0000965 }
hinoka@google.comb1b54572014-04-16 22:29:23 +0000966 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
967 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000968 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
969 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +0000970 if hasattr(options, 'with_tags') and options.with_tags:
971 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000972 elif revision_ref and revision_ref.startswith('refs/tags/'):
973 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000974 return git_cache.Mirror(url, **mirror_kwargs)
975
John Budorick882c91e2018-07-12 22:11:41 +0000976 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800977 """Update a git mirror by fetching the latest commits from the remote,
978 unless mirror already contains revision whose type is sha1 hash.
979 """
John Budorick882c91e2018-07-12 22:11:41 +0000980 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800981 if options.verbose:
982 self.Print('skipping mirror update, it has rev=%s already' % revision,
983 timestamp=False)
984 return
985
szager@chromium.org3ec84f62014-08-22 21:00:22 +0000986 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000987 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000988 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000989 depth = 10
990 else:
991 depth = 10000
992 else:
993 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +0000994 mirror.populate(verbose=options.verbose,
995 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +0000996 depth=depth,
997 ignore_lock=getattr(options, 'ignore_locks', False),
998 lock_timeout=getattr(options, 'lock_timeout', 0))
999 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001000
John Budorick882c91e2018-07-12 22:11:41 +00001001 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001002 """Clone a git repository from the given URL.
1003
msb@chromium.org786fb682010-06-02 15:16:23 +00001004 Once we've cloned the repo, we checkout a working branch if the specified
1005 revision is a branch head. If it is a tag or a specific commit, then we
1006 leave HEAD detached as it makes future updates simpler -- in this case the
1007 user should first create a new branch or switch to an existing branch before
1008 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001009 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001010 # git clone doesn't seem to insert a newline properly before printing
1011 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001012 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +00001013 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +00001014 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001015 if self.cache_dir:
1016 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001017 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001018 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001019 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001020 # If the parent directory does not exist, Git clone on Windows will not
1021 # create it, so we need to do it manually.
1022 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001023 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001024
1025 template_dir = None
1026 if hasattr(options, 'no_history') and options.no_history:
John Budorick882c91e2018-07-12 22:11:41 +00001027 if gclient_utils.IsGitSha(revision):
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001028 # In the case of a subproject, the pinned sha is not necessarily the
1029 # head of the remote branch (so we can't just use --depth=N). Instead,
1030 # we tell git to fetch all the remote objects from SHA..HEAD by means of
1031 # a template git dir which has a 'shallow' file pointing to the sha.
1032 template_dir = tempfile.mkdtemp(
1033 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
1034 dir=parent_dir)
1035 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
1036 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
1037 template_file.write(revision)
1038 clone_cmd.append('--template=' + template_dir)
1039 else:
1040 # Otherwise, we're just interested in the HEAD. Just use --depth.
1041 clone_cmd.append('--depth=1')
1042
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001043 tmp_dir = tempfile.mkdtemp(
1044 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
1045 dir=parent_dir)
1046 try:
1047 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +01001048 if self.print_outbuf:
1049 print_stdout = True
1050 stdout = gclient_utils.WriteToStdout(self.out_fh)
1051 else:
1052 print_stdout = False
1053 stdout = self.out_fh
1054 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
1055 print_stdout=print_stdout, stdout=stdout)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001056 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +00001057 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1058 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001059 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001060 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001061 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001062 finally:
1063 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001064 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001065 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001066 if template_dir:
1067 gclient_utils.rmtree(template_dir)
Edward Lemur579c9862018-07-13 23:17:51 +00001068 self._SetFetchConfig(options)
1069 self._Fetch(options, prune=options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001070 revision = self._AutoFetchRef(options, revision)
1071 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1072 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001073 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001074 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001075 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +00001076 ('Checked out %s to a detached HEAD. Before making any commits\n'
1077 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
1078 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
John Budorick882c91e2018-07-12 22:11:41 +00001079 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001080
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001081 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001082 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001083 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001084 raise gclient_utils.Error("Background task requires input. Rerun "
1085 "gclient with --jobs=1 so that\n"
1086 "interaction is possible.")
1087 try:
1088 return raw_input(prompt)
1089 except KeyboardInterrupt:
1090 # Hide the exception.
1091 sys.exit(1)
1092
1093
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001094 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001095 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001096 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001097 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001098 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001099 revision = upstream
1100 if newbase:
1101 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001102 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001103 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001104 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001105 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001106 printed_path = True
1107 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001108 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001109
1110 if merge:
1111 merge_output = self._Capture(['merge', revision])
1112 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001113 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001114 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001115
1116 # Build the rebase command here using the args
1117 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1118 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001119 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001120 rebase_cmd.append('--verbose')
1121 if newbase:
1122 rebase_cmd.extend(['--onto', newbase])
1123 rebase_cmd.append(upstream)
1124 if branch:
1125 rebase_cmd.append(branch)
1126
1127 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001128 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001129 except subprocess2.CalledProcessError as e:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001130 if (re.match(r'cannot rebase: you have unstaged changes', e.stderr) or
1131 re.match(r'cannot rebase: your index contains uncommitted changes',
1132 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001133 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001134 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001135 'Cannot rebase because of unstaged changes.\n'
1136 '\'git reset --hard HEAD\' ?\n'
1137 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001138 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001139 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001140 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001141 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001142 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001143 break
1144 elif re.match(r'quit|q', rebase_action, re.I):
1145 raise gclient_utils.Error("Please merge or rebase manually\n"
1146 "cd %s && git " % self.checkout_path
1147 + "%s" % ' '.join(rebase_cmd))
1148 elif re.match(r'show|s', rebase_action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001149 self.Print('%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001150 continue
1151 else:
1152 gclient_utils.Error("Input not recognized")
1153 continue
1154 elif re.search(r'^CONFLICT', e.stdout, re.M):
1155 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1156 "Fix the conflict and run gclient again.\n"
1157 "See 'man git-rebase' for details.\n")
1158 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001159 self.Print(e.stdout.strip())
1160 self.Print('Rebase produced error output:\n%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001161 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1162 "manually.\ncd %s && git " %
1163 self.checkout_path
1164 + "%s" % ' '.join(rebase_cmd))
1165
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001166 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001167 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001168 # Make the output a little prettier. It's nice to have some
1169 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001170 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001171
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001172 @staticmethod
1173 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001174 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1175 if not ok:
1176 raise gclient_utils.Error('git version %s < minimum required %s' %
1177 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001178
John Budorick882c91e2018-07-12 22:11:41 +00001179 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001180 # Special case handling if all 3 conditions are met:
1181 # * the mirros have recently changed, but deps destination remains same,
1182 # * the git histories of mirrors are conflicting.
1183 # * git cache is used
1184 # This manifests itself in current checkout having invalid HEAD commit on
1185 # most git operations. Since git cache is used, just deleted the .git
1186 # folder, and re-create it by cloning.
1187 try:
1188 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1189 except subprocess2.CalledProcessError as e:
1190 if ('fatal: bad object HEAD' in e.stderr
1191 and self.cache_dir and self.cache_dir in url):
1192 self.Print((
1193 'Likely due to DEPS change with git cache_dir, '
1194 'the current commit points to no longer existing object.\n'
1195 '%s' % e)
1196 )
1197 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001198 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001199 else:
1200 raise
1201
msb@chromium.org786fb682010-06-02 15:16:23 +00001202 def _IsRebasing(self):
1203 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1204 # have a plumbing command to determine whether a rebase is in progress, so
1205 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1206 g = os.path.join(self.checkout_path, '.git')
1207 return (
1208 os.path.isdir(os.path.join(g, "rebase-merge")) or
1209 os.path.isdir(os.path.join(g, "rebase-apply")))
1210
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001211 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001212 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1213 if os.path.exists(lockfile):
1214 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001215 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001216 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1217 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001218 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001219
msb@chromium.org786fb682010-06-02 15:16:23 +00001220 # Make sure the tree is clean; see git-rebase.sh for reference
1221 try:
1222 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001223 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001224 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001225 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001226 '\tYou have unstaged changes.\n'
1227 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001228 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001229 try:
1230 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001231 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001232 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001233 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001234 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001235 '\tYour index contains uncommitted changes\n'
1236 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001237 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001238
agable83faed02016-10-24 14:37:10 -07001239 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001240 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1241 # reference by a commit). If not, error out -- most likely a rebase is
1242 # in progress, try to detect so we can give a better error.
1243 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001244 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1245 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001246 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001247 # Commit is not contained by any rev. See if the user is rebasing:
1248 if self._IsRebasing():
1249 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001250 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001251 '\tAlready in a conflict, i.e. (no branch).\n'
1252 '\tFix the conflict and run gclient again.\n'
1253 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1254 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001255 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001256 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001257 name = ('saved-by-gclient-' +
1258 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001259 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001260 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001261 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001262
msb@chromium.org5bde4852009-12-14 16:47:12 +00001263 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001264 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001265 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001266 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001267 return None
1268 return branch
1269
borenet@google.comc3e09d22014-04-10 13:58:18 +00001270 def _Capture(self, args, **kwargs):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001271 kwargs.setdefault('cwd', self.checkout_path)
1272 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001273 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001274 env = scm.GIT.ApplyEnvVars(kwargs)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001275 ret = subprocess2.check_output(['git'] + args, env=env, **kwargs)
1276 if strip:
1277 ret = ret.strip()
1278 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001279
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001280 def _Checkout(self, options, ref, force=False, quiet=None):
1281 """Performs a 'git-checkout' operation.
1282
1283 Args:
1284 options: The configured option set
1285 ref: (str) The branch/commit to checkout
1286 quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
1287 'None', the behavior is inferred from 'options.verbose'.
1288 Returns: (str) The output of the checkout operation
1289 """
1290 if quiet is None:
1291 quiet = (not options.verbose)
1292 checkout_args = ['checkout']
1293 if force:
1294 checkout_args.append('--force')
1295 if quiet:
1296 checkout_args.append('--quiet')
1297 checkout_args.append(ref)
1298 return self._Capture(checkout_args)
1299
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001300 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1301 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001302 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001303 # When updating, the ref is modified to be a remote ref .
1304 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1305 # Try to reverse that mapping.
1306 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1307 if original_ref:
1308 refspec = original_ref + ':' + refspec
1309 # When a mirror is configured, it only fetches
1310 # refs/{heads,branch-heads,tags}/*.
1311 # If asked to fetch other refs, we must fetch those directly from the
1312 # repository, and not from the mirror.
1313 if not original_ref.startswith(
1314 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1315 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001316 fetch_cmd = cfg + [
1317 'fetch',
1318 remote or self.remote,
1319 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001320 if refspec:
1321 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001322
1323 if prune:
1324 fetch_cmd.append('--prune')
1325 if options.verbose:
1326 fetch_cmd.append('--verbose')
1327 elif quiet:
1328 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001329 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001330
1331 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1332 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1333
Edward Lemur579c9862018-07-13 23:17:51 +00001334 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001335 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1336 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001337 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001338 try:
1339 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1340 options)
1341 self._Run(['config', 'remote.%s.fetch' % self.remote,
1342 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1343 except subprocess2.CalledProcessError as e:
1344 # If exit code was 5, it means we attempted to unset a config that
1345 # didn't exist. Ignore it.
1346 if e.returncode != 5:
1347 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001348 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001349 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001350 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1351 '^\\+refs/branch-heads/\\*:.*$']
1352 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001353 if hasattr(options, 'with_tags') and options.with_tags:
1354 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1355 '+refs/tags/*:refs/tags/*',
1356 '^\\+refs/tags/\\*:.*$']
1357 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001358
John Budorick882c91e2018-07-12 22:11:41 +00001359 def _AutoFetchRef(self, options, revision):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001360 """Attempts to fetch |revision| if not available in local repo.
1361
1362 Returns possibly updated revision."""
John Budorick882c91e2018-07-12 22:11:41 +00001363 try:
1364 self._Capture(['rev-parse', revision])
1365 except subprocess2.CalledProcessError:
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001366 self._Fetch(options, refspec=revision)
1367 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1368 return revision
1369
dnj@chromium.org680f2172014-06-25 00:39:32 +00001370 def _Run(self, args, options, show_header=True, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001371 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001372 kwargs.setdefault('cwd', self.checkout_path)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001373 kwargs.setdefault('stdout', self.out_fh)
szager@chromium.org848fd492014-04-09 19:06:44 +00001374 kwargs['filter_fn'] = self.filter
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001375 kwargs.setdefault('print_stdout', False)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001376 env = scm.GIT.ApplyEnvVars(kwargs)
agable@chromium.org772efaf2014-04-01 02:35:44 +00001377 cmd = ['git'] + args
dnj@chromium.org680f2172014-06-25 00:39:32 +00001378 if show_header:
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001379 gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs)
1380 else:
1381 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001382
1383
1384class CipdPackage(object):
1385 """A representation of a single CIPD package."""
1386
John Budorickd3ba72b2018-03-20 12:27:42 -07001387 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001388 self._authority_for_subdir = authority_for_subdir
1389 self._name = name
1390 self._version = version
1391
1392 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001393 def authority_for_subdir(self):
1394 """Whether this package has authority to act on behalf of its subdir.
1395
1396 Some operations should only be performed once per subdirectory. A package
1397 that has authority for its subdirectory is the only package that should
1398 perform such operations.
1399
1400 Returns:
1401 bool; whether this package has subdir authority.
1402 """
1403 return self._authority_for_subdir
1404
1405 @property
1406 def name(self):
1407 return self._name
1408
1409 @property
1410 def version(self):
1411 return self._version
1412
1413
1414class CipdRoot(object):
1415 """A representation of a single CIPD root."""
1416 def __init__(self, root_dir, service_url):
1417 self._all_packages = set()
1418 self._mutator_lock = threading.Lock()
1419 self._packages_by_subdir = collections.defaultdict(list)
1420 self._root_dir = root_dir
1421 self._service_url = service_url
1422
1423 def add_package(self, subdir, package, version):
1424 """Adds a package to this CIPD root.
1425
1426 As far as clients are concerned, this grants both root and subdir authority
1427 to packages arbitrarily. (The implementation grants root authority to the
1428 first package added and subdir authority to the first package added for that
1429 subdir, but clients should not depend on or expect that behavior.)
1430
1431 Args:
1432 subdir: str; relative path to where the package should be installed from
1433 the cipd root directory.
1434 package: str; the cipd package name.
1435 version: str; the cipd package version.
1436 Returns:
1437 CipdPackage; the package that was created and added to this root.
1438 """
1439 with self._mutator_lock:
1440 cipd_package = CipdPackage(
1441 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001442 not self._packages_by_subdir[subdir])
1443 self._all_packages.add(cipd_package)
1444 self._packages_by_subdir[subdir].append(cipd_package)
1445 return cipd_package
1446
1447 def packages(self, subdir):
1448 """Get the list of configured packages for the given subdir."""
1449 return list(self._packages_by_subdir[subdir])
1450
1451 def clobber(self):
1452 """Remove the .cipd directory.
1453
1454 This is useful for forcing ensure to redownload and reinitialize all
1455 packages.
1456 """
1457 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001458 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001459 try:
1460 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1461 except OSError:
1462 if os.path.exists(cipd_cache_dir):
1463 raise
1464
1465 @contextlib.contextmanager
1466 def _create_ensure_file(self):
1467 try:
1468 ensure_file = None
1469 with tempfile.NamedTemporaryFile(
Raul Tambreb946b232019-03-26 14:48:46 +00001470 suffix='.ensure', delete=False, mode='w') as ensure_file:
John Budorick302bb842018-07-17 23:49:17 +00001471 ensure_file.write('$ParanoidMode CheckPresence\n\n')
Raul Tambreb946b232019-03-26 14:48:46 +00001472 for subdir, packages in sorted(self._packages_by_subdir.items()):
John Budorick0f7b2002018-01-19 15:46:17 -08001473 ensure_file.write('@Subdir %s\n' % subdir)
Edward Lemurfbb06aa2018-06-11 20:43:06 +00001474 for package in sorted(packages, key=lambda p: p.name):
John Budorick0f7b2002018-01-19 15:46:17 -08001475 ensure_file.write('%s %s\n' % (package.name, package.version))
1476 ensure_file.write('\n')
1477 yield ensure_file.name
1478 finally:
1479 if ensure_file is not None and os.path.exists(ensure_file.name):
1480 os.remove(ensure_file.name)
1481
1482 def ensure(self):
1483 """Run `cipd ensure`."""
1484 with self._mutator_lock:
1485 with self._create_ensure_file() as ensure_file:
1486 cmd = [
1487 'cipd', 'ensure',
1488 '-log-level', 'error',
1489 '-root', self.root_dir,
1490 '-ensure-file', ensure_file,
1491 ]
1492 gclient_utils.CheckCallAndFilterAndHeader(cmd)
1493
John Budorickd3ba72b2018-03-20 12:27:42 -07001494 def run(self, command):
1495 if command == 'update':
1496 self.ensure()
1497 elif command == 'revert':
1498 self.clobber()
1499 self.ensure()
1500
John Budorick0f7b2002018-01-19 15:46:17 -08001501 def created_package(self, package):
1502 """Checks whether this root created the given package.
1503
1504 Args:
1505 package: CipdPackage; the package to check.
1506 Returns:
1507 bool; whether this root created the given package.
1508 """
1509 return package in self._all_packages
1510
1511 @property
1512 def root_dir(self):
1513 return self._root_dir
1514
1515 @property
1516 def service_url(self):
1517 return self._service_url
1518
1519
1520class CipdWrapper(SCMWrapper):
1521 """Wrapper for CIPD.
1522
1523 Currently only supports chrome-infra-packages.appspot.com.
1524 """
John Budorick3929e9e2018-02-04 18:18:07 -08001525 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001526
1527 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1528 out_cb=None, root=None, package=None):
1529 super(CipdWrapper, self).__init__(
1530 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1531 out_cb=out_cb)
1532 assert root.created_package(package)
1533 self._package = package
1534 self._root = root
1535
1536 #override
1537 def GetCacheMirror(self):
1538 return None
1539
1540 #override
1541 def GetActualRemoteURL(self, options):
1542 return self._root.service_url
1543
1544 #override
1545 def DoesRemoteURLMatch(self, options):
1546 del options
1547 return True
1548
1549 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001550 """Does nothing.
1551
1552 CIPD packages should be reverted at the root by running
1553 `CipdRoot.run('revert')`.
1554 """
1555 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001556
1557 def diff(self, options, args, file_list):
1558 """CIPD has no notion of diffing."""
1559 pass
1560
1561 def pack(self, options, args, file_list):
1562 """CIPD has no notion of diffing."""
1563 pass
1564
1565 def revinfo(self, options, args, file_list):
1566 """Grab the instance ID."""
1567 try:
1568 tmpdir = tempfile.mkdtemp()
1569 describe_json_path = os.path.join(tmpdir, 'describe.json')
1570 cmd = [
1571 'cipd', 'describe',
1572 self._package.name,
1573 '-log-level', 'error',
1574 '-version', self._package.version,
1575 '-json-output', describe_json_path
1576 ]
1577 gclient_utils.CheckCallAndFilter(
1578 cmd, filter_fn=lambda _line: None, print_stdout=False)
1579 with open(describe_json_path) as f:
1580 describe_json = json.load(f)
1581 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1582 finally:
1583 gclient_utils.rmtree(tmpdir)
1584
1585 def status(self, options, args, file_list):
1586 pass
1587
1588 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001589 """Does nothing.
1590
1591 CIPD packages should be updated at the root by running
1592 `CipdRoot.run('update')`.
1593 """
1594 pass