blob: 51d86279d6e352b231c3cd544c6c7fa61d9a7830 [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(
Raul Tambrecd862e32019-05-10 21:19:00 +0000259 # Filter to remove base if it is None.
260 list(filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only',
261 base])
262 )).split()
Aaron Gablef4068aa2017-12-12 15:14:09 -0800263
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000264 def diff(self, options, _args, _file_list):
Aaron Gable1853f662018-02-12 15:45:56 -0800265 _, revision = gclient_utils.SplitUrlRevision(self.url)
266 if not revision:
267 revision = 'refs/remotes/%s/master' % self.remote
268 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000269
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000270 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000271 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000272 repository.
273
274 The patch file is generated from a diff of the merge base of HEAD and
275 its upstream branch.
276 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700277 try:
278 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
279 except subprocess2.CalledProcessError:
280 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000281 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700282 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000283 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000284 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000285
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800286 def _Scrub(self, target, options):
287 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000288 quiet = []
289 if not options.verbose:
290 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800291 self._Run(['reset', '--hard', target] + quiet, options)
292 if options.force and options.delete_unversioned_trees:
293 # where `target` is a commit that contains both upper and lower case
294 # versions of the same file on a case insensitive filesystem, we are
295 # actually in a broken state here. The index will have both 'a' and 'A',
296 # but only one of them will exist on the disk. To progress, we delete
297 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800298 output = self._Capture([
299 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800300 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800301 # --porcelain (v1) looks like:
302 # XY filename
303 try:
304 filename = line[3:]
305 self.Print('_____ Deleting residual after reset: %r.' % filename)
306 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800307 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800308 except OSError:
309 pass
310
John Budorick882c91e2018-07-12 22:11:41 +0000311 def _FetchAndReset(self, revision, file_list, options):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800312 """Equivalent to git fetch; git reset."""
Edward Lemur579c9862018-07-13 23:17:51 +0000313 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000314
dnj@chromium.org680f2172014-06-25 00:39:32 +0000315 self._Fetch(options, prune=True, quiet=options.verbose)
John Budorick882c91e2018-07-12 22:11:41 +0000316 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000317 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800318 files = self._Capture(
319 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambreb946b232019-03-26 14:48:46 +0000320 file_list.extend(
321 [os.path.join(self.checkout_path, f.decode()) for f in files])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000322
szager@chromium.org8a139702014-06-20 15:55:01 +0000323 def _DisableHooks(self):
324 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
325 if not os.path.isdir(hook_dir):
326 return
327 for f in os.listdir(hook_dir):
328 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000329 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
330 if os.path.exists(disabled_hook_path):
331 os.remove(disabled_hook_path)
332 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000333
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000334 def _maybe_break_locks(self, options):
335 """This removes all .lock files from this repo's .git directory, if the
336 user passed the --break_repo_locks command line flag.
337
338 In particular, this will cleanup index.lock files, as well as ref lock
339 files.
340 """
341 if options.break_repo_locks:
342 git_dir = os.path.join(self.checkout_path, '.git')
343 for path, _, filenames in os.walk(git_dir):
344 for filename in filenames:
345 if filename.endswith('.lock'):
346 to_break = os.path.join(path, filename)
347 self.Print('breaking lock: %s' % (to_break,))
348 try:
349 os.remove(to_break)
350 except OSError as ex:
351 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
352 raise
353
Edward Lemur8c665652019-05-08 20:23:33 +0000354 def apply_patch_ref(self, patch_repo, patch_ref, target_ref, options,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000355 file_list):
356 """Apply a patch on top of the revision we're synced at.
357
358 The patch ref is given by |patch_repo|@|patch_ref|, and the current revision
359 is |base_rev|.
Edward Lemur8c665652019-05-08 20:23:33 +0000360 We also need the |target_ref| that the patch was uploaded against. We use
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000361 it to find a merge base between |patch_rev| and |base_rev|, so we can find
362 what commits constitute the patch:
363
364 Graphically, it looks like this:
365
Edward Lemur8c665652019-05-08 20:23:33 +0000366 ... -> merge_base -> [possibly already landed commits] -> target_ref
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000367 \
368 -> [possibly not yet landed dependent CLs] -> patch_rev
369
370 Next, we apply the commits |merge_base..patch_rev| on top of whatever is
371 currently checked out, denoted |base_rev|. Typically, it'd be a revision
Edward Lemur8c665652019-05-08 20:23:33 +0000372 from |target_ref|, but this is not required.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000373
374 Graphically, we cherry pick |merge_base..patch_rev| on top of |base_rev|:
375
376 ... -> base_rev -> [possibly not yet landed dependent CLs] -> patch_rev
377
378 After application, if |options.reset_patch_ref| is specified, we soft reset
379 the just cherry-picked changes, keeping them in git index only.
380
381 Args:
382 patch_repo: The patch origin. e.g. 'https://foo.googlesource.com/bar'
383 patch_ref: The ref to the patch. e.g. 'refs/changes/1234/34/1'.
Edward Lemur8c665652019-05-08 20:23:33 +0000384 target_ref: The ref the patch was uploaded against.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000385 e.g. 'refs/heads/master' or 'refs/heads/infra/config'.
386 options: The options passed to gclient.
387 file_list: A list where modified files will be appended.
388 """
389
Edward Lemurca7d8812018-07-24 17:42:45 +0000390 # Abort any cherry-picks in progress.
391 try:
392 self._Capture(['cherry-pick', '--abort'])
393 except subprocess2.CalledProcessError:
394 pass
395
Edward Lesmesc621b212018-03-21 20:26:56 -0400396 base_rev = self._Capture(['rev-parse', 'HEAD'])
Edward Lemura0ffbe42019-05-01 16:52:18 +0000397
Edward Lemur1d6478a2019-05-21 23:40:01 +0000398 if (not target_ref
399 or not target_ref.startswith(('refs/heads/', 'refs/branch-heads/'))):
Edward Lemur8c665652019-05-08 20:23:33 +0000400 # TODO(ehmaldonado): Support all refs.
401 raise gclient_utils.Error(
Edward Lemur1d6478a2019-05-21 23:40:01 +0000402 'target_ref must be given and must be in refs/heads/** or '
403 'refs/branch-heads/**. Got %s instead.' % target_ref)
Edward Lemur8c665652019-05-08 20:23:33 +0000404 elif target_ref == 'refs/heads/master':
405 # We handle refs/heads/master separately because bot_update treats it
406 # differently than other refs: it will fetch refs/heads/foo to
407 # refs/heads/foo, but refs/heads/master to refs/remotes/origin/master.
408 # It's not strictly necessary, but it simplifies the rest of the code.
409 target_ref = 'refs/remotes/%s/master' % self.remote
410 elif scm.GIT.IsValidRevision(self.checkout_path, target_ref):
411 # The target ref for the change is already available, so we don't need to
412 # do anything.
413 # This is a common case. When applying a patch to a top-level solution,
414 # bot_update will fetch the target ref for the change and sync the
415 # solution to it.
416 pass
Edward Lemura0ffbe42019-05-01 16:52:18 +0000417 else:
Edward Lemur8c665652019-05-08 20:23:33 +0000418 # The target ref is not available. We check next for a remote version of
419 # it (e.g. refs/remotes/origin/<branch>) and fetch it if it's not
420 # available.
421 self.Print('%s is not an existing ref.' % target_ref)
422 original_target_ref = target_ref
423 target_ref = ''.join(scm.GIT.RefToRemoteRef(target_ref, self.remote))
424 self.Print('Trying with %s' % target_ref)
425 if not scm.GIT.IsValidRevision(self.checkout_path, target_ref):
426 self.Print(
427 '%s is not an existing ref either. Will proceed to fetch it.'
428 % target_ref)
429 url, _ = gclient_utils.SplitUrlRevision(self.url)
430 mirror = self._GetMirror(url, options, target_ref)
431 if mirror:
432 self._UpdateMirrorIfNotContains(
433 mirror, options, 'branch', target_ref, original_target_ref)
434 self._Fetch(options, refspec=target_ref)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000435
Edward Lesmesc621b212018-03-21 20:26:56 -0400436 self.Print('===Applying patch ref===')
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000437 self.Print('Patch ref is %r @ %r. Target branch for patch is %r. '
438 'Current HEAD is %r. Current dir is %r' % (
Edward Lemur8c665652019-05-08 20:23:33 +0000439 patch_repo, patch_ref, target_ref, base_rev,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000440 self.checkout_path))
Edward Lesmesc621b212018-03-21 20:26:56 -0400441 self._Capture(['reset', '--hard'])
442 self._Capture(['fetch', patch_repo, patch_ref])
Edward Lemurca7d8812018-07-24 17:42:45 +0000443 patch_rev = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400444
Edward Lemurca7d8812018-07-24 17:42:45 +0000445 try:
446 if not options.rebase_patch_ref:
447 self._Capture(['checkout', patch_rev])
448 else:
449 # Find the merge-base between the branch_rev and patch_rev to find out
450 # the changes we need to cherry-pick on top of base_rev.
Edward Lemur8c665652019-05-08 20:23:33 +0000451 merge_base = self._Capture(['merge-base', target_ref, patch_rev])
Edward Lemurca7d8812018-07-24 17:42:45 +0000452 self.Print('Merge base of %s and %s is %s' % (
Edward Lemur8c665652019-05-08 20:23:33 +0000453 target_ref, patch_rev, merge_base))
Edward Lemurca7d8812018-07-24 17:42:45 +0000454 if merge_base == patch_rev:
455 # If the merge-base is patch_rev, it means patch_rev is already part
456 # of the history, so just check it out.
457 self._Capture(['checkout', patch_rev])
458 else:
459 # If a change was uploaded on top of another change, which has already
460 # landed, one of the commits in the cherry-pick range will be
461 # redundant, since it has already landed and its changes incorporated
462 # in the tree.
463 # We pass '--keep-redundant-commits' to ignore those changes.
464 self._Capture(['cherry-pick', merge_base + '..' + patch_rev,
465 '--keep-redundant-commits'])
466
467 if file_list is not None:
468 file_list.extend(self._GetDiffFilenames(base_rev))
469
470 except subprocess2.CalledProcessError as e:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000471 self.Print('Failed to apply patch.')
Edward Lemur8c665652019-05-08 20:23:33 +0000472 self.Print('Patch ref is %r @ %r. Target ref for patch is %r. '
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000473 'Current HEAD is %r. Current dir is %r' % (
Edward Lemur8c665652019-05-08 20:23:33 +0000474 patch_repo, patch_ref, target_ref, base_rev,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000475 self.checkout_path))
Edward Lemurca7d8812018-07-24 17:42:45 +0000476 self.Print('git returned non-zero exit status %s:\n%s' % (
477 e.returncode, e.stderr))
478 # Print the current status so that developers know what changes caused the
479 # patch failure, since git cherry-pick doesn't show that information.
480 self.Print(self._Capture(['status']))
John Budorick2c984a02018-07-18 23:24:13 +0000481 try:
Edward Lemurca7d8812018-07-24 17:42:45 +0000482 self._Capture(['cherry-pick', '--abort'])
483 except subprocess2.CalledProcessError:
484 pass
485 raise
486
Edward Lesmesc621b212018-03-21 20:26:56 -0400487 if options.reset_patch_ref:
488 self._Capture(['reset', '--soft', base_rev])
489
msb@chromium.orge28e4982009-09-25 20:51:45 +0000490 def update(self, options, args, file_list):
491 """Runs git to update or transparently checkout the working copy.
492
493 All updated files will be appended to file_list.
494
495 Raises:
496 Error: if can't get URL for relative path.
497 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000498 if args:
499 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
500
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000501 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000502
John Budorick882c91e2018-07-12 22:11:41 +0000503 # If a dependency is not pinned, track the default remote branch.
504 default_rev = 'refs/remotes/%s/master' % self.remote
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000505 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000506 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000507 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000508 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000509 # Override the revision number.
510 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000511 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000512 # Check again for a revision in case an initial ref was specified
513 # in the url, for example bla.git@refs/heads/custombranch
514 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000515 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000516 if not revision:
517 revision = default_rev
msb@chromium.orge28e4982009-09-25 20:51:45 +0000518
szager@chromium.org8a139702014-06-20 15:55:01 +0000519 if managed:
520 self._DisableHooks()
521
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000522 printed_path = False
523 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000524 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700525 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000526 verbose = ['--verbose']
527 printed_path = True
528
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000529 revision_ref = revision
530 if ':' in revision:
531 revision_ref, _, revision = revision.partition(':')
532
533 mirror = self._GetMirror(url, options, revision_ref)
534 if mirror:
535 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000536
John Budorick882c91e2018-07-12 22:11:41 +0000537 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
538 if remote_ref:
539 # Rewrite remote refs to their local equivalents.
540 revision = ''.join(remote_ref)
541 rev_type = "branch"
542 elif revision.startswith('refs/'):
543 # Local branch? We probably don't want to support, since DEPS should
544 # always specify branches as they are in the upstream repo.
545 rev_type = "branch"
546 else:
547 # hash is also a tag, only make a distinction at checkout
548 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000549
primiano@chromium.org1c127382015-02-17 11:15:40 +0000550 # If we are going to introduce a new project, there is a possibility that
551 # we are syncing back to a state where the project was originally a
552 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
553 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
554 # In such case, we might have a backup of the former .git folder, which can
555 # be used to avoid re-fetching the entire repo again (useful for bisects).
556 backup_dir = self.GetGitBackupDirPath()
557 target_dir = os.path.join(self.checkout_path, '.git')
558 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
559 gclient_utils.safe_makedirs(self.checkout_path)
560 os.rename(backup_dir, target_dir)
561 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800562 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000563
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000564 if (not os.path.exists(self.checkout_path) or
565 (os.path.isdir(self.checkout_path) and
566 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000567 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000568 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000569 try:
John Budorick882c91e2018-07-12 22:11:41 +0000570 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000571 except subprocess2.CalledProcessError:
572 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000573 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000574 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800575 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000576 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000577 file_list.extend(
578 [os.path.join(self.checkout_path, f.decode()) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000579 if mirror:
580 self._Capture(
581 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000582 if not verbose:
583 # Make the output a little prettier. It's nice to have some whitespace
584 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000585 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000586 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000587
John Budorick21a51b32018-09-19 19:39:20 +0000588 if mirror:
589 self._Capture(
590 ['remote', 'set-url', '--push', 'origin', mirror.url])
591
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000592 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000593 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000594 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
595 return self._Capture(['rev-parse', '--verify', 'HEAD'])
596
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000597 self._maybe_break_locks(options)
598
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000599 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000600 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000601
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000602 # See if the url has changed (the unittests use git://foo for the url, let
603 # that through).
604 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
605 return_early = False
606 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
607 # unit test pass. (and update the comment above)
608 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
609 # This allows devs to use experimental repos which have a different url
610 # but whose branch(s) are the same as official repos.
Raul Tambrecd862e32019-05-10 21:19:00 +0000611 if (current_url.rstrip('/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000612 subprocess2.capture(
Aaron Gableac9b0f32019-04-18 17:38:37 +0000613 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000614 cwd=self.checkout_path).strip() != 'False'):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000615 self.Print('_____ switching %s to a new upstream' % self.relpath)
iannucci@chromium.org78514212014-08-20 23:08:00 +0000616 if not (options.force or options.reset):
617 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700618 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000619 # Switch over to the new upstream
620 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000621 if mirror:
622 with open(os.path.join(
623 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
624 'w') as fh:
625 fh.write(os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000626 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
627 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000628
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000629 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000630 else:
John Budorick882c91e2018-07-12 22:11:41 +0000631 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000632
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000633 if return_early:
634 return self._Capture(['rev-parse', '--verify', 'HEAD'])
635
msb@chromium.org5bde4852009-12-14 16:47:12 +0000636 cur_branch = self._GetCurrentBranch()
637
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000638 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000639 # 0) HEAD is detached. Probably from our initial clone.
640 # - make sure HEAD is contained by a named ref, then update.
641 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700642 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000643 # - try to rebase onto the new hash or branch
644 # 2) current branch is tracking a remote branch with local committed
645 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000646 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000647 # 3) current branch is tracking a remote branch w/or w/out changes, and
648 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000649 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000650 # 4) current branch is tracking a remote branch, but DEPS switches to a
651 # different remote branch, and
652 # a) current branch has no local changes, and --force:
653 # - checkout new branch
654 # b) current branch has local changes, and --force and --reset:
655 # - checkout new branch
656 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000657
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000658 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
659 # a tracking branch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000660 # or 'master' if not a tracking branch (it's based on a specific rev/hash)
661 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000662 if cur_branch is None:
663 upstream_branch = None
664 current_type = "detached"
665 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000666 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000667 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
668 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
669 current_type = "hash"
670 logging.debug("Current branch is not tracking an upstream (remote)"
671 " branch.")
672 elif upstream_branch.startswith('refs/remotes'):
673 current_type = "branch"
674 else:
675 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000676
Edward Lemur579c9862018-07-13 23:17:51 +0000677 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000678
Michael Spang73fac912019-03-08 18:44:19 +0000679 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000680 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000681 self._Fetch(options, prune=options.force)
682
683 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
684 sha_only=True):
685 # Update the remotes first so we have all the refs.
686 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
687 cwd=self.checkout_path)
688 if verbose:
689 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000690
John Budorick882c91e2018-07-12 22:11:41 +0000691 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200692
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000693 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000694 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000695 target = 'HEAD'
696 if options.upstream and upstream_branch:
697 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800698 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000699
msb@chromium.org786fb682010-06-02 15:16:23 +0000700 if current_type == 'detached':
701 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800702 # We just did a Scrub, this is as clean as it's going to get. In
703 # particular if HEAD is a commit that contains two versions of the same
704 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
705 # to actually "Clean" the checkout; that commit is uncheckoutable on this
706 # system. The best we can do is carry forward to the checkout step.
707 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000708 self._CheckClean(revision)
709 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000710 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000711 self.Print('Up-to-date; skipping checkout.')
712 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000713 # 'git checkout' may need to overwrite existing untracked files. Allow
714 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000715 self._Checkout(
716 options,
John Budorick882c91e2018-07-12 22:11:41 +0000717 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000718 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000719 quiet=True,
720 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000721 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000722 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000723 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000724 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700725 # Can't find a merge-base since we don't know our upstream. That makes
726 # this command VERY likely to produce a rebase failure. For now we
727 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000728 upstream_branch = self.remote
729 if options.revision or deps_revision:
730 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700731 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700732 printed_path=printed_path, merge=options.merge)
733 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000734 elif rev_type == 'hash':
735 # case 2
736 self._AttemptRebase(upstream_branch, file_list, options,
737 newbase=revision, printed_path=printed_path,
738 merge=options.merge)
739 printed_path = True
740 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000741 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000742 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000743 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000744 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000745 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000746 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000747 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000748 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
749 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000750 force_switch = False
751 if options.force:
752 try:
John Budorick882c91e2018-07-12 22:11:41 +0000753 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000754 # case 4a
755 force_switch = True
756 except gclient_utils.Error as e:
757 if options.reset:
758 # case 4b
759 force_switch = True
760 else:
761 switch_error = '%s\n%s' % (e.message, switch_error)
762 if force_switch:
763 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000764 (upstream_branch, new_base))
765 switch_branch = 'gclient_' + remote_ref[1]
766 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000767 self._Checkout(options, switch_branch, force=True, quiet=True)
768 else:
769 # case 4c
770 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000771 else:
772 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800773 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000774 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000775 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000776 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000777 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000778 if options.merge:
779 merge_args.append('--ff')
780 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000781 merge_args.append('--ff-only')
782 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000783 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000784 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700785 rebase_files = []
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000786 if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr):
787 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000788 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700789 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000790 printed_path = True
791 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000792 if not options.auto_rebase:
793 try:
794 action = self._AskForData(
795 'Cannot %s, attempt to rebase? '
796 '(y)es / (q)uit / (s)kip : ' %
797 ('merge' if options.merge else 'fast-forward merge'),
798 options)
799 except ValueError:
800 raise gclient_utils.Error('Invalid Character')
801 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700802 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000803 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000804 printed_path = True
805 break
806 elif re.match(r'quit|q', action, re.I):
807 raise gclient_utils.Error("Can't fast-forward, please merge or "
808 "rebase manually.\n"
809 "cd %s && git " % self.checkout_path
810 + "rebase %s" % upstream_branch)
811 elif re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000812 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000813 return
814 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000815 self.Print('Input not recognized')
smut@google.com27c9c8a2014-09-11 19:57:55 +0000816 elif re.match("error: Your local changes to '.*' would be "
817 "overwritten by merge. Aborting.\nPlease, commit your "
818 "changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000819 e.stderr):
820 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000821 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700822 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000823 printed_path = True
824 raise gclient_utils.Error(e.stderr)
825 else:
826 # Some other problem happened with the merge
827 logging.error("Error during fast-forward merge in %s!" % self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000828 self.Print(e.stderr)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000829 raise
830 else:
831 # Fast-forward merge was successful
832 if not re.match('Already up-to-date.', merge_output) or verbose:
833 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700834 self.Print('_____ %s at %s' % (self.relpath, revision),
835 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000836 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000837 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000838 if not verbose:
839 # Make the output a little prettier. It's nice to have some
840 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000841 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000842
agablec3937b92016-10-25 10:13:03 -0700843 if file_list is not None:
844 file_list.extend(
845 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000846
847 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000848 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700849 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000850 '\nConflict while rebasing this branch.\n'
851 'Fix the conflict and run gclient again.\n'
852 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700853 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000854
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000855 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000856 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
857 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000858
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000859 # If --reset and --delete_unversioned_trees are specified, remove any
860 # untracked directories.
861 if options.reset and options.delete_unversioned_trees:
862 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
863 # merge-base by default), so doesn't include untracked files. So we use
864 # 'git ls-files --directory --others --exclude-standard' here directly.
865 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800866 ['-c', 'core.quotePath=false', 'ls-files',
867 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000868 self.checkout_path)
869 for path in (p for p in paths.splitlines() if p.endswith('/')):
870 full_path = os.path.join(self.checkout_path, path)
871 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000872 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000873 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000874
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000875 return self._Capture(['rev-parse', '--verify', 'HEAD'])
876
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000877 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000878 """Reverts local modifications.
879
880 All reverted files will be appended to file_list.
881 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000882 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000883 # revert won't work if the directory doesn't exist. It needs to
884 # checkout instead.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000885 self.Print('_____ %s is missing, synching instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000886 # Don't reuse the args.
887 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000888
889 default_rev = "refs/heads/master"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000890 if options.upstream:
891 if self._GetCurrentBranch():
892 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
893 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000894 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000895 if not deps_revision:
896 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000897 if deps_revision.startswith('refs/heads/'):
898 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700899 try:
900 deps_revision = self.GetUsableRev(deps_revision, options)
901 except NoUsableRevError as e:
902 # If the DEPS entry's url and hash changed, try to update the origin.
903 # See also http://crbug.com/520067.
904 logging.warn(
905 'Couldn\'t find usable revision, will retrying to update instead: %s',
906 e.message)
907 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000908
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000909 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800910 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000911
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800912 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000913 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000914
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000915 if file_list is not None:
916 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
917
918 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000919 """Returns revision"""
920 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000921
msb@chromium.orge28e4982009-09-25 20:51:45 +0000922 def runhooks(self, options, args, file_list):
923 self.status(options, args, file_list)
924
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000925 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000926 """Display status information."""
927 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000928 self.Print('________ couldn\'t run status in %s:\n'
929 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000930 else:
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700931 try:
932 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
933 except subprocess2.CalledProcessError:
934 merge_base = []
Aaron Gablef4068aa2017-12-12 15:14:09 -0800935 self._Run(
936 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
937 options, stdout=self.out_fh, always=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000938 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800939 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000940 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000941
smutae7ea312016-07-18 11:59:41 -0700942 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700943 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700944 sha1 = None
945 if not os.path.isdir(self.checkout_path):
946 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800947 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700948
949 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
950 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700951 else:
agable41e3a6c2016-10-20 11:36:56 -0700952 # May exist in origin, but we don't have it yet, so fetch and look
953 # again.
954 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -0700955 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
956 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700957
958 if not sha1:
959 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800960 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -0700961
962 return sha1
963
primiano@chromium.org1c127382015-02-17 11:15:40 +0000964 def GetGitBackupDirPath(self):
965 """Returns the path where the .git folder for the current project can be
966 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
967 return os.path.join(self._root_dir,
968 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
969
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000970 def _GetMirror(self, url, options, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000971 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +0000972 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000973 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000974 mirror_kwargs = {
975 'print_func': self.filter,
John Budorick882c91e2018-07-12 22:11:41 +0000976 'refs': []
hinoka@google.comb1b54572014-04-16 22:29:23 +0000977 }
hinoka@google.comb1b54572014-04-16 22:29:23 +0000978 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
979 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000980 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
981 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +0000982 if hasattr(options, 'with_tags') and options.with_tags:
983 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000984 elif revision_ref and revision_ref.startswith('refs/tags/'):
985 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000986 return git_cache.Mirror(url, **mirror_kwargs)
987
John Budorick882c91e2018-07-12 22:11:41 +0000988 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800989 """Update a git mirror by fetching the latest commits from the remote,
990 unless mirror already contains revision whose type is sha1 hash.
991 """
John Budorick882c91e2018-07-12 22:11:41 +0000992 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800993 if options.verbose:
994 self.Print('skipping mirror update, it has rev=%s already' % revision,
995 timestamp=False)
996 return
997
szager@chromium.org3ec84f62014-08-22 21:00:22 +0000998 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000999 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001000 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +00001001 depth = 10
1002 else:
1003 depth = 10000
1004 else:
1005 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +00001006 mirror.populate(verbose=options.verbose,
1007 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +00001008 depth=depth,
1009 ignore_lock=getattr(options, 'ignore_locks', False),
1010 lock_timeout=getattr(options, 'lock_timeout', 0))
1011 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001012
John Budorick882c91e2018-07-12 22:11:41 +00001013 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001014 """Clone a git repository from the given URL.
1015
msb@chromium.org786fb682010-06-02 15:16:23 +00001016 Once we've cloned the repo, we checkout a working branch if the specified
1017 revision is a branch head. If it is a tag or a specific commit, then we
1018 leave HEAD detached as it makes future updates simpler -- in this case the
1019 user should first create a new branch or switch to an existing branch before
1020 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001021 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001022 # git clone doesn't seem to insert a newline properly before printing
1023 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001024 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +00001025 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +00001026 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001027 if self.cache_dir:
1028 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001029 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001030 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001031 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001032 # If the parent directory does not exist, Git clone on Windows will not
1033 # create it, so we need to do it manually.
1034 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001035 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001036
1037 template_dir = None
1038 if hasattr(options, 'no_history') and options.no_history:
John Budorick882c91e2018-07-12 22:11:41 +00001039 if gclient_utils.IsGitSha(revision):
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001040 # In the case of a subproject, the pinned sha is not necessarily the
1041 # head of the remote branch (so we can't just use --depth=N). Instead,
1042 # we tell git to fetch all the remote objects from SHA..HEAD by means of
1043 # a template git dir which has a 'shallow' file pointing to the sha.
1044 template_dir = tempfile.mkdtemp(
1045 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
1046 dir=parent_dir)
1047 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
1048 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
1049 template_file.write(revision)
1050 clone_cmd.append('--template=' + template_dir)
1051 else:
1052 # Otherwise, we're just interested in the HEAD. Just use --depth.
1053 clone_cmd.append('--depth=1')
1054
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001055 tmp_dir = tempfile.mkdtemp(
1056 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
1057 dir=parent_dir)
1058 try:
1059 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +01001060 if self.print_outbuf:
1061 print_stdout = True
1062 stdout = gclient_utils.WriteToStdout(self.out_fh)
1063 else:
1064 print_stdout = False
1065 stdout = self.out_fh
1066 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
1067 print_stdout=print_stdout, stdout=stdout)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001068 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +00001069 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1070 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001071 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001072 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001073 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001074 finally:
1075 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001076 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001077 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001078 if template_dir:
1079 gclient_utils.rmtree(template_dir)
Edward Lemur579c9862018-07-13 23:17:51 +00001080 self._SetFetchConfig(options)
1081 self._Fetch(options, prune=options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001082 revision = self._AutoFetchRef(options, revision)
1083 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1084 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001085 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001086 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001087 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +00001088 ('Checked out %s to a detached HEAD. Before making any commits\n'
1089 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
1090 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
John Budorick882c91e2018-07-12 22:11:41 +00001091 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001092
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001093 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001094 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001095 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001096 raise gclient_utils.Error("Background task requires input. Rerun "
1097 "gclient with --jobs=1 so that\n"
1098 "interaction is possible.")
1099 try:
1100 return raw_input(prompt)
1101 except KeyboardInterrupt:
1102 # Hide the exception.
1103 sys.exit(1)
1104
1105
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001106 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001107 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001108 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001109 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001110 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001111 revision = upstream
1112 if newbase:
1113 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001114 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001115 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001116 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001117 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001118 printed_path = True
1119 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001120 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001121
1122 if merge:
1123 merge_output = self._Capture(['merge', revision])
1124 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001125 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001126 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001127
1128 # Build the rebase command here using the args
1129 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1130 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001131 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001132 rebase_cmd.append('--verbose')
1133 if newbase:
1134 rebase_cmd.extend(['--onto', newbase])
1135 rebase_cmd.append(upstream)
1136 if branch:
1137 rebase_cmd.append(branch)
1138
1139 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001140 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001141 except subprocess2.CalledProcessError as e:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001142 if (re.match(r'cannot rebase: you have unstaged changes', e.stderr) or
1143 re.match(r'cannot rebase: your index contains uncommitted changes',
1144 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001145 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001146 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001147 'Cannot rebase because of unstaged changes.\n'
1148 '\'git reset --hard HEAD\' ?\n'
1149 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001150 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001151 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001152 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001153 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001154 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001155 break
1156 elif re.match(r'quit|q', rebase_action, re.I):
1157 raise gclient_utils.Error("Please merge or rebase manually\n"
1158 "cd %s && git " % self.checkout_path
1159 + "%s" % ' '.join(rebase_cmd))
1160 elif re.match(r'show|s', rebase_action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001161 self.Print('%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001162 continue
1163 else:
1164 gclient_utils.Error("Input not recognized")
1165 continue
1166 elif re.search(r'^CONFLICT', e.stdout, re.M):
1167 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1168 "Fix the conflict and run gclient again.\n"
1169 "See 'man git-rebase' for details.\n")
1170 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001171 self.Print(e.stdout.strip())
1172 self.Print('Rebase produced error output:\n%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001173 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1174 "manually.\ncd %s && git " %
1175 self.checkout_path
1176 + "%s" % ' '.join(rebase_cmd))
1177
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001178 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001179 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001180 # Make the output a little prettier. It's nice to have some
1181 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001182 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001183
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001184 @staticmethod
1185 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001186 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1187 if not ok:
1188 raise gclient_utils.Error('git version %s < minimum required %s' %
1189 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001190
John Budorick882c91e2018-07-12 22:11:41 +00001191 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001192 # Special case handling if all 3 conditions are met:
1193 # * the mirros have recently changed, but deps destination remains same,
1194 # * the git histories of mirrors are conflicting.
1195 # * git cache is used
1196 # This manifests itself in current checkout having invalid HEAD commit on
1197 # most git operations. Since git cache is used, just deleted the .git
1198 # folder, and re-create it by cloning.
1199 try:
1200 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1201 except subprocess2.CalledProcessError as e:
1202 if ('fatal: bad object HEAD' in e.stderr
1203 and self.cache_dir and self.cache_dir in url):
1204 self.Print((
1205 'Likely due to DEPS change with git cache_dir, '
1206 'the current commit points to no longer existing object.\n'
1207 '%s' % e)
1208 )
1209 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001210 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001211 else:
1212 raise
1213
msb@chromium.org786fb682010-06-02 15:16:23 +00001214 def _IsRebasing(self):
1215 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1216 # have a plumbing command to determine whether a rebase is in progress, so
1217 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1218 g = os.path.join(self.checkout_path, '.git')
1219 return (
1220 os.path.isdir(os.path.join(g, "rebase-merge")) or
1221 os.path.isdir(os.path.join(g, "rebase-apply")))
1222
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001223 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001224 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1225 if os.path.exists(lockfile):
1226 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001227 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001228 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1229 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001230 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001231
msb@chromium.org786fb682010-06-02 15:16:23 +00001232 # Make sure the tree is clean; see git-rebase.sh for reference
1233 try:
1234 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001235 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001236 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001237 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001238 '\tYou have unstaged changes.\n'
1239 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001240 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001241 try:
1242 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001243 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001244 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001245 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001246 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001247 '\tYour index contains uncommitted changes\n'
1248 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001249 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001250
agable83faed02016-10-24 14:37:10 -07001251 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001252 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1253 # reference by a commit). If not, error out -- most likely a rebase is
1254 # in progress, try to detect so we can give a better error.
1255 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001256 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1257 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001258 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001259 # Commit is not contained by any rev. See if the user is rebasing:
1260 if self._IsRebasing():
1261 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001262 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001263 '\tAlready in a conflict, i.e. (no branch).\n'
1264 '\tFix the conflict and run gclient again.\n'
1265 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1266 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001267 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001268 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001269 name = ('saved-by-gclient-' +
1270 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001271 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001272 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001273 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001274
msb@chromium.org5bde4852009-12-14 16:47:12 +00001275 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001276 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001277 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001278 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001279 return None
1280 return branch
1281
borenet@google.comc3e09d22014-04-10 13:58:18 +00001282 def _Capture(self, args, **kwargs):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001283 kwargs.setdefault('cwd', self.checkout_path)
1284 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001285 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001286 env = scm.GIT.ApplyEnvVars(kwargs)
Raul Tambrecd862e32019-05-10 21:19:00 +00001287 ret = subprocess2.check_output(
1288 ['git'] + args, env=env, **kwargs).decode('utf-8')
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001289 if strip:
1290 ret = ret.strip()
1291 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001292
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001293 def _Checkout(self, options, ref, force=False, quiet=None):
1294 """Performs a 'git-checkout' operation.
1295
1296 Args:
1297 options: The configured option set
1298 ref: (str) The branch/commit to checkout
1299 quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
1300 'None', the behavior is inferred from 'options.verbose'.
1301 Returns: (str) The output of the checkout operation
1302 """
1303 if quiet is None:
1304 quiet = (not options.verbose)
1305 checkout_args = ['checkout']
1306 if force:
1307 checkout_args.append('--force')
1308 if quiet:
1309 checkout_args.append('--quiet')
1310 checkout_args.append(ref)
1311 return self._Capture(checkout_args)
1312
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001313 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1314 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001315 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001316 # When updating, the ref is modified to be a remote ref .
1317 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1318 # Try to reverse that mapping.
1319 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1320 if original_ref:
1321 refspec = original_ref + ':' + refspec
1322 # When a mirror is configured, it only fetches
1323 # refs/{heads,branch-heads,tags}/*.
1324 # If asked to fetch other refs, we must fetch those directly from the
1325 # repository, and not from the mirror.
1326 if not original_ref.startswith(
1327 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1328 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001329 fetch_cmd = cfg + [
1330 'fetch',
1331 remote or self.remote,
1332 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001333 if refspec:
1334 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001335
1336 if prune:
1337 fetch_cmd.append('--prune')
1338 if options.verbose:
1339 fetch_cmd.append('--verbose')
1340 elif quiet:
1341 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001342 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001343
1344 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1345 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1346
Edward Lemur579c9862018-07-13 23:17:51 +00001347 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001348 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1349 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001350 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001351 try:
1352 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1353 options)
1354 self._Run(['config', 'remote.%s.fetch' % self.remote,
1355 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1356 except subprocess2.CalledProcessError as e:
1357 # If exit code was 5, it means we attempted to unset a config that
1358 # didn't exist. Ignore it.
1359 if e.returncode != 5:
1360 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001361 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001362 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001363 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1364 '^\\+refs/branch-heads/\\*:.*$']
1365 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001366 if hasattr(options, 'with_tags') and options.with_tags:
1367 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1368 '+refs/tags/*:refs/tags/*',
1369 '^\\+refs/tags/\\*:.*$']
1370 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001371
John Budorick882c91e2018-07-12 22:11:41 +00001372 def _AutoFetchRef(self, options, revision):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001373 """Attempts to fetch |revision| if not available in local repo.
1374
1375 Returns possibly updated revision."""
John Budorick882c91e2018-07-12 22:11:41 +00001376 try:
1377 self._Capture(['rev-parse', revision])
1378 except subprocess2.CalledProcessError:
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001379 self._Fetch(options, refspec=revision)
1380 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1381 return revision
1382
dnj@chromium.org680f2172014-06-25 00:39:32 +00001383 def _Run(self, args, options, show_header=True, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001384 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001385 kwargs.setdefault('cwd', self.checkout_path)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001386 kwargs.setdefault('stdout', self.out_fh)
szager@chromium.org848fd492014-04-09 19:06:44 +00001387 kwargs['filter_fn'] = self.filter
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001388 kwargs.setdefault('print_stdout', False)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001389 env = scm.GIT.ApplyEnvVars(kwargs)
agable@chromium.org772efaf2014-04-01 02:35:44 +00001390 cmd = ['git'] + args
dnj@chromium.org680f2172014-06-25 00:39:32 +00001391 if show_header:
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001392 gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs)
1393 else:
1394 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001395
1396
1397class CipdPackage(object):
1398 """A representation of a single CIPD package."""
1399
John Budorickd3ba72b2018-03-20 12:27:42 -07001400 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001401 self._authority_for_subdir = authority_for_subdir
1402 self._name = name
1403 self._version = version
1404
1405 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001406 def authority_for_subdir(self):
1407 """Whether this package has authority to act on behalf of its subdir.
1408
1409 Some operations should only be performed once per subdirectory. A package
1410 that has authority for its subdirectory is the only package that should
1411 perform such operations.
1412
1413 Returns:
1414 bool; whether this package has subdir authority.
1415 """
1416 return self._authority_for_subdir
1417
1418 @property
1419 def name(self):
1420 return self._name
1421
1422 @property
1423 def version(self):
1424 return self._version
1425
1426
1427class CipdRoot(object):
1428 """A representation of a single CIPD root."""
1429 def __init__(self, root_dir, service_url):
1430 self._all_packages = set()
1431 self._mutator_lock = threading.Lock()
1432 self._packages_by_subdir = collections.defaultdict(list)
1433 self._root_dir = root_dir
1434 self._service_url = service_url
1435
1436 def add_package(self, subdir, package, version):
1437 """Adds a package to this CIPD root.
1438
1439 As far as clients are concerned, this grants both root and subdir authority
1440 to packages arbitrarily. (The implementation grants root authority to the
1441 first package added and subdir authority to the first package added for that
1442 subdir, but clients should not depend on or expect that behavior.)
1443
1444 Args:
1445 subdir: str; relative path to where the package should be installed from
1446 the cipd root directory.
1447 package: str; the cipd package name.
1448 version: str; the cipd package version.
1449 Returns:
1450 CipdPackage; the package that was created and added to this root.
1451 """
1452 with self._mutator_lock:
1453 cipd_package = CipdPackage(
1454 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001455 not self._packages_by_subdir[subdir])
1456 self._all_packages.add(cipd_package)
1457 self._packages_by_subdir[subdir].append(cipd_package)
1458 return cipd_package
1459
1460 def packages(self, subdir):
1461 """Get the list of configured packages for the given subdir."""
1462 return list(self._packages_by_subdir[subdir])
1463
1464 def clobber(self):
1465 """Remove the .cipd directory.
1466
1467 This is useful for forcing ensure to redownload and reinitialize all
1468 packages.
1469 """
1470 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001471 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001472 try:
1473 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1474 except OSError:
1475 if os.path.exists(cipd_cache_dir):
1476 raise
1477
1478 @contextlib.contextmanager
1479 def _create_ensure_file(self):
1480 try:
1481 ensure_file = None
1482 with tempfile.NamedTemporaryFile(
Raul Tambreb946b232019-03-26 14:48:46 +00001483 suffix='.ensure', delete=False, mode='w') as ensure_file:
John Budorick302bb842018-07-17 23:49:17 +00001484 ensure_file.write('$ParanoidMode CheckPresence\n\n')
Raul Tambreb946b232019-03-26 14:48:46 +00001485 for subdir, packages in sorted(self._packages_by_subdir.items()):
John Budorick0f7b2002018-01-19 15:46:17 -08001486 ensure_file.write('@Subdir %s\n' % subdir)
Edward Lemurfbb06aa2018-06-11 20:43:06 +00001487 for package in sorted(packages, key=lambda p: p.name):
John Budorick0f7b2002018-01-19 15:46:17 -08001488 ensure_file.write('%s %s\n' % (package.name, package.version))
1489 ensure_file.write('\n')
1490 yield ensure_file.name
1491 finally:
1492 if ensure_file is not None and os.path.exists(ensure_file.name):
1493 os.remove(ensure_file.name)
1494
1495 def ensure(self):
1496 """Run `cipd ensure`."""
1497 with self._mutator_lock:
1498 with self._create_ensure_file() as ensure_file:
1499 cmd = [
1500 'cipd', 'ensure',
1501 '-log-level', 'error',
1502 '-root', self.root_dir,
1503 '-ensure-file', ensure_file,
1504 ]
1505 gclient_utils.CheckCallAndFilterAndHeader(cmd)
1506
John Budorickd3ba72b2018-03-20 12:27:42 -07001507 def run(self, command):
1508 if command == 'update':
1509 self.ensure()
1510 elif command == 'revert':
1511 self.clobber()
1512 self.ensure()
1513
John Budorick0f7b2002018-01-19 15:46:17 -08001514 def created_package(self, package):
1515 """Checks whether this root created the given package.
1516
1517 Args:
1518 package: CipdPackage; the package to check.
1519 Returns:
1520 bool; whether this root created the given package.
1521 """
1522 return package in self._all_packages
1523
1524 @property
1525 def root_dir(self):
1526 return self._root_dir
1527
1528 @property
1529 def service_url(self):
1530 return self._service_url
1531
1532
1533class CipdWrapper(SCMWrapper):
1534 """Wrapper for CIPD.
1535
1536 Currently only supports chrome-infra-packages.appspot.com.
1537 """
John Budorick3929e9e2018-02-04 18:18:07 -08001538 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001539
1540 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1541 out_cb=None, root=None, package=None):
1542 super(CipdWrapper, self).__init__(
1543 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1544 out_cb=out_cb)
1545 assert root.created_package(package)
1546 self._package = package
1547 self._root = root
1548
1549 #override
1550 def GetCacheMirror(self):
1551 return None
1552
1553 #override
1554 def GetActualRemoteURL(self, options):
1555 return self._root.service_url
1556
1557 #override
1558 def DoesRemoteURLMatch(self, options):
1559 del options
1560 return True
1561
1562 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001563 """Does nothing.
1564
1565 CIPD packages should be reverted at the root by running
1566 `CipdRoot.run('revert')`.
1567 """
1568 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001569
1570 def diff(self, options, args, file_list):
1571 """CIPD has no notion of diffing."""
1572 pass
1573
1574 def pack(self, options, args, file_list):
1575 """CIPD has no notion of diffing."""
1576 pass
1577
1578 def revinfo(self, options, args, file_list):
1579 """Grab the instance ID."""
1580 try:
1581 tmpdir = tempfile.mkdtemp()
1582 describe_json_path = os.path.join(tmpdir, 'describe.json')
1583 cmd = [
1584 'cipd', 'describe',
1585 self._package.name,
1586 '-log-level', 'error',
1587 '-version', self._package.version,
1588 '-json-output', describe_json_path
1589 ]
1590 gclient_utils.CheckCallAndFilter(
1591 cmd, filter_fn=lambda _line: None, print_stdout=False)
1592 with open(describe_json_path) as f:
1593 describe_json = json.load(f)
1594 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1595 finally:
1596 gclient_utils.rmtree(tmpdir)
1597
1598 def status(self, options, args, file_list):
1599 pass
1600
1601 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001602 """Does nothing.
1603
1604 CIPD packages should be updated at the root by running
1605 `CipdRoot.run('update')`.
1606 """
1607 pass