blob: efb69504f36cbf251cdc6f0c202ac1d87d5215bc [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(
259 # Filter to remove base if it is None.
260 filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only', base])
261 ).split()
262
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(
320 [os.path.join(self.checkout_path, f.decode()) 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 Lemur6a4e31b2018-08-10 19:59:02 +0000353 # TODO(ehmaldonado): Remove after bot_update is modified to pass the patch's
354 # branch.
355 def _GetTargetBranchForCommit(self, commit):
Edward Lemurca7d8812018-07-24 17:42:45 +0000356 """Get the remote branch a commit is part of."""
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000357 _WELL_KNOWN_BRANCHES = [
358 'refs/remotes/origin/master',
359 'refs/remotes/origin/infra/config',
360 'refs/remotes/origin/lkgr',
361 ]
362 for branch in _WELL_KNOWN_BRANCHES:
363 if scm.GIT.IsAncestor(self.checkout_path, commit, branch):
364 return branch
Edward Lemurca7d8812018-07-24 17:42:45 +0000365 remote_refs = self._Capture(
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000366 ['for-each-ref', 'refs/remotes/%s' % self.remote,
Edward Lemurca7d8812018-07-24 17:42:45 +0000367 '--format=%(refname)']).splitlines()
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000368 for ref in sorted(remote_refs, reverse=True):
Edward Lemurca7d8812018-07-24 17:42:45 +0000369 if scm.GIT.IsAncestor(self.checkout_path, commit, ref):
370 return ref
371 self.Print('Failed to find a remote ref that contains %s. '
372 'Candidate refs were %s.' % (commit, remote_refs))
Edward Lemura0ffbe42019-05-01 16:52:18 +0000373 return None
Edward Lemurca7d8812018-07-24 17:42:45 +0000374
Edward Lemur8c665652019-05-08 20:23:33 +0000375 def apply_patch_ref(self, patch_repo, patch_ref, target_ref, options,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000376 file_list):
377 """Apply a patch on top of the revision we're synced at.
378
379 The patch ref is given by |patch_repo|@|patch_ref|, and the current revision
380 is |base_rev|.
Edward Lemur8c665652019-05-08 20:23:33 +0000381 We also need the |target_ref| that the patch was uploaded against. We use
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000382 it to find a merge base between |patch_rev| and |base_rev|, so we can find
383 what commits constitute the patch:
384
385 Graphically, it looks like this:
386
Edward Lemur8c665652019-05-08 20:23:33 +0000387 ... -> merge_base -> [possibly already landed commits] -> target_ref
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000388 \
389 -> [possibly not yet landed dependent CLs] -> patch_rev
390
391 Next, we apply the commits |merge_base..patch_rev| on top of whatever is
392 currently checked out, denoted |base_rev|. Typically, it'd be a revision
Edward Lemur8c665652019-05-08 20:23:33 +0000393 from |target_ref|, but this is not required.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000394
395 Graphically, we cherry pick |merge_base..patch_rev| on top of |base_rev|:
396
397 ... -> base_rev -> [possibly not yet landed dependent CLs] -> patch_rev
398
399 After application, if |options.reset_patch_ref| is specified, we soft reset
400 the just cherry-picked changes, keeping them in git index only.
401
402 Args:
403 patch_repo: The patch origin. e.g. 'https://foo.googlesource.com/bar'
404 patch_ref: The ref to the patch. e.g. 'refs/changes/1234/34/1'.
Edward Lemur8c665652019-05-08 20:23:33 +0000405 target_ref: The ref the patch was uploaded against.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000406 e.g. 'refs/heads/master' or 'refs/heads/infra/config'.
407 options: The options passed to gclient.
408 file_list: A list where modified files will be appended.
409 """
410
Edward Lemurca7d8812018-07-24 17:42:45 +0000411 # Abort any cherry-picks in progress.
412 try:
413 self._Capture(['cherry-pick', '--abort'])
414 except subprocess2.CalledProcessError:
415 pass
416
Edward Lesmesc621b212018-03-21 20:26:56 -0400417 base_rev = self._Capture(['rev-parse', 'HEAD'])
Edward Lemura0ffbe42019-05-01 16:52:18 +0000418
Edward Lemur8c665652019-05-08 20:23:33 +0000419 if not target_ref:
420 target_ref = self._GetTargetBranchForCommit(base_rev) or base_rev
421 elif not target_ref.startswith(('refs/heads/', 'refs/branch-heads/')):
422 # TODO(ehmaldonado): Support all refs.
423 raise gclient_utils.Error(
424 'target_ref must be in refs/heads/** or refs/branch-heads/**. '
425 'Got %s instead.' % target_ref)
426 elif target_ref == 'refs/heads/master':
427 # We handle refs/heads/master separately because bot_update treats it
428 # differently than other refs: it will fetch refs/heads/foo to
429 # refs/heads/foo, but refs/heads/master to refs/remotes/origin/master.
430 # It's not strictly necessary, but it simplifies the rest of the code.
431 target_ref = 'refs/remotes/%s/master' % self.remote
432 elif scm.GIT.IsValidRevision(self.checkout_path, target_ref):
433 # The target ref for the change is already available, so we don't need to
434 # do anything.
435 # This is a common case. When applying a patch to a top-level solution,
436 # bot_update will fetch the target ref for the change and sync the
437 # solution to it.
438 pass
Edward Lemura0ffbe42019-05-01 16:52:18 +0000439 else:
Edward Lemur8c665652019-05-08 20:23:33 +0000440 # The target ref is not available. We check next for a remote version of
441 # it (e.g. refs/remotes/origin/<branch>) and fetch it if it's not
442 # available.
443 self.Print('%s is not an existing ref.' % target_ref)
444 original_target_ref = target_ref
445 target_ref = ''.join(scm.GIT.RefToRemoteRef(target_ref, self.remote))
446 self.Print('Trying with %s' % target_ref)
447 if not scm.GIT.IsValidRevision(self.checkout_path, target_ref):
448 self.Print(
449 '%s is not an existing ref either. Will proceed to fetch it.'
450 % target_ref)
451 url, _ = gclient_utils.SplitUrlRevision(self.url)
452 mirror = self._GetMirror(url, options, target_ref)
453 if mirror:
454 self._UpdateMirrorIfNotContains(
455 mirror, options, 'branch', target_ref, original_target_ref)
456 self._Fetch(options, refspec=target_ref)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000457
Edward Lesmesc621b212018-03-21 20:26:56 -0400458 self.Print('===Applying patch ref===')
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000459 self.Print('Patch ref is %r @ %r. Target branch for patch is %r. '
460 'Current HEAD is %r. Current dir is %r' % (
Edward Lemur8c665652019-05-08 20:23:33 +0000461 patch_repo, patch_ref, target_ref, base_rev,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000462 self.checkout_path))
Edward Lesmesc621b212018-03-21 20:26:56 -0400463 self._Capture(['reset', '--hard'])
464 self._Capture(['fetch', patch_repo, patch_ref])
Edward Lemurca7d8812018-07-24 17:42:45 +0000465 patch_rev = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400466
Edward Lemurca7d8812018-07-24 17:42:45 +0000467 try:
468 if not options.rebase_patch_ref:
469 self._Capture(['checkout', patch_rev])
470 else:
471 # Find the merge-base between the branch_rev and patch_rev to find out
472 # the changes we need to cherry-pick on top of base_rev.
Edward Lemur8c665652019-05-08 20:23:33 +0000473 merge_base = self._Capture(['merge-base', target_ref, patch_rev])
Edward Lemurca7d8812018-07-24 17:42:45 +0000474 self.Print('Merge base of %s and %s is %s' % (
Edward Lemur8c665652019-05-08 20:23:33 +0000475 target_ref, patch_rev, merge_base))
Edward Lemurca7d8812018-07-24 17:42:45 +0000476 if merge_base == patch_rev:
477 # If the merge-base is patch_rev, it means patch_rev is already part
478 # of the history, so just check it out.
479 self._Capture(['checkout', patch_rev])
480 else:
481 # If a change was uploaded on top of another change, which has already
482 # landed, one of the commits in the cherry-pick range will be
483 # redundant, since it has already landed and its changes incorporated
484 # in the tree.
485 # We pass '--keep-redundant-commits' to ignore those changes.
486 self._Capture(['cherry-pick', merge_base + '..' + patch_rev,
487 '--keep-redundant-commits'])
488
489 if file_list is not None:
490 file_list.extend(self._GetDiffFilenames(base_rev))
491
492 except subprocess2.CalledProcessError as e:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000493 self.Print('Failed to apply patch.')
Edward Lemur8c665652019-05-08 20:23:33 +0000494 self.Print('Patch ref is %r @ %r. Target ref for patch is %r. '
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000495 'Current HEAD is %r. Current dir is %r' % (
Edward Lemur8c665652019-05-08 20:23:33 +0000496 patch_repo, patch_ref, target_ref, base_rev,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000497 self.checkout_path))
Edward Lemurca7d8812018-07-24 17:42:45 +0000498 self.Print('git returned non-zero exit status %s:\n%s' % (
499 e.returncode, e.stderr))
500 # Print the current status so that developers know what changes caused the
501 # patch failure, since git cherry-pick doesn't show that information.
502 self.Print(self._Capture(['status']))
John Budorick2c984a02018-07-18 23:24:13 +0000503 try:
Edward Lemurca7d8812018-07-24 17:42:45 +0000504 self._Capture(['cherry-pick', '--abort'])
505 except subprocess2.CalledProcessError:
506 pass
507 raise
508
Edward Lesmesc621b212018-03-21 20:26:56 -0400509 if options.reset_patch_ref:
510 self._Capture(['reset', '--soft', base_rev])
511
msb@chromium.orge28e4982009-09-25 20:51:45 +0000512 def update(self, options, args, file_list):
513 """Runs git to update or transparently checkout the working copy.
514
515 All updated files will be appended to file_list.
516
517 Raises:
518 Error: if can't get URL for relative path.
519 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000520 if args:
521 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
522
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000523 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000524
John Budorick882c91e2018-07-12 22:11:41 +0000525 # If a dependency is not pinned, track the default remote branch.
526 default_rev = 'refs/remotes/%s/master' % self.remote
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000527 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000528 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000529 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000530 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000531 # Override the revision number.
532 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000533 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000534 # Check again for a revision in case an initial ref was specified
535 # in the url, for example bla.git@refs/heads/custombranch
536 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000537 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000538 if not revision:
539 revision = default_rev
msb@chromium.orge28e4982009-09-25 20:51:45 +0000540
szager@chromium.org8a139702014-06-20 15:55:01 +0000541 if managed:
542 self._DisableHooks()
543
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000544 printed_path = False
545 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000546 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700547 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000548 verbose = ['--verbose']
549 printed_path = True
550
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000551 revision_ref = revision
552 if ':' in revision:
553 revision_ref, _, revision = revision.partition(':')
554
555 mirror = self._GetMirror(url, options, revision_ref)
556 if mirror:
557 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000558
John Budorick882c91e2018-07-12 22:11:41 +0000559 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
560 if remote_ref:
561 # Rewrite remote refs to their local equivalents.
562 revision = ''.join(remote_ref)
563 rev_type = "branch"
564 elif revision.startswith('refs/'):
565 # Local branch? We probably don't want to support, since DEPS should
566 # always specify branches as they are in the upstream repo.
567 rev_type = "branch"
568 else:
569 # hash is also a tag, only make a distinction at checkout
570 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000571
primiano@chromium.org1c127382015-02-17 11:15:40 +0000572 # If we are going to introduce a new project, there is a possibility that
573 # we are syncing back to a state where the project was originally a
574 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
575 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
576 # In such case, we might have a backup of the former .git folder, which can
577 # be used to avoid re-fetching the entire repo again (useful for bisects).
578 backup_dir = self.GetGitBackupDirPath()
579 target_dir = os.path.join(self.checkout_path, '.git')
580 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
581 gclient_utils.safe_makedirs(self.checkout_path)
582 os.rename(backup_dir, target_dir)
583 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800584 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000585
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000586 if (not os.path.exists(self.checkout_path) or
587 (os.path.isdir(self.checkout_path) and
588 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000589 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000590 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000591 try:
John Budorick882c91e2018-07-12 22:11:41 +0000592 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000593 except subprocess2.CalledProcessError:
594 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000595 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000596 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800597 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000598 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000599 file_list.extend(
600 [os.path.join(self.checkout_path, f.decode()) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000601 if mirror:
602 self._Capture(
603 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000604 if not verbose:
605 # Make the output a little prettier. It's nice to have some whitespace
606 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000607 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000608 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000609
John Budorick21a51b32018-09-19 19:39:20 +0000610 if mirror:
611 self._Capture(
612 ['remote', 'set-url', '--push', 'origin', mirror.url])
613
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000614 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000615 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000616 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
617 return self._Capture(['rev-parse', '--verify', 'HEAD'])
618
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000619 self._maybe_break_locks(options)
620
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000621 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000622 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000623
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000624 # See if the url has changed (the unittests use git://foo for the url, let
625 # that through).
626 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
627 return_early = False
628 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
629 # unit test pass. (and update the comment above)
630 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
631 # This allows devs to use experimental repos which have a different url
632 # but whose branch(s) are the same as official repos.
Raul Tambreb946b232019-03-26 14:48:46 +0000633 if (current_url.rstrip(b'/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000634 subprocess2.capture(
Aaron Gableac9b0f32019-04-18 17:38:37 +0000635 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000636 cwd=self.checkout_path).strip() != 'False'):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000637 self.Print('_____ switching %s to a new upstream' % self.relpath)
iannucci@chromium.org78514212014-08-20 23:08:00 +0000638 if not (options.force or options.reset):
639 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700640 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000641 # Switch over to the new upstream
642 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000643 if mirror:
644 with open(os.path.join(
645 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
646 'w') as fh:
647 fh.write(os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000648 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
649 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000650
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000651 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000652 else:
John Budorick882c91e2018-07-12 22:11:41 +0000653 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000654
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000655 if return_early:
656 return self._Capture(['rev-parse', '--verify', 'HEAD'])
657
msb@chromium.org5bde4852009-12-14 16:47:12 +0000658 cur_branch = self._GetCurrentBranch()
659
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000660 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000661 # 0) HEAD is detached. Probably from our initial clone.
662 # - make sure HEAD is contained by a named ref, then update.
663 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700664 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000665 # - try to rebase onto the new hash or branch
666 # 2) current branch is tracking a remote branch with local committed
667 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000668 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000669 # 3) current branch is tracking a remote branch w/or w/out changes, and
670 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000671 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000672 # 4) current branch is tracking a remote branch, but DEPS switches to a
673 # different remote branch, and
674 # a) current branch has no local changes, and --force:
675 # - checkout new branch
676 # b) current branch has local changes, and --force and --reset:
677 # - checkout new branch
678 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000679
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000680 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
681 # a tracking branch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000682 # or 'master' if not a tracking branch (it's based on a specific rev/hash)
683 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000684 if cur_branch is None:
685 upstream_branch = None
686 current_type = "detached"
687 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000688 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000689 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
690 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
691 current_type = "hash"
692 logging.debug("Current branch is not tracking an upstream (remote)"
693 " branch.")
694 elif upstream_branch.startswith('refs/remotes'):
695 current_type = "branch"
696 else:
697 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000698
Edward Lemur579c9862018-07-13 23:17:51 +0000699 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000700
Michael Spang73fac912019-03-08 18:44:19 +0000701 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000702 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000703 self._Fetch(options, prune=options.force)
704
705 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
706 sha_only=True):
707 # Update the remotes first so we have all the refs.
708 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
709 cwd=self.checkout_path)
710 if verbose:
711 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000712
John Budorick882c91e2018-07-12 22:11:41 +0000713 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200714
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000715 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000716 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000717 target = 'HEAD'
718 if options.upstream and upstream_branch:
719 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800720 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000721
msb@chromium.org786fb682010-06-02 15:16:23 +0000722 if current_type == 'detached':
723 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800724 # We just did a Scrub, this is as clean as it's going to get. In
725 # particular if HEAD is a commit that contains two versions of the same
726 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
727 # to actually "Clean" the checkout; that commit is uncheckoutable on this
728 # system. The best we can do is carry forward to the checkout step.
729 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000730 self._CheckClean(revision)
731 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000732 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000733 self.Print('Up-to-date; skipping checkout.')
734 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000735 # 'git checkout' may need to overwrite existing untracked files. Allow
736 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000737 self._Checkout(
738 options,
John Budorick882c91e2018-07-12 22:11:41 +0000739 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000740 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000741 quiet=True,
742 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000743 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000744 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000745 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000746 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700747 # Can't find a merge-base since we don't know our upstream. That makes
748 # this command VERY likely to produce a rebase failure. For now we
749 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000750 upstream_branch = self.remote
751 if options.revision or deps_revision:
752 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700753 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700754 printed_path=printed_path, merge=options.merge)
755 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000756 elif rev_type == 'hash':
757 # case 2
758 self._AttemptRebase(upstream_branch, file_list, options,
759 newbase=revision, printed_path=printed_path,
760 merge=options.merge)
761 printed_path = True
762 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000763 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000764 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000765 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000766 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000767 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000768 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000769 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000770 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
771 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000772 force_switch = False
773 if options.force:
774 try:
John Budorick882c91e2018-07-12 22:11:41 +0000775 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000776 # case 4a
777 force_switch = True
778 except gclient_utils.Error as e:
779 if options.reset:
780 # case 4b
781 force_switch = True
782 else:
783 switch_error = '%s\n%s' % (e.message, switch_error)
784 if force_switch:
785 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000786 (upstream_branch, new_base))
787 switch_branch = 'gclient_' + remote_ref[1]
788 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000789 self._Checkout(options, switch_branch, force=True, quiet=True)
790 else:
791 # case 4c
792 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000793 else:
794 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800795 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000796 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000797 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000798 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000799 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000800 if options.merge:
801 merge_args.append('--ff')
802 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000803 merge_args.append('--ff-only')
804 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000805 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000806 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700807 rebase_files = []
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000808 if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr):
809 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000810 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700811 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000812 printed_path = True
813 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000814 if not options.auto_rebase:
815 try:
816 action = self._AskForData(
817 'Cannot %s, attempt to rebase? '
818 '(y)es / (q)uit / (s)kip : ' %
819 ('merge' if options.merge else 'fast-forward merge'),
820 options)
821 except ValueError:
822 raise gclient_utils.Error('Invalid Character')
823 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700824 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000825 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000826 printed_path = True
827 break
828 elif re.match(r'quit|q', action, re.I):
829 raise gclient_utils.Error("Can't fast-forward, please merge or "
830 "rebase manually.\n"
831 "cd %s && git " % self.checkout_path
832 + "rebase %s" % upstream_branch)
833 elif re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000834 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000835 return
836 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000837 self.Print('Input not recognized')
smut@google.com27c9c8a2014-09-11 19:57:55 +0000838 elif re.match("error: Your local changes to '.*' would be "
839 "overwritten by merge. Aborting.\nPlease, commit your "
840 "changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000841 e.stderr):
842 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000843 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700844 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000845 printed_path = True
846 raise gclient_utils.Error(e.stderr)
847 else:
848 # Some other problem happened with the merge
849 logging.error("Error during fast-forward merge in %s!" % self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000850 self.Print(e.stderr)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000851 raise
852 else:
853 # Fast-forward merge was successful
854 if not re.match('Already up-to-date.', merge_output) or verbose:
855 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700856 self.Print('_____ %s at %s' % (self.relpath, revision),
857 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000858 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000859 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000860 if not verbose:
861 # Make the output a little prettier. It's nice to have some
862 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000863 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000864
agablec3937b92016-10-25 10:13:03 -0700865 if file_list is not None:
866 file_list.extend(
867 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000868
869 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000870 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700871 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000872 '\nConflict while rebasing this branch.\n'
873 'Fix the conflict and run gclient again.\n'
874 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700875 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000876
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000877 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000878 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
879 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000880
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000881 # If --reset and --delete_unversioned_trees are specified, remove any
882 # untracked directories.
883 if options.reset and options.delete_unversioned_trees:
884 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
885 # merge-base by default), so doesn't include untracked files. So we use
886 # 'git ls-files --directory --others --exclude-standard' here directly.
887 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800888 ['-c', 'core.quotePath=false', 'ls-files',
889 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000890 self.checkout_path)
891 for path in (p for p in paths.splitlines() if p.endswith('/')):
892 full_path = os.path.join(self.checkout_path, path)
893 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000894 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000895 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000896
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000897 return self._Capture(['rev-parse', '--verify', 'HEAD'])
898
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000899 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000900 """Reverts local modifications.
901
902 All reverted files will be appended to file_list.
903 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000904 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000905 # revert won't work if the directory doesn't exist. It needs to
906 # checkout instead.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000907 self.Print('_____ %s is missing, synching instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000908 # Don't reuse the args.
909 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000910
911 default_rev = "refs/heads/master"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000912 if options.upstream:
913 if self._GetCurrentBranch():
914 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
915 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000916 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000917 if not deps_revision:
918 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000919 if deps_revision.startswith('refs/heads/'):
920 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700921 try:
922 deps_revision = self.GetUsableRev(deps_revision, options)
923 except NoUsableRevError as e:
924 # If the DEPS entry's url and hash changed, try to update the origin.
925 # See also http://crbug.com/520067.
926 logging.warn(
927 'Couldn\'t find usable revision, will retrying to update instead: %s',
928 e.message)
929 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000930
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000931 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800932 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000933
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800934 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000935 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000936
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000937 if file_list is not None:
938 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
939
940 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000941 """Returns revision"""
942 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000943
msb@chromium.orge28e4982009-09-25 20:51:45 +0000944 def runhooks(self, options, args, file_list):
945 self.status(options, args, file_list)
946
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000947 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000948 """Display status information."""
949 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000950 self.Print('________ couldn\'t run status in %s:\n'
951 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000952 else:
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700953 try:
954 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
955 except subprocess2.CalledProcessError:
956 merge_base = []
Aaron Gablef4068aa2017-12-12 15:14:09 -0800957 self._Run(
958 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
959 options, stdout=self.out_fh, always=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000960 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800961 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000962 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000963
smutae7ea312016-07-18 11:59:41 -0700964 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700965 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700966 sha1 = None
967 if not os.path.isdir(self.checkout_path):
968 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800969 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700970
971 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
972 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700973 else:
agable41e3a6c2016-10-20 11:36:56 -0700974 # May exist in origin, but we don't have it yet, so fetch and look
975 # again.
976 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -0700977 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
978 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700979
980 if not sha1:
981 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800982 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -0700983
984 return sha1
985
primiano@chromium.org1c127382015-02-17 11:15:40 +0000986 def GetGitBackupDirPath(self):
987 """Returns the path where the .git folder for the current project can be
988 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
989 return os.path.join(self._root_dir,
990 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
991
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000992 def _GetMirror(self, url, options, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000993 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +0000994 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000995 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000996 mirror_kwargs = {
997 'print_func': self.filter,
John Budorick882c91e2018-07-12 22:11:41 +0000998 'refs': []
hinoka@google.comb1b54572014-04-16 22:29:23 +0000999 }
hinoka@google.comb1b54572014-04-16 22:29:23 +00001000 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
1001 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001002 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
1003 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001004 if hasattr(options, 'with_tags') and options.with_tags:
1005 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +00001006 elif revision_ref and revision_ref.startswith('refs/tags/'):
1007 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001008 return git_cache.Mirror(url, **mirror_kwargs)
1009
John Budorick882c91e2018-07-12 22:11:41 +00001010 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001011 """Update a git mirror by fetching the latest commits from the remote,
1012 unless mirror already contains revision whose type is sha1 hash.
1013 """
John Budorick882c91e2018-07-12 22:11:41 +00001014 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -08001015 if options.verbose:
1016 self.Print('skipping mirror update, it has rev=%s already' % revision,
1017 timestamp=False)
1018 return
1019
szager@chromium.org3ec84f62014-08-22 21:00:22 +00001020 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +00001021 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001022 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +00001023 depth = 10
1024 else:
1025 depth = 10000
1026 else:
1027 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +00001028 mirror.populate(verbose=options.verbose,
1029 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +00001030 depth=depth,
1031 ignore_lock=getattr(options, 'ignore_locks', False),
1032 lock_timeout=getattr(options, 'lock_timeout', 0))
1033 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001034
John Budorick882c91e2018-07-12 22:11:41 +00001035 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001036 """Clone a git repository from the given URL.
1037
msb@chromium.org786fb682010-06-02 15:16:23 +00001038 Once we've cloned the repo, we checkout a working branch if the specified
1039 revision is a branch head. If it is a tag or a specific commit, then we
1040 leave HEAD detached as it makes future updates simpler -- in this case the
1041 user should first create a new branch or switch to an existing branch before
1042 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001043 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001044 # git clone doesn't seem to insert a newline properly before printing
1045 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001046 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +00001047 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +00001048 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001049 if self.cache_dir:
1050 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001051 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001052 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001053 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001054 # If the parent directory does not exist, Git clone on Windows will not
1055 # create it, so we need to do it manually.
1056 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001057 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001058
1059 template_dir = None
1060 if hasattr(options, 'no_history') and options.no_history:
John Budorick882c91e2018-07-12 22:11:41 +00001061 if gclient_utils.IsGitSha(revision):
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001062 # In the case of a subproject, the pinned sha is not necessarily the
1063 # head of the remote branch (so we can't just use --depth=N). Instead,
1064 # we tell git to fetch all the remote objects from SHA..HEAD by means of
1065 # a template git dir which has a 'shallow' file pointing to the sha.
1066 template_dir = tempfile.mkdtemp(
1067 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
1068 dir=parent_dir)
1069 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
1070 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
1071 template_file.write(revision)
1072 clone_cmd.append('--template=' + template_dir)
1073 else:
1074 # Otherwise, we're just interested in the HEAD. Just use --depth.
1075 clone_cmd.append('--depth=1')
1076
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001077 tmp_dir = tempfile.mkdtemp(
1078 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
1079 dir=parent_dir)
1080 try:
1081 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +01001082 if self.print_outbuf:
1083 print_stdout = True
1084 stdout = gclient_utils.WriteToStdout(self.out_fh)
1085 else:
1086 print_stdout = False
1087 stdout = self.out_fh
1088 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
1089 print_stdout=print_stdout, stdout=stdout)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001090 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +00001091 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1092 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001093 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001094 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001095 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001096 finally:
1097 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001098 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001099 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001100 if template_dir:
1101 gclient_utils.rmtree(template_dir)
Edward Lemur579c9862018-07-13 23:17:51 +00001102 self._SetFetchConfig(options)
1103 self._Fetch(options, prune=options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001104 revision = self._AutoFetchRef(options, revision)
1105 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1106 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001107 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001108 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001109 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +00001110 ('Checked out %s to a detached HEAD. Before making any commits\n'
1111 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
1112 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
John Budorick882c91e2018-07-12 22:11:41 +00001113 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001114
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001115 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001116 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001117 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001118 raise gclient_utils.Error("Background task requires input. Rerun "
1119 "gclient with --jobs=1 so that\n"
1120 "interaction is possible.")
1121 try:
1122 return raw_input(prompt)
1123 except KeyboardInterrupt:
1124 # Hide the exception.
1125 sys.exit(1)
1126
1127
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001128 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001129 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001130 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001131 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001132 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001133 revision = upstream
1134 if newbase:
1135 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001136 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001137 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001138 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001139 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001140 printed_path = True
1141 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001142 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001143
1144 if merge:
1145 merge_output = self._Capture(['merge', revision])
1146 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001147 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001148 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001149
1150 # Build the rebase command here using the args
1151 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1152 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001153 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001154 rebase_cmd.append('--verbose')
1155 if newbase:
1156 rebase_cmd.extend(['--onto', newbase])
1157 rebase_cmd.append(upstream)
1158 if branch:
1159 rebase_cmd.append(branch)
1160
1161 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001162 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001163 except subprocess2.CalledProcessError as e:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001164 if (re.match(r'cannot rebase: you have unstaged changes', e.stderr) or
1165 re.match(r'cannot rebase: your index contains uncommitted changes',
1166 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001167 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001168 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001169 'Cannot rebase because of unstaged changes.\n'
1170 '\'git reset --hard HEAD\' ?\n'
1171 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001172 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001173 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001174 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001175 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001176 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001177 break
1178 elif re.match(r'quit|q', rebase_action, re.I):
1179 raise gclient_utils.Error("Please merge or rebase manually\n"
1180 "cd %s && git " % self.checkout_path
1181 + "%s" % ' '.join(rebase_cmd))
1182 elif re.match(r'show|s', rebase_action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001183 self.Print('%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001184 continue
1185 else:
1186 gclient_utils.Error("Input not recognized")
1187 continue
1188 elif re.search(r'^CONFLICT', e.stdout, re.M):
1189 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1190 "Fix the conflict and run gclient again.\n"
1191 "See 'man git-rebase' for details.\n")
1192 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001193 self.Print(e.stdout.strip())
1194 self.Print('Rebase produced error output:\n%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001195 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1196 "manually.\ncd %s && git " %
1197 self.checkout_path
1198 + "%s" % ' '.join(rebase_cmd))
1199
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001200 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001201 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001202 # Make the output a little prettier. It's nice to have some
1203 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001204 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001205
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001206 @staticmethod
1207 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001208 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1209 if not ok:
1210 raise gclient_utils.Error('git version %s < minimum required %s' %
1211 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001212
John Budorick882c91e2018-07-12 22:11:41 +00001213 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001214 # Special case handling if all 3 conditions are met:
1215 # * the mirros have recently changed, but deps destination remains same,
1216 # * the git histories of mirrors are conflicting.
1217 # * git cache is used
1218 # This manifests itself in current checkout having invalid HEAD commit on
1219 # most git operations. Since git cache is used, just deleted the .git
1220 # folder, and re-create it by cloning.
1221 try:
1222 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1223 except subprocess2.CalledProcessError as e:
1224 if ('fatal: bad object HEAD' in e.stderr
1225 and self.cache_dir and self.cache_dir in url):
1226 self.Print((
1227 'Likely due to DEPS change with git cache_dir, '
1228 'the current commit points to no longer existing object.\n'
1229 '%s' % e)
1230 )
1231 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001232 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001233 else:
1234 raise
1235
msb@chromium.org786fb682010-06-02 15:16:23 +00001236 def _IsRebasing(self):
1237 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1238 # have a plumbing command to determine whether a rebase is in progress, so
1239 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1240 g = os.path.join(self.checkout_path, '.git')
1241 return (
1242 os.path.isdir(os.path.join(g, "rebase-merge")) or
1243 os.path.isdir(os.path.join(g, "rebase-apply")))
1244
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001245 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001246 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1247 if os.path.exists(lockfile):
1248 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001249 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001250 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1251 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001252 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001253
msb@chromium.org786fb682010-06-02 15:16:23 +00001254 # Make sure the tree is clean; see git-rebase.sh for reference
1255 try:
1256 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001257 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001258 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001259 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001260 '\tYou have unstaged changes.\n'
1261 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001262 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001263 try:
1264 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001265 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001266 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001267 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001268 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001269 '\tYour index contains uncommitted changes\n'
1270 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001271 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001272
agable83faed02016-10-24 14:37:10 -07001273 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001274 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1275 # reference by a commit). If not, error out -- most likely a rebase is
1276 # in progress, try to detect so we can give a better error.
1277 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001278 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1279 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001280 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001281 # Commit is not contained by any rev. See if the user is rebasing:
1282 if self._IsRebasing():
1283 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001284 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001285 '\tAlready in a conflict, i.e. (no branch).\n'
1286 '\tFix the conflict and run gclient again.\n'
1287 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1288 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001289 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001290 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001291 name = ('saved-by-gclient-' +
1292 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001293 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001294 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001295 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001296
msb@chromium.org5bde4852009-12-14 16:47:12 +00001297 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001298 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001299 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001300 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001301 return None
1302 return branch
1303
borenet@google.comc3e09d22014-04-10 13:58:18 +00001304 def _Capture(self, args, **kwargs):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001305 kwargs.setdefault('cwd', self.checkout_path)
1306 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001307 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001308 env = scm.GIT.ApplyEnvVars(kwargs)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001309 ret = subprocess2.check_output(['git'] + args, env=env, **kwargs)
1310 if strip:
1311 ret = ret.strip()
1312 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001313
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001314 def _Checkout(self, options, ref, force=False, quiet=None):
1315 """Performs a 'git-checkout' operation.
1316
1317 Args:
1318 options: The configured option set
1319 ref: (str) The branch/commit to checkout
1320 quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
1321 'None', the behavior is inferred from 'options.verbose'.
1322 Returns: (str) The output of the checkout operation
1323 """
1324 if quiet is None:
1325 quiet = (not options.verbose)
1326 checkout_args = ['checkout']
1327 if force:
1328 checkout_args.append('--force')
1329 if quiet:
1330 checkout_args.append('--quiet')
1331 checkout_args.append(ref)
1332 return self._Capture(checkout_args)
1333
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001334 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1335 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001336 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001337 # When updating, the ref is modified to be a remote ref .
1338 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1339 # Try to reverse that mapping.
1340 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1341 if original_ref:
1342 refspec = original_ref + ':' + refspec
1343 # When a mirror is configured, it only fetches
1344 # refs/{heads,branch-heads,tags}/*.
1345 # If asked to fetch other refs, we must fetch those directly from the
1346 # repository, and not from the mirror.
1347 if not original_ref.startswith(
1348 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1349 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001350 fetch_cmd = cfg + [
1351 'fetch',
1352 remote or self.remote,
1353 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001354 if refspec:
1355 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001356
1357 if prune:
1358 fetch_cmd.append('--prune')
1359 if options.verbose:
1360 fetch_cmd.append('--verbose')
1361 elif quiet:
1362 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001363 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001364
1365 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1366 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1367
Edward Lemur579c9862018-07-13 23:17:51 +00001368 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001369 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1370 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001371 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001372 try:
1373 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1374 options)
1375 self._Run(['config', 'remote.%s.fetch' % self.remote,
1376 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1377 except subprocess2.CalledProcessError as e:
1378 # If exit code was 5, it means we attempted to unset a config that
1379 # didn't exist. Ignore it.
1380 if e.returncode != 5:
1381 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001382 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001383 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001384 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1385 '^\\+refs/branch-heads/\\*:.*$']
1386 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001387 if hasattr(options, 'with_tags') and options.with_tags:
1388 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1389 '+refs/tags/*:refs/tags/*',
1390 '^\\+refs/tags/\\*:.*$']
1391 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001392
John Budorick882c91e2018-07-12 22:11:41 +00001393 def _AutoFetchRef(self, options, revision):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001394 """Attempts to fetch |revision| if not available in local repo.
1395
1396 Returns possibly updated revision."""
John Budorick882c91e2018-07-12 22:11:41 +00001397 try:
1398 self._Capture(['rev-parse', revision])
1399 except subprocess2.CalledProcessError:
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001400 self._Fetch(options, refspec=revision)
1401 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1402 return revision
1403
dnj@chromium.org680f2172014-06-25 00:39:32 +00001404 def _Run(self, args, options, show_header=True, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001405 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001406 kwargs.setdefault('cwd', self.checkout_path)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001407 kwargs.setdefault('stdout', self.out_fh)
szager@chromium.org848fd492014-04-09 19:06:44 +00001408 kwargs['filter_fn'] = self.filter
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001409 kwargs.setdefault('print_stdout', False)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001410 env = scm.GIT.ApplyEnvVars(kwargs)
agable@chromium.org772efaf2014-04-01 02:35:44 +00001411 cmd = ['git'] + args
dnj@chromium.org680f2172014-06-25 00:39:32 +00001412 if show_header:
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001413 gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs)
1414 else:
1415 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001416
1417
1418class CipdPackage(object):
1419 """A representation of a single CIPD package."""
1420
John Budorickd3ba72b2018-03-20 12:27:42 -07001421 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001422 self._authority_for_subdir = authority_for_subdir
1423 self._name = name
1424 self._version = version
1425
1426 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001427 def authority_for_subdir(self):
1428 """Whether this package has authority to act on behalf of its subdir.
1429
1430 Some operations should only be performed once per subdirectory. A package
1431 that has authority for its subdirectory is the only package that should
1432 perform such operations.
1433
1434 Returns:
1435 bool; whether this package has subdir authority.
1436 """
1437 return self._authority_for_subdir
1438
1439 @property
1440 def name(self):
1441 return self._name
1442
1443 @property
1444 def version(self):
1445 return self._version
1446
1447
1448class CipdRoot(object):
1449 """A representation of a single CIPD root."""
1450 def __init__(self, root_dir, service_url):
1451 self._all_packages = set()
1452 self._mutator_lock = threading.Lock()
1453 self._packages_by_subdir = collections.defaultdict(list)
1454 self._root_dir = root_dir
1455 self._service_url = service_url
1456
1457 def add_package(self, subdir, package, version):
1458 """Adds a package to this CIPD root.
1459
1460 As far as clients are concerned, this grants both root and subdir authority
1461 to packages arbitrarily. (The implementation grants root authority to the
1462 first package added and subdir authority to the first package added for that
1463 subdir, but clients should not depend on or expect that behavior.)
1464
1465 Args:
1466 subdir: str; relative path to where the package should be installed from
1467 the cipd root directory.
1468 package: str; the cipd package name.
1469 version: str; the cipd package version.
1470 Returns:
1471 CipdPackage; the package that was created and added to this root.
1472 """
1473 with self._mutator_lock:
1474 cipd_package = CipdPackage(
1475 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001476 not self._packages_by_subdir[subdir])
1477 self._all_packages.add(cipd_package)
1478 self._packages_by_subdir[subdir].append(cipd_package)
1479 return cipd_package
1480
1481 def packages(self, subdir):
1482 """Get the list of configured packages for the given subdir."""
1483 return list(self._packages_by_subdir[subdir])
1484
1485 def clobber(self):
1486 """Remove the .cipd directory.
1487
1488 This is useful for forcing ensure to redownload and reinitialize all
1489 packages.
1490 """
1491 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001492 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001493 try:
1494 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1495 except OSError:
1496 if os.path.exists(cipd_cache_dir):
1497 raise
1498
1499 @contextlib.contextmanager
1500 def _create_ensure_file(self):
1501 try:
1502 ensure_file = None
1503 with tempfile.NamedTemporaryFile(
Raul Tambreb946b232019-03-26 14:48:46 +00001504 suffix='.ensure', delete=False, mode='w') as ensure_file:
John Budorick302bb842018-07-17 23:49:17 +00001505 ensure_file.write('$ParanoidMode CheckPresence\n\n')
Raul Tambreb946b232019-03-26 14:48:46 +00001506 for subdir, packages in sorted(self._packages_by_subdir.items()):
John Budorick0f7b2002018-01-19 15:46:17 -08001507 ensure_file.write('@Subdir %s\n' % subdir)
Edward Lemurfbb06aa2018-06-11 20:43:06 +00001508 for package in sorted(packages, key=lambda p: p.name):
John Budorick0f7b2002018-01-19 15:46:17 -08001509 ensure_file.write('%s %s\n' % (package.name, package.version))
1510 ensure_file.write('\n')
1511 yield ensure_file.name
1512 finally:
1513 if ensure_file is not None and os.path.exists(ensure_file.name):
1514 os.remove(ensure_file.name)
1515
1516 def ensure(self):
1517 """Run `cipd ensure`."""
1518 with self._mutator_lock:
1519 with self._create_ensure_file() as ensure_file:
1520 cmd = [
1521 'cipd', 'ensure',
1522 '-log-level', 'error',
1523 '-root', self.root_dir,
1524 '-ensure-file', ensure_file,
1525 ]
1526 gclient_utils.CheckCallAndFilterAndHeader(cmd)
1527
John Budorickd3ba72b2018-03-20 12:27:42 -07001528 def run(self, command):
1529 if command == 'update':
1530 self.ensure()
1531 elif command == 'revert':
1532 self.clobber()
1533 self.ensure()
1534
John Budorick0f7b2002018-01-19 15:46:17 -08001535 def created_package(self, package):
1536 """Checks whether this root created the given package.
1537
1538 Args:
1539 package: CipdPackage; the package to check.
1540 Returns:
1541 bool; whether this root created the given package.
1542 """
1543 return package in self._all_packages
1544
1545 @property
1546 def root_dir(self):
1547 return self._root_dir
1548
1549 @property
1550 def service_url(self):
1551 return self._service_url
1552
1553
1554class CipdWrapper(SCMWrapper):
1555 """Wrapper for CIPD.
1556
1557 Currently only supports chrome-infra-packages.appspot.com.
1558 """
John Budorick3929e9e2018-02-04 18:18:07 -08001559 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001560
1561 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1562 out_cb=None, root=None, package=None):
1563 super(CipdWrapper, self).__init__(
1564 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1565 out_cb=out_cb)
1566 assert root.created_package(package)
1567 self._package = package
1568 self._root = root
1569
1570 #override
1571 def GetCacheMirror(self):
1572 return None
1573
1574 #override
1575 def GetActualRemoteURL(self, options):
1576 return self._root.service_url
1577
1578 #override
1579 def DoesRemoteURLMatch(self, options):
1580 del options
1581 return True
1582
1583 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001584 """Does nothing.
1585
1586 CIPD packages should be reverted at the root by running
1587 `CipdRoot.run('revert')`.
1588 """
1589 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001590
1591 def diff(self, options, args, file_list):
1592 """CIPD has no notion of diffing."""
1593 pass
1594
1595 def pack(self, options, args, file_list):
1596 """CIPD has no notion of diffing."""
1597 pass
1598
1599 def revinfo(self, options, args, file_list):
1600 """Grab the instance ID."""
1601 try:
1602 tmpdir = tempfile.mkdtemp()
1603 describe_json_path = os.path.join(tmpdir, 'describe.json')
1604 cmd = [
1605 'cipd', 'describe',
1606 self._package.name,
1607 '-log-level', 'error',
1608 '-version', self._package.version,
1609 '-json-output', describe_json_path
1610 ]
1611 gclient_utils.CheckCallAndFilter(
1612 cmd, filter_fn=lambda _line: None, print_stdout=False)
1613 with open(describe_json_path) as f:
1614 describe_json = json.load(f)
1615 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1616 finally:
1617 gclient_utils.rmtree(tmpdir)
1618
1619 def status(self, options, args, file_list):
1620 pass
1621
1622 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001623 """Does nothing.
1624
1625 CIPD packages should be updated at the root by running
1626 `CipdRoot.run('update')`.
1627 """
1628 pass