blob: e940a69d2c74a1d60f2e5eeea10c0e3b9faadd47 [file] [log] [blame]
steveblock@chromium.org93567042012-02-15 01:02:26 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00005"""Gclient-specific SCM-specific operations."""
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00006
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00007from __future__ import print_function
8
John Budorick0f7b2002018-01-19 15:46:17 -08009import collections
10import contextlib
borenet@google.comb2256212014-05-07 20:57:28 +000011import errno
John Budorick0f7b2002018-01-19 15:46:17 -080012import json
maruel@chromium.org754960e2009-09-21 12:31:05 +000013import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import os
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000015import posixpath
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000016import re
maruel@chromium.org90541732011-04-01 17:54:18 +000017import sys
ilevy@chromium.org3534aa52013-07-20 01:58:08 +000018import tempfile
John Budorick0f7b2002018-01-19 15:46:17 -080019import threading
zty@chromium.org6279e8a2014-02-13 01:45:25 +000020import traceback
Raul Tambreb946b232019-03-26 14:48:46 +000021
22try:
23 import urlparse
24except ImportError: # For Py3 compatibility
25 import urllib.parse as urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000026
hinoka@google.com2f2ca142014-01-07 03:59:18 +000027import download_from_google_storage
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000028import gclient_utils
szager@chromium.org848fd492014-04-09 19:06:44 +000029import git_cache
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000030import scm
borenet@google.comb2256212014-05-07 20:57:28 +000031import shutil
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000032import subprocess2
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000033
34
szager@chromium.org71cbb502013-04-19 23:30:15 +000035THIS_FILE_PATH = os.path.abspath(__file__)
36
hinoka@google.com2f2ca142014-01-07 03:59:18 +000037GSUTIL_DEFAULT_PATH = os.path.join(
hinoka@chromium.orgb091aa52014-12-20 01:47:31 +000038 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
hinoka@google.com2f2ca142014-01-07 03:59:18 +000039
maruel@chromium.org79d62372015-06-01 18:50:55 +000040
smutae7ea312016-07-18 11:59:41 -070041class NoUsableRevError(gclient_utils.Error):
42 """Raised if requested revision isn't found in checkout."""
43
44
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000045class DiffFiltererWrapper(object):
46 """Simple base class which tracks which file is being diffed and
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000047 replaces instances of its file name in the original and
agable41e3a6c2016-10-20 11:36:56 -070048 working copy lines of the git diff output."""
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000049 index_string = None
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000050 original_prefix = "--- "
51 working_prefix = "+++ "
52
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000053 def __init__(self, relpath, print_func):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000054 # Note that we always use '/' as the path separator to be
agable41e3a6c2016-10-20 11:36:56 -070055 # consistent with cygwin-style output on Windows
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000056 self._relpath = relpath.replace("\\", "/")
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000057 self._current_file = None
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000058 self._print_func = print_func
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000059
maruel@chromium.org6e29d572010-06-04 17:32:20 +000060 def SetCurrentFile(self, current_file):
61 self._current_file = current_file
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000062
iannucci@chromium.org3830a672013-02-19 20:15:14 +000063 @property
64 def _replacement_file(self):
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000065 return posixpath.join(self._relpath, self._current_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000066
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000067 def _Replace(self, line):
68 return line.replace(self._current_file, self._replacement_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000069
70 def Filter(self, line):
71 if (line.startswith(self.index_string)):
72 self.SetCurrentFile(line[len(self.index_string):])
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000073 line = self._Replace(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000074 else:
75 if (line.startswith(self.original_prefix) or
76 line.startswith(self.working_prefix)):
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000077 line = self._Replace(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000078 self._print_func(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000079
80
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000081class GitDiffFilterer(DiffFiltererWrapper):
82 index_string = "diff --git "
83
84 def SetCurrentFile(self, current_file):
85 # Get filename by parsing "a/<filename> b/<filename>"
86 self._current_file = current_file[:(len(current_file)/2)][2:]
87
88 def _Replace(self, line):
89 return re.sub("[a|b]/" + self._current_file, self._replacement_file, line)
90
91
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000092# SCMWrapper base class
93
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000094class SCMWrapper(object):
95 """Add necessary glue between all the supported SCM.
96
msb@chromium.orgd6504212010-01-13 17:34:31 +000097 This is the abstraction layer to bind to different SCM.
98 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000099 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
Edward Lemur231f5ea2018-01-31 19:02:36 +0100100 out_cb=None, print_outbuf=False):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000101 self.url = url
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +0000102 self._root_dir = root_dir
103 if self._root_dir:
104 self._root_dir = self._root_dir.replace('/', os.sep)
105 self.relpath = relpath
106 if self.relpath:
107 self.relpath = self.relpath.replace('/', os.sep)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000108 if self.relpath and self._root_dir:
109 self.checkout_path = os.path.join(self._root_dir, self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000110 if out_fh is None:
111 out_fh = sys.stdout
112 self.out_fh = out_fh
113 self.out_cb = out_cb
Edward Lemur231f5ea2018-01-31 19:02:36 +0100114 self.print_outbuf = print_outbuf
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000115
116 def Print(self, *args, **kwargs):
117 kwargs.setdefault('file', self.out_fh)
118 if kwargs.pop('timestamp', True):
119 self.out_fh.write('[%s] ' % gclient_utils.Elapsed())
120 print(*args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000121
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000122 def RunCommand(self, command, options, args, file_list=None):
agabledebf6c82016-12-21 12:50:12 -0800123 commands = ['update', 'updatesingle', 'revert',
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000124 'revinfo', 'status', 'diff', 'pack', 'runhooks']
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000125
126 if not command in commands:
127 raise gclient_utils.Error('Unknown command %s' % command)
128
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000129 if not command in dir(self):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000130 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % (
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000131 command, self.__class__.__name__))
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000132
133 return getattr(self, command)(options, args, file_list)
134
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000135 @staticmethod
136 def _get_first_remote_url(checkout_path):
137 log = scm.GIT.Capture(
138 ['config', '--local', '--get-regexp', r'remote.*.url'],
139 cwd=checkout_path)
140 # Get the second token of the first line of the log.
141 return log.splitlines()[0].split(' ', 1)[1]
142
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000143 def GetCacheMirror(self):
Robert Iannuccia19649b2018-06-29 16:31:45 +0000144 if getattr(self, 'cache_dir', None):
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000145 url, _ = gclient_utils.SplitUrlRevision(self.url)
146 return git_cache.Mirror(url)
147 return None
148
smut@google.comd33eab32014-07-07 19:35:18 +0000149 def GetActualRemoteURL(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000150 """Attempt to determine the remote URL for this SCMWrapper."""
smut@google.comd33eab32014-07-07 19:35:18 +0000151 # Git
borenet@google.combda475e2014-03-24 19:04:45 +0000152 if os.path.exists(os.path.join(self.checkout_path, '.git')):
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000153 actual_remote_url = self._get_first_remote_url(self.checkout_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000154
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000155 mirror = self.GetCacheMirror()
156 # If the cache is used, obtain the actual remote URL from there.
157 if (mirror and mirror.exists() and
158 mirror.mirror_path.replace('\\', '/') ==
159 actual_remote_url.replace('\\', '/')):
160 actual_remote_url = self._get_first_remote_url(mirror.mirror_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000161 return actual_remote_url
borenet@google.com88d10082014-03-21 17:24:48 +0000162 return None
163
borenet@google.com4e9be262014-04-08 19:40:30 +0000164 def DoesRemoteURLMatch(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000165 """Determine whether the remote URL of this checkout is the expected URL."""
166 if not os.path.exists(self.checkout_path):
167 # A checkout which doesn't exist can't be broken.
168 return True
169
smut@google.comd33eab32014-07-07 19:35:18 +0000170 actual_remote_url = self.GetActualRemoteURL(options)
borenet@google.com88d10082014-03-21 17:24:48 +0000171 if actual_remote_url:
borenet@google.com8156c9f2014-04-01 16:41:36 +0000172 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip('/')
173 == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/'))
borenet@google.com88d10082014-03-21 17:24:48 +0000174 else:
175 # This may occur if the self.checkout_path exists but does not contain a
agable41e3a6c2016-10-20 11:36:56 -0700176 # valid git checkout.
borenet@google.com88d10082014-03-21 17:24:48 +0000177 return False
178
borenet@google.comb09097a2014-04-09 19:09:08 +0000179 def _DeleteOrMove(self, force):
180 """Delete the checkout directory or move it out of the way.
181
182 Args:
183 force: bool; if True, delete the directory. Otherwise, just move it.
184 """
borenet@google.comb2256212014-05-07 20:57:28 +0000185 if force and os.environ.get('CHROME_HEADLESS') == '1':
186 self.Print('_____ Conflicting directory found in %s. Removing.'
187 % self.checkout_path)
188 gclient_utils.AddWarning('Conflicting directory %s deleted.'
189 % self.checkout_path)
190 gclient_utils.rmtree(self.checkout_path)
191 else:
192 bad_scm_dir = os.path.join(self._root_dir, '_bad_scm',
193 os.path.dirname(self.relpath))
194
195 try:
196 os.makedirs(bad_scm_dir)
197 except OSError as e:
198 if e.errno != errno.EEXIST:
199 raise
200
201 dest_path = tempfile.mkdtemp(
202 prefix=os.path.basename(self.relpath),
203 dir=bad_scm_dir)
204 self.Print('_____ Conflicting directory found in %s. Moving to %s.'
205 % (self.checkout_path, dest_path))
206 gclient_utils.AddWarning('Conflicting directory %s moved to %s.'
207 % (self.checkout_path, dest_path))
208 shutil.move(self.checkout_path, dest_path)
borenet@google.comb09097a2014-04-09 19:09:08 +0000209
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000210
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000211class GitWrapper(SCMWrapper):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000212 """Wrapper for Git"""
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000213 name = 'git'
maruel@chromium.org1a60dca2013-11-26 14:06:26 +0000214 remote = 'origin'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000215
Robert Iannuccia19649b2018-06-29 16:31:45 +0000216 @property
217 def cache_dir(self):
218 try:
219 return git_cache.Mirror.GetCachePath()
220 except RuntimeError:
221 return None
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000222
John Budorick0f7b2002018-01-19 15:46:17 -0800223 def __init__(self, url=None, *args, **kwargs):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000224 """Removes 'git+' fake prefix from git URL."""
Henrique Ferreiroe72279d2019-04-17 12:01:50 +0000225 if url and (url.startswith('git+http://') or
226 url.startswith('git+https://')):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000227 url = url[4:]
John Budorick0f7b2002018-01-19 15:46:17 -0800228 SCMWrapper.__init__(self, url, *args, **kwargs)
szager@chromium.org848fd492014-04-09 19:06:44 +0000229 filter_kwargs = { 'time_throttle': 1, 'out_fh': self.out_fh }
230 if self.out_cb:
231 filter_kwargs['predicate'] = self.out_cb
232 self.filter = gclient_utils.GitFilter(**filter_kwargs)
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000233
mukai@chromium.org9e3e82c2012-04-18 12:55:43 +0000234 @staticmethod
235 def BinaryExists():
236 """Returns true if the command exists."""
237 try:
238 # We assume git is newer than 1.7. See: crbug.com/114483
239 result, version = scm.GIT.AssertVersion('1.7')
240 if not result:
241 raise gclient_utils.Error('Git version is older than 1.7: %s' % version)
242 return result
243 except OSError:
244 return False
245
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000246 def GetCheckoutRoot(self):
247 return scm.GIT.GetCheckoutRoot(self.checkout_path)
248
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000249 def GetRevisionDate(self, _revision):
floitsch@google.comeaab7842011-04-28 09:07:58 +0000250 """Returns the given revision's date in ISO-8601 format (which contains the
251 time zone)."""
252 # TODO(floitsch): get the time-stamp of the given revision and not just the
253 # time-stamp of the currently checked out revision.
254 return self._Capture(['log', '-n', '1', '--format=%ai'])
255
Aaron Gablef4068aa2017-12-12 15:14:09 -0800256 def _GetDiffFilenames(self, base):
257 """Returns the names of files modified since base."""
258 return self._Capture(
Raul Tambrecd862e32019-05-10 21:19:00 +0000259 # Filter to remove base if it is None.
260 list(filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only',
261 base])
262 )).split()
Aaron Gablef4068aa2017-12-12 15:14:09 -0800263
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000264 def diff(self, options, _args, _file_list):
Aaron Gable1853f662018-02-12 15:45:56 -0800265 _, revision = gclient_utils.SplitUrlRevision(self.url)
266 if not revision:
267 revision = 'refs/remotes/%s/master' % self.remote
268 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000269
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000270 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000271 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000272 repository.
273
274 The patch file is generated from a diff of the merge base of HEAD and
275 its upstream branch.
276 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700277 try:
278 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
279 except subprocess2.CalledProcessError:
280 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000281 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700282 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000283 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000284 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000285
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800286 def _Scrub(self, target, options):
287 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000288 quiet = []
289 if not options.verbose:
290 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800291 self._Run(['reset', '--hard', target] + quiet, options)
292 if options.force and options.delete_unversioned_trees:
293 # where `target` is a commit that contains both upper and lower case
294 # versions of the same file on a case insensitive filesystem, we are
295 # actually in a broken state here. The index will have both 'a' and 'A',
296 # but only one of them will exist on the disk. To progress, we delete
297 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800298 output = self._Capture([
299 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800300 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800301 # --porcelain (v1) looks like:
302 # XY filename
303 try:
304 filename = line[3:]
305 self.Print('_____ Deleting residual after reset: %r.' % filename)
306 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800307 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800308 except OSError:
309 pass
310
John Budorick882c91e2018-07-12 22:11:41 +0000311 def _FetchAndReset(self, revision, file_list, options):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800312 """Equivalent to git fetch; git reset."""
Edward Lemur579c9862018-07-13 23:17:51 +0000313 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000314
dnj@chromium.org680f2172014-06-25 00:39:32 +0000315 self._Fetch(options, prune=True, quiet=options.verbose)
John Budorick882c91e2018-07-12 22:11:41 +0000316 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000317 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800318 files = self._Capture(
319 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambreb946b232019-03-26 14:48:46 +0000320 file_list.extend(
321 [os.path.join(self.checkout_path, f.decode()) for f in files])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000322
szager@chromium.org8a139702014-06-20 15:55:01 +0000323 def _DisableHooks(self):
324 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
325 if not os.path.isdir(hook_dir):
326 return
327 for f in os.listdir(hook_dir):
328 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000329 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
330 if os.path.exists(disabled_hook_path):
331 os.remove(disabled_hook_path)
332 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000333
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000334 def _maybe_break_locks(self, options):
335 """This removes all .lock files from this repo's .git directory, if the
336 user passed the --break_repo_locks command line flag.
337
338 In particular, this will cleanup index.lock files, as well as ref lock
339 files.
340 """
341 if options.break_repo_locks:
342 git_dir = os.path.join(self.checkout_path, '.git')
343 for path, _, filenames in os.walk(git_dir):
344 for filename in filenames:
345 if filename.endswith('.lock'):
346 to_break = os.path.join(path, filename)
347 self.Print('breaking lock: %s' % (to_break,))
348 try:
349 os.remove(to_break)
350 except OSError as ex:
351 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
352 raise
353
Edward Lesmes9a01eb22019-05-22 00:48:01 +0000354 # TODO(ehmaldonado): Remove after bot_update is modified to pass the patch's
355 # branch.
356 def _GetTargetBranchForCommit(self, commit):
357 """Get the remote branch a commit is part of."""
358 _WELL_KNOWN_BRANCHES = [
359 'refs/remotes/origin/master',
360 'refs/remotes/origin/infra/config',
361 'refs/remotes/origin/lkgr',
362 ]
363 for branch in _WELL_KNOWN_BRANCHES:
364 if scm.GIT.IsAncestor(self.checkout_path, commit, branch):
365 return branch
366 remote_refs = self._Capture(
367 ['for-each-ref', 'refs/remotes/%s' % self.remote,
368 '--format=%(refname)']).splitlines()
369 for ref in sorted(remote_refs, reverse=True):
370 if scm.GIT.IsAncestor(self.checkout_path, commit, ref):
371 return ref
372 self.Print('Failed to find a remote ref that contains %s. '
373 'Candidate refs were %s.' % (commit, remote_refs))
374 return None
375
Edward Lemur8c665652019-05-08 20:23:33 +0000376 def apply_patch_ref(self, patch_repo, patch_ref, target_ref, options,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000377 file_list):
378 """Apply a patch on top of the revision we're synced at.
379
380 The patch ref is given by |patch_repo|@|patch_ref|, and the current revision
381 is |base_rev|.
Edward Lemur8c665652019-05-08 20:23:33 +0000382 We also need the |target_ref| that the patch was uploaded against. We use
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000383 it to find a merge base between |patch_rev| and |base_rev|, so we can find
384 what commits constitute the patch:
385
386 Graphically, it looks like this:
387
Edward Lemur8c665652019-05-08 20:23:33 +0000388 ... -> merge_base -> [possibly already landed commits] -> target_ref
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000389 \
390 -> [possibly not yet landed dependent CLs] -> patch_rev
391
392 Next, we apply the commits |merge_base..patch_rev| on top of whatever is
393 currently checked out, denoted |base_rev|. Typically, it'd be a revision
Edward Lemur8c665652019-05-08 20:23:33 +0000394 from |target_ref|, but this is not required.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000395
396 Graphically, we cherry pick |merge_base..patch_rev| on top of |base_rev|:
397
398 ... -> base_rev -> [possibly not yet landed dependent CLs] -> patch_rev
399
400 After application, if |options.reset_patch_ref| is specified, we soft reset
401 the just cherry-picked changes, keeping them in git index only.
402
403 Args:
404 patch_repo: The patch origin. e.g. 'https://foo.googlesource.com/bar'
405 patch_ref: The ref to the patch. e.g. 'refs/changes/1234/34/1'.
Edward Lemur8c665652019-05-08 20:23:33 +0000406 target_ref: The ref the patch was uploaded against.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000407 e.g. 'refs/heads/master' or 'refs/heads/infra/config'.
408 options: The options passed to gclient.
409 file_list: A list where modified files will be appended.
410 """
411
Edward Lemurca7d8812018-07-24 17:42:45 +0000412 # Abort any cherry-picks in progress.
413 try:
414 self._Capture(['cherry-pick', '--abort'])
415 except subprocess2.CalledProcessError:
416 pass
417
Edward Lesmesc621b212018-03-21 20:26:56 -0400418 base_rev = self._Capture(['rev-parse', 'HEAD'])
Edward Lemura0ffbe42019-05-01 16:52:18 +0000419
Edward Lesmes9a01eb22019-05-22 00:48:01 +0000420 if not target_ref:
421 target_ref = self._GetTargetBranchForCommit(base_rev) or base_rev
422 elif not target_ref.startswith(('refs/heads/', 'refs/branch-heads/')):
Edward Lemur8c665652019-05-08 20:23:33 +0000423 # TODO(ehmaldonado): Support all refs.
424 raise gclient_utils.Error(
Edward Lesmes9a01eb22019-05-22 00:48:01 +0000425 'target_ref must be in refs/heads/** or refs/branch-heads/**. '
426 'Got %s instead.' % target_ref)
Edward Lemur8c665652019-05-08 20:23:33 +0000427 elif target_ref == 'refs/heads/master':
428 # We handle refs/heads/master separately because bot_update treats it
429 # differently than other refs: it will fetch refs/heads/foo to
430 # refs/heads/foo, but refs/heads/master to refs/remotes/origin/master.
431 # It's not strictly necessary, but it simplifies the rest of the code.
432 target_ref = 'refs/remotes/%s/master' % self.remote
433 elif scm.GIT.IsValidRevision(self.checkout_path, target_ref):
434 # The target ref for the change is already available, so we don't need to
435 # do anything.
436 # This is a common case. When applying a patch to a top-level solution,
437 # bot_update will fetch the target ref for the change and sync the
438 # solution to it.
439 pass
Edward Lemura0ffbe42019-05-01 16:52:18 +0000440 else:
Edward Lemur8c665652019-05-08 20:23:33 +0000441 # The target ref is not available. We check next for a remote version of
442 # it (e.g. refs/remotes/origin/<branch>) and fetch it if it's not
443 # available.
444 self.Print('%s is not an existing ref.' % target_ref)
445 original_target_ref = target_ref
446 target_ref = ''.join(scm.GIT.RefToRemoteRef(target_ref, self.remote))
447 self.Print('Trying with %s' % target_ref)
448 if not scm.GIT.IsValidRevision(self.checkout_path, target_ref):
449 self.Print(
450 '%s is not an existing ref either. Will proceed to fetch it.'
451 % target_ref)
452 url, _ = gclient_utils.SplitUrlRevision(self.url)
453 mirror = self._GetMirror(url, options, target_ref)
454 if mirror:
455 self._UpdateMirrorIfNotContains(
456 mirror, options, 'branch', target_ref, original_target_ref)
457 self._Fetch(options, refspec=target_ref)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000458
Edward Lesmesc621b212018-03-21 20:26:56 -0400459 self.Print('===Applying patch ref===')
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000460 self.Print('Patch ref is %r @ %r. Target branch for patch is %r. '
461 'Current HEAD is %r. Current dir is %r' % (
Edward Lemur8c665652019-05-08 20:23:33 +0000462 patch_repo, patch_ref, target_ref, base_rev,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000463 self.checkout_path))
Edward Lesmesc621b212018-03-21 20:26:56 -0400464 self._Capture(['reset', '--hard'])
465 self._Capture(['fetch', patch_repo, patch_ref])
Edward Lemurca7d8812018-07-24 17:42:45 +0000466 patch_rev = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400467
Edward Lemurca7d8812018-07-24 17:42:45 +0000468 try:
469 if not options.rebase_patch_ref:
470 self._Capture(['checkout', patch_rev])
471 else:
472 # Find the merge-base between the branch_rev and patch_rev to find out
473 # the changes we need to cherry-pick on top of base_rev.
Edward Lemur8c665652019-05-08 20:23:33 +0000474 merge_base = self._Capture(['merge-base', target_ref, patch_rev])
Edward Lemurca7d8812018-07-24 17:42:45 +0000475 self.Print('Merge base of %s and %s is %s' % (
Edward Lemur8c665652019-05-08 20:23:33 +0000476 target_ref, patch_rev, merge_base))
Edward Lemurca7d8812018-07-24 17:42:45 +0000477 if merge_base == patch_rev:
478 # If the merge-base is patch_rev, it means patch_rev is already part
479 # of the history, so just check it out.
480 self._Capture(['checkout', patch_rev])
481 else:
482 # If a change was uploaded on top of another change, which has already
483 # landed, one of the commits in the cherry-pick range will be
484 # redundant, since it has already landed and its changes incorporated
485 # in the tree.
486 # We pass '--keep-redundant-commits' to ignore those changes.
487 self._Capture(['cherry-pick', merge_base + '..' + patch_rev,
488 '--keep-redundant-commits'])
489
490 if file_list is not None:
491 file_list.extend(self._GetDiffFilenames(base_rev))
492
493 except subprocess2.CalledProcessError as e:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000494 self.Print('Failed to apply patch.')
Edward Lemur8c665652019-05-08 20:23:33 +0000495 self.Print('Patch ref is %r @ %r. Target ref for patch is %r. '
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000496 'Current HEAD is %r. Current dir is %r' % (
Edward Lemur8c665652019-05-08 20:23:33 +0000497 patch_repo, patch_ref, target_ref, base_rev,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000498 self.checkout_path))
Edward Lemurca7d8812018-07-24 17:42:45 +0000499 self.Print('git returned non-zero exit status %s:\n%s' % (
500 e.returncode, e.stderr))
501 # Print the current status so that developers know what changes caused the
502 # patch failure, since git cherry-pick doesn't show that information.
503 self.Print(self._Capture(['status']))
John Budorick2c984a02018-07-18 23:24:13 +0000504 try:
Edward Lemurca7d8812018-07-24 17:42:45 +0000505 self._Capture(['cherry-pick', '--abort'])
506 except subprocess2.CalledProcessError:
507 pass
508 raise
509
Edward Lesmesc621b212018-03-21 20:26:56 -0400510 if options.reset_patch_ref:
511 self._Capture(['reset', '--soft', base_rev])
512
msb@chromium.orge28e4982009-09-25 20:51:45 +0000513 def update(self, options, args, file_list):
514 """Runs git to update or transparently checkout the working copy.
515
516 All updated files will be appended to file_list.
517
518 Raises:
519 Error: if can't get URL for relative path.
520 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000521 if args:
522 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
523
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000524 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000525
John Budorick882c91e2018-07-12 22:11:41 +0000526 # If a dependency is not pinned, track the default remote branch.
527 default_rev = 'refs/remotes/%s/master' % self.remote
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000528 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000529 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000530 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000531 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000532 # Override the revision number.
533 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000534 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000535 # Check again for a revision in case an initial ref was specified
536 # in the url, for example bla.git@refs/heads/custombranch
537 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000538 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000539 if not revision:
540 revision = default_rev
msb@chromium.orge28e4982009-09-25 20:51:45 +0000541
szager@chromium.org8a139702014-06-20 15:55:01 +0000542 if managed:
543 self._DisableHooks()
544
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000545 printed_path = False
546 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000547 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700548 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000549 verbose = ['--verbose']
550 printed_path = True
551
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000552 revision_ref = revision
553 if ':' in revision:
554 revision_ref, _, revision = revision.partition(':')
555
556 mirror = self._GetMirror(url, options, revision_ref)
557 if mirror:
558 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000559
John Budorick882c91e2018-07-12 22:11:41 +0000560 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
561 if remote_ref:
562 # Rewrite remote refs to their local equivalents.
563 revision = ''.join(remote_ref)
564 rev_type = "branch"
565 elif revision.startswith('refs/'):
566 # Local branch? We probably don't want to support, since DEPS should
567 # always specify branches as they are in the upstream repo.
568 rev_type = "branch"
569 else:
570 # hash is also a tag, only make a distinction at checkout
571 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000572
primiano@chromium.org1c127382015-02-17 11:15:40 +0000573 # If we are going to introduce a new project, there is a possibility that
574 # we are syncing back to a state where the project was originally a
575 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
576 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
577 # In such case, we might have a backup of the former .git folder, which can
578 # be used to avoid re-fetching the entire repo again (useful for bisects).
579 backup_dir = self.GetGitBackupDirPath()
580 target_dir = os.path.join(self.checkout_path, '.git')
581 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
582 gclient_utils.safe_makedirs(self.checkout_path)
583 os.rename(backup_dir, target_dir)
584 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800585 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000586
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000587 if (not os.path.exists(self.checkout_path) or
588 (os.path.isdir(self.checkout_path) and
589 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000590 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000591 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000592 try:
John Budorick882c91e2018-07-12 22:11:41 +0000593 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000594 except subprocess2.CalledProcessError:
595 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000596 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000597 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800598 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000599 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000600 file_list.extend(
601 [os.path.join(self.checkout_path, f.decode()) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000602 if mirror:
603 self._Capture(
604 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000605 if not verbose:
606 # Make the output a little prettier. It's nice to have some whitespace
607 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000608 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000609 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000610
John Budorick21a51b32018-09-19 19:39:20 +0000611 if mirror:
612 self._Capture(
613 ['remote', 'set-url', '--push', 'origin', mirror.url])
614
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000615 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000616 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000617 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
618 return self._Capture(['rev-parse', '--verify', 'HEAD'])
619
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000620 self._maybe_break_locks(options)
621
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000622 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000623 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000624
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000625 # See if the url has changed (the unittests use git://foo for the url, let
626 # that through).
627 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
628 return_early = False
629 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
630 # unit test pass. (and update the comment above)
631 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
632 # This allows devs to use experimental repos which have a different url
633 # but whose branch(s) are the same as official repos.
Raul Tambrecd862e32019-05-10 21:19:00 +0000634 if (current_url.rstrip('/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000635 subprocess2.capture(
Aaron Gableac9b0f32019-04-18 17:38:37 +0000636 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000637 cwd=self.checkout_path).strip() != 'False'):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000638 self.Print('_____ switching %s to a new upstream' % self.relpath)
iannucci@chromium.org78514212014-08-20 23:08:00 +0000639 if not (options.force or options.reset):
640 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700641 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000642 # Switch over to the new upstream
643 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000644 if mirror:
645 with open(os.path.join(
646 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
647 'w') as fh:
648 fh.write(os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000649 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
650 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000651
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000652 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000653 else:
John Budorick882c91e2018-07-12 22:11:41 +0000654 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000655
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000656 if return_early:
657 return self._Capture(['rev-parse', '--verify', 'HEAD'])
658
msb@chromium.org5bde4852009-12-14 16:47:12 +0000659 cur_branch = self._GetCurrentBranch()
660
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000661 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000662 # 0) HEAD is detached. Probably from our initial clone.
663 # - make sure HEAD is contained by a named ref, then update.
664 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700665 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000666 # - try to rebase onto the new hash or branch
667 # 2) current branch is tracking a remote branch with local committed
668 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000669 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000670 # 3) current branch is tracking a remote branch w/or w/out changes, and
671 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000672 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000673 # 4) current branch is tracking a remote branch, but DEPS switches to a
674 # different remote branch, and
675 # a) current branch has no local changes, and --force:
676 # - checkout new branch
677 # b) current branch has local changes, and --force and --reset:
678 # - checkout new branch
679 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000680
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000681 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
682 # a tracking branch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000683 # or 'master' if not a tracking branch (it's based on a specific rev/hash)
684 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000685 if cur_branch is None:
686 upstream_branch = None
687 current_type = "detached"
688 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000689 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000690 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
691 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
692 current_type = "hash"
693 logging.debug("Current branch is not tracking an upstream (remote)"
694 " branch.")
695 elif upstream_branch.startswith('refs/remotes'):
696 current_type = "branch"
697 else:
698 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000699
Edward Lemur579c9862018-07-13 23:17:51 +0000700 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000701
Michael Spang73fac912019-03-08 18:44:19 +0000702 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000703 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000704 self._Fetch(options, prune=options.force)
705
706 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
707 sha_only=True):
708 # Update the remotes first so we have all the refs.
709 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
710 cwd=self.checkout_path)
711 if verbose:
712 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000713
John Budorick882c91e2018-07-12 22:11:41 +0000714 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200715
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000716 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000717 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000718 target = 'HEAD'
719 if options.upstream and upstream_branch:
720 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800721 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000722
msb@chromium.org786fb682010-06-02 15:16:23 +0000723 if current_type == 'detached':
724 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800725 # We just did a Scrub, this is as clean as it's going to get. In
726 # particular if HEAD is a commit that contains two versions of the same
727 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
728 # to actually "Clean" the checkout; that commit is uncheckoutable on this
729 # system. The best we can do is carry forward to the checkout step.
730 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000731 self._CheckClean(revision)
732 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000733 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000734 self.Print('Up-to-date; skipping checkout.')
735 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000736 # 'git checkout' may need to overwrite existing untracked files. Allow
737 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000738 self._Checkout(
739 options,
John Budorick882c91e2018-07-12 22:11:41 +0000740 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000741 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000742 quiet=True,
743 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000744 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000745 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000746 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000747 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700748 # Can't find a merge-base since we don't know our upstream. That makes
749 # this command VERY likely to produce a rebase failure. For now we
750 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000751 upstream_branch = self.remote
752 if options.revision or deps_revision:
753 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700754 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700755 printed_path=printed_path, merge=options.merge)
756 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000757 elif rev_type == 'hash':
758 # case 2
759 self._AttemptRebase(upstream_branch, file_list, options,
760 newbase=revision, printed_path=printed_path,
761 merge=options.merge)
762 printed_path = True
763 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000764 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000765 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000766 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000767 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000768 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000769 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000770 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000771 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
772 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000773 force_switch = False
774 if options.force:
775 try:
John Budorick882c91e2018-07-12 22:11:41 +0000776 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000777 # case 4a
778 force_switch = True
779 except gclient_utils.Error as e:
780 if options.reset:
781 # case 4b
782 force_switch = True
783 else:
784 switch_error = '%s\n%s' % (e.message, switch_error)
785 if force_switch:
786 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000787 (upstream_branch, new_base))
788 switch_branch = 'gclient_' + remote_ref[1]
789 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000790 self._Checkout(options, switch_branch, force=True, quiet=True)
791 else:
792 # case 4c
793 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000794 else:
795 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800796 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000797 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000798 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000799 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000800 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000801 if options.merge:
802 merge_args.append('--ff')
803 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000804 merge_args.append('--ff-only')
805 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000806 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000807 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700808 rebase_files = []
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000809 if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr):
810 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000811 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700812 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000813 printed_path = True
814 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000815 if not options.auto_rebase:
816 try:
817 action = self._AskForData(
818 'Cannot %s, attempt to rebase? '
819 '(y)es / (q)uit / (s)kip : ' %
820 ('merge' if options.merge else 'fast-forward merge'),
821 options)
822 except ValueError:
823 raise gclient_utils.Error('Invalid Character')
824 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700825 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000826 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000827 printed_path = True
828 break
829 elif re.match(r'quit|q', action, re.I):
830 raise gclient_utils.Error("Can't fast-forward, please merge or "
831 "rebase manually.\n"
832 "cd %s && git " % self.checkout_path
833 + "rebase %s" % upstream_branch)
834 elif re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000835 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000836 return
837 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000838 self.Print('Input not recognized')
smut@google.com27c9c8a2014-09-11 19:57:55 +0000839 elif re.match("error: Your local changes to '.*' would be "
840 "overwritten by merge. Aborting.\nPlease, commit your "
841 "changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000842 e.stderr):
843 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000844 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700845 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000846 printed_path = True
847 raise gclient_utils.Error(e.stderr)
848 else:
849 # Some other problem happened with the merge
850 logging.error("Error during fast-forward merge in %s!" % self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000851 self.Print(e.stderr)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000852 raise
853 else:
854 # Fast-forward merge was successful
855 if not re.match('Already up-to-date.', merge_output) or verbose:
856 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700857 self.Print('_____ %s at %s' % (self.relpath, revision),
858 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000859 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000860 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000861 if not verbose:
862 # Make the output a little prettier. It's nice to have some
863 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000864 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000865
agablec3937b92016-10-25 10:13:03 -0700866 if file_list is not None:
867 file_list.extend(
868 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000869
870 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000871 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700872 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000873 '\nConflict while rebasing this branch.\n'
874 'Fix the conflict and run gclient again.\n'
875 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700876 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000877
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000878 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000879 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
880 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000881
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000882 # If --reset and --delete_unversioned_trees are specified, remove any
883 # untracked directories.
884 if options.reset and options.delete_unversioned_trees:
885 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
886 # merge-base by default), so doesn't include untracked files. So we use
887 # 'git ls-files --directory --others --exclude-standard' here directly.
888 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800889 ['-c', 'core.quotePath=false', 'ls-files',
890 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000891 self.checkout_path)
892 for path in (p for p in paths.splitlines() if p.endswith('/')):
893 full_path = os.path.join(self.checkout_path, path)
894 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000895 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000896 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000897
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000898 return self._Capture(['rev-parse', '--verify', 'HEAD'])
899
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000900 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000901 """Reverts local modifications.
902
903 All reverted files will be appended to file_list.
904 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000905 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000906 # revert won't work if the directory doesn't exist. It needs to
907 # checkout instead.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000908 self.Print('_____ %s is missing, synching instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000909 # Don't reuse the args.
910 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000911
912 default_rev = "refs/heads/master"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000913 if options.upstream:
914 if self._GetCurrentBranch():
915 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
916 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000917 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000918 if not deps_revision:
919 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000920 if deps_revision.startswith('refs/heads/'):
921 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700922 try:
923 deps_revision = self.GetUsableRev(deps_revision, options)
924 except NoUsableRevError as e:
925 # If the DEPS entry's url and hash changed, try to update the origin.
926 # See also http://crbug.com/520067.
927 logging.warn(
928 'Couldn\'t find usable revision, will retrying to update instead: %s',
929 e.message)
930 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000931
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000932 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800933 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000934
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800935 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000936 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000937
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000938 if file_list is not None:
939 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
940
941 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000942 """Returns revision"""
943 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000944
msb@chromium.orge28e4982009-09-25 20:51:45 +0000945 def runhooks(self, options, args, file_list):
946 self.status(options, args, file_list)
947
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000948 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000949 """Display status information."""
950 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000951 self.Print('________ couldn\'t run status in %s:\n'
952 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000953 else:
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700954 try:
955 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
956 except subprocess2.CalledProcessError:
957 merge_base = []
Aaron Gablef4068aa2017-12-12 15:14:09 -0800958 self._Run(
959 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
960 options, stdout=self.out_fh, always=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000961 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800962 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000963 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000964
smutae7ea312016-07-18 11:59:41 -0700965 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700966 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700967 sha1 = None
968 if not os.path.isdir(self.checkout_path):
969 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800970 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700971
972 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
973 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700974 else:
agable41e3a6c2016-10-20 11:36:56 -0700975 # May exist in origin, but we don't have it yet, so fetch and look
976 # again.
977 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -0700978 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
979 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700980
981 if not sha1:
982 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800983 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -0700984
985 return sha1
986
primiano@chromium.org1c127382015-02-17 11:15:40 +0000987 def GetGitBackupDirPath(self):
988 """Returns the path where the .git folder for the current project can be
989 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
990 return os.path.join(self._root_dir,
991 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
992
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000993 def _GetMirror(self, url, options, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000994 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +0000995 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000996 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000997 mirror_kwargs = {
998 'print_func': self.filter,
John Budorick882c91e2018-07-12 22:11:41 +0000999 'refs': []
hinoka@google.comb1b54572014-04-16 22:29:23 +00001000 }
hinoka@google.comb1b54572014-04-16 22:29:23 +00001001 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1002 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001003 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
1004 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001005 if hasattr(options, 'with_tags') and options.with_tags:
1006 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001007 elif revision_ref and revision_ref.startswith('refs/tags/'):
1008 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001009 return git_cache.Mirror(url, **mirror_kwargs)
1010
John Budorick882c91e2018-07-12 22:11:41 +00001011 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001012 """Update a git mirror by fetching the latest commits from the remote,
1013 unless mirror already contains revision whose type is sha1 hash.
1014 """
John Budorick882c91e2018-07-12 22:11:41 +00001015 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001016 if options.verbose:
1017 self.Print('skipping mirror update, it has rev=%s already' % revision,
1018 timestamp=False)
1019 return
1020
szager@chromium.org3ec84f62014-08-22 21:00:22 +00001021 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +00001022 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001023 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +00001024 depth = 10
1025 else:
1026 depth = 10000
1027 else:
1028 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +00001029 mirror.populate(verbose=options.verbose,
1030 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +00001031 depth=depth,
1032 ignore_lock=getattr(options, 'ignore_locks', False),
1033 lock_timeout=getattr(options, 'lock_timeout', 0))
1034 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001035
John Budorick882c91e2018-07-12 22:11:41 +00001036 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001037 """Clone a git repository from the given URL.
1038
msb@chromium.org786fb682010-06-02 15:16:23 +00001039 Once we've cloned the repo, we checkout a working branch if the specified
1040 revision is a branch head. If it is a tag or a specific commit, then we
1041 leave HEAD detached as it makes future updates simpler -- in this case the
1042 user should first create a new branch or switch to an existing branch before
1043 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001044 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001045 # git clone doesn't seem to insert a newline properly before printing
1046 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001047 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +00001048 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +00001049 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001050 if self.cache_dir:
1051 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001052 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001053 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001054 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001055 # If the parent directory does not exist, Git clone on Windows will not
1056 # create it, so we need to do it manually.
1057 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001058 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001059
1060 template_dir = None
1061 if hasattr(options, 'no_history') and options.no_history:
John Budorick882c91e2018-07-12 22:11:41 +00001062 if gclient_utils.IsGitSha(revision):
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001063 # In the case of a subproject, the pinned sha is not necessarily the
1064 # head of the remote branch (so we can't just use --depth=N). Instead,
1065 # we tell git to fetch all the remote objects from SHA..HEAD by means of
1066 # a template git dir which has a 'shallow' file pointing to the sha.
1067 template_dir = tempfile.mkdtemp(
1068 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
1069 dir=parent_dir)
1070 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
1071 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
1072 template_file.write(revision)
1073 clone_cmd.append('--template=' + template_dir)
1074 else:
1075 # Otherwise, we're just interested in the HEAD. Just use --depth.
1076 clone_cmd.append('--depth=1')
1077
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001078 tmp_dir = tempfile.mkdtemp(
1079 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
1080 dir=parent_dir)
1081 try:
1082 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +01001083 if self.print_outbuf:
1084 print_stdout = True
1085 stdout = gclient_utils.WriteToStdout(self.out_fh)
1086 else:
1087 print_stdout = False
1088 stdout = self.out_fh
1089 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
1090 print_stdout=print_stdout, stdout=stdout)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001091 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +00001092 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1093 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001094 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001095 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001096 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001097 finally:
1098 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001099 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001100 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001101 if template_dir:
1102 gclient_utils.rmtree(template_dir)
Edward Lemur579c9862018-07-13 23:17:51 +00001103 self._SetFetchConfig(options)
1104 self._Fetch(options, prune=options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001105 revision = self._AutoFetchRef(options, revision)
1106 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1107 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001108 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001109 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001110 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +00001111 ('Checked out %s to a detached HEAD. Before making any commits\n'
1112 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
1113 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
John Budorick882c91e2018-07-12 22:11:41 +00001114 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001115
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001116 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001117 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001118 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001119 raise gclient_utils.Error("Background task requires input. Rerun "
1120 "gclient with --jobs=1 so that\n"
1121 "interaction is possible.")
1122 try:
1123 return raw_input(prompt)
1124 except KeyboardInterrupt:
1125 # Hide the exception.
1126 sys.exit(1)
1127
1128
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001129 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001130 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001131 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001132 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001133 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001134 revision = upstream
1135 if newbase:
1136 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001137 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001138 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001139 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001140 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001141 printed_path = True
1142 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001143 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001144
1145 if merge:
1146 merge_output = self._Capture(['merge', revision])
1147 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001148 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001149 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001150
1151 # Build the rebase command here using the args
1152 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1153 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001154 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001155 rebase_cmd.append('--verbose')
1156 if newbase:
1157 rebase_cmd.extend(['--onto', newbase])
1158 rebase_cmd.append(upstream)
1159 if branch:
1160 rebase_cmd.append(branch)
1161
1162 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001163 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001164 except subprocess2.CalledProcessError as e:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001165 if (re.match(r'cannot rebase: you have unstaged changes', e.stderr) or
1166 re.match(r'cannot rebase: your index contains uncommitted changes',
1167 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001168 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001169 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001170 'Cannot rebase because of unstaged changes.\n'
1171 '\'git reset --hard HEAD\' ?\n'
1172 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001173 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001174 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001175 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001176 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001177 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001178 break
1179 elif re.match(r'quit|q', rebase_action, re.I):
1180 raise gclient_utils.Error("Please merge or rebase manually\n"
1181 "cd %s && git " % self.checkout_path
1182 + "%s" % ' '.join(rebase_cmd))
1183 elif re.match(r'show|s', rebase_action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001184 self.Print('%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001185 continue
1186 else:
1187 gclient_utils.Error("Input not recognized")
1188 continue
1189 elif re.search(r'^CONFLICT', e.stdout, re.M):
1190 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1191 "Fix the conflict and run gclient again.\n"
1192 "See 'man git-rebase' for details.\n")
1193 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001194 self.Print(e.stdout.strip())
1195 self.Print('Rebase produced error output:\n%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001196 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1197 "manually.\ncd %s && git " %
1198 self.checkout_path
1199 + "%s" % ' '.join(rebase_cmd))
1200
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001201 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001202 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001203 # Make the output a little prettier. It's nice to have some
1204 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001205 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001206
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001207 @staticmethod
1208 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001209 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1210 if not ok:
1211 raise gclient_utils.Error('git version %s < minimum required %s' %
1212 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001213
John Budorick882c91e2018-07-12 22:11:41 +00001214 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001215 # Special case handling if all 3 conditions are met:
1216 # * the mirros have recently changed, but deps destination remains same,
1217 # * the git histories of mirrors are conflicting.
1218 # * git cache is used
1219 # This manifests itself in current checkout having invalid HEAD commit on
1220 # most git operations. Since git cache is used, just deleted the .git
1221 # folder, and re-create it by cloning.
1222 try:
1223 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1224 except subprocess2.CalledProcessError as e:
1225 if ('fatal: bad object HEAD' in e.stderr
1226 and self.cache_dir and self.cache_dir in url):
1227 self.Print((
1228 'Likely due to DEPS change with git cache_dir, '
1229 'the current commit points to no longer existing object.\n'
1230 '%s' % e)
1231 )
1232 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001233 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001234 else:
1235 raise
1236
msb@chromium.org786fb682010-06-02 15:16:23 +00001237 def _IsRebasing(self):
1238 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1239 # have a plumbing command to determine whether a rebase is in progress, so
1240 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1241 g = os.path.join(self.checkout_path, '.git')
1242 return (
1243 os.path.isdir(os.path.join(g, "rebase-merge")) or
1244 os.path.isdir(os.path.join(g, "rebase-apply")))
1245
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001246 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001247 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1248 if os.path.exists(lockfile):
1249 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001250 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001251 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1252 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001253 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001254
msb@chromium.org786fb682010-06-02 15:16:23 +00001255 # Make sure the tree is clean; see git-rebase.sh for reference
1256 try:
1257 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001258 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001259 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001260 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001261 '\tYou have unstaged changes.\n'
1262 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001263 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001264 try:
1265 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001266 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001267 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001268 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001269 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001270 '\tYour index contains uncommitted changes\n'
1271 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001272 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001273
agable83faed02016-10-24 14:37:10 -07001274 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001275 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1276 # reference by a commit). If not, error out -- most likely a rebase is
1277 # in progress, try to detect so we can give a better error.
1278 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001279 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1280 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001281 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001282 # Commit is not contained by any rev. See if the user is rebasing:
1283 if self._IsRebasing():
1284 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001285 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001286 '\tAlready in a conflict, i.e. (no branch).\n'
1287 '\tFix the conflict and run gclient again.\n'
1288 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1289 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001290 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001291 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001292 name = ('saved-by-gclient-' +
1293 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001294 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001295 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001296 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001297
msb@chromium.org5bde4852009-12-14 16:47:12 +00001298 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001299 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001300 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001301 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001302 return None
1303 return branch
1304
borenet@google.comc3e09d22014-04-10 13:58:18 +00001305 def _Capture(self, args, **kwargs):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001306 kwargs.setdefault('cwd', self.checkout_path)
1307 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001308 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001309 env = scm.GIT.ApplyEnvVars(kwargs)
Raul Tambrecd862e32019-05-10 21:19:00 +00001310 ret = subprocess2.check_output(
1311 ['git'] + args, env=env, **kwargs).decode('utf-8')
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001312 if strip:
1313 ret = ret.strip()
1314 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001315
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001316 def _Checkout(self, options, ref, force=False, quiet=None):
1317 """Performs a 'git-checkout' operation.
1318
1319 Args:
1320 options: The configured option set
1321 ref: (str) The branch/commit to checkout
1322 quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
1323 'None', the behavior is inferred from 'options.verbose'.
1324 Returns: (str) The output of the checkout operation
1325 """
1326 if quiet is None:
1327 quiet = (not options.verbose)
1328 checkout_args = ['checkout']
1329 if force:
1330 checkout_args.append('--force')
1331 if quiet:
1332 checkout_args.append('--quiet')
1333 checkout_args.append(ref)
1334 return self._Capture(checkout_args)
1335
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001336 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1337 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001338 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001339 # When updating, the ref is modified to be a remote ref .
1340 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1341 # Try to reverse that mapping.
1342 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1343 if original_ref:
1344 refspec = original_ref + ':' + refspec
1345 # When a mirror is configured, it only fetches
1346 # refs/{heads,branch-heads,tags}/*.
1347 # If asked to fetch other refs, we must fetch those directly from the
1348 # repository, and not from the mirror.
1349 if not original_ref.startswith(
1350 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1351 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001352 fetch_cmd = cfg + [
1353 'fetch',
1354 remote or self.remote,
1355 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001356 if refspec:
1357 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001358
1359 if prune:
1360 fetch_cmd.append('--prune')
1361 if options.verbose:
1362 fetch_cmd.append('--verbose')
1363 elif quiet:
1364 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001365 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001366
1367 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1368 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1369
Edward Lemur579c9862018-07-13 23:17:51 +00001370 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001371 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1372 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001373 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001374 try:
1375 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1376 options)
1377 self._Run(['config', 'remote.%s.fetch' % self.remote,
1378 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1379 except subprocess2.CalledProcessError as e:
1380 # If exit code was 5, it means we attempted to unset a config that
1381 # didn't exist. Ignore it.
1382 if e.returncode != 5:
1383 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001384 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001385 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001386 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1387 '^\\+refs/branch-heads/\\*:.*$']
1388 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001389 if hasattr(options, 'with_tags') and options.with_tags:
1390 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1391 '+refs/tags/*:refs/tags/*',
1392 '^\\+refs/tags/\\*:.*$']
1393 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001394
John Budorick882c91e2018-07-12 22:11:41 +00001395 def _AutoFetchRef(self, options, revision):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001396 """Attempts to fetch |revision| if not available in local repo.
1397
1398 Returns possibly updated revision."""
John Budorick882c91e2018-07-12 22:11:41 +00001399 try:
1400 self._Capture(['rev-parse', revision])
1401 except subprocess2.CalledProcessError:
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001402 self._Fetch(options, refspec=revision)
1403 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1404 return revision
1405
dnj@chromium.org680f2172014-06-25 00:39:32 +00001406 def _Run(self, args, options, show_header=True, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001407 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001408 kwargs.setdefault('cwd', self.checkout_path)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001409 kwargs.setdefault('stdout', self.out_fh)
szager@chromium.org848fd492014-04-09 19:06:44 +00001410 kwargs['filter_fn'] = self.filter
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001411 kwargs.setdefault('print_stdout', False)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001412 env = scm.GIT.ApplyEnvVars(kwargs)
agable@chromium.org772efaf2014-04-01 02:35:44 +00001413 cmd = ['git'] + args
dnj@chromium.org680f2172014-06-25 00:39:32 +00001414 if show_header:
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001415 gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs)
1416 else:
1417 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001418
1419
1420class CipdPackage(object):
1421 """A representation of a single CIPD package."""
1422
John Budorickd3ba72b2018-03-20 12:27:42 -07001423 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001424 self._authority_for_subdir = authority_for_subdir
1425 self._name = name
1426 self._version = version
1427
1428 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001429 def authority_for_subdir(self):
1430 """Whether this package has authority to act on behalf of its subdir.
1431
1432 Some operations should only be performed once per subdirectory. A package
1433 that has authority for its subdirectory is the only package that should
1434 perform such operations.
1435
1436 Returns:
1437 bool; whether this package has subdir authority.
1438 """
1439 return self._authority_for_subdir
1440
1441 @property
1442 def name(self):
1443 return self._name
1444
1445 @property
1446 def version(self):
1447 return self._version
1448
1449
1450class CipdRoot(object):
1451 """A representation of a single CIPD root."""
1452 def __init__(self, root_dir, service_url):
1453 self._all_packages = set()
1454 self._mutator_lock = threading.Lock()
1455 self._packages_by_subdir = collections.defaultdict(list)
1456 self._root_dir = root_dir
1457 self._service_url = service_url
1458
1459 def add_package(self, subdir, package, version):
1460 """Adds a package to this CIPD root.
1461
1462 As far as clients are concerned, this grants both root and subdir authority
1463 to packages arbitrarily. (The implementation grants root authority to the
1464 first package added and subdir authority to the first package added for that
1465 subdir, but clients should not depend on or expect that behavior.)
1466
1467 Args:
1468 subdir: str; relative path to where the package should be installed from
1469 the cipd root directory.
1470 package: str; the cipd package name.
1471 version: str; the cipd package version.
1472 Returns:
1473 CipdPackage; the package that was created and added to this root.
1474 """
1475 with self._mutator_lock:
1476 cipd_package = CipdPackage(
1477 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001478 not self._packages_by_subdir[subdir])
1479 self._all_packages.add(cipd_package)
1480 self._packages_by_subdir[subdir].append(cipd_package)
1481 return cipd_package
1482
1483 def packages(self, subdir):
1484 """Get the list of configured packages for the given subdir."""
1485 return list(self._packages_by_subdir[subdir])
1486
1487 def clobber(self):
1488 """Remove the .cipd directory.
1489
1490 This is useful for forcing ensure to redownload and reinitialize all
1491 packages.
1492 """
1493 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001494 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001495 try:
1496 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1497 except OSError:
1498 if os.path.exists(cipd_cache_dir):
1499 raise
1500
1501 @contextlib.contextmanager
1502 def _create_ensure_file(self):
1503 try:
1504 ensure_file = None
1505 with tempfile.NamedTemporaryFile(
Raul Tambreb946b232019-03-26 14:48:46 +00001506 suffix='.ensure', delete=False, mode='w') as ensure_file:
John Budorick302bb842018-07-17 23:49:17 +00001507 ensure_file.write('$ParanoidMode CheckPresence\n\n')
Raul Tambreb946b232019-03-26 14:48:46 +00001508 for subdir, packages in sorted(self._packages_by_subdir.items()):
John Budorick0f7b2002018-01-19 15:46:17 -08001509 ensure_file.write('@Subdir %s\n' % subdir)
Edward Lemurfbb06aa2018-06-11 20:43:06 +00001510 for package in sorted(packages, key=lambda p: p.name):
John Budorick0f7b2002018-01-19 15:46:17 -08001511 ensure_file.write('%s %s\n' % (package.name, package.version))
1512 ensure_file.write('\n')
1513 yield ensure_file.name
1514 finally:
1515 if ensure_file is not None and os.path.exists(ensure_file.name):
1516 os.remove(ensure_file.name)
1517
1518 def ensure(self):
1519 """Run `cipd ensure`."""
1520 with self._mutator_lock:
1521 with self._create_ensure_file() as ensure_file:
1522 cmd = [
1523 'cipd', 'ensure',
1524 '-log-level', 'error',
1525 '-root', self.root_dir,
1526 '-ensure-file', ensure_file,
1527 ]
1528 gclient_utils.CheckCallAndFilterAndHeader(cmd)
1529
John Budorickd3ba72b2018-03-20 12:27:42 -07001530 def run(self, command):
1531 if command == 'update':
1532 self.ensure()
1533 elif command == 'revert':
1534 self.clobber()
1535 self.ensure()
1536
John Budorick0f7b2002018-01-19 15:46:17 -08001537 def created_package(self, package):
1538 """Checks whether this root created the given package.
1539
1540 Args:
1541 package: CipdPackage; the package to check.
1542 Returns:
1543 bool; whether this root created the given package.
1544 """
1545 return package in self._all_packages
1546
1547 @property
1548 def root_dir(self):
1549 return self._root_dir
1550
1551 @property
1552 def service_url(self):
1553 return self._service_url
1554
1555
1556class CipdWrapper(SCMWrapper):
1557 """Wrapper for CIPD.
1558
1559 Currently only supports chrome-infra-packages.appspot.com.
1560 """
John Budorick3929e9e2018-02-04 18:18:07 -08001561 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001562
1563 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1564 out_cb=None, root=None, package=None):
1565 super(CipdWrapper, self).__init__(
1566 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1567 out_cb=out_cb)
1568 assert root.created_package(package)
1569 self._package = package
1570 self._root = root
1571
1572 #override
1573 def GetCacheMirror(self):
1574 return None
1575
1576 #override
1577 def GetActualRemoteURL(self, options):
1578 return self._root.service_url
1579
1580 #override
1581 def DoesRemoteURLMatch(self, options):
1582 del options
1583 return True
1584
1585 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001586 """Does nothing.
1587
1588 CIPD packages should be reverted at the root by running
1589 `CipdRoot.run('revert')`.
1590 """
1591 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001592
1593 def diff(self, options, args, file_list):
1594 """CIPD has no notion of diffing."""
1595 pass
1596
1597 def pack(self, options, args, file_list):
1598 """CIPD has no notion of diffing."""
1599 pass
1600
1601 def revinfo(self, options, args, file_list):
1602 """Grab the instance ID."""
1603 try:
1604 tmpdir = tempfile.mkdtemp()
1605 describe_json_path = os.path.join(tmpdir, 'describe.json')
1606 cmd = [
1607 'cipd', 'describe',
1608 self._package.name,
1609 '-log-level', 'error',
1610 '-version', self._package.version,
1611 '-json-output', describe_json_path
1612 ]
1613 gclient_utils.CheckCallAndFilter(
1614 cmd, filter_fn=lambda _line: None, print_stdout=False)
1615 with open(describe_json_path) as f:
1616 describe_json = json.load(f)
1617 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1618 finally:
1619 gclient_utils.rmtree(tmpdir)
1620
1621 def status(self, options, args, file_list):
1622 pass
1623
1624 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001625 """Does nothing.
1626
1627 CIPD packages should be updated at the root by running
1628 `CipdRoot.run('update')`.
1629 """
1630 pass