blob: 4debb299bdf4a57381e40206a6e82bba383c41ec [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
27import gclient_utils
szager@chromium.org848fd492014-04-09 19:06:44 +000028import git_cache
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000029import scm
borenet@google.comb2256212014-05-07 20:57:28 +000030import shutil
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000031import subprocess2
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000032
33
szager@chromium.org71cbb502013-04-19 23:30:15 +000034THIS_FILE_PATH = os.path.abspath(__file__)
35
hinoka@google.com2f2ca142014-01-07 03:59:18 +000036GSUTIL_DEFAULT_PATH = os.path.join(
hinoka@chromium.orgb091aa52014-12-20 01:47:31 +000037 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
hinoka@google.com2f2ca142014-01-07 03:59:18 +000038
maruel@chromium.org79d62372015-06-01 18:50:55 +000039
smutae7ea312016-07-18 11:59:41 -070040class NoUsableRevError(gclient_utils.Error):
41 """Raised if requested revision isn't found in checkout."""
42
43
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000044class DiffFiltererWrapper(object):
45 """Simple base class which tracks which file is being diffed and
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000046 replaces instances of its file name in the original and
agable41e3a6c2016-10-20 11:36:56 -070047 working copy lines of the git diff output."""
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000048 index_string = None
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000049 original_prefix = "--- "
50 working_prefix = "+++ "
51
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000052 def __init__(self, relpath, print_func):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000053 # Note that we always use '/' as the path separator to be
agable41e3a6c2016-10-20 11:36:56 -070054 # consistent with cygwin-style output on Windows
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000055 self._relpath = relpath.replace("\\", "/")
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000056 self._current_file = None
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000057 self._print_func = print_func
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000058
maruel@chromium.org6e29d572010-06-04 17:32:20 +000059 def SetCurrentFile(self, current_file):
60 self._current_file = current_file
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000061
iannucci@chromium.org3830a672013-02-19 20:15:14 +000062 @property
63 def _replacement_file(self):
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000064 return posixpath.join(self._relpath, self._current_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000065
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000066 def _Replace(self, line):
67 return line.replace(self._current_file, self._replacement_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000068
69 def Filter(self, line):
70 if (line.startswith(self.index_string)):
71 self.SetCurrentFile(line[len(self.index_string):])
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000072 line = self._Replace(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000073 else:
74 if (line.startswith(self.original_prefix) or
75 line.startswith(self.working_prefix)):
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000076 line = self._Replace(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000077 self._print_func(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000078
79
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000080class GitDiffFilterer(DiffFiltererWrapper):
81 index_string = "diff --git "
82
83 def SetCurrentFile(self, current_file):
84 # Get filename by parsing "a/<filename> b/<filename>"
85 self._current_file = current_file[:(len(current_file)/2)][2:]
86
87 def _Replace(self, line):
88 return re.sub("[a|b]/" + self._current_file, self._replacement_file, line)
89
90
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000091# SCMWrapper base class
92
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000093class SCMWrapper(object):
94 """Add necessary glue between all the supported SCM.
95
msb@chromium.orgd6504212010-01-13 17:34:31 +000096 This is the abstraction layer to bind to different SCM.
97 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000098 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
Edward Lemur231f5ea2018-01-31 19:02:36 +010099 out_cb=None, print_outbuf=False):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000100 self.url = url
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +0000101 self._root_dir = root_dir
102 if self._root_dir:
103 self._root_dir = self._root_dir.replace('/', os.sep)
104 self.relpath = relpath
105 if self.relpath:
106 self.relpath = self.relpath.replace('/', os.sep)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000107 if self.relpath and self._root_dir:
108 self.checkout_path = os.path.join(self._root_dir, self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000109 if out_fh is None:
110 out_fh = sys.stdout
111 self.out_fh = out_fh
112 self.out_cb = out_cb
Edward Lemur231f5ea2018-01-31 19:02:36 +0100113 self.print_outbuf = print_outbuf
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000114
115 def Print(self, *args, **kwargs):
116 kwargs.setdefault('file', self.out_fh)
117 if kwargs.pop('timestamp', True):
118 self.out_fh.write('[%s] ' % gclient_utils.Elapsed())
119 print(*args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000120
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000121 def RunCommand(self, command, options, args, file_list=None):
agabledebf6c82016-12-21 12:50:12 -0800122 commands = ['update', 'updatesingle', 'revert',
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000123 'revinfo', 'status', 'diff', 'pack', 'runhooks']
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000124
125 if not command in commands:
126 raise gclient_utils.Error('Unknown command %s' % command)
127
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000128 if not command in dir(self):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000129 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % (
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000130 command, self.__class__.__name__))
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000131
132 return getattr(self, command)(options, args, file_list)
133
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000134 @staticmethod
135 def _get_first_remote_url(checkout_path):
136 log = scm.GIT.Capture(
137 ['config', '--local', '--get-regexp', r'remote.*.url'],
138 cwd=checkout_path)
139 # Get the second token of the first line of the log.
140 return log.splitlines()[0].split(' ', 1)[1]
141
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000142 def GetCacheMirror(self):
Robert Iannuccia19649b2018-06-29 16:31:45 +0000143 if getattr(self, 'cache_dir', None):
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000144 url, _ = gclient_utils.SplitUrlRevision(self.url)
145 return git_cache.Mirror(url)
146 return None
147
smut@google.comd33eab32014-07-07 19:35:18 +0000148 def GetActualRemoteURL(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000149 """Attempt to determine the remote URL for this SCMWrapper."""
smut@google.comd33eab32014-07-07 19:35:18 +0000150 # Git
borenet@google.combda475e2014-03-24 19:04:45 +0000151 if os.path.exists(os.path.join(self.checkout_path, '.git')):
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000152 actual_remote_url = self._get_first_remote_url(self.checkout_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000153
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000154 mirror = self.GetCacheMirror()
155 # If the cache is used, obtain the actual remote URL from there.
156 if (mirror and mirror.exists() and
157 mirror.mirror_path.replace('\\', '/') ==
158 actual_remote_url.replace('\\', '/')):
159 actual_remote_url = self._get_first_remote_url(mirror.mirror_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000160 return actual_remote_url
borenet@google.com88d10082014-03-21 17:24:48 +0000161 return None
162
borenet@google.com4e9be262014-04-08 19:40:30 +0000163 def DoesRemoteURLMatch(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000164 """Determine whether the remote URL of this checkout is the expected URL."""
165 if not os.path.exists(self.checkout_path):
166 # A checkout which doesn't exist can't be broken.
167 return True
168
smut@google.comd33eab32014-07-07 19:35:18 +0000169 actual_remote_url = self.GetActualRemoteURL(options)
borenet@google.com88d10082014-03-21 17:24:48 +0000170 if actual_remote_url:
borenet@google.com8156c9f2014-04-01 16:41:36 +0000171 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip('/')
172 == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/'))
borenet@google.com88d10082014-03-21 17:24:48 +0000173 else:
174 # This may occur if the self.checkout_path exists but does not contain a
agable41e3a6c2016-10-20 11:36:56 -0700175 # valid git checkout.
borenet@google.com88d10082014-03-21 17:24:48 +0000176 return False
177
borenet@google.comb09097a2014-04-09 19:09:08 +0000178 def _DeleteOrMove(self, force):
179 """Delete the checkout directory or move it out of the way.
180
181 Args:
182 force: bool; if True, delete the directory. Otherwise, just move it.
183 """
borenet@google.comb2256212014-05-07 20:57:28 +0000184 if force and os.environ.get('CHROME_HEADLESS') == '1':
185 self.Print('_____ Conflicting directory found in %s. Removing.'
186 % self.checkout_path)
187 gclient_utils.AddWarning('Conflicting directory %s deleted.'
188 % self.checkout_path)
189 gclient_utils.rmtree(self.checkout_path)
190 else:
191 bad_scm_dir = os.path.join(self._root_dir, '_bad_scm',
192 os.path.dirname(self.relpath))
193
194 try:
195 os.makedirs(bad_scm_dir)
196 except OSError as e:
197 if e.errno != errno.EEXIST:
198 raise
199
200 dest_path = tempfile.mkdtemp(
201 prefix=os.path.basename(self.relpath),
202 dir=bad_scm_dir)
203 self.Print('_____ Conflicting directory found in %s. Moving to %s.'
204 % (self.checkout_path, dest_path))
205 gclient_utils.AddWarning('Conflicting directory %s moved to %s.'
206 % (self.checkout_path, dest_path))
207 shutil.move(self.checkout_path, dest_path)
borenet@google.comb09097a2014-04-09 19:09:08 +0000208
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000209
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000210class GitWrapper(SCMWrapper):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000211 """Wrapper for Git"""
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000212 name = 'git'
maruel@chromium.org1a60dca2013-11-26 14:06:26 +0000213 remote = 'origin'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000214
Robert Iannuccia19649b2018-06-29 16:31:45 +0000215 @property
216 def cache_dir(self):
217 try:
218 return git_cache.Mirror.GetCachePath()
219 except RuntimeError:
220 return None
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000221
John Budorick0f7b2002018-01-19 15:46:17 -0800222 def __init__(self, url=None, *args, **kwargs):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000223 """Removes 'git+' fake prefix from git URL."""
Henrique Ferreiroe72279d2019-04-17 12:01:50 +0000224 if url and (url.startswith('git+http://') or
225 url.startswith('git+https://')):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000226 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(
Raul Tambrecd862e32019-05-10 21:19:00 +0000258 # Filter to remove base if it is None.
259 list(filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only',
260 base])
261 )).split()
Aaron Gablef4068aa2017-12-12 15:14:09 -0800262
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000263 def diff(self, options, _args, _file_list):
Aaron Gable1853f662018-02-12 15:45:56 -0800264 _, revision = gclient_utils.SplitUrlRevision(self.url)
265 if not revision:
266 revision = 'refs/remotes/%s/master' % self.remote
267 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000268
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000269 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000270 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000271 repository.
272
273 The patch file is generated from a diff of the merge base of HEAD and
274 its upstream branch.
275 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700276 try:
277 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
278 except subprocess2.CalledProcessError:
279 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000280 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700281 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000282 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000283 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000284
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800285 def _Scrub(self, target, options):
286 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000287 quiet = []
288 if not options.verbose:
289 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800290 self._Run(['reset', '--hard', target] + quiet, options)
291 if options.force and options.delete_unversioned_trees:
292 # where `target` is a commit that contains both upper and lower case
293 # versions of the same file on a case insensitive filesystem, we are
294 # actually in a broken state here. The index will have both 'a' and 'A',
295 # but only one of them will exist on the disk. To progress, we delete
296 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800297 output = self._Capture([
298 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800299 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800300 # --porcelain (v1) looks like:
301 # XY filename
302 try:
303 filename = line[3:]
304 self.Print('_____ Deleting residual after reset: %r.' % filename)
305 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800306 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800307 except OSError:
308 pass
309
John Budorick882c91e2018-07-12 22:11:41 +0000310 def _FetchAndReset(self, revision, file_list, options):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800311 """Equivalent to git fetch; git reset."""
Edward Lemur579c9862018-07-13 23:17:51 +0000312 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000313
dnj@chromium.org680f2172014-06-25 00:39:32 +0000314 self._Fetch(options, prune=True, quiet=options.verbose)
John Budorick882c91e2018-07-12 22:11:41 +0000315 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000316 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800317 files = self._Capture(
318 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambreb946b232019-03-26 14:48:46 +0000319 file_list.extend(
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000320 [os.path.join(self.checkout_path, f) for f in files])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000321
szager@chromium.org8a139702014-06-20 15:55:01 +0000322 def _DisableHooks(self):
323 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
324 if not os.path.isdir(hook_dir):
325 return
326 for f in os.listdir(hook_dir):
327 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000328 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
329 if os.path.exists(disabled_hook_path):
330 os.remove(disabled_hook_path)
331 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000332
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000333 def _maybe_break_locks(self, options):
334 """This removes all .lock files from this repo's .git directory, if the
335 user passed the --break_repo_locks command line flag.
336
337 In particular, this will cleanup index.lock files, as well as ref lock
338 files.
339 """
340 if options.break_repo_locks:
341 git_dir = os.path.join(self.checkout_path, '.git')
342 for path, _, filenames in os.walk(git_dir):
343 for filename in filenames:
344 if filename.endswith('.lock'):
345 to_break = os.path.join(path, filename)
346 self.Print('breaking lock: %s' % (to_break,))
347 try:
348 os.remove(to_break)
349 except OSError as ex:
350 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
351 raise
352
Edward Lemur3acbc742019-05-30 17:57:35 +0000353 def apply_patch_ref(self, patch_repo, patch_rev, target_rev, options,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000354 file_list):
355 """Apply a patch on top of the revision we're synced at.
356
Edward Lemur3acbc742019-05-30 17:57:35 +0000357 The patch ref is given by |patch_repo|@|patch_rev|.
358 |target_rev| is usually the branch that the |patch_rev| was uploaded against
359 (e.g. 'refs/heads/master'), but this is not required.
360
361 We cherry-pick all commits reachable from |patch_rev| on top of the curret
362 HEAD, excluding those reachable from |target_rev|
363 (i.e. git cherry-pick target_rev..patch_rev).
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000364
365 Graphically, it looks like this:
366
Edward Lemur3acbc742019-05-30 17:57:35 +0000367 ... -> o -> [possibly already landed commits] -> target_rev
368 \
369 -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000370
Edward Lemur3acbc742019-05-30 17:57:35 +0000371 The final checkout state is then:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000372
Edward Lemur3acbc742019-05-30 17:57:35 +0000373 ... -> HEAD -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000374
375 After application, if |options.reset_patch_ref| is specified, we soft reset
Edward Lemur3acbc742019-05-30 17:57:35 +0000376 the cherry-picked changes, keeping them in git index only.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000377
378 Args:
Edward Lemur3acbc742019-05-30 17:57:35 +0000379 patch_repo: The patch origin.
380 e.g. 'https://foo.googlesource.com/bar'
381 patch_rev: The revision to patch.
382 e.g. 'refs/changes/1234/34/1'.
383 target_rev: The revision to use when finding the merge base.
384 Typically, the branch that 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 Lemur3acbc742019-05-30 17:57:35 +0000398 if not target_rev:
Edward Lemur4c5c8ab2019-06-07 15:58:13 +0000399 raise gclient_utils.Error('A target revision for the patch must be given')
Edward Lemur3acbc742019-05-30 17:57:35 +0000400 elif target_rev.startswith('refs/heads/'):
401 # If |target_rev| is in refs/heads/**, try first to find the corresponding
402 # remote ref for it, since |target_rev| might point to a local ref which
403 # is not up to date with the corresponding remote ref.
404 remote_ref = ''.join(scm.GIT.RefToRemoteRef(target_rev, self.remote))
405 self.Print('Trying the correspondig remote ref for %r: %r\n' % (
406 target_rev, remote_ref))
407 if scm.GIT.IsValidRevision(self.checkout_path, remote_ref):
408 target_rev = remote_ref
409 elif not scm.GIT.IsValidRevision(self.checkout_path, target_rev):
410 # Fetch |target_rev| if it's not already available.
411 url, _ = gclient_utils.SplitUrlRevision(self.url)
412 mirror = self._GetMirror(url, options, target_rev)
413 if mirror:
414 rev_type = 'branch' if target_rev.startswith('refs/') else 'hash'
415 self._UpdateMirrorIfNotContains(mirror, options, rev_type, target_rev)
416 self._Fetch(options, refspec=target_rev)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000417
Edward Lemur3acbc742019-05-30 17:57:35 +0000418 self.Print('===Applying patch===')
419 self.Print('Revision to patch is %r @ %r.' % (patch_repo, patch_rev))
420 self.Print('Will cherrypick %r .. %r on top of %r.' % (
421 target_rev, patch_rev, base_rev))
422 self.Print('Current dir is %r' % self.checkout_path)
Edward Lesmesc621b212018-03-21 20:26:56 -0400423 self._Capture(['reset', '--hard'])
Edward Lemur3acbc742019-05-30 17:57:35 +0000424 self._Capture(['fetch', patch_repo, patch_rev])
Edward Lemurca7d8812018-07-24 17:42:45 +0000425 patch_rev = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400426
Edward Lemur3acbc742019-05-30 17:57:35 +0000427 if not options.rebase_patch_ref:
428 self._Capture(['checkout', patch_rev])
429 else:
430 try:
431 if scm.GIT.IsAncestor(self.checkout_path, patch_rev, target_rev):
432 # If |patch_rev| is an ancestor of |target_rev|, check it out.
Edward Lemurca7d8812018-07-24 17:42:45 +0000433 self._Capture(['checkout', patch_rev])
434 else:
435 # If a change was uploaded on top of another change, which has already
436 # landed, one of the commits in the cherry-pick range will be
437 # redundant, since it has already landed and its changes incorporated
438 # in the tree.
439 # We pass '--keep-redundant-commits' to ignore those changes.
Edward Lemur3acbc742019-05-30 17:57:35 +0000440 self._Capture(['cherry-pick', target_rev + '..' + patch_rev,
Edward Lemurca7d8812018-07-24 17:42:45 +0000441 '--keep-redundant-commits'])
442
Edward Lemur3acbc742019-05-30 17:57:35 +0000443 except subprocess2.CalledProcessError as e:
444 self.Print('Failed to apply patch.')
445 self.Print('Revision to patch was %r @ %r.' % (patch_repo, patch_rev))
446 self.Print('Tried to cherrypick %r .. %r on top of %r.' % (
447 target_rev, patch_rev, base_rev))
448 self.Print('Current dir is %r' % self.checkout_path)
449 self.Print('git returned non-zero exit status %s:\n%s' % (
Edward Lemur979fa782019-08-13 22:44:05 +0000450 e.returncode, e.stderr.decode('utf-8')))
Edward Lemur3acbc742019-05-30 17:57:35 +0000451 # Print the current status so that developers know what changes caused
452 # the patch failure, since git cherry-pick doesn't show that
453 # information.
454 self.Print(self._Capture(['status']))
455 try:
456 self._Capture(['cherry-pick', '--abort'])
457 except subprocess2.CalledProcessError:
458 pass
459 raise
Edward Lemurca7d8812018-07-24 17:42:45 +0000460
Edward Lemur3acbc742019-05-30 17:57:35 +0000461 if file_list is not None:
462 file_list.extend(self._GetDiffFilenames(base_rev))
Edward Lemurca7d8812018-07-24 17:42:45 +0000463
Edward Lesmesc621b212018-03-21 20:26:56 -0400464 if options.reset_patch_ref:
465 self._Capture(['reset', '--soft', base_rev])
466
msb@chromium.orge28e4982009-09-25 20:51:45 +0000467 def update(self, options, args, file_list):
468 """Runs git to update or transparently checkout the working copy.
469
470 All updated files will be appended to file_list.
471
472 Raises:
473 Error: if can't get URL for relative path.
474 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000475 if args:
476 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
477
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000478 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000479
John Budorick882c91e2018-07-12 22:11:41 +0000480 # If a dependency is not pinned, track the default remote branch.
481 default_rev = 'refs/remotes/%s/master' % self.remote
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000482 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000483 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000484 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000485 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000486 # Override the revision number.
487 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000488 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000489 # Check again for a revision in case an initial ref was specified
490 # in the url, for example bla.git@refs/heads/custombranch
491 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000492 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000493 if not revision:
494 revision = default_rev
msb@chromium.orge28e4982009-09-25 20:51:45 +0000495
szager@chromium.org8a139702014-06-20 15:55:01 +0000496 if managed:
497 self._DisableHooks()
498
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000499 printed_path = False
500 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000501 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700502 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000503 verbose = ['--verbose']
504 printed_path = True
505
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000506 revision_ref = revision
507 if ':' in revision:
508 revision_ref, _, revision = revision.partition(':')
509
510 mirror = self._GetMirror(url, options, revision_ref)
511 if mirror:
512 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000513
John Budorick882c91e2018-07-12 22:11:41 +0000514 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
515 if remote_ref:
516 # Rewrite remote refs to their local equivalents.
517 revision = ''.join(remote_ref)
518 rev_type = "branch"
519 elif revision.startswith('refs/'):
520 # Local branch? We probably don't want to support, since DEPS should
521 # always specify branches as they are in the upstream repo.
522 rev_type = "branch"
523 else:
524 # hash is also a tag, only make a distinction at checkout
525 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000526
primiano@chromium.org1c127382015-02-17 11:15:40 +0000527 # If we are going to introduce a new project, there is a possibility that
528 # we are syncing back to a state where the project was originally a
529 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
530 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
531 # In such case, we might have a backup of the former .git folder, which can
532 # be used to avoid re-fetching the entire repo again (useful for bisects).
533 backup_dir = self.GetGitBackupDirPath()
534 target_dir = os.path.join(self.checkout_path, '.git')
535 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
536 gclient_utils.safe_makedirs(self.checkout_path)
537 os.rename(backup_dir, target_dir)
538 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800539 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000540
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000541 if (not os.path.exists(self.checkout_path) or
542 (os.path.isdir(self.checkout_path) and
543 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000544 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000545 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000546 try:
John Budorick882c91e2018-07-12 22:11:41 +0000547 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000548 except subprocess2.CalledProcessError:
549 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000550 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000551 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800552 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000553 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000554 file_list.extend(
Edward Lemur979fa782019-08-13 22:44:05 +0000555 [os.path.join(self.checkout_path, f) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000556 if mirror:
557 self._Capture(
558 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000559 if not verbose:
560 # Make the output a little prettier. It's nice to have some whitespace
561 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000562 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000563 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000564
John Budorick21a51b32018-09-19 19:39:20 +0000565 if mirror:
566 self._Capture(
567 ['remote', 'set-url', '--push', 'origin', mirror.url])
568
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000569 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000570 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000571 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
572 return self._Capture(['rev-parse', '--verify', 'HEAD'])
573
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000574 self._maybe_break_locks(options)
575
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000576 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000577 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000578
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000579 # See if the url has changed (the unittests use git://foo for the url, let
580 # that through).
581 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
582 return_early = False
583 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
584 # unit test pass. (and update the comment above)
585 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
586 # This allows devs to use experimental repos which have a different url
587 # but whose branch(s) are the same as official repos.
Raul Tambrecd862e32019-05-10 21:19:00 +0000588 if (current_url.rstrip('/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000589 subprocess2.capture(
Aaron Gableac9b0f32019-04-18 17:38:37 +0000590 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000591 cwd=self.checkout_path).strip() != 'False'):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000592 self.Print('_____ switching %s to a new upstream' % self.relpath)
iannucci@chromium.org78514212014-08-20 23:08:00 +0000593 if not (options.force or options.reset):
594 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700595 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000596 # Switch over to the new upstream
597 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000598 if mirror:
599 with open(os.path.join(
600 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
601 'w') as fh:
602 fh.write(os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000603 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
604 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000605
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000606 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000607 else:
John Budorick882c91e2018-07-12 22:11:41 +0000608 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000609
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000610 if return_early:
611 return self._Capture(['rev-parse', '--verify', 'HEAD'])
612
msb@chromium.org5bde4852009-12-14 16:47:12 +0000613 cur_branch = self._GetCurrentBranch()
614
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000615 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000616 # 0) HEAD is detached. Probably from our initial clone.
617 # - make sure HEAD is contained by a named ref, then update.
618 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700619 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000620 # - try to rebase onto the new hash or branch
621 # 2) current branch is tracking a remote branch with local committed
622 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000623 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000624 # 3) current branch is tracking a remote branch w/or w/out changes, and
625 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000626 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000627 # 4) current branch is tracking a remote branch, but DEPS switches to a
628 # different remote branch, and
629 # a) current branch has no local changes, and --force:
630 # - checkout new branch
631 # b) current branch has local changes, and --force and --reset:
632 # - checkout new branch
633 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000634
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000635 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
636 # a tracking branch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000637 # or 'master' if not a tracking branch (it's based on a specific rev/hash)
638 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000639 if cur_branch is None:
640 upstream_branch = None
641 current_type = "detached"
642 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000643 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000644 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
645 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
646 current_type = "hash"
647 logging.debug("Current branch is not tracking an upstream (remote)"
648 " branch.")
649 elif upstream_branch.startswith('refs/remotes'):
650 current_type = "branch"
651 else:
652 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000653
Edward Lemur579c9862018-07-13 23:17:51 +0000654 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000655
Michael Spang73fac912019-03-08 18:44:19 +0000656 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000657 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000658 self._Fetch(options, prune=options.force)
659
660 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
661 sha_only=True):
662 # Update the remotes first so we have all the refs.
663 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
664 cwd=self.checkout_path)
665 if verbose:
666 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000667
John Budorick882c91e2018-07-12 22:11:41 +0000668 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200669
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000670 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000671 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000672 target = 'HEAD'
673 if options.upstream and upstream_branch:
674 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800675 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000676
msb@chromium.org786fb682010-06-02 15:16:23 +0000677 if current_type == 'detached':
678 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800679 # We just did a Scrub, this is as clean as it's going to get. In
680 # particular if HEAD is a commit that contains two versions of the same
681 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
682 # to actually "Clean" the checkout; that commit is uncheckoutable on this
683 # system. The best we can do is carry forward to the checkout step.
684 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000685 self._CheckClean(revision)
686 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000687 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000688 self.Print('Up-to-date; skipping checkout.')
689 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000690 # 'git checkout' may need to overwrite existing untracked files. Allow
691 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000692 self._Checkout(
693 options,
John Budorick882c91e2018-07-12 22:11:41 +0000694 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000695 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000696 quiet=True,
697 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000698 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000699 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000700 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000701 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700702 # Can't find a merge-base since we don't know our upstream. That makes
703 # this command VERY likely to produce a rebase failure. For now we
704 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000705 upstream_branch = self.remote
706 if options.revision or deps_revision:
707 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700708 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700709 printed_path=printed_path, merge=options.merge)
710 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000711 elif rev_type == 'hash':
712 # case 2
713 self._AttemptRebase(upstream_branch, file_list, options,
714 newbase=revision, printed_path=printed_path,
715 merge=options.merge)
716 printed_path = True
717 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000718 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000719 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000720 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000721 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000722 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000723 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000724 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000725 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
726 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000727 force_switch = False
728 if options.force:
729 try:
John Budorick882c91e2018-07-12 22:11:41 +0000730 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000731 # case 4a
732 force_switch = True
733 except gclient_utils.Error as e:
734 if options.reset:
735 # case 4b
736 force_switch = True
737 else:
738 switch_error = '%s\n%s' % (e.message, switch_error)
739 if force_switch:
740 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000741 (upstream_branch, new_base))
742 switch_branch = 'gclient_' + remote_ref[1]
743 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000744 self._Checkout(options, switch_branch, force=True, quiet=True)
745 else:
746 # case 4c
747 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000748 else:
749 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800750 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000751 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000752 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000753 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000754 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000755 if options.merge:
756 merge_args.append('--ff')
757 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000758 merge_args.append('--ff-only')
759 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000760 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000761 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700762 rebase_files = []
Edward Lemur979fa782019-08-13 22:44:05 +0000763 if re.match(b'fatal: Not possible to fast-forward, aborting.',
764 e.stderr):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000765 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000766 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700767 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000768 printed_path = True
769 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000770 if not options.auto_rebase:
771 try:
772 action = self._AskForData(
773 'Cannot %s, attempt to rebase? '
774 '(y)es / (q)uit / (s)kip : ' %
775 ('merge' if options.merge else 'fast-forward merge'),
776 options)
777 except ValueError:
778 raise gclient_utils.Error('Invalid Character')
779 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700780 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000781 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000782 printed_path = True
783 break
784 elif re.match(r'quit|q', action, re.I):
785 raise gclient_utils.Error("Can't fast-forward, please merge or "
786 "rebase manually.\n"
787 "cd %s && git " % self.checkout_path
788 + "rebase %s" % upstream_branch)
789 elif re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000790 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000791 return
792 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000793 self.Print('Input not recognized')
Edward Lemur979fa782019-08-13 22:44:05 +0000794 elif re.match(b"error: Your local changes to '.*' would be "
795 b"overwritten by merge. Aborting.\nPlease, commit your "
796 b"changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000797 e.stderr):
798 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000799 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700800 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000801 printed_path = True
Edward Lemur979fa782019-08-13 22:44:05 +0000802 raise gclient_utils.Error(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000803 else:
804 # Some other problem happened with the merge
805 logging.error("Error during fast-forward merge in %s!" % self.relpath)
Edward Lemur979fa782019-08-13 22:44:05 +0000806 self.Print(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000807 raise
808 else:
809 # Fast-forward merge was successful
810 if not re.match('Already up-to-date.', merge_output) or verbose:
811 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700812 self.Print('_____ %s at %s' % (self.relpath, revision),
813 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000814 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000815 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000816 if not verbose:
817 # Make the output a little prettier. It's nice to have some
818 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000819 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000820
agablec3937b92016-10-25 10:13:03 -0700821 if file_list is not None:
822 file_list.extend(
823 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000824
825 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000826 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700827 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000828 '\nConflict while rebasing this branch.\n'
829 'Fix the conflict and run gclient again.\n'
830 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700831 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000832
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000833 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000834 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
835 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000836
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000837 # If --reset and --delete_unversioned_trees are specified, remove any
838 # untracked directories.
839 if options.reset and options.delete_unversioned_trees:
840 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
841 # merge-base by default), so doesn't include untracked files. So we use
842 # 'git ls-files --directory --others --exclude-standard' here directly.
843 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800844 ['-c', 'core.quotePath=false', 'ls-files',
845 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000846 self.checkout_path)
847 for path in (p for p in paths.splitlines() if p.endswith('/')):
848 full_path = os.path.join(self.checkout_path, path)
849 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000850 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000851 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000852
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000853 return self._Capture(['rev-parse', '--verify', 'HEAD'])
854
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000855 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000856 """Reverts local modifications.
857
858 All reverted files will be appended to file_list.
859 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000860 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000861 # revert won't work if the directory doesn't exist. It needs to
862 # checkout instead.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000863 self.Print('_____ %s is missing, synching instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000864 # Don't reuse the args.
865 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000866
867 default_rev = "refs/heads/master"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000868 if options.upstream:
869 if self._GetCurrentBranch():
870 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
871 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000872 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000873 if not deps_revision:
874 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000875 if deps_revision.startswith('refs/heads/'):
876 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700877 try:
878 deps_revision = self.GetUsableRev(deps_revision, options)
879 except NoUsableRevError as e:
880 # If the DEPS entry's url and hash changed, try to update the origin.
881 # See also http://crbug.com/520067.
882 logging.warn(
883 'Couldn\'t find usable revision, will retrying to update instead: %s',
884 e.message)
885 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000886
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000887 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800888 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000889
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800890 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000891 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000892
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000893 if file_list is not None:
894 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
895
896 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000897 """Returns revision"""
898 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000899
msb@chromium.orge28e4982009-09-25 20:51:45 +0000900 def runhooks(self, options, args, file_list):
901 self.status(options, args, file_list)
902
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000903 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000904 """Display status information."""
905 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000906 self.Print('________ couldn\'t run status in %s:\n'
907 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000908 else:
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700909 try:
910 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
911 except subprocess2.CalledProcessError:
912 merge_base = []
Aaron Gablef4068aa2017-12-12 15:14:09 -0800913 self._Run(
914 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
Edward Lemur24146be2019-08-01 21:44:52 +0000915 options, always_show_header=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000916 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800917 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000918 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000919
smutae7ea312016-07-18 11:59:41 -0700920 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700921 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700922 sha1 = None
923 if not os.path.isdir(self.checkout_path):
924 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800925 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700926
927 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
928 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700929 else:
agable41e3a6c2016-10-20 11:36:56 -0700930 # May exist in origin, but we don't have it yet, so fetch and look
931 # again.
932 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -0700933 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
934 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700935
936 if not sha1:
937 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800938 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -0700939
940 return sha1
941
primiano@chromium.org1c127382015-02-17 11:15:40 +0000942 def GetGitBackupDirPath(self):
943 """Returns the path where the .git folder for the current project can be
944 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
945 return os.path.join(self._root_dir,
946 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
947
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000948 def _GetMirror(self, url, options, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000949 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +0000950 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000951 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000952 mirror_kwargs = {
953 'print_func': self.filter,
John Budorick882c91e2018-07-12 22:11:41 +0000954 'refs': []
hinoka@google.comb1b54572014-04-16 22:29:23 +0000955 }
hinoka@google.comb1b54572014-04-16 22:29:23 +0000956 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
957 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000958 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
959 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +0000960 if hasattr(options, 'with_tags') and options.with_tags:
961 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000962 elif revision_ref and revision_ref.startswith('refs/tags/'):
963 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000964 return git_cache.Mirror(url, **mirror_kwargs)
965
John Budorick882c91e2018-07-12 22:11:41 +0000966 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800967 """Update a git mirror by fetching the latest commits from the remote,
968 unless mirror already contains revision whose type is sha1 hash.
969 """
John Budorick882c91e2018-07-12 22:11:41 +0000970 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800971 if options.verbose:
972 self.Print('skipping mirror update, it has rev=%s already' % revision,
973 timestamp=False)
974 return
975
szager@chromium.org3ec84f62014-08-22 21:00:22 +0000976 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000977 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000978 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000979 depth = 10
980 else:
981 depth = 10000
982 else:
983 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +0000984 mirror.populate(verbose=options.verbose,
985 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +0000986 depth=depth,
987 ignore_lock=getattr(options, 'ignore_locks', False),
988 lock_timeout=getattr(options, 'lock_timeout', 0))
989 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000990
John Budorick882c91e2018-07-12 22:11:41 +0000991 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000992 """Clone a git repository from the given URL.
993
msb@chromium.org786fb682010-06-02 15:16:23 +0000994 Once we've cloned the repo, we checkout a working branch if the specified
995 revision is a branch head. If it is a tag or a specific commit, then we
996 leave HEAD detached as it makes future updates simpler -- in this case the
997 user should first create a new branch or switch to an existing branch before
998 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000999 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001000 # git clone doesn't seem to insert a newline properly before printing
1001 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001002 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +00001003 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +00001004 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001005 if self.cache_dir:
1006 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001007 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001008 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001009 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001010 # If the parent directory does not exist, Git clone on Windows will not
1011 # create it, so we need to do it manually.
1012 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001013 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001014
1015 template_dir = None
1016 if hasattr(options, 'no_history') and options.no_history:
John Budorick882c91e2018-07-12 22:11:41 +00001017 if gclient_utils.IsGitSha(revision):
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001018 # In the case of a subproject, the pinned sha is not necessarily the
1019 # head of the remote branch (so we can't just use --depth=N). Instead,
1020 # we tell git to fetch all the remote objects from SHA..HEAD by means of
1021 # a template git dir which has a 'shallow' file pointing to the sha.
1022 template_dir = tempfile.mkdtemp(
1023 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
1024 dir=parent_dir)
1025 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
1026 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
1027 template_file.write(revision)
1028 clone_cmd.append('--template=' + template_dir)
1029 else:
1030 # Otherwise, we're just interested in the HEAD. Just use --depth.
1031 clone_cmd.append('--depth=1')
1032
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001033 tmp_dir = tempfile.mkdtemp(
1034 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
1035 dir=parent_dir)
1036 try:
1037 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +01001038 if self.print_outbuf:
1039 print_stdout = True
Edward Lemur24146be2019-08-01 21:44:52 +00001040 filter_fn = None
Edward Lemur231f5ea2018-01-31 19:02:36 +01001041 else:
1042 print_stdout = False
Edward Lemur24146be2019-08-01 21:44:52 +00001043 filter_fn = self.filter
Edward Lemur231f5ea2018-01-31 19:02:36 +01001044 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
Edward Lemur24146be2019-08-01 21:44:52 +00001045 print_stdout=print_stdout, filter_fn=filter_fn)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001046 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +00001047 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1048 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001049 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001050 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001051 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001052 finally:
1053 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001054 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001055 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001056 if template_dir:
1057 gclient_utils.rmtree(template_dir)
Edward Lemur579c9862018-07-13 23:17:51 +00001058 self._SetFetchConfig(options)
1059 self._Fetch(options, prune=options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001060 revision = self._AutoFetchRef(options, revision)
1061 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1062 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001063 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001064 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001065 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +00001066 ('Checked out %s to a detached HEAD. Before making any commits\n'
1067 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
1068 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
John Budorick882c91e2018-07-12 22:11:41 +00001069 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001070
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001071 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001072 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001073 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001074 raise gclient_utils.Error("Background task requires input. Rerun "
1075 "gclient with --jobs=1 so that\n"
1076 "interaction is possible.")
1077 try:
1078 return raw_input(prompt)
1079 except KeyboardInterrupt:
1080 # Hide the exception.
1081 sys.exit(1)
1082
1083
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001084 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001085 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001086 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001087 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001088 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001089 revision = upstream
1090 if newbase:
1091 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001092 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001093 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001094 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001095 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001096 printed_path = True
1097 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001098 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001099
1100 if merge:
1101 merge_output = self._Capture(['merge', revision])
1102 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001103 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001104 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001105
1106 # Build the rebase command here using the args
1107 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1108 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001109 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001110 rebase_cmd.append('--verbose')
1111 if newbase:
1112 rebase_cmd.extend(['--onto', newbase])
1113 rebase_cmd.append(upstream)
1114 if branch:
1115 rebase_cmd.append(branch)
1116
1117 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001118 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001119 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001120 if (re.match(br'cannot rebase: you have unstaged changes', e.stderr) or
1121 re.match(br'cannot rebase: your index contains uncommitted changes',
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001122 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001123 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001124 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001125 'Cannot rebase because of unstaged changes.\n'
1126 '\'git reset --hard HEAD\' ?\n'
1127 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001128 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001129 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001130 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001131 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001132 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001133 break
1134 elif re.match(r'quit|q', rebase_action, re.I):
1135 raise gclient_utils.Error("Please merge or rebase manually\n"
1136 "cd %s && git " % self.checkout_path
1137 + "%s" % ' '.join(rebase_cmd))
1138 elif re.match(r'show|s', rebase_action, re.I):
Edward Lemur979fa782019-08-13 22:44:05 +00001139 self.Print('%s' % e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001140 continue
1141 else:
1142 gclient_utils.Error("Input not recognized")
1143 continue
Edward Lemur979fa782019-08-13 22:44:05 +00001144 elif re.search(br'^CONFLICT', e.stdout, re.M):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001145 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1146 "Fix the conflict and run gclient again.\n"
1147 "See 'man git-rebase' for details.\n")
1148 else:
Edward Lemur979fa782019-08-13 22:44:05 +00001149 self.Print(e.stdout.decode('utf-8').strip())
1150 self.Print('Rebase produced error output:\n%s' %
1151 e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001152 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1153 "manually.\ncd %s && git " %
1154 self.checkout_path
1155 + "%s" % ' '.join(rebase_cmd))
1156
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001157 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001158 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001159 # Make the output a little prettier. It's nice to have some
1160 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001161 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001162
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001163 @staticmethod
1164 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001165 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1166 if not ok:
1167 raise gclient_utils.Error('git version %s < minimum required %s' %
1168 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001169
John Budorick882c91e2018-07-12 22:11:41 +00001170 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001171 # Special case handling if all 3 conditions are met:
1172 # * the mirros have recently changed, but deps destination remains same,
1173 # * the git histories of mirrors are conflicting.
1174 # * git cache is used
1175 # This manifests itself in current checkout having invalid HEAD commit on
1176 # most git operations. Since git cache is used, just deleted the .git
1177 # folder, and re-create it by cloning.
1178 try:
1179 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1180 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001181 if (b'fatal: bad object HEAD' in e.stderr
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001182 and self.cache_dir and self.cache_dir in url):
1183 self.Print((
1184 'Likely due to DEPS change with git cache_dir, '
1185 'the current commit points to no longer existing object.\n'
1186 '%s' % e)
1187 )
1188 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001189 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001190 else:
1191 raise
1192
msb@chromium.org786fb682010-06-02 15:16:23 +00001193 def _IsRebasing(self):
1194 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1195 # have a plumbing command to determine whether a rebase is in progress, so
1196 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1197 g = os.path.join(self.checkout_path, '.git')
1198 return (
1199 os.path.isdir(os.path.join(g, "rebase-merge")) or
1200 os.path.isdir(os.path.join(g, "rebase-apply")))
1201
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001202 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001203 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1204 if os.path.exists(lockfile):
1205 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001206 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001207 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1208 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001209 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001210
msb@chromium.org786fb682010-06-02 15:16:23 +00001211 # Make sure the tree is clean; see git-rebase.sh for reference
1212 try:
1213 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001214 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001215 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001216 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001217 '\tYou have unstaged changes.\n'
1218 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001219 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001220 try:
1221 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001222 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001223 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001224 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001225 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001226 '\tYour index contains uncommitted changes\n'
1227 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001228 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001229
agable83faed02016-10-24 14:37:10 -07001230 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001231 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1232 # reference by a commit). If not, error out -- most likely a rebase is
1233 # in progress, try to detect so we can give a better error.
1234 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001235 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1236 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001237 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001238 # Commit is not contained by any rev. See if the user is rebasing:
1239 if self._IsRebasing():
1240 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001241 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001242 '\tAlready in a conflict, i.e. (no branch).\n'
1243 '\tFix the conflict and run gclient again.\n'
1244 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1245 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001246 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001247 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001248 name = ('saved-by-gclient-' +
1249 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001250 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001251 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001252 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001253
msb@chromium.org5bde4852009-12-14 16:47:12 +00001254 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001255 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001256 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001257 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001258 return None
1259 return branch
1260
borenet@google.comc3e09d22014-04-10 13:58:18 +00001261 def _Capture(self, args, **kwargs):
Mike Frysinger286fb162019-09-30 03:14:10 +00001262 set_git_dir = 'cwd' not in kwargs
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001263 kwargs.setdefault('cwd', self.checkout_path)
1264 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001265 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001266 env = scm.GIT.ApplyEnvVars(kwargs)
Mike Frysinger286fb162019-09-30 03:14:10 +00001267 # If an explicit cwd isn't set, then default to the .git/ subdir so we get
1268 # stricter behavior. This can be useful in cases of slight corruption --
1269 # we don't accidentally go corrupting parent git checks too. See
1270 # https://crbug.com/1000825 for an example.
1271 if set_git_dir:
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001272 git_dir = os.path.abspath(os.path.join(self.checkout_path, '.git'))
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001273 # Depending on how the .gclient file was defined, self.checkout_path
1274 # might be set to a unicode string, not a regular string; on Windows
1275 # Python2, we can't set env vars to be unicode strings, so we
1276 # forcibly cast the value to a string before setting it.
1277 env.setdefault('GIT_DIR', str(git_dir))
Raul Tambrecd862e32019-05-10 21:19:00 +00001278 ret = subprocess2.check_output(
1279 ['git'] + args, env=env, **kwargs).decode('utf-8')
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001280 if strip:
1281 ret = ret.strip()
Erik Chene16ffff2019-10-14 20:35:53 +00001282 self.Print('Finished running: %s %s' % ('git', ' '.join(args)))
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001283 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001284
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001285 def _Checkout(self, options, ref, force=False, quiet=None):
1286 """Performs a 'git-checkout' operation.
1287
1288 Args:
1289 options: The configured option set
1290 ref: (str) The branch/commit to checkout
1291 quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
1292 'None', the behavior is inferred from 'options.verbose'.
1293 Returns: (str) The output of the checkout operation
1294 """
1295 if quiet is None:
1296 quiet = (not options.verbose)
1297 checkout_args = ['checkout']
1298 if force:
1299 checkout_args.append('--force')
1300 if quiet:
1301 checkout_args.append('--quiet')
1302 checkout_args.append(ref)
1303 return self._Capture(checkout_args)
1304
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001305 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1306 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001307 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001308 # When updating, the ref is modified to be a remote ref .
1309 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1310 # Try to reverse that mapping.
1311 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1312 if original_ref:
1313 refspec = original_ref + ':' + refspec
1314 # When a mirror is configured, it only fetches
1315 # refs/{heads,branch-heads,tags}/*.
1316 # If asked to fetch other refs, we must fetch those directly from the
1317 # repository, and not from the mirror.
1318 if not original_ref.startswith(
1319 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1320 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001321 fetch_cmd = cfg + [
1322 'fetch',
1323 remote or self.remote,
1324 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001325 if refspec:
1326 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001327
1328 if prune:
1329 fetch_cmd.append('--prune')
1330 if options.verbose:
1331 fetch_cmd.append('--verbose')
1332 elif quiet:
1333 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001334 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001335
1336 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1337 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1338
Edward Lemur579c9862018-07-13 23:17:51 +00001339 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001340 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1341 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001342 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001343 try:
1344 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1345 options)
1346 self._Run(['config', 'remote.%s.fetch' % self.remote,
1347 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1348 except subprocess2.CalledProcessError as e:
1349 # If exit code was 5, it means we attempted to unset a config that
1350 # didn't exist. Ignore it.
1351 if e.returncode != 5:
1352 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001353 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001354 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001355 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1356 '^\\+refs/branch-heads/\\*:.*$']
1357 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001358 if hasattr(options, 'with_tags') and options.with_tags:
1359 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1360 '+refs/tags/*:refs/tags/*',
1361 '^\\+refs/tags/\\*:.*$']
1362 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001363
John Budorick882c91e2018-07-12 22:11:41 +00001364 def _AutoFetchRef(self, options, revision):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001365 """Attempts to fetch |revision| if not available in local repo.
1366
1367 Returns possibly updated revision."""
John Budorick882c91e2018-07-12 22:11:41 +00001368 try:
1369 self._Capture(['rev-parse', revision])
1370 except subprocess2.CalledProcessError:
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001371 self._Fetch(options, refspec=revision)
1372 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1373 return revision
1374
Edward Lemur24146be2019-08-01 21:44:52 +00001375 def _Run(self, args, options, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001376 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001377 kwargs.setdefault('cwd', self.checkout_path)
Edward Lemur24146be2019-08-01 21:44:52 +00001378 kwargs.setdefault('filter_fn', self.filter)
1379 kwargs.setdefault('show_header', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001380 env = scm.GIT.ApplyEnvVars(kwargs)
agable@chromium.org772efaf2014-04-01 02:35:44 +00001381 cmd = ['git'] + args
Edward Lemur24146be2019-08-01 21:44:52 +00001382 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001383
1384
1385class CipdPackage(object):
1386 """A representation of a single CIPD package."""
1387
John Budorickd3ba72b2018-03-20 12:27:42 -07001388 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001389 self._authority_for_subdir = authority_for_subdir
1390 self._name = name
1391 self._version = version
1392
1393 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001394 def authority_for_subdir(self):
1395 """Whether this package has authority to act on behalf of its subdir.
1396
1397 Some operations should only be performed once per subdirectory. A package
1398 that has authority for its subdirectory is the only package that should
1399 perform such operations.
1400
1401 Returns:
1402 bool; whether this package has subdir authority.
1403 """
1404 return self._authority_for_subdir
1405
1406 @property
1407 def name(self):
1408 return self._name
1409
1410 @property
1411 def version(self):
1412 return self._version
1413
1414
1415class CipdRoot(object):
1416 """A representation of a single CIPD root."""
1417 def __init__(self, root_dir, service_url):
1418 self._all_packages = set()
1419 self._mutator_lock = threading.Lock()
1420 self._packages_by_subdir = collections.defaultdict(list)
1421 self._root_dir = root_dir
1422 self._service_url = service_url
1423
1424 def add_package(self, subdir, package, version):
1425 """Adds a package to this CIPD root.
1426
1427 As far as clients are concerned, this grants both root and subdir authority
1428 to packages arbitrarily. (The implementation grants root authority to the
1429 first package added and subdir authority to the first package added for that
1430 subdir, but clients should not depend on or expect that behavior.)
1431
1432 Args:
1433 subdir: str; relative path to where the package should be installed from
1434 the cipd root directory.
1435 package: str; the cipd package name.
1436 version: str; the cipd package version.
1437 Returns:
1438 CipdPackage; the package that was created and added to this root.
1439 """
1440 with self._mutator_lock:
1441 cipd_package = CipdPackage(
1442 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001443 not self._packages_by_subdir[subdir])
1444 self._all_packages.add(cipd_package)
1445 self._packages_by_subdir[subdir].append(cipd_package)
1446 return cipd_package
1447
1448 def packages(self, subdir):
1449 """Get the list of configured packages for the given subdir."""
1450 return list(self._packages_by_subdir[subdir])
1451
1452 def clobber(self):
1453 """Remove the .cipd directory.
1454
1455 This is useful for forcing ensure to redownload and reinitialize all
1456 packages.
1457 """
1458 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001459 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001460 try:
1461 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1462 except OSError:
1463 if os.path.exists(cipd_cache_dir):
1464 raise
1465
1466 @contextlib.contextmanager
1467 def _create_ensure_file(self):
1468 try:
1469 ensure_file = None
1470 with tempfile.NamedTemporaryFile(
Raul Tambreb946b232019-03-26 14:48:46 +00001471 suffix='.ensure', delete=False, mode='w') as ensure_file:
John Budorick302bb842018-07-17 23:49:17 +00001472 ensure_file.write('$ParanoidMode CheckPresence\n\n')
Raul Tambreb946b232019-03-26 14:48:46 +00001473 for subdir, packages in sorted(self._packages_by_subdir.items()):
John Budorick0f7b2002018-01-19 15:46:17 -08001474 ensure_file.write('@Subdir %s\n' % subdir)
Edward Lemurfbb06aa2018-06-11 20:43:06 +00001475 for package in sorted(packages, key=lambda p: p.name):
John Budorick0f7b2002018-01-19 15:46:17 -08001476 ensure_file.write('%s %s\n' % (package.name, package.version))
1477 ensure_file.write('\n')
1478 yield ensure_file.name
1479 finally:
1480 if ensure_file is not None and os.path.exists(ensure_file.name):
1481 os.remove(ensure_file.name)
1482
1483 def ensure(self):
1484 """Run `cipd ensure`."""
1485 with self._mutator_lock:
1486 with self._create_ensure_file() as ensure_file:
1487 cmd = [
1488 'cipd', 'ensure',
1489 '-log-level', 'error',
1490 '-root', self.root_dir,
1491 '-ensure-file', ensure_file,
1492 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001493 gclient_utils.CheckCallAndFilter(
1494 cmd, print_stdout=True, show_header=True)
John Budorick0f7b2002018-01-19 15:46:17 -08001495
John Budorickd3ba72b2018-03-20 12:27:42 -07001496 def run(self, command):
1497 if command == 'update':
1498 self.ensure()
1499 elif command == 'revert':
1500 self.clobber()
1501 self.ensure()
1502
John Budorick0f7b2002018-01-19 15:46:17 -08001503 def created_package(self, package):
1504 """Checks whether this root created the given package.
1505
1506 Args:
1507 package: CipdPackage; the package to check.
1508 Returns:
1509 bool; whether this root created the given package.
1510 """
1511 return package in self._all_packages
1512
1513 @property
1514 def root_dir(self):
1515 return self._root_dir
1516
1517 @property
1518 def service_url(self):
1519 return self._service_url
1520
1521
1522class CipdWrapper(SCMWrapper):
1523 """Wrapper for CIPD.
1524
1525 Currently only supports chrome-infra-packages.appspot.com.
1526 """
John Budorick3929e9e2018-02-04 18:18:07 -08001527 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001528
1529 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1530 out_cb=None, root=None, package=None):
1531 super(CipdWrapper, self).__init__(
1532 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1533 out_cb=out_cb)
1534 assert root.created_package(package)
1535 self._package = package
1536 self._root = root
1537
1538 #override
1539 def GetCacheMirror(self):
1540 return None
1541
1542 #override
1543 def GetActualRemoteURL(self, options):
1544 return self._root.service_url
1545
1546 #override
1547 def DoesRemoteURLMatch(self, options):
1548 del options
1549 return True
1550
1551 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001552 """Does nothing.
1553
1554 CIPD packages should be reverted at the root by running
1555 `CipdRoot.run('revert')`.
1556 """
1557 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001558
1559 def diff(self, options, args, file_list):
1560 """CIPD has no notion of diffing."""
1561 pass
1562
1563 def pack(self, options, args, file_list):
1564 """CIPD has no notion of diffing."""
1565 pass
1566
1567 def revinfo(self, options, args, file_list):
1568 """Grab the instance ID."""
1569 try:
1570 tmpdir = tempfile.mkdtemp()
1571 describe_json_path = os.path.join(tmpdir, 'describe.json')
1572 cmd = [
1573 'cipd', 'describe',
1574 self._package.name,
1575 '-log-level', 'error',
1576 '-version', self._package.version,
1577 '-json-output', describe_json_path
1578 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001579 gclient_utils.CheckCallAndFilter(cmd)
John Budorick0f7b2002018-01-19 15:46:17 -08001580 with open(describe_json_path) as f:
1581 describe_json = json.load(f)
1582 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1583 finally:
1584 gclient_utils.rmtree(tmpdir)
1585
1586 def status(self, options, args, file_list):
1587 pass
1588
1589 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001590 """Does nothing.
1591
1592 CIPD packages should be updated at the root by running
1593 `CipdRoot.run('update')`.
1594 """
1595 pass