blob: eabafa56d90f2d4e5929009fe6d2f2fcae2ab712 [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))
Edward Lemur3acbc742019-05-30 17:57:35 +0000420 self.Print('Current dir is %r' % self.checkout_path)
Edward Lesmesc621b212018-03-21 20:26:56 -0400421 self._Capture(['reset', '--hard'])
danakjd5c0b562019-11-08 17:27:47 +0000422 self._Capture(['fetch', '--no-tags', patch_repo, patch_rev])
Edward Lemurca7d8812018-07-24 17:42:45 +0000423 patch_rev = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400424
Edward Lemur3acbc742019-05-30 17:57:35 +0000425 if not options.rebase_patch_ref:
426 self._Capture(['checkout', patch_rev])
Robert Iannuccic39a7782019-11-01 18:30:33 +0000427 # Adjust base_rev to be the first parent of our checked out patch ref;
428 # This will allow us to correctly extend `file_list`, and will show the
429 # correct file-list to programs which do `git diff --cached` expecting to
430 # see the patch diff.
431 base_rev = self._Capture(['rev-parse', patch_rev+'~'])
432
Edward Lemur3acbc742019-05-30 17:57:35 +0000433 else:
Robert Iannuccic39a7782019-11-01 18:30:33 +0000434 self.Print('Will cherrypick %r .. %r on top of %r.' % (
435 target_rev, patch_rev, base_rev))
Edward Lemur3acbc742019-05-30 17:57:35 +0000436 try:
437 if scm.GIT.IsAncestor(self.checkout_path, patch_rev, target_rev):
438 # If |patch_rev| is an ancestor of |target_rev|, check it out.
Edward Lemurca7d8812018-07-24 17:42:45 +0000439 self._Capture(['checkout', patch_rev])
440 else:
441 # If a change was uploaded on top of another change, which has already
442 # landed, one of the commits in the cherry-pick range will be
443 # redundant, since it has already landed and its changes incorporated
444 # in the tree.
445 # We pass '--keep-redundant-commits' to ignore those changes.
Edward Lemur3acbc742019-05-30 17:57:35 +0000446 self._Capture(['cherry-pick', target_rev + '..' + patch_rev,
Edward Lemurca7d8812018-07-24 17:42:45 +0000447 '--keep-redundant-commits'])
448
Edward Lemur3acbc742019-05-30 17:57:35 +0000449 except subprocess2.CalledProcessError as e:
450 self.Print('Failed to apply patch.')
451 self.Print('Revision to patch was %r @ %r.' % (patch_repo, patch_rev))
452 self.Print('Tried to cherrypick %r .. %r on top of %r.' % (
453 target_rev, patch_rev, base_rev))
454 self.Print('Current dir is %r' % self.checkout_path)
455 self.Print('git returned non-zero exit status %s:\n%s' % (
Edward Lemur979fa782019-08-13 22:44:05 +0000456 e.returncode, e.stderr.decode('utf-8')))
Edward Lemur3acbc742019-05-30 17:57:35 +0000457 # Print the current status so that developers know what changes caused
458 # the patch failure, since git cherry-pick doesn't show that
459 # information.
460 self.Print(self._Capture(['status']))
461 try:
462 self._Capture(['cherry-pick', '--abort'])
463 except subprocess2.CalledProcessError:
464 pass
465 raise
Edward Lemurca7d8812018-07-24 17:42:45 +0000466
Edward Lemur3acbc742019-05-30 17:57:35 +0000467 if file_list is not None:
468 file_list.extend(self._GetDiffFilenames(base_rev))
Edward Lemurca7d8812018-07-24 17:42:45 +0000469
Edward Lesmesc621b212018-03-21 20:26:56 -0400470 if options.reset_patch_ref:
471 self._Capture(['reset', '--soft', base_rev])
472
msb@chromium.orge28e4982009-09-25 20:51:45 +0000473 def update(self, options, args, file_list):
474 """Runs git to update or transparently checkout the working copy.
475
476 All updated files will be appended to file_list.
477
478 Raises:
479 Error: if can't get URL for relative path.
480 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000481 if args:
482 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
483
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000484 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000485
John Budorick882c91e2018-07-12 22:11:41 +0000486 # If a dependency is not pinned, track the default remote branch.
487 default_rev = 'refs/remotes/%s/master' % self.remote
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000488 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000489 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000490 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000491 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000492 # Override the revision number.
493 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000494 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000495 # Check again for a revision in case an initial ref was specified
496 # in the url, for example bla.git@refs/heads/custombranch
497 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000498 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000499 if not revision:
500 revision = default_rev
msb@chromium.orge28e4982009-09-25 20:51:45 +0000501
szager@chromium.org8a139702014-06-20 15:55:01 +0000502 if managed:
503 self._DisableHooks()
504
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000505 printed_path = False
506 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000507 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700508 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000509 verbose = ['--verbose']
510 printed_path = True
511
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000512 revision_ref = revision
513 if ':' in revision:
514 revision_ref, _, revision = revision.partition(':')
515
516 mirror = self._GetMirror(url, options, revision_ref)
517 if mirror:
518 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000519
John Budorick882c91e2018-07-12 22:11:41 +0000520 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
521 if remote_ref:
522 # Rewrite remote refs to their local equivalents.
523 revision = ''.join(remote_ref)
524 rev_type = "branch"
525 elif revision.startswith('refs/'):
526 # Local branch? We probably don't want to support, since DEPS should
527 # always specify branches as they are in the upstream repo.
528 rev_type = "branch"
529 else:
530 # hash is also a tag, only make a distinction at checkout
531 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000532
primiano@chromium.org1c127382015-02-17 11:15:40 +0000533 # If we are going to introduce a new project, there is a possibility that
534 # we are syncing back to a state where the project was originally a
535 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
536 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
537 # In such case, we might have a backup of the former .git folder, which can
538 # be used to avoid re-fetching the entire repo again (useful for bisects).
539 backup_dir = self.GetGitBackupDirPath()
540 target_dir = os.path.join(self.checkout_path, '.git')
541 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
542 gclient_utils.safe_makedirs(self.checkout_path)
543 os.rename(backup_dir, target_dir)
544 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800545 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000546
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000547 if (not os.path.exists(self.checkout_path) or
548 (os.path.isdir(self.checkout_path) and
549 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000550 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000551 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000552 try:
John Budorick882c91e2018-07-12 22:11:41 +0000553 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000554 except subprocess2.CalledProcessError:
555 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000556 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000557 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800558 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000559 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000560 file_list.extend(
Edward Lemur979fa782019-08-13 22:44:05 +0000561 [os.path.join(self.checkout_path, f) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000562 if mirror:
563 self._Capture(
564 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000565 if not verbose:
566 # Make the output a little prettier. It's nice to have some whitespace
567 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000568 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000569 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000570
John Budorick21a51b32018-09-19 19:39:20 +0000571 if mirror:
572 self._Capture(
573 ['remote', 'set-url', '--push', 'origin', mirror.url])
574
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000575 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000576 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000577 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
578 return self._Capture(['rev-parse', '--verify', 'HEAD'])
579
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000580 self._maybe_break_locks(options)
581
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000582 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000583 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000584
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000585 # See if the url has changed (the unittests use git://foo for the url, let
586 # that through).
587 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
588 return_early = False
589 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
590 # unit test pass. (and update the comment above)
591 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
592 # This allows devs to use experimental repos which have a different url
593 # but whose branch(s) are the same as official repos.
Raul Tambrecd862e32019-05-10 21:19:00 +0000594 if (current_url.rstrip('/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000595 subprocess2.capture(
Aaron Gableac9b0f32019-04-18 17:38:37 +0000596 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000597 cwd=self.checkout_path).strip() != 'False'):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000598 self.Print('_____ switching %s to a new upstream' % self.relpath)
iannucci@chromium.org78514212014-08-20 23:08:00 +0000599 if not (options.force or options.reset):
600 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700601 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000602 # Switch over to the new upstream
603 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000604 if mirror:
605 with open(os.path.join(
606 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
607 'w') as fh:
608 fh.write(os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000609 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
610 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000611
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000612 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000613 else:
John Budorick882c91e2018-07-12 22:11:41 +0000614 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000615
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000616 if return_early:
617 return self._Capture(['rev-parse', '--verify', 'HEAD'])
618
msb@chromium.org5bde4852009-12-14 16:47:12 +0000619 cur_branch = self._GetCurrentBranch()
620
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000621 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000622 # 0) HEAD is detached. Probably from our initial clone.
623 # - make sure HEAD is contained by a named ref, then update.
624 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700625 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000626 # - try to rebase onto the new hash or branch
627 # 2) current branch is tracking a remote branch with local committed
628 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000629 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000630 # 3) current branch is tracking a remote branch w/or w/out changes, and
631 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000632 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000633 # 4) current branch is tracking a remote branch, but DEPS switches to a
634 # different remote branch, and
635 # a) current branch has no local changes, and --force:
636 # - checkout new branch
637 # b) current branch has local changes, and --force and --reset:
638 # - checkout new branch
639 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000640
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000641 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
642 # a tracking branch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000643 # or 'master' if not a tracking branch (it's based on a specific rev/hash)
644 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000645 if cur_branch is None:
646 upstream_branch = None
647 current_type = "detached"
648 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000649 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000650 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
651 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
652 current_type = "hash"
653 logging.debug("Current branch is not tracking an upstream (remote)"
654 " branch.")
655 elif upstream_branch.startswith('refs/remotes'):
656 current_type = "branch"
657 else:
658 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000659
Edward Lemur579c9862018-07-13 23:17:51 +0000660 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000661
Michael Spang73fac912019-03-08 18:44:19 +0000662 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000663 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000664 self._Fetch(options, prune=options.force)
665
666 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
667 sha_only=True):
668 # Update the remotes first so we have all the refs.
669 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
670 cwd=self.checkout_path)
671 if verbose:
672 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000673
John Budorick882c91e2018-07-12 22:11:41 +0000674 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200675
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000676 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000677 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000678 target = 'HEAD'
679 if options.upstream and upstream_branch:
680 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800681 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000682
msb@chromium.org786fb682010-06-02 15:16:23 +0000683 if current_type == 'detached':
684 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800685 # We just did a Scrub, this is as clean as it's going to get. In
686 # particular if HEAD is a commit that contains two versions of the same
687 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
688 # to actually "Clean" the checkout; that commit is uncheckoutable on this
689 # system. The best we can do is carry forward to the checkout step.
690 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000691 self._CheckClean(revision)
692 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000693 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000694 self.Print('Up-to-date; skipping checkout.')
695 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000696 # 'git checkout' may need to overwrite existing untracked files. Allow
697 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000698 self._Checkout(
699 options,
John Budorick882c91e2018-07-12 22:11:41 +0000700 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000701 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000702 quiet=True,
703 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000704 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000705 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000706 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000707 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700708 # Can't find a merge-base since we don't know our upstream. That makes
709 # this command VERY likely to produce a rebase failure. For now we
710 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000711 upstream_branch = self.remote
712 if options.revision or deps_revision:
713 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700714 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700715 printed_path=printed_path, merge=options.merge)
716 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000717 elif rev_type == 'hash':
718 # case 2
719 self._AttemptRebase(upstream_branch, file_list, options,
720 newbase=revision, printed_path=printed_path,
721 merge=options.merge)
722 printed_path = True
723 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000724 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000725 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000726 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000727 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000728 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000729 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000730 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000731 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
732 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000733 force_switch = False
734 if options.force:
735 try:
John Budorick882c91e2018-07-12 22:11:41 +0000736 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000737 # case 4a
738 force_switch = True
739 except gclient_utils.Error as e:
740 if options.reset:
741 # case 4b
742 force_switch = True
743 else:
744 switch_error = '%s\n%s' % (e.message, switch_error)
745 if force_switch:
746 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000747 (upstream_branch, new_base))
748 switch_branch = 'gclient_' + remote_ref[1]
749 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000750 self._Checkout(options, switch_branch, force=True, quiet=True)
751 else:
752 # case 4c
753 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000754 else:
755 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800756 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000757 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000758 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000759 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000760 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000761 if options.merge:
762 merge_args.append('--ff')
763 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000764 merge_args.append('--ff-only')
765 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000766 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000767 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700768 rebase_files = []
Edward Lemur979fa782019-08-13 22:44:05 +0000769 if re.match(b'fatal: Not possible to fast-forward, aborting.',
770 e.stderr):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000771 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000772 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700773 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000774 printed_path = True
775 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000776 if not options.auto_rebase:
777 try:
778 action = self._AskForData(
779 'Cannot %s, attempt to rebase? '
780 '(y)es / (q)uit / (s)kip : ' %
781 ('merge' if options.merge else 'fast-forward merge'),
782 options)
783 except ValueError:
784 raise gclient_utils.Error('Invalid Character')
785 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700786 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000787 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000788 printed_path = True
789 break
790 elif re.match(r'quit|q', action, re.I):
791 raise gclient_utils.Error("Can't fast-forward, please merge or "
792 "rebase manually.\n"
793 "cd %s && git " % self.checkout_path
794 + "rebase %s" % upstream_branch)
795 elif re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000796 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000797 return
798 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000799 self.Print('Input not recognized')
Edward Lemur979fa782019-08-13 22:44:05 +0000800 elif re.match(b"error: Your local changes to '.*' would be "
801 b"overwritten by merge. Aborting.\nPlease, commit your "
802 b"changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000803 e.stderr):
804 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000805 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700806 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000807 printed_path = True
Edward Lemur979fa782019-08-13 22:44:05 +0000808 raise gclient_utils.Error(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000809 else:
810 # Some other problem happened with the merge
811 logging.error("Error during fast-forward merge in %s!" % self.relpath)
Edward Lemur979fa782019-08-13 22:44:05 +0000812 self.Print(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000813 raise
814 else:
815 # Fast-forward merge was successful
816 if not re.match('Already up-to-date.', merge_output) or verbose:
817 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700818 self.Print('_____ %s at %s' % (self.relpath, revision),
819 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000820 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000821 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000822 if not verbose:
823 # Make the output a little prettier. It's nice to have some
824 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000825 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000826
agablec3937b92016-10-25 10:13:03 -0700827 if file_list is not None:
828 file_list.extend(
829 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000830
831 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000832 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700833 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000834 '\nConflict while rebasing this branch.\n'
835 'Fix the conflict and run gclient again.\n'
836 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700837 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000838
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000839 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000840 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
841 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000842
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000843 # If --reset and --delete_unversioned_trees are specified, remove any
844 # untracked directories.
845 if options.reset and options.delete_unversioned_trees:
846 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
847 # merge-base by default), so doesn't include untracked files. So we use
848 # 'git ls-files --directory --others --exclude-standard' here directly.
849 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800850 ['-c', 'core.quotePath=false', 'ls-files',
851 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000852 self.checkout_path)
853 for path in (p for p in paths.splitlines() if p.endswith('/')):
854 full_path = os.path.join(self.checkout_path, path)
855 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000856 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000857 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000858
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000859 return self._Capture(['rev-parse', '--verify', 'HEAD'])
860
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000861 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000862 """Reverts local modifications.
863
864 All reverted files will be appended to file_list.
865 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000866 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000867 # revert won't work if the directory doesn't exist. It needs to
868 # checkout instead.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000869 self.Print('_____ %s is missing, synching instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000870 # Don't reuse the args.
871 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000872
873 default_rev = "refs/heads/master"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000874 if options.upstream:
875 if self._GetCurrentBranch():
876 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
877 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000878 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000879 if not deps_revision:
880 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000881 if deps_revision.startswith('refs/heads/'):
882 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700883 try:
884 deps_revision = self.GetUsableRev(deps_revision, options)
885 except NoUsableRevError as e:
886 # If the DEPS entry's url and hash changed, try to update the origin.
887 # See also http://crbug.com/520067.
888 logging.warn(
889 'Couldn\'t find usable revision, will retrying to update instead: %s',
890 e.message)
891 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000892
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000893 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800894 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000895
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800896 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000897 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000898
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000899 if file_list is not None:
900 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
901
902 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000903 """Returns revision"""
904 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000905
msb@chromium.orge28e4982009-09-25 20:51:45 +0000906 def runhooks(self, options, args, file_list):
907 self.status(options, args, file_list)
908
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000909 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000910 """Display status information."""
911 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000912 self.Print('________ couldn\'t run status in %s:\n'
913 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000914 else:
Anthony Politobb457342019-11-15 22:26:01 +0000915 merge_base = []
916 if self.url:
917 _, base_rev = gclient_utils.SplitUrlRevision(self.url)
918 if base_rev:
919 merge_base = [base_rev]
Aaron Gablef4068aa2017-12-12 15:14:09 -0800920 self._Run(
921 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
Edward Lemur24146be2019-08-01 21:44:52 +0000922 options, always_show_header=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000923 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800924 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000925 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000926
smutae7ea312016-07-18 11:59:41 -0700927 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700928 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700929 sha1 = None
930 if not os.path.isdir(self.checkout_path):
931 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800932 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700933
934 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
935 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700936 else:
agable41e3a6c2016-10-20 11:36:56 -0700937 # May exist in origin, but we don't have it yet, so fetch and look
938 # again.
939 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -0700940 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
941 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700942
943 if not sha1:
944 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800945 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -0700946
947 return sha1
948
primiano@chromium.org1c127382015-02-17 11:15:40 +0000949 def GetGitBackupDirPath(self):
950 """Returns the path where the .git folder for the current project can be
951 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
952 return os.path.join(self._root_dir,
953 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
954
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000955 def _GetMirror(self, url, options, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000956 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +0000957 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000958 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000959 mirror_kwargs = {
960 'print_func': self.filter,
John Budorick882c91e2018-07-12 22:11:41 +0000961 'refs': []
hinoka@google.comb1b54572014-04-16 22:29:23 +0000962 }
hinoka@google.comb1b54572014-04-16 22:29:23 +0000963 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
964 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000965 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
966 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +0000967 if hasattr(options, 'with_tags') and options.with_tags:
968 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000969 elif revision_ref and revision_ref.startswith('refs/tags/'):
970 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000971 return git_cache.Mirror(url, **mirror_kwargs)
972
John Budorick882c91e2018-07-12 22:11:41 +0000973 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800974 """Update a git mirror by fetching the latest commits from the remote,
975 unless mirror already contains revision whose type is sha1 hash.
976 """
John Budorick882c91e2018-07-12 22:11:41 +0000977 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800978 if options.verbose:
979 self.Print('skipping mirror update, it has rev=%s already' % revision,
980 timestamp=False)
981 return
982
szager@chromium.org3ec84f62014-08-22 21:00:22 +0000983 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000984 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000985 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000986 depth = 10
987 else:
988 depth = 10000
989 else:
990 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +0000991 mirror.populate(verbose=options.verbose,
992 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +0000993 depth=depth,
994 ignore_lock=getattr(options, 'ignore_locks', False),
995 lock_timeout=getattr(options, 'lock_timeout', 0))
996 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000997
John Budorick882c91e2018-07-12 22:11:41 +0000998 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000999 """Clone a git repository from the given URL.
1000
msb@chromium.org786fb682010-06-02 15:16:23 +00001001 Once we've cloned the repo, we checkout a working branch if the specified
1002 revision is a branch head. If it is a tag or a specific commit, then we
1003 leave HEAD detached as it makes future updates simpler -- in this case the
1004 user should first create a new branch or switch to an existing branch before
1005 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001006 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001007 # git clone doesn't seem to insert a newline properly before printing
1008 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001009 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +00001010 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +00001011 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001012 if self.cache_dir:
1013 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001014 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001015 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001016 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001017 # If the parent directory does not exist, Git clone on Windows will not
1018 # create it, so we need to do it manually.
1019 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001020 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001021
1022 template_dir = None
1023 if hasattr(options, 'no_history') and options.no_history:
John Budorick882c91e2018-07-12 22:11:41 +00001024 if gclient_utils.IsGitSha(revision):
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001025 # In the case of a subproject, the pinned sha is not necessarily the
1026 # head of the remote branch (so we can't just use --depth=N). Instead,
1027 # we tell git to fetch all the remote objects from SHA..HEAD by means of
1028 # a template git dir which has a 'shallow' file pointing to the sha.
1029 template_dir = tempfile.mkdtemp(
1030 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
1031 dir=parent_dir)
1032 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
1033 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
1034 template_file.write(revision)
1035 clone_cmd.append('--template=' + template_dir)
1036 else:
1037 # Otherwise, we're just interested in the HEAD. Just use --depth.
1038 clone_cmd.append('--depth=1')
1039
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001040 tmp_dir = tempfile.mkdtemp(
1041 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
1042 dir=parent_dir)
1043 try:
1044 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +01001045 if self.print_outbuf:
1046 print_stdout = True
Edward Lemur24146be2019-08-01 21:44:52 +00001047 filter_fn = None
Edward Lemur231f5ea2018-01-31 19:02:36 +01001048 else:
1049 print_stdout = False
Edward Lemur24146be2019-08-01 21:44:52 +00001050 filter_fn = self.filter
Edward Lemur231f5ea2018-01-31 19:02:36 +01001051 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
Edward Lemur24146be2019-08-01 21:44:52 +00001052 print_stdout=print_stdout, filter_fn=filter_fn)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001053 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +00001054 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1055 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001056 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001057 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001058 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001059 finally:
1060 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001061 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001062 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001063 if template_dir:
1064 gclient_utils.rmtree(template_dir)
Edward Lemur579c9862018-07-13 23:17:51 +00001065 self._SetFetchConfig(options)
1066 self._Fetch(options, prune=options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001067 revision = self._AutoFetchRef(options, revision)
1068 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1069 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001070 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001071 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001072 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +00001073 ('Checked out %s to a detached HEAD. Before making any commits\n'
1074 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
1075 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
John Budorick882c91e2018-07-12 22:11:41 +00001076 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001077
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001078 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001079 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001080 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001081 raise gclient_utils.Error("Background task requires input. Rerun "
1082 "gclient with --jobs=1 so that\n"
1083 "interaction is possible.")
1084 try:
1085 return raw_input(prompt)
1086 except KeyboardInterrupt:
1087 # Hide the exception.
1088 sys.exit(1)
1089
1090
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001091 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001092 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001093 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001094 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001095 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001096 revision = upstream
1097 if newbase:
1098 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001099 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001100 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001101 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001102 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001103 printed_path = True
1104 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001105 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001106
1107 if merge:
1108 merge_output = self._Capture(['merge', revision])
1109 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001110 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001111 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001112
1113 # Build the rebase command here using the args
1114 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1115 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001116 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001117 rebase_cmd.append('--verbose')
1118 if newbase:
1119 rebase_cmd.extend(['--onto', newbase])
1120 rebase_cmd.append(upstream)
1121 if branch:
1122 rebase_cmd.append(branch)
1123
1124 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001125 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001126 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001127 if (re.match(br'cannot rebase: you have unstaged changes', e.stderr) or
1128 re.match(br'cannot rebase: your index contains uncommitted changes',
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001129 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001130 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001131 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001132 'Cannot rebase because of unstaged changes.\n'
1133 '\'git reset --hard HEAD\' ?\n'
1134 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001135 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001136 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001137 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001138 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001139 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001140 break
1141 elif re.match(r'quit|q', rebase_action, re.I):
1142 raise gclient_utils.Error("Please merge or rebase manually\n"
1143 "cd %s && git " % self.checkout_path
1144 + "%s" % ' '.join(rebase_cmd))
1145 elif re.match(r'show|s', rebase_action, re.I):
Edward Lemur979fa782019-08-13 22:44:05 +00001146 self.Print('%s' % e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001147 continue
1148 else:
1149 gclient_utils.Error("Input not recognized")
1150 continue
Edward Lemur979fa782019-08-13 22:44:05 +00001151 elif re.search(br'^CONFLICT', e.stdout, re.M):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001152 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1153 "Fix the conflict and run gclient again.\n"
1154 "See 'man git-rebase' for details.\n")
1155 else:
Edward Lemur979fa782019-08-13 22:44:05 +00001156 self.Print(e.stdout.decode('utf-8').strip())
1157 self.Print('Rebase produced error output:\n%s' %
1158 e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001159 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1160 "manually.\ncd %s && git " %
1161 self.checkout_path
1162 + "%s" % ' '.join(rebase_cmd))
1163
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001164 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001165 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001166 # Make the output a little prettier. It's nice to have some
1167 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001168 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001169
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001170 @staticmethod
1171 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001172 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1173 if not ok:
1174 raise gclient_utils.Error('git version %s < minimum required %s' %
1175 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001176
John Budorick882c91e2018-07-12 22:11:41 +00001177 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001178 # Special case handling if all 3 conditions are met:
1179 # * the mirros have recently changed, but deps destination remains same,
1180 # * the git histories of mirrors are conflicting.
1181 # * git cache is used
1182 # This manifests itself in current checkout having invalid HEAD commit on
1183 # most git operations. Since git cache is used, just deleted the .git
1184 # folder, and re-create it by cloning.
1185 try:
1186 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1187 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001188 if (b'fatal: bad object HEAD' in e.stderr
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001189 and self.cache_dir and self.cache_dir in url):
1190 self.Print((
1191 'Likely due to DEPS change with git cache_dir, '
1192 'the current commit points to no longer existing object.\n'
1193 '%s' % e)
1194 )
1195 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001196 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001197 else:
1198 raise
1199
msb@chromium.org786fb682010-06-02 15:16:23 +00001200 def _IsRebasing(self):
1201 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1202 # have a plumbing command to determine whether a rebase is in progress, so
1203 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1204 g = os.path.join(self.checkout_path, '.git')
1205 return (
1206 os.path.isdir(os.path.join(g, "rebase-merge")) or
1207 os.path.isdir(os.path.join(g, "rebase-apply")))
1208
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001209 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001210 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1211 if os.path.exists(lockfile):
1212 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001213 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001214 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1215 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001216 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001217
msb@chromium.org786fb682010-06-02 15:16:23 +00001218 # Make sure the tree is clean; see git-rebase.sh for reference
1219 try:
1220 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001221 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001222 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001223 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001224 '\tYou have unstaged changes.\n'
1225 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001226 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001227 try:
1228 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001229 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001230 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001231 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001232 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001233 '\tYour index contains uncommitted changes\n'
1234 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001235 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001236
agable83faed02016-10-24 14:37:10 -07001237 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001238 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1239 # reference by a commit). If not, error out -- most likely a rebase is
1240 # in progress, try to detect so we can give a better error.
1241 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001242 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1243 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001244 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001245 # Commit is not contained by any rev. See if the user is rebasing:
1246 if self._IsRebasing():
1247 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001248 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001249 '\tAlready in a conflict, i.e. (no branch).\n'
1250 '\tFix the conflict and run gclient again.\n'
1251 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1252 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001253 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001254 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001255 name = ('saved-by-gclient-' +
1256 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001257 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001258 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001259 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001260
msb@chromium.org5bde4852009-12-14 16:47:12 +00001261 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001262 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001263 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001264 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001265 return None
1266 return branch
1267
borenet@google.comc3e09d22014-04-10 13:58:18 +00001268 def _Capture(self, args, **kwargs):
Mike Frysinger286fb162019-09-30 03:14:10 +00001269 set_git_dir = 'cwd' not in kwargs
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001270 kwargs.setdefault('cwd', self.checkout_path)
1271 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001272 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001273 env = scm.GIT.ApplyEnvVars(kwargs)
Mike Frysinger286fb162019-09-30 03:14:10 +00001274 # If an explicit cwd isn't set, then default to the .git/ subdir so we get
1275 # stricter behavior. This can be useful in cases of slight corruption --
1276 # we don't accidentally go corrupting parent git checks too. See
1277 # https://crbug.com/1000825 for an example.
1278 if set_git_dir:
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001279 git_dir = os.path.abspath(os.path.join(self.checkout_path, '.git'))
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001280 # Depending on how the .gclient file was defined, self.checkout_path
1281 # might be set to a unicode string, not a regular string; on Windows
1282 # Python2, we can't set env vars to be unicode strings, so we
1283 # forcibly cast the value to a string before setting it.
1284 env.setdefault('GIT_DIR', str(git_dir))
Raul Tambrecd862e32019-05-10 21:19:00 +00001285 ret = subprocess2.check_output(
1286 ['git'] + args, env=env, **kwargs).decode('utf-8')
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001287 if strip:
1288 ret = ret.strip()
Erik Chene16ffff2019-10-14 20:35:53 +00001289 self.Print('Finished running: %s %s' % ('git', ' '.join(args)))
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001290 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001291
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001292 def _Checkout(self, options, ref, force=False, quiet=None):
1293 """Performs a 'git-checkout' operation.
1294
1295 Args:
1296 options: The configured option set
1297 ref: (str) The branch/commit to checkout
1298 quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
1299 'None', the behavior is inferred from 'options.verbose'.
1300 Returns: (str) The output of the checkout operation
1301 """
1302 if quiet is None:
1303 quiet = (not options.verbose)
1304 checkout_args = ['checkout']
1305 if force:
1306 checkout_args.append('--force')
1307 if quiet:
1308 checkout_args.append('--quiet')
1309 checkout_args.append(ref)
1310 return self._Capture(checkout_args)
1311
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001312 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1313 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001314 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001315 # When updating, the ref is modified to be a remote ref .
1316 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1317 # Try to reverse that mapping.
1318 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1319 if original_ref:
1320 refspec = original_ref + ':' + refspec
1321 # When a mirror is configured, it only fetches
1322 # refs/{heads,branch-heads,tags}/*.
1323 # If asked to fetch other refs, we must fetch those directly from the
1324 # repository, and not from the mirror.
1325 if not original_ref.startswith(
1326 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1327 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001328 fetch_cmd = cfg + [
1329 'fetch',
1330 remote or self.remote,
1331 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001332 if refspec:
1333 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001334
1335 if prune:
1336 fetch_cmd.append('--prune')
1337 if options.verbose:
1338 fetch_cmd.append('--verbose')
danakjd5c0b562019-11-08 17:27:47 +00001339 if not hasattr(options, 'with_tags') or not options.with_tags:
1340 fetch_cmd.append('--no-tags')
dnj@chromium.org680f2172014-06-25 00:39:32 +00001341 elif quiet:
1342 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001343 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001344
1345 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1346 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1347
Edward Lemur579c9862018-07-13 23:17:51 +00001348 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001349 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1350 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001351 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001352 try:
1353 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1354 options)
1355 self._Run(['config', 'remote.%s.fetch' % self.remote,
1356 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1357 except subprocess2.CalledProcessError as e:
1358 # If exit code was 5, it means we attempted to unset a config that
1359 # didn't exist. Ignore it.
1360 if e.returncode != 5:
1361 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001362 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001363 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001364 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1365 '^\\+refs/branch-heads/\\*:.*$']
1366 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001367 if hasattr(options, 'with_tags') and options.with_tags:
1368 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1369 '+refs/tags/*:refs/tags/*',
1370 '^\\+refs/tags/\\*:.*$']
1371 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001372
John Budorick882c91e2018-07-12 22:11:41 +00001373 def _AutoFetchRef(self, options, revision):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001374 """Attempts to fetch |revision| if not available in local repo.
1375
1376 Returns possibly updated revision."""
John Budorick882c91e2018-07-12 22:11:41 +00001377 try:
1378 self._Capture(['rev-parse', revision])
1379 except subprocess2.CalledProcessError:
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001380 self._Fetch(options, refspec=revision)
1381 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1382 return revision
1383
Edward Lemur24146be2019-08-01 21:44:52 +00001384 def _Run(self, args, options, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001385 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001386 kwargs.setdefault('cwd', self.checkout_path)
Edward Lemur24146be2019-08-01 21:44:52 +00001387 kwargs.setdefault('filter_fn', self.filter)
1388 kwargs.setdefault('show_header', True)
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
Edward Lemur24146be2019-08-01 21:44:52 +00001391 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001392
1393
1394class CipdPackage(object):
1395 """A representation of a single CIPD package."""
1396
John Budorickd3ba72b2018-03-20 12:27:42 -07001397 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001398 self._authority_for_subdir = authority_for_subdir
1399 self._name = name
1400 self._version = version
1401
1402 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001403 def authority_for_subdir(self):
1404 """Whether this package has authority to act on behalf of its subdir.
1405
1406 Some operations should only be performed once per subdirectory. A package
1407 that has authority for its subdirectory is the only package that should
1408 perform such operations.
1409
1410 Returns:
1411 bool; whether this package has subdir authority.
1412 """
1413 return self._authority_for_subdir
1414
1415 @property
1416 def name(self):
1417 return self._name
1418
1419 @property
1420 def version(self):
1421 return self._version
1422
1423
1424class CipdRoot(object):
1425 """A representation of a single CIPD root."""
1426 def __init__(self, root_dir, service_url):
1427 self._all_packages = set()
1428 self._mutator_lock = threading.Lock()
1429 self._packages_by_subdir = collections.defaultdict(list)
1430 self._root_dir = root_dir
1431 self._service_url = service_url
1432
1433 def add_package(self, subdir, package, version):
1434 """Adds a package to this CIPD root.
1435
1436 As far as clients are concerned, this grants both root and subdir authority
1437 to packages arbitrarily. (The implementation grants root authority to the
1438 first package added and subdir authority to the first package added for that
1439 subdir, but clients should not depend on or expect that behavior.)
1440
1441 Args:
1442 subdir: str; relative path to where the package should be installed from
1443 the cipd root directory.
1444 package: str; the cipd package name.
1445 version: str; the cipd package version.
1446 Returns:
1447 CipdPackage; the package that was created and added to this root.
1448 """
1449 with self._mutator_lock:
1450 cipd_package = CipdPackage(
1451 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001452 not self._packages_by_subdir[subdir])
1453 self._all_packages.add(cipd_package)
1454 self._packages_by_subdir[subdir].append(cipd_package)
1455 return cipd_package
1456
1457 def packages(self, subdir):
1458 """Get the list of configured packages for the given subdir."""
1459 return list(self._packages_by_subdir[subdir])
1460
1461 def clobber(self):
1462 """Remove the .cipd directory.
1463
1464 This is useful for forcing ensure to redownload and reinitialize all
1465 packages.
1466 """
1467 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001468 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001469 try:
1470 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1471 except OSError:
1472 if os.path.exists(cipd_cache_dir):
1473 raise
1474
1475 @contextlib.contextmanager
1476 def _create_ensure_file(self):
1477 try:
Edward Lesmes05934952019-12-19 20:38:09 +00001478 contents = '$ParanoidMode CheckPresence\n\n'
1479 for subdir, packages in sorted(self._packages_by_subdir.items()):
1480 contents += '@Subdir %s\n' % subdir
1481 for package in sorted(packages, key=lambda p: p.name):
1482 contents += '%s %s\n' % (package.name, package.version)
1483 contents += '\n'
John Budorick0f7b2002018-01-19 15:46:17 -08001484 ensure_file = None
1485 with tempfile.NamedTemporaryFile(
Edward Lesmes05934952019-12-19 20:38:09 +00001486 suffix='.ensure', delete=False, mode='wb') as ensure_file:
1487 ensure_file.write(contents.encode('utf-8', 'replace'))
John Budorick0f7b2002018-01-19 15:46:17 -08001488 yield ensure_file.name
1489 finally:
1490 if ensure_file is not None and os.path.exists(ensure_file.name):
1491 os.remove(ensure_file.name)
1492
1493 def ensure(self):
1494 """Run `cipd ensure`."""
1495 with self._mutator_lock:
1496 with self._create_ensure_file() as ensure_file:
1497 cmd = [
1498 'cipd', 'ensure',
1499 '-log-level', 'error',
1500 '-root', self.root_dir,
1501 '-ensure-file', ensure_file,
1502 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001503 gclient_utils.CheckCallAndFilter(
1504 cmd, print_stdout=True, show_header=True)
John Budorick0f7b2002018-01-19 15:46:17 -08001505
John Budorickd3ba72b2018-03-20 12:27:42 -07001506 def run(self, command):
1507 if command == 'update':
1508 self.ensure()
1509 elif command == 'revert':
1510 self.clobber()
1511 self.ensure()
1512
John Budorick0f7b2002018-01-19 15:46:17 -08001513 def created_package(self, package):
1514 """Checks whether this root created the given package.
1515
1516 Args:
1517 package: CipdPackage; the package to check.
1518 Returns:
1519 bool; whether this root created the given package.
1520 """
1521 return package in self._all_packages
1522
1523 @property
1524 def root_dir(self):
1525 return self._root_dir
1526
1527 @property
1528 def service_url(self):
1529 return self._service_url
1530
1531
1532class CipdWrapper(SCMWrapper):
1533 """Wrapper for CIPD.
1534
1535 Currently only supports chrome-infra-packages.appspot.com.
1536 """
John Budorick3929e9e2018-02-04 18:18:07 -08001537 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001538
1539 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1540 out_cb=None, root=None, package=None):
1541 super(CipdWrapper, self).__init__(
1542 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1543 out_cb=out_cb)
1544 assert root.created_package(package)
1545 self._package = package
1546 self._root = root
1547
1548 #override
1549 def GetCacheMirror(self):
1550 return None
1551
1552 #override
1553 def GetActualRemoteURL(self, options):
1554 return self._root.service_url
1555
1556 #override
1557 def DoesRemoteURLMatch(self, options):
1558 del options
1559 return True
1560
1561 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001562 """Does nothing.
1563
1564 CIPD packages should be reverted at the root by running
1565 `CipdRoot.run('revert')`.
1566 """
1567 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001568
1569 def diff(self, options, args, file_list):
1570 """CIPD has no notion of diffing."""
1571 pass
1572
1573 def pack(self, options, args, file_list):
1574 """CIPD has no notion of diffing."""
1575 pass
1576
1577 def revinfo(self, options, args, file_list):
1578 """Grab the instance ID."""
1579 try:
1580 tmpdir = tempfile.mkdtemp()
1581 describe_json_path = os.path.join(tmpdir, 'describe.json')
1582 cmd = [
1583 'cipd', 'describe',
1584 self._package.name,
1585 '-log-level', 'error',
1586 '-version', self._package.version,
1587 '-json-output', describe_json_path
1588 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001589 gclient_utils.CheckCallAndFilter(cmd)
John Budorick0f7b2002018-01-19 15:46:17 -08001590 with open(describe_json_path) as f:
1591 describe_json = json.load(f)
1592 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1593 finally:
1594 gclient_utils.rmtree(tmpdir)
1595
1596 def status(self, options, args, file_list):
1597 pass
1598
1599 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001600 """Does nothing.
1601
1602 CIPD packages should be updated at the root by running
1603 `CipdRoot.run('update')`.
1604 """
1605 pass