blob: e102e80734168e9758b6164d8f3bc08ae53a5ffb [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."""
225 if url.startswith('git+http://') or url.startswith('git+https://'):
226 url = url[4:]
John Budorick0f7b2002018-01-19 15:46:17 -0800227 SCMWrapper.__init__(self, url, *args, **kwargs)
szager@chromium.org848fd492014-04-09 19:06:44 +0000228 filter_kwargs = { 'time_throttle': 1, 'out_fh': self.out_fh }
229 if self.out_cb:
230 filter_kwargs['predicate'] = self.out_cb
231 self.filter = gclient_utils.GitFilter(**filter_kwargs)
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000232
mukai@chromium.org9e3e82c2012-04-18 12:55:43 +0000233 @staticmethod
234 def BinaryExists():
235 """Returns true if the command exists."""
236 try:
237 # We assume git is newer than 1.7. See: crbug.com/114483
238 result, version = scm.GIT.AssertVersion('1.7')
239 if not result:
240 raise gclient_utils.Error('Git version is older than 1.7: %s' % version)
241 return result
242 except OSError:
243 return False
244
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000245 def GetCheckoutRoot(self):
246 return scm.GIT.GetCheckoutRoot(self.checkout_path)
247
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000248 def GetRevisionDate(self, _revision):
floitsch@google.comeaab7842011-04-28 09:07:58 +0000249 """Returns the given revision's date in ISO-8601 format (which contains the
250 time zone)."""
251 # TODO(floitsch): get the time-stamp of the given revision and not just the
252 # time-stamp of the currently checked out revision.
253 return self._Capture(['log', '-n', '1', '--format=%ai'])
254
Aaron Gablef4068aa2017-12-12 15:14:09 -0800255 def _GetDiffFilenames(self, base):
256 """Returns the names of files modified since base."""
257 return self._Capture(
258 # Filter to remove base if it is None.
259 filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only', base])
260 ).split()
261
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000262 def diff(self, options, _args, _file_list):
Aaron Gable1853f662018-02-12 15:45:56 -0800263 _, revision = gclient_utils.SplitUrlRevision(self.url)
264 if not revision:
265 revision = 'refs/remotes/%s/master' % self.remote
266 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000267
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000268 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000269 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000270 repository.
271
272 The patch file is generated from a diff of the merge base of HEAD and
273 its upstream branch.
274 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700275 try:
276 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
277 except subprocess2.CalledProcessError:
278 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000279 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700280 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000281 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000282 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000283
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800284 def _Scrub(self, target, options):
285 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000286 quiet = []
287 if not options.verbose:
288 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800289 self._Run(['reset', '--hard', target] + quiet, options)
290 if options.force and options.delete_unversioned_trees:
291 # where `target` is a commit that contains both upper and lower case
292 # versions of the same file on a case insensitive filesystem, we are
293 # actually in a broken state here. The index will have both 'a' and 'A',
294 # but only one of them will exist on the disk. To progress, we delete
295 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800296 output = self._Capture([
297 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800298 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800299 # --porcelain (v1) looks like:
300 # XY filename
301 try:
302 filename = line[3:]
303 self.Print('_____ Deleting residual after reset: %r.' % filename)
304 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800305 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800306 except OSError:
307 pass
308
John Budorick882c91e2018-07-12 22:11:41 +0000309 def _FetchAndReset(self, revision, file_list, options):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800310 """Equivalent to git fetch; git reset."""
Edward Lemur579c9862018-07-13 23:17:51 +0000311 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000312
dnj@chromium.org680f2172014-06-25 00:39:32 +0000313 self._Fetch(options, prune=True, quiet=options.verbose)
John Budorick882c91e2018-07-12 22:11:41 +0000314 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000315 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800316 files = self._Capture(
317 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambreb946b232019-03-26 14:48:46 +0000318 file_list.extend(
319 [os.path.join(self.checkout_path, f.decode()) for f in files])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000320
szager@chromium.org8a139702014-06-20 15:55:01 +0000321 def _DisableHooks(self):
322 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
323 if not os.path.isdir(hook_dir):
324 return
325 for f in os.listdir(hook_dir):
326 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000327 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
328 if os.path.exists(disabled_hook_path):
329 os.remove(disabled_hook_path)
330 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000331
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000332 def _maybe_break_locks(self, options):
333 """This removes all .lock files from this repo's .git directory, if the
334 user passed the --break_repo_locks command line flag.
335
336 In particular, this will cleanup index.lock files, as well as ref lock
337 files.
338 """
339 if options.break_repo_locks:
340 git_dir = os.path.join(self.checkout_path, '.git')
341 for path, _, filenames in os.walk(git_dir):
342 for filename in filenames:
343 if filename.endswith('.lock'):
344 to_break = os.path.join(path, filename)
345 self.Print('breaking lock: %s' % (to_break,))
346 try:
347 os.remove(to_break)
348 except OSError as ex:
349 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
350 raise
351
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000352 # TODO(ehmaldonado): Remove after bot_update is modified to pass the patch's
353 # branch.
354 def _GetTargetBranchForCommit(self, commit):
Edward Lemurca7d8812018-07-24 17:42:45 +0000355 """Get the remote branch a commit is part of."""
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000356 _WELL_KNOWN_BRANCHES = [
357 'refs/remotes/origin/master',
358 'refs/remotes/origin/infra/config',
359 'refs/remotes/origin/lkgr',
360 ]
361 for branch in _WELL_KNOWN_BRANCHES:
362 if scm.GIT.IsAncestor(self.checkout_path, commit, branch):
363 return branch
Edward Lemurca7d8812018-07-24 17:42:45 +0000364 remote_refs = self._Capture(
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000365 ['for-each-ref', 'refs/remotes/%s' % self.remote,
Edward Lemurca7d8812018-07-24 17:42:45 +0000366 '--format=%(refname)']).splitlines()
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000367 for ref in sorted(remote_refs, reverse=True):
Edward Lemurca7d8812018-07-24 17:42:45 +0000368 if scm.GIT.IsAncestor(self.checkout_path, commit, ref):
369 return ref
370 self.Print('Failed to find a remote ref that contains %s. '
371 'Candidate refs were %s.' % (commit, remote_refs))
372 # Fallback to the commit we got.
373 # This means that apply_path_ref will try to find the merge-base between the
374 # patch and the commit (which is most likely the commit) and cherry-pick
375 # everything in between.
376 return commit
377
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000378 def apply_patch_ref(self, patch_repo, patch_ref, target_branch, options,
379 file_list):
380 """Apply a patch on top of the revision we're synced at.
381
382 The patch ref is given by |patch_repo|@|patch_ref|, and the current revision
383 is |base_rev|.
384 We also need the |target_branch| that the patch was uploaded against. We use
385 it to find a merge base between |patch_rev| and |base_rev|, so we can find
386 what commits constitute the patch:
387
388 Graphically, it looks like this:
389
390 ... -> merge_base -> [possibly already landed commits] -> target_branch
391 \
392 -> [possibly not yet landed dependent CLs] -> patch_rev
393
394 Next, we apply the commits |merge_base..patch_rev| on top of whatever is
395 currently checked out, denoted |base_rev|. Typically, it'd be a revision
396 from |target_branch|, but this is not required.
397
398 Graphically, we cherry pick |merge_base..patch_rev| on top of |base_rev|:
399
400 ... -> base_rev -> [possibly not yet landed dependent CLs] -> patch_rev
401
402 After application, if |options.reset_patch_ref| is specified, we soft reset
403 the just cherry-picked changes, keeping them in git index only.
404
405 Args:
406 patch_repo: The patch origin. e.g. 'https://foo.googlesource.com/bar'
407 patch_ref: The ref to the patch. e.g. 'refs/changes/1234/34/1'.
408 target_branch: The branch the patch was uploaded against.
409 e.g. 'refs/heads/master' or 'refs/heads/infra/config'.
410 options: The options passed to gclient.
411 file_list: A list where modified files will be appended.
412 """
413
Edward Lemurca7d8812018-07-24 17:42:45 +0000414 # Abort any cherry-picks in progress.
415 try:
416 self._Capture(['cherry-pick', '--abort'])
417 except subprocess2.CalledProcessError:
418 pass
419
Edward Lesmesc621b212018-03-21 20:26:56 -0400420 base_rev = self._Capture(['rev-parse', 'HEAD'])
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000421 target_branch = target_branch or self._GetTargetBranchForCommit(base_rev)
Edward Lesmesc621b212018-03-21 20:26:56 -0400422 self.Print('===Applying patch ref===')
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000423 self.Print('Patch ref is %r @ %r. Target branch for patch is %r. '
424 'Current HEAD is %r. Current dir is %r' % (
425 patch_repo, patch_ref, target_branch, base_rev,
426 self.checkout_path))
Edward Lesmesc621b212018-03-21 20:26:56 -0400427 self._Capture(['reset', '--hard'])
428 self._Capture(['fetch', patch_repo, patch_ref])
Edward Lemurca7d8812018-07-24 17:42:45 +0000429 patch_rev = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400430
Edward Lemurca7d8812018-07-24 17:42:45 +0000431 try:
432 if not options.rebase_patch_ref:
433 self._Capture(['checkout', patch_rev])
434 else:
435 # Find the merge-base between the branch_rev and patch_rev to find out
436 # the changes we need to cherry-pick on top of base_rev.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000437 merge_base = self._Capture(['merge-base', target_branch, patch_rev])
Edward Lemurca7d8812018-07-24 17:42:45 +0000438 self.Print('Merge base of %s and %s is %s' % (
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000439 target_branch, patch_rev, merge_base))
Edward Lemurca7d8812018-07-24 17:42:45 +0000440 if merge_base == patch_rev:
441 # If the merge-base is patch_rev, it means patch_rev is already part
442 # of the history, so just check it out.
443 self._Capture(['checkout', patch_rev])
444 else:
445 # If a change was uploaded on top of another change, which has already
446 # landed, one of the commits in the cherry-pick range will be
447 # redundant, since it has already landed and its changes incorporated
448 # in the tree.
449 # We pass '--keep-redundant-commits' to ignore those changes.
450 self._Capture(['cherry-pick', merge_base + '..' + patch_rev,
451 '--keep-redundant-commits'])
452
453 if file_list is not None:
454 file_list.extend(self._GetDiffFilenames(base_rev))
455
456 except subprocess2.CalledProcessError as e:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000457 self.Print('Failed to apply patch.')
458 self.Print('Patch ref is %r @ %r. Target branch for patch is %r. '
459 'Current HEAD is %r. Current dir is %r' % (
460 patch_repo, patch_ref, target_branch, base_rev,
461 self.checkout_path))
Edward Lemurca7d8812018-07-24 17:42:45 +0000462 self.Print('git returned non-zero exit status %s:\n%s' % (
463 e.returncode, e.stderr))
464 # Print the current status so that developers know what changes caused the
465 # patch failure, since git cherry-pick doesn't show that information.
466 self.Print(self._Capture(['status']))
John Budorick2c984a02018-07-18 23:24:13 +0000467 try:
Edward Lemurca7d8812018-07-24 17:42:45 +0000468 self._Capture(['cherry-pick', '--abort'])
469 except subprocess2.CalledProcessError:
470 pass
471 raise
472
Edward Lesmesc621b212018-03-21 20:26:56 -0400473 if options.reset_patch_ref:
474 self._Capture(['reset', '--soft', base_rev])
475
msb@chromium.orge28e4982009-09-25 20:51:45 +0000476 def update(self, options, args, file_list):
477 """Runs git to update or transparently checkout the working copy.
478
479 All updated files will be appended to file_list.
480
481 Raises:
482 Error: if can't get URL for relative path.
483 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000484 if args:
485 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
486
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000487 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000488
John Budorick882c91e2018-07-12 22:11:41 +0000489 # If a dependency is not pinned, track the default remote branch.
490 default_rev = 'refs/remotes/%s/master' % self.remote
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000491 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000492 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000493 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000494 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000495 # Override the revision number.
496 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000497 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000498 # Check again for a revision in case an initial ref was specified
499 # in the url, for example bla.git@refs/heads/custombranch
500 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000501 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000502 if not revision:
503 revision = default_rev
msb@chromium.orge28e4982009-09-25 20:51:45 +0000504
szager@chromium.org8a139702014-06-20 15:55:01 +0000505 if managed:
506 self._DisableHooks()
507
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000508 printed_path = False
509 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000510 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700511 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000512 verbose = ['--verbose']
513 printed_path = True
514
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000515 revision_ref = revision
516 if ':' in revision:
517 revision_ref, _, revision = revision.partition(':')
518
519 mirror = self._GetMirror(url, options, revision_ref)
520 if mirror:
521 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000522
John Budorick882c91e2018-07-12 22:11:41 +0000523 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
524 if remote_ref:
525 # Rewrite remote refs to their local equivalents.
526 revision = ''.join(remote_ref)
527 rev_type = "branch"
528 elif revision.startswith('refs/'):
529 # Local branch? We probably don't want to support, since DEPS should
530 # always specify branches as they are in the upstream repo.
531 rev_type = "branch"
532 else:
533 # hash is also a tag, only make a distinction at checkout
534 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000535
primiano@chromium.org1c127382015-02-17 11:15:40 +0000536 # If we are going to introduce a new project, there is a possibility that
537 # we are syncing back to a state where the project was originally a
538 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
539 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
540 # In such case, we might have a backup of the former .git folder, which can
541 # be used to avoid re-fetching the entire repo again (useful for bisects).
542 backup_dir = self.GetGitBackupDirPath()
543 target_dir = os.path.join(self.checkout_path, '.git')
544 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
545 gclient_utils.safe_makedirs(self.checkout_path)
546 os.rename(backup_dir, target_dir)
547 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800548 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000549
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000550 if (not os.path.exists(self.checkout_path) or
551 (os.path.isdir(self.checkout_path) and
552 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000553 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000554 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000555 try:
John Budorick882c91e2018-07-12 22:11:41 +0000556 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000557 except subprocess2.CalledProcessError:
558 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000559 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000560 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800561 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000562 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000563 file_list.extend(
564 [os.path.join(self.checkout_path, f.decode()) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000565 if mirror:
566 self._Capture(
567 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000568 if not verbose:
569 # Make the output a little prettier. It's nice to have some whitespace
570 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000571 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000572 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000573
John Budorick21a51b32018-09-19 19:39:20 +0000574 if mirror:
575 self._Capture(
576 ['remote', 'set-url', '--push', 'origin', mirror.url])
577
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000578 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000579 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000580 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
581 return self._Capture(['rev-parse', '--verify', 'HEAD'])
582
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000583 self._maybe_break_locks(options)
584
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000585 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000586 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000587
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000588 # See if the url has changed (the unittests use git://foo for the url, let
589 # that through).
590 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
591 return_early = False
592 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
593 # unit test pass. (and update the comment above)
594 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
595 # This allows devs to use experimental repos which have a different url
596 # but whose branch(s) are the same as official repos.
Raul Tambreb946b232019-03-26 14:48:46 +0000597 if (current_url.rstrip(b'/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000598 subprocess2.capture(
Raul Tambreb946b232019-03-26 14:48:46 +0000599 ['git', 'config',
600 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000601 cwd=self.checkout_path).strip() != 'False'):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000602 self.Print('_____ switching %s to a new upstream' % self.relpath)
iannucci@chromium.org78514212014-08-20 23:08:00 +0000603 if not (options.force or options.reset):
604 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700605 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000606 # Switch over to the new upstream
607 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000608 if mirror:
609 with open(os.path.join(
610 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
611 'w') as fh:
612 fh.write(os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000613 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
614 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000615
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000616 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000617 else:
John Budorick882c91e2018-07-12 22:11:41 +0000618 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000619
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000620 if return_early:
621 return self._Capture(['rev-parse', '--verify', 'HEAD'])
622
msb@chromium.org5bde4852009-12-14 16:47:12 +0000623 cur_branch = self._GetCurrentBranch()
624
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000625 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000626 # 0) HEAD is detached. Probably from our initial clone.
627 # - make sure HEAD is contained by a named ref, then update.
628 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700629 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000630 # - try to rebase onto the new hash or branch
631 # 2) current branch is tracking a remote branch with local committed
632 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000633 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000634 # 3) current branch is tracking a remote branch w/or w/out changes, and
635 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000636 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000637 # 4) current branch is tracking a remote branch, but DEPS switches to a
638 # different remote branch, and
639 # a) current branch has no local changes, and --force:
640 # - checkout new branch
641 # b) current branch has local changes, and --force and --reset:
642 # - checkout new branch
643 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000644
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000645 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
646 # a tracking branch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000647 # or 'master' if not a tracking branch (it's based on a specific rev/hash)
648 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000649 if cur_branch is None:
650 upstream_branch = None
651 current_type = "detached"
652 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000653 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000654 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
655 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
656 current_type = "hash"
657 logging.debug("Current branch is not tracking an upstream (remote)"
658 " branch.")
659 elif upstream_branch.startswith('refs/remotes'):
660 current_type = "branch"
661 else:
662 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000663
Edward Lemur579c9862018-07-13 23:17:51 +0000664 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000665
Michael Spang73fac912019-03-08 18:44:19 +0000666 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000667 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000668 self._Fetch(options, prune=options.force)
669
670 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
671 sha_only=True):
672 # Update the remotes first so we have all the refs.
673 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
674 cwd=self.checkout_path)
675 if verbose:
676 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000677
John Budorick882c91e2018-07-12 22:11:41 +0000678 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200679
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000680 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000681 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000682 target = 'HEAD'
683 if options.upstream and upstream_branch:
684 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800685 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000686
msb@chromium.org786fb682010-06-02 15:16:23 +0000687 if current_type == 'detached':
688 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800689 # We just did a Scrub, this is as clean as it's going to get. In
690 # particular if HEAD is a commit that contains two versions of the same
691 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
692 # to actually "Clean" the checkout; that commit is uncheckoutable on this
693 # system. The best we can do is carry forward to the checkout step.
694 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000695 self._CheckClean(revision)
696 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000697 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000698 self.Print('Up-to-date; skipping checkout.')
699 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000700 # 'git checkout' may need to overwrite existing untracked files. Allow
701 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000702 self._Checkout(
703 options,
John Budorick882c91e2018-07-12 22:11:41 +0000704 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000705 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000706 quiet=True,
707 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000708 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000709 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000710 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000711 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700712 # Can't find a merge-base since we don't know our upstream. That makes
713 # this command VERY likely to produce a rebase failure. For now we
714 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000715 upstream_branch = self.remote
716 if options.revision or deps_revision:
717 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700718 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700719 printed_path=printed_path, merge=options.merge)
720 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000721 elif rev_type == 'hash':
722 # case 2
723 self._AttemptRebase(upstream_branch, file_list, options,
724 newbase=revision, printed_path=printed_path,
725 merge=options.merge)
726 printed_path = True
727 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000728 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000729 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000730 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000731 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000732 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000733 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000734 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000735 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
736 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000737 force_switch = False
738 if options.force:
739 try:
John Budorick882c91e2018-07-12 22:11:41 +0000740 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000741 # case 4a
742 force_switch = True
743 except gclient_utils.Error as e:
744 if options.reset:
745 # case 4b
746 force_switch = True
747 else:
748 switch_error = '%s\n%s' % (e.message, switch_error)
749 if force_switch:
750 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000751 (upstream_branch, new_base))
752 switch_branch = 'gclient_' + remote_ref[1]
753 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000754 self._Checkout(options, switch_branch, force=True, quiet=True)
755 else:
756 # case 4c
757 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000758 else:
759 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800760 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000761 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000762 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000763 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000764 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000765 if options.merge:
766 merge_args.append('--ff')
767 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000768 merge_args.append('--ff-only')
769 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000770 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000771 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700772 rebase_files = []
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000773 if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr):
774 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000775 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700776 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000777 printed_path = True
778 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000779 if not options.auto_rebase:
780 try:
781 action = self._AskForData(
782 'Cannot %s, attempt to rebase? '
783 '(y)es / (q)uit / (s)kip : ' %
784 ('merge' if options.merge else 'fast-forward merge'),
785 options)
786 except ValueError:
787 raise gclient_utils.Error('Invalid Character')
788 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700789 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000790 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000791 printed_path = True
792 break
793 elif re.match(r'quit|q', action, re.I):
794 raise gclient_utils.Error("Can't fast-forward, please merge or "
795 "rebase manually.\n"
796 "cd %s && git " % self.checkout_path
797 + "rebase %s" % upstream_branch)
798 elif re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000799 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000800 return
801 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000802 self.Print('Input not recognized')
smut@google.com27c9c8a2014-09-11 19:57:55 +0000803 elif re.match("error: Your local changes to '.*' would be "
804 "overwritten by merge. Aborting.\nPlease, commit your "
805 "changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000806 e.stderr):
807 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000808 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700809 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000810 printed_path = True
811 raise gclient_utils.Error(e.stderr)
812 else:
813 # Some other problem happened with the merge
814 logging.error("Error during fast-forward merge in %s!" % self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000815 self.Print(e.stderr)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000816 raise
817 else:
818 # Fast-forward merge was successful
819 if not re.match('Already up-to-date.', merge_output) or verbose:
820 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700821 self.Print('_____ %s at %s' % (self.relpath, revision),
822 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000823 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000824 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000825 if not verbose:
826 # Make the output a little prettier. It's nice to have some
827 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000828 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000829
agablec3937b92016-10-25 10:13:03 -0700830 if file_list is not None:
831 file_list.extend(
832 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000833
834 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000835 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700836 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000837 '\nConflict while rebasing this branch.\n'
838 'Fix the conflict and run gclient again.\n'
839 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700840 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000841
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000842 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000843 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
844 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000845
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000846 # If --reset and --delete_unversioned_trees are specified, remove any
847 # untracked directories.
848 if options.reset and options.delete_unversioned_trees:
849 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
850 # merge-base by default), so doesn't include untracked files. So we use
851 # 'git ls-files --directory --others --exclude-standard' here directly.
852 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800853 ['-c', 'core.quotePath=false', 'ls-files',
854 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000855 self.checkout_path)
856 for path in (p for p in paths.splitlines() if p.endswith('/')):
857 full_path = os.path.join(self.checkout_path, path)
858 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000859 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000860 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000861
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000862 return self._Capture(['rev-parse', '--verify', 'HEAD'])
863
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000864 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000865 """Reverts local modifications.
866
867 All reverted files will be appended to file_list.
868 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000869 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000870 # revert won't work if the directory doesn't exist. It needs to
871 # checkout instead.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000872 self.Print('_____ %s is missing, synching instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000873 # Don't reuse the args.
874 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000875
876 default_rev = "refs/heads/master"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000877 if options.upstream:
878 if self._GetCurrentBranch():
879 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
880 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000881 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000882 if not deps_revision:
883 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000884 if deps_revision.startswith('refs/heads/'):
885 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700886 try:
887 deps_revision = self.GetUsableRev(deps_revision, options)
888 except NoUsableRevError as e:
889 # If the DEPS entry's url and hash changed, try to update the origin.
890 # See also http://crbug.com/520067.
891 logging.warn(
892 'Couldn\'t find usable revision, will retrying to update instead: %s',
893 e.message)
894 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000895
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000896 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800897 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000898
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800899 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000900 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000901
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000902 if file_list is not None:
903 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
904
905 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000906 """Returns revision"""
907 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000908
msb@chromium.orge28e4982009-09-25 20:51:45 +0000909 def runhooks(self, options, args, file_list):
910 self.status(options, args, file_list)
911
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000912 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000913 """Display status information."""
914 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000915 self.Print('________ couldn\'t run status in %s:\n'
916 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000917 else:
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700918 try:
919 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
920 except subprocess2.CalledProcessError:
921 merge_base = []
Aaron Gablef4068aa2017-12-12 15:14:09 -0800922 self._Run(
923 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
924 options, stdout=self.out_fh, always=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000925 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800926 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000927 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000928
smutae7ea312016-07-18 11:59:41 -0700929 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700930 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700931 sha1 = None
932 if not os.path.isdir(self.checkout_path):
933 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800934 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700935
936 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
937 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700938 else:
agable41e3a6c2016-10-20 11:36:56 -0700939 # May exist in origin, but we don't have it yet, so fetch and look
940 # again.
941 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -0700942 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
943 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700944
945 if not sha1:
946 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800947 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -0700948
949 return sha1
950
primiano@chromium.org1c127382015-02-17 11:15:40 +0000951 def GetGitBackupDirPath(self):
952 """Returns the path where the .git folder for the current project can be
953 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
954 return os.path.join(self._root_dir,
955 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
956
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000957 def _GetMirror(self, url, options, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000958 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +0000959 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000960 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000961 mirror_kwargs = {
962 'print_func': self.filter,
John Budorick882c91e2018-07-12 22:11:41 +0000963 'refs': []
hinoka@google.comb1b54572014-04-16 22:29:23 +0000964 }
hinoka@google.comb1b54572014-04-16 22:29:23 +0000965 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
966 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000967 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
968 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +0000969 if hasattr(options, 'with_tags') and options.with_tags:
970 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000971 elif revision_ref and revision_ref.startswith('refs/tags/'):
972 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000973 return git_cache.Mirror(url, **mirror_kwargs)
974
John Budorick882c91e2018-07-12 22:11:41 +0000975 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800976 """Update a git mirror by fetching the latest commits from the remote,
977 unless mirror already contains revision whose type is sha1 hash.
978 """
John Budorick882c91e2018-07-12 22:11:41 +0000979 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800980 if options.verbose:
981 self.Print('skipping mirror update, it has rev=%s already' % revision,
982 timestamp=False)
983 return
984
szager@chromium.org3ec84f62014-08-22 21:00:22 +0000985 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000986 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000987 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000988 depth = 10
989 else:
990 depth = 10000
991 else:
992 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +0000993 mirror.populate(verbose=options.verbose,
994 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +0000995 depth=depth,
996 ignore_lock=getattr(options, 'ignore_locks', False),
997 lock_timeout=getattr(options, 'lock_timeout', 0))
998 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000999
John Budorick882c91e2018-07-12 22:11:41 +00001000 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001001 """Clone a git repository from the given URL.
1002
msb@chromium.org786fb682010-06-02 15:16:23 +00001003 Once we've cloned the repo, we checkout a working branch if the specified
1004 revision is a branch head. If it is a tag or a specific commit, then we
1005 leave HEAD detached as it makes future updates simpler -- in this case the
1006 user should first create a new branch or switch to an existing branch before
1007 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001008 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001009 # git clone doesn't seem to insert a newline properly before printing
1010 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001011 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +00001012 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +00001013 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001014 if self.cache_dir:
1015 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001016 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001017 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001018 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001019 # If the parent directory does not exist, Git clone on Windows will not
1020 # create it, so we need to do it manually.
1021 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001022 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001023
1024 template_dir = None
1025 if hasattr(options, 'no_history') and options.no_history:
John Budorick882c91e2018-07-12 22:11:41 +00001026 if gclient_utils.IsGitSha(revision):
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001027 # In the case of a subproject, the pinned sha is not necessarily the
1028 # head of the remote branch (so we can't just use --depth=N). Instead,
1029 # we tell git to fetch all the remote objects from SHA..HEAD by means of
1030 # a template git dir which has a 'shallow' file pointing to the sha.
1031 template_dir = tempfile.mkdtemp(
1032 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
1033 dir=parent_dir)
1034 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
1035 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
1036 template_file.write(revision)
1037 clone_cmd.append('--template=' + template_dir)
1038 else:
1039 # Otherwise, we're just interested in the HEAD. Just use --depth.
1040 clone_cmd.append('--depth=1')
1041
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001042 tmp_dir = tempfile.mkdtemp(
1043 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
1044 dir=parent_dir)
1045 try:
1046 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +01001047 if self.print_outbuf:
1048 print_stdout = True
1049 stdout = gclient_utils.WriteToStdout(self.out_fh)
1050 else:
1051 print_stdout = False
1052 stdout = self.out_fh
1053 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
1054 print_stdout=print_stdout, stdout=stdout)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001055 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +00001056 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1057 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001058 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001059 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001060 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001061 finally:
1062 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001063 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001064 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001065 if template_dir:
1066 gclient_utils.rmtree(template_dir)
Edward Lemur579c9862018-07-13 23:17:51 +00001067 self._SetFetchConfig(options)
1068 self._Fetch(options, prune=options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001069 revision = self._AutoFetchRef(options, revision)
1070 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1071 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001072 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001073 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001074 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +00001075 ('Checked out %s to a detached HEAD. Before making any commits\n'
1076 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
1077 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
John Budorick882c91e2018-07-12 22:11:41 +00001078 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001079
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001080 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001081 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001082 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001083 raise gclient_utils.Error("Background task requires input. Rerun "
1084 "gclient with --jobs=1 so that\n"
1085 "interaction is possible.")
1086 try:
1087 return raw_input(prompt)
1088 except KeyboardInterrupt:
1089 # Hide the exception.
1090 sys.exit(1)
1091
1092
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001093 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001094 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001095 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001096 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001097 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001098 revision = upstream
1099 if newbase:
1100 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001101 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001102 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001103 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001104 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001105 printed_path = True
1106 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001107 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001108
1109 if merge:
1110 merge_output = self._Capture(['merge', revision])
1111 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001112 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001113 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001114
1115 # Build the rebase command here using the args
1116 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1117 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001118 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001119 rebase_cmd.append('--verbose')
1120 if newbase:
1121 rebase_cmd.extend(['--onto', newbase])
1122 rebase_cmd.append(upstream)
1123 if branch:
1124 rebase_cmd.append(branch)
1125
1126 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001127 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001128 except subprocess2.CalledProcessError as e:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001129 if (re.match(r'cannot rebase: you have unstaged changes', e.stderr) or
1130 re.match(r'cannot rebase: your index contains uncommitted changes',
1131 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001132 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001133 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001134 'Cannot rebase because of unstaged changes.\n'
1135 '\'git reset --hard HEAD\' ?\n'
1136 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001137 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001138 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001139 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001140 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001141 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001142 break
1143 elif re.match(r'quit|q', rebase_action, re.I):
1144 raise gclient_utils.Error("Please merge or rebase manually\n"
1145 "cd %s && git " % self.checkout_path
1146 + "%s" % ' '.join(rebase_cmd))
1147 elif re.match(r'show|s', rebase_action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001148 self.Print('%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001149 continue
1150 else:
1151 gclient_utils.Error("Input not recognized")
1152 continue
1153 elif re.search(r'^CONFLICT', e.stdout, re.M):
1154 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1155 "Fix the conflict and run gclient again.\n"
1156 "See 'man git-rebase' for details.\n")
1157 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001158 self.Print(e.stdout.strip())
1159 self.Print('Rebase produced error output:\n%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001160 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1161 "manually.\ncd %s && git " %
1162 self.checkout_path
1163 + "%s" % ' '.join(rebase_cmd))
1164
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001165 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001166 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001167 # Make the output a little prettier. It's nice to have some
1168 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001169 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001170
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001171 @staticmethod
1172 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001173 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1174 if not ok:
1175 raise gclient_utils.Error('git version %s < minimum required %s' %
1176 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001177
John Budorick882c91e2018-07-12 22:11:41 +00001178 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001179 # Special case handling if all 3 conditions are met:
1180 # * the mirros have recently changed, but deps destination remains same,
1181 # * the git histories of mirrors are conflicting.
1182 # * git cache is used
1183 # This manifests itself in current checkout having invalid HEAD commit on
1184 # most git operations. Since git cache is used, just deleted the .git
1185 # folder, and re-create it by cloning.
1186 try:
1187 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1188 except subprocess2.CalledProcessError as e:
1189 if ('fatal: bad object HEAD' in e.stderr
1190 and self.cache_dir and self.cache_dir in url):
1191 self.Print((
1192 'Likely due to DEPS change with git cache_dir, '
1193 'the current commit points to no longer existing object.\n'
1194 '%s' % e)
1195 )
1196 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001197 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001198 else:
1199 raise
1200
msb@chromium.org786fb682010-06-02 15:16:23 +00001201 def _IsRebasing(self):
1202 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1203 # have a plumbing command to determine whether a rebase is in progress, so
1204 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1205 g = os.path.join(self.checkout_path, '.git')
1206 return (
1207 os.path.isdir(os.path.join(g, "rebase-merge")) or
1208 os.path.isdir(os.path.join(g, "rebase-apply")))
1209
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001210 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001211 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1212 if os.path.exists(lockfile):
1213 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001214 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001215 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1216 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001217 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001218
msb@chromium.org786fb682010-06-02 15:16:23 +00001219 # Make sure the tree is clean; see git-rebase.sh for reference
1220 try:
1221 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001222 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001223 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001224 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001225 '\tYou have unstaged changes.\n'
1226 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001227 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001228 try:
1229 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001230 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001231 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001232 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001233 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001234 '\tYour index contains uncommitted changes\n'
1235 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001236 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001237
agable83faed02016-10-24 14:37:10 -07001238 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001239 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1240 # reference by a commit). If not, error out -- most likely a rebase is
1241 # in progress, try to detect so we can give a better error.
1242 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001243 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1244 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001245 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001246 # Commit is not contained by any rev. See if the user is rebasing:
1247 if self._IsRebasing():
1248 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001249 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001250 '\tAlready in a conflict, i.e. (no branch).\n'
1251 '\tFix the conflict and run gclient again.\n'
1252 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1253 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001254 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001255 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001256 name = ('saved-by-gclient-' +
1257 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001258 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001259 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001260 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001261
msb@chromium.org5bde4852009-12-14 16:47:12 +00001262 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001263 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001264 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001265 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001266 return None
1267 return branch
1268
borenet@google.comc3e09d22014-04-10 13:58:18 +00001269 def _Capture(self, args, **kwargs):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001270 kwargs.setdefault('cwd', self.checkout_path)
1271 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001272 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001273 env = scm.GIT.ApplyEnvVars(kwargs)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001274 ret = subprocess2.check_output(['git'] + args, env=env, **kwargs)
1275 if strip:
1276 ret = ret.strip()
1277 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001278
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001279 def _Checkout(self, options, ref, force=False, quiet=None):
1280 """Performs a 'git-checkout' operation.
1281
1282 Args:
1283 options: The configured option set
1284 ref: (str) The branch/commit to checkout
1285 quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
1286 'None', the behavior is inferred from 'options.verbose'.
1287 Returns: (str) The output of the checkout operation
1288 """
1289 if quiet is None:
1290 quiet = (not options.verbose)
1291 checkout_args = ['checkout']
1292 if force:
1293 checkout_args.append('--force')
1294 if quiet:
1295 checkout_args.append('--quiet')
1296 checkout_args.append(ref)
1297 return self._Capture(checkout_args)
1298
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001299 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1300 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001301 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001302 # When updating, the ref is modified to be a remote ref .
1303 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1304 # Try to reverse that mapping.
1305 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1306 if original_ref:
1307 refspec = original_ref + ':' + refspec
1308 # When a mirror is configured, it only fetches
1309 # refs/{heads,branch-heads,tags}/*.
1310 # If asked to fetch other refs, we must fetch those directly from the
1311 # repository, and not from the mirror.
1312 if not original_ref.startswith(
1313 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1314 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001315 fetch_cmd = cfg + [
1316 'fetch',
1317 remote or self.remote,
1318 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001319 if refspec:
1320 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001321
1322 if prune:
1323 fetch_cmd.append('--prune')
1324 if options.verbose:
1325 fetch_cmd.append('--verbose')
1326 elif quiet:
1327 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001328 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001329
1330 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1331 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1332
Edward Lemur579c9862018-07-13 23:17:51 +00001333 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001334 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1335 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001336 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001337 try:
1338 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1339 options)
1340 self._Run(['config', 'remote.%s.fetch' % self.remote,
1341 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1342 except subprocess2.CalledProcessError as e:
1343 # If exit code was 5, it means we attempted to unset a config that
1344 # didn't exist. Ignore it.
1345 if e.returncode != 5:
1346 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001347 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001348 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001349 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1350 '^\\+refs/branch-heads/\\*:.*$']
1351 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001352 if hasattr(options, 'with_tags') and options.with_tags:
1353 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1354 '+refs/tags/*:refs/tags/*',
1355 '^\\+refs/tags/\\*:.*$']
1356 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001357
John Budorick882c91e2018-07-12 22:11:41 +00001358 def _AutoFetchRef(self, options, revision):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001359 """Attempts to fetch |revision| if not available in local repo.
1360
1361 Returns possibly updated revision."""
John Budorick882c91e2018-07-12 22:11:41 +00001362 try:
1363 self._Capture(['rev-parse', revision])
1364 except subprocess2.CalledProcessError:
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001365 self._Fetch(options, refspec=revision)
1366 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1367 return revision
1368
dnj@chromium.org680f2172014-06-25 00:39:32 +00001369 def _Run(self, args, options, show_header=True, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001370 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001371 kwargs.setdefault('cwd', self.checkout_path)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001372 kwargs.setdefault('stdout', self.out_fh)
szager@chromium.org848fd492014-04-09 19:06:44 +00001373 kwargs['filter_fn'] = self.filter
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001374 kwargs.setdefault('print_stdout', False)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001375 env = scm.GIT.ApplyEnvVars(kwargs)
agable@chromium.org772efaf2014-04-01 02:35:44 +00001376 cmd = ['git'] + args
dnj@chromium.org680f2172014-06-25 00:39:32 +00001377 if show_header:
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001378 gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs)
1379 else:
1380 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001381
1382
1383class CipdPackage(object):
1384 """A representation of a single CIPD package."""
1385
John Budorickd3ba72b2018-03-20 12:27:42 -07001386 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001387 self._authority_for_subdir = authority_for_subdir
1388 self._name = name
1389 self._version = version
1390
1391 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001392 def authority_for_subdir(self):
1393 """Whether this package has authority to act on behalf of its subdir.
1394
1395 Some operations should only be performed once per subdirectory. A package
1396 that has authority for its subdirectory is the only package that should
1397 perform such operations.
1398
1399 Returns:
1400 bool; whether this package has subdir authority.
1401 """
1402 return self._authority_for_subdir
1403
1404 @property
1405 def name(self):
1406 return self._name
1407
1408 @property
1409 def version(self):
1410 return self._version
1411
1412
1413class CipdRoot(object):
1414 """A representation of a single CIPD root."""
1415 def __init__(self, root_dir, service_url):
1416 self._all_packages = set()
1417 self._mutator_lock = threading.Lock()
1418 self._packages_by_subdir = collections.defaultdict(list)
1419 self._root_dir = root_dir
1420 self._service_url = service_url
1421
1422 def add_package(self, subdir, package, version):
1423 """Adds a package to this CIPD root.
1424
1425 As far as clients are concerned, this grants both root and subdir authority
1426 to packages arbitrarily. (The implementation grants root authority to the
1427 first package added and subdir authority to the first package added for that
1428 subdir, but clients should not depend on or expect that behavior.)
1429
1430 Args:
1431 subdir: str; relative path to where the package should be installed from
1432 the cipd root directory.
1433 package: str; the cipd package name.
1434 version: str; the cipd package version.
1435 Returns:
1436 CipdPackage; the package that was created and added to this root.
1437 """
1438 with self._mutator_lock:
1439 cipd_package = CipdPackage(
1440 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001441 not self._packages_by_subdir[subdir])
1442 self._all_packages.add(cipd_package)
1443 self._packages_by_subdir[subdir].append(cipd_package)
1444 return cipd_package
1445
1446 def packages(self, subdir):
1447 """Get the list of configured packages for the given subdir."""
1448 return list(self._packages_by_subdir[subdir])
1449
1450 def clobber(self):
1451 """Remove the .cipd directory.
1452
1453 This is useful for forcing ensure to redownload and reinitialize all
1454 packages.
1455 """
1456 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001457 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001458 try:
1459 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1460 except OSError:
1461 if os.path.exists(cipd_cache_dir):
1462 raise
1463
1464 @contextlib.contextmanager
1465 def _create_ensure_file(self):
1466 try:
1467 ensure_file = None
1468 with tempfile.NamedTemporaryFile(
Raul Tambreb946b232019-03-26 14:48:46 +00001469 suffix='.ensure', delete=False, mode='w') as ensure_file:
John Budorick302bb842018-07-17 23:49:17 +00001470 ensure_file.write('$ParanoidMode CheckPresence\n\n')
Raul Tambreb946b232019-03-26 14:48:46 +00001471 for subdir, packages in sorted(self._packages_by_subdir.items()):
John Budorick0f7b2002018-01-19 15:46:17 -08001472 ensure_file.write('@Subdir %s\n' % subdir)
Edward Lemurfbb06aa2018-06-11 20:43:06 +00001473 for package in sorted(packages, key=lambda p: p.name):
John Budorick0f7b2002018-01-19 15:46:17 -08001474 ensure_file.write('%s %s\n' % (package.name, package.version))
1475 ensure_file.write('\n')
1476 yield ensure_file.name
1477 finally:
1478 if ensure_file is not None and os.path.exists(ensure_file.name):
1479 os.remove(ensure_file.name)
1480
1481 def ensure(self):
1482 """Run `cipd ensure`."""
1483 with self._mutator_lock:
1484 with self._create_ensure_file() as ensure_file:
1485 cmd = [
1486 'cipd', 'ensure',
1487 '-log-level', 'error',
1488 '-root', self.root_dir,
1489 '-ensure-file', ensure_file,
1490 ]
1491 gclient_utils.CheckCallAndFilterAndHeader(cmd)
1492
John Budorickd3ba72b2018-03-20 12:27:42 -07001493 def run(self, command):
1494 if command == 'update':
1495 self.ensure()
1496 elif command == 'revert':
1497 self.clobber()
1498 self.ensure()
1499
John Budorick0f7b2002018-01-19 15:46:17 -08001500 def created_package(self, package):
1501 """Checks whether this root created the given package.
1502
1503 Args:
1504 package: CipdPackage; the package to check.
1505 Returns:
1506 bool; whether this root created the given package.
1507 """
1508 return package in self._all_packages
1509
1510 @property
1511 def root_dir(self):
1512 return self._root_dir
1513
1514 @property
1515 def service_url(self):
1516 return self._service_url
1517
1518
1519class CipdWrapper(SCMWrapper):
1520 """Wrapper for CIPD.
1521
1522 Currently only supports chrome-infra-packages.appspot.com.
1523 """
John Budorick3929e9e2018-02-04 18:18:07 -08001524 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001525
1526 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1527 out_cb=None, root=None, package=None):
1528 super(CipdWrapper, self).__init__(
1529 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1530 out_cb=out_cb)
1531 assert root.created_package(package)
1532 self._package = package
1533 self._root = root
1534
1535 #override
1536 def GetCacheMirror(self):
1537 return None
1538
1539 #override
1540 def GetActualRemoteURL(self, options):
1541 return self._root.service_url
1542
1543 #override
1544 def DoesRemoteURLMatch(self, options):
1545 del options
1546 return True
1547
1548 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001549 """Does nothing.
1550
1551 CIPD packages should be reverted at the root by running
1552 `CipdRoot.run('revert')`.
1553 """
1554 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001555
1556 def diff(self, options, args, file_list):
1557 """CIPD has no notion of diffing."""
1558 pass
1559
1560 def pack(self, options, args, file_list):
1561 """CIPD has no notion of diffing."""
1562 pass
1563
1564 def revinfo(self, options, args, file_list):
1565 """Grab the instance ID."""
1566 try:
1567 tmpdir = tempfile.mkdtemp()
1568 describe_json_path = os.path.join(tmpdir, 'describe.json')
1569 cmd = [
1570 'cipd', 'describe',
1571 self._package.name,
1572 '-log-level', 'error',
1573 '-version', self._package.version,
1574 '-json-output', describe_json_path
1575 ]
1576 gclient_utils.CheckCallAndFilter(
1577 cmd, filter_fn=lambda _line: None, print_stdout=False)
1578 with open(describe_json_path) as f:
1579 describe_json = json.load(f)
1580 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1581 finally:
1582 gclient_utils.rmtree(tmpdir)
1583
1584 def status(self, options, args, file_list):
1585 pass
1586
1587 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001588 """Does nothing.
1589
1590 CIPD packages should be updated at the root by running
1591 `CipdRoot.run('update')`.
1592 """
1593 pass