blob: eab0ad098dffdf36ffc9194195bd30be6d0674f1 [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 Lemur6a4e31b2018-08-10 19:59:02 +0000375 def apply_patch_ref(self, patch_repo, patch_ref, target_branch, options,
376 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|.
381 We also need the |target_branch| that the patch was uploaded against. We use
382 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
387 ... -> merge_base -> [possibly already landed commits] -> target_branch
388 \
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
393 from |target_branch|, but this is not required.
394
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'.
405 target_branch: The branch the patch was uploaded against.
406 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
419 if target_branch:
420 # Convert the target branch to a remote ref if possible.
421 remote_ref = scm.GIT.RefToRemoteRef(target_branch, self.remote)
422 if remote_ref:
423 target_branch = ''.join(remote_ref)
424 else:
425 target_branch = self._GetTargetBranchForCommit(base_rev)
426
427 # Fallback to the commit we got.
428 # This means that apply_path_ref will try to find the merge-base between the
429 # patch and the commit (which is most likely the commit) and cherry-pick
430 # everything in between.
431 if not target_branch:
432 target_branch = base_rev
433
Edward Lesmesc621b212018-03-21 20:26:56 -0400434 self.Print('===Applying patch ref===')
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000435 self.Print('Patch ref is %r @ %r. Target branch for patch is %r. '
436 'Current HEAD is %r. Current dir is %r' % (
437 patch_repo, patch_ref, target_branch, base_rev,
438 self.checkout_path))
Edward Lesmesc621b212018-03-21 20:26:56 -0400439 self._Capture(['reset', '--hard'])
440 self._Capture(['fetch', patch_repo, patch_ref])
Edward Lemurca7d8812018-07-24 17:42:45 +0000441 patch_rev = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400442
Edward Lemurca7d8812018-07-24 17:42:45 +0000443 try:
444 if not options.rebase_patch_ref:
445 self._Capture(['checkout', patch_rev])
446 else:
447 # Find the merge-base between the branch_rev and patch_rev to find out
448 # the changes we need to cherry-pick on top of base_rev.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000449 merge_base = self._Capture(['merge-base', target_branch, patch_rev])
Edward Lemurca7d8812018-07-24 17:42:45 +0000450 self.Print('Merge base of %s and %s is %s' % (
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000451 target_branch, patch_rev, merge_base))
Edward Lemurca7d8812018-07-24 17:42:45 +0000452 if merge_base == patch_rev:
453 # If the merge-base is patch_rev, it means patch_rev is already part
454 # of the history, so just check it out.
455 self._Capture(['checkout', patch_rev])
456 else:
457 # If a change was uploaded on top of another change, which has already
458 # landed, one of the commits in the cherry-pick range will be
459 # redundant, since it has already landed and its changes incorporated
460 # in the tree.
461 # We pass '--keep-redundant-commits' to ignore those changes.
462 self._Capture(['cherry-pick', merge_base + '..' + patch_rev,
463 '--keep-redundant-commits'])
464
465 if file_list is not None:
466 file_list.extend(self._GetDiffFilenames(base_rev))
467
468 except subprocess2.CalledProcessError as e:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000469 self.Print('Failed to apply patch.')
470 self.Print('Patch ref is %r @ %r. Target branch for patch is %r. '
471 'Current HEAD is %r. Current dir is %r' % (
472 patch_repo, patch_ref, target_branch, base_rev,
473 self.checkout_path))
Edward Lemurca7d8812018-07-24 17:42:45 +0000474 self.Print('git returned non-zero exit status %s:\n%s' % (
475 e.returncode, e.stderr))
476 # Print the current status so that developers know what changes caused the
477 # patch failure, since git cherry-pick doesn't show that information.
478 self.Print(self._Capture(['status']))
John Budorick2c984a02018-07-18 23:24:13 +0000479 try:
Edward Lemurca7d8812018-07-24 17:42:45 +0000480 self._Capture(['cherry-pick', '--abort'])
481 except subprocess2.CalledProcessError:
482 pass
483 raise
484
Edward Lesmesc621b212018-03-21 20:26:56 -0400485 if options.reset_patch_ref:
486 self._Capture(['reset', '--soft', base_rev])
487
msb@chromium.orge28e4982009-09-25 20:51:45 +0000488 def update(self, options, args, file_list):
489 """Runs git to update or transparently checkout the working copy.
490
491 All updated files will be appended to file_list.
492
493 Raises:
494 Error: if can't get URL for relative path.
495 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000496 if args:
497 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
498
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000499 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000500
John Budorick882c91e2018-07-12 22:11:41 +0000501 # If a dependency is not pinned, track the default remote branch.
502 default_rev = 'refs/remotes/%s/master' % self.remote
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000503 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000504 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000505 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000506 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000507 # Override the revision number.
508 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000509 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000510 # Check again for a revision in case an initial ref was specified
511 # in the url, for example bla.git@refs/heads/custombranch
512 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000513 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000514 if not revision:
515 revision = default_rev
msb@chromium.orge28e4982009-09-25 20:51:45 +0000516
szager@chromium.org8a139702014-06-20 15:55:01 +0000517 if managed:
518 self._DisableHooks()
519
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000520 printed_path = False
521 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000522 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700523 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000524 verbose = ['--verbose']
525 printed_path = True
526
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000527 revision_ref = revision
528 if ':' in revision:
529 revision_ref, _, revision = revision.partition(':')
530
531 mirror = self._GetMirror(url, options, revision_ref)
532 if mirror:
533 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000534
John Budorick882c91e2018-07-12 22:11:41 +0000535 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
536 if remote_ref:
537 # Rewrite remote refs to their local equivalents.
538 revision = ''.join(remote_ref)
539 rev_type = "branch"
540 elif revision.startswith('refs/'):
541 # Local branch? We probably don't want to support, since DEPS should
542 # always specify branches as they are in the upstream repo.
543 rev_type = "branch"
544 else:
545 # hash is also a tag, only make a distinction at checkout
546 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000547
primiano@chromium.org1c127382015-02-17 11:15:40 +0000548 # If we are going to introduce a new project, there is a possibility that
549 # we are syncing back to a state where the project was originally a
550 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
551 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
552 # In such case, we might have a backup of the former .git folder, which can
553 # be used to avoid re-fetching the entire repo again (useful for bisects).
554 backup_dir = self.GetGitBackupDirPath()
555 target_dir = os.path.join(self.checkout_path, '.git')
556 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
557 gclient_utils.safe_makedirs(self.checkout_path)
558 os.rename(backup_dir, target_dir)
559 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800560 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000561
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000562 if (not os.path.exists(self.checkout_path) or
563 (os.path.isdir(self.checkout_path) and
564 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000565 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000566 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000567 try:
John Budorick882c91e2018-07-12 22:11:41 +0000568 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000569 except subprocess2.CalledProcessError:
570 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000571 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000572 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800573 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000574 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000575 file_list.extend(
576 [os.path.join(self.checkout_path, f.decode()) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000577 if mirror:
578 self._Capture(
579 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000580 if not verbose:
581 # Make the output a little prettier. It's nice to have some whitespace
582 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000583 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000584 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000585
John Budorick21a51b32018-09-19 19:39:20 +0000586 if mirror:
587 self._Capture(
588 ['remote', 'set-url', '--push', 'origin', mirror.url])
589
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000590 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000591 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000592 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
593 return self._Capture(['rev-parse', '--verify', 'HEAD'])
594
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000595 self._maybe_break_locks(options)
596
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000597 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000598 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000599
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000600 # See if the url has changed (the unittests use git://foo for the url, let
601 # that through).
602 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
603 return_early = False
604 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
605 # unit test pass. (and update the comment above)
606 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
607 # This allows devs to use experimental repos which have a different url
608 # but whose branch(s) are the same as official repos.
Raul Tambreb946b232019-03-26 14:48:46 +0000609 if (current_url.rstrip(b'/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000610 subprocess2.capture(
Aaron Gableac9b0f32019-04-18 17:38:37 +0000611 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000612 cwd=self.checkout_path).strip() != 'False'):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000613 self.Print('_____ switching %s to a new upstream' % self.relpath)
iannucci@chromium.org78514212014-08-20 23:08:00 +0000614 if not (options.force or options.reset):
615 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700616 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000617 # Switch over to the new upstream
618 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000619 if mirror:
620 with open(os.path.join(
621 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
622 'w') as fh:
623 fh.write(os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000624 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
625 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000626
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000627 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000628 else:
John Budorick882c91e2018-07-12 22:11:41 +0000629 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000630
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000631 if return_early:
632 return self._Capture(['rev-parse', '--verify', 'HEAD'])
633
msb@chromium.org5bde4852009-12-14 16:47:12 +0000634 cur_branch = self._GetCurrentBranch()
635
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000636 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000637 # 0) HEAD is detached. Probably from our initial clone.
638 # - make sure HEAD is contained by a named ref, then update.
639 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700640 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000641 # - try to rebase onto the new hash or branch
642 # 2) current branch is tracking a remote branch with local committed
643 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000644 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000645 # 3) current branch is tracking a remote branch w/or w/out changes, and
646 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000647 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000648 # 4) current branch is tracking a remote branch, but DEPS switches to a
649 # different remote branch, and
650 # a) current branch has no local changes, and --force:
651 # - checkout new branch
652 # b) current branch has local changes, and --force and --reset:
653 # - checkout new branch
654 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000655
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000656 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
657 # a tracking branch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000658 # or 'master' if not a tracking branch (it's based on a specific rev/hash)
659 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000660 if cur_branch is None:
661 upstream_branch = None
662 current_type = "detached"
663 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000664 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000665 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
666 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
667 current_type = "hash"
668 logging.debug("Current branch is not tracking an upstream (remote)"
669 " branch.")
670 elif upstream_branch.startswith('refs/remotes'):
671 current_type = "branch"
672 else:
673 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000674
Edward Lemur579c9862018-07-13 23:17:51 +0000675 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000676
Michael Spang73fac912019-03-08 18:44:19 +0000677 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000678 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000679 self._Fetch(options, prune=options.force)
680
681 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
682 sha_only=True):
683 # Update the remotes first so we have all the refs.
684 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
685 cwd=self.checkout_path)
686 if verbose:
687 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000688
John Budorick882c91e2018-07-12 22:11:41 +0000689 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200690
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000691 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000692 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000693 target = 'HEAD'
694 if options.upstream and upstream_branch:
695 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800696 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000697
msb@chromium.org786fb682010-06-02 15:16:23 +0000698 if current_type == 'detached':
699 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800700 # We just did a Scrub, this is as clean as it's going to get. In
701 # particular if HEAD is a commit that contains two versions of the same
702 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
703 # to actually "Clean" the checkout; that commit is uncheckoutable on this
704 # system. The best we can do is carry forward to the checkout step.
705 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000706 self._CheckClean(revision)
707 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000708 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000709 self.Print('Up-to-date; skipping checkout.')
710 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000711 # 'git checkout' may need to overwrite existing untracked files. Allow
712 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000713 self._Checkout(
714 options,
John Budorick882c91e2018-07-12 22:11:41 +0000715 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000716 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000717 quiet=True,
718 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000719 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000720 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000721 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000722 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700723 # Can't find a merge-base since we don't know our upstream. That makes
724 # this command VERY likely to produce a rebase failure. For now we
725 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000726 upstream_branch = self.remote
727 if options.revision or deps_revision:
728 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700729 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700730 printed_path=printed_path, merge=options.merge)
731 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000732 elif rev_type == 'hash':
733 # case 2
734 self._AttemptRebase(upstream_branch, file_list, options,
735 newbase=revision, printed_path=printed_path,
736 merge=options.merge)
737 printed_path = True
738 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000739 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000740 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000741 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000742 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000743 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000744 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000745 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000746 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
747 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000748 force_switch = False
749 if options.force:
750 try:
John Budorick882c91e2018-07-12 22:11:41 +0000751 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000752 # case 4a
753 force_switch = True
754 except gclient_utils.Error as e:
755 if options.reset:
756 # case 4b
757 force_switch = True
758 else:
759 switch_error = '%s\n%s' % (e.message, switch_error)
760 if force_switch:
761 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000762 (upstream_branch, new_base))
763 switch_branch = 'gclient_' + remote_ref[1]
764 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000765 self._Checkout(options, switch_branch, force=True, quiet=True)
766 else:
767 # case 4c
768 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000769 else:
770 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800771 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000772 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000773 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000774 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000775 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000776 if options.merge:
777 merge_args.append('--ff')
778 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000779 merge_args.append('--ff-only')
780 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000781 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000782 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700783 rebase_files = []
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000784 if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr):
785 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000786 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700787 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000788 printed_path = True
789 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000790 if not options.auto_rebase:
791 try:
792 action = self._AskForData(
793 'Cannot %s, attempt to rebase? '
794 '(y)es / (q)uit / (s)kip : ' %
795 ('merge' if options.merge else 'fast-forward merge'),
796 options)
797 except ValueError:
798 raise gclient_utils.Error('Invalid Character')
799 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700800 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000801 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000802 printed_path = True
803 break
804 elif re.match(r'quit|q', action, re.I):
805 raise gclient_utils.Error("Can't fast-forward, please merge or "
806 "rebase manually.\n"
807 "cd %s && git " % self.checkout_path
808 + "rebase %s" % upstream_branch)
809 elif re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000810 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000811 return
812 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000813 self.Print('Input not recognized')
smut@google.com27c9c8a2014-09-11 19:57:55 +0000814 elif re.match("error: Your local changes to '.*' would be "
815 "overwritten by merge. Aborting.\nPlease, commit your "
816 "changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000817 e.stderr):
818 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000819 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700820 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000821 printed_path = True
822 raise gclient_utils.Error(e.stderr)
823 else:
824 # Some other problem happened with the merge
825 logging.error("Error during fast-forward merge in %s!" % self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000826 self.Print(e.stderr)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000827 raise
828 else:
829 # Fast-forward merge was successful
830 if not re.match('Already up-to-date.', merge_output) or verbose:
831 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700832 self.Print('_____ %s at %s' % (self.relpath, revision),
833 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000834 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000835 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000836 if not verbose:
837 # Make the output a little prettier. It's nice to have some
838 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000839 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000840
agablec3937b92016-10-25 10:13:03 -0700841 if file_list is not None:
842 file_list.extend(
843 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000844
845 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000846 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700847 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000848 '\nConflict while rebasing this branch.\n'
849 'Fix the conflict and run gclient again.\n'
850 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700851 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000852
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000853 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000854 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
855 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000856
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000857 # If --reset and --delete_unversioned_trees are specified, remove any
858 # untracked directories.
859 if options.reset and options.delete_unversioned_trees:
860 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
861 # merge-base by default), so doesn't include untracked files. So we use
862 # 'git ls-files --directory --others --exclude-standard' here directly.
863 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800864 ['-c', 'core.quotePath=false', 'ls-files',
865 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000866 self.checkout_path)
867 for path in (p for p in paths.splitlines() if p.endswith('/')):
868 full_path = os.path.join(self.checkout_path, path)
869 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000870 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000871 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000872
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000873 return self._Capture(['rev-parse', '--verify', 'HEAD'])
874
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000875 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000876 """Reverts local modifications.
877
878 All reverted files will be appended to file_list.
879 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000880 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000881 # revert won't work if the directory doesn't exist. It needs to
882 # checkout instead.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000883 self.Print('_____ %s is missing, synching instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000884 # Don't reuse the args.
885 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000886
887 default_rev = "refs/heads/master"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000888 if options.upstream:
889 if self._GetCurrentBranch():
890 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
891 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000892 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000893 if not deps_revision:
894 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000895 if deps_revision.startswith('refs/heads/'):
896 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700897 try:
898 deps_revision = self.GetUsableRev(deps_revision, options)
899 except NoUsableRevError as e:
900 # If the DEPS entry's url and hash changed, try to update the origin.
901 # See also http://crbug.com/520067.
902 logging.warn(
903 'Couldn\'t find usable revision, will retrying to update instead: %s',
904 e.message)
905 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000906
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000907 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800908 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000909
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800910 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000911 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000912
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000913 if file_list is not None:
914 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
915
916 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000917 """Returns revision"""
918 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000919
msb@chromium.orge28e4982009-09-25 20:51:45 +0000920 def runhooks(self, options, args, file_list):
921 self.status(options, args, file_list)
922
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000923 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000924 """Display status information."""
925 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000926 self.Print('________ couldn\'t run status in %s:\n'
927 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000928 else:
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700929 try:
930 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
931 except subprocess2.CalledProcessError:
932 merge_base = []
Aaron Gablef4068aa2017-12-12 15:14:09 -0800933 self._Run(
934 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
935 options, stdout=self.out_fh, always=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000936 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800937 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000938 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000939
smutae7ea312016-07-18 11:59:41 -0700940 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700941 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700942 sha1 = None
943 if not os.path.isdir(self.checkout_path):
944 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800945 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700946
947 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
948 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700949 else:
agable41e3a6c2016-10-20 11:36:56 -0700950 # May exist in origin, but we don't have it yet, so fetch and look
951 # again.
952 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -0700953 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
954 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700955
956 if not sha1:
957 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800958 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -0700959
960 return sha1
961
primiano@chromium.org1c127382015-02-17 11:15:40 +0000962 def GetGitBackupDirPath(self):
963 """Returns the path where the .git folder for the current project can be
964 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
965 return os.path.join(self._root_dir,
966 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
967
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000968 def _GetMirror(self, url, options, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000969 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +0000970 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000971 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000972 mirror_kwargs = {
973 'print_func': self.filter,
John Budorick882c91e2018-07-12 22:11:41 +0000974 'refs': []
hinoka@google.comb1b54572014-04-16 22:29:23 +0000975 }
hinoka@google.comb1b54572014-04-16 22:29:23 +0000976 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
977 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000978 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
979 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +0000980 if hasattr(options, 'with_tags') and options.with_tags:
981 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000982 elif revision_ref and revision_ref.startswith('refs/tags/'):
983 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000984 return git_cache.Mirror(url, **mirror_kwargs)
985
John Budorick882c91e2018-07-12 22:11:41 +0000986 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800987 """Update a git mirror by fetching the latest commits from the remote,
988 unless mirror already contains revision whose type is sha1 hash.
989 """
John Budorick882c91e2018-07-12 22:11:41 +0000990 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800991 if options.verbose:
992 self.Print('skipping mirror update, it has rev=%s already' % revision,
993 timestamp=False)
994 return
995
szager@chromium.org3ec84f62014-08-22 21:00:22 +0000996 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000997 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000998 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000999 depth = 10
1000 else:
1001 depth = 10000
1002 else:
1003 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +00001004 mirror.populate(verbose=options.verbose,
1005 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +00001006 depth=depth,
1007 ignore_lock=getattr(options, 'ignore_locks', False),
1008 lock_timeout=getattr(options, 'lock_timeout', 0))
1009 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001010
John Budorick882c91e2018-07-12 22:11:41 +00001011 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001012 """Clone a git repository from the given URL.
1013
msb@chromium.org786fb682010-06-02 15:16:23 +00001014 Once we've cloned the repo, we checkout a working branch if the specified
1015 revision is a branch head. If it is a tag or a specific commit, then we
1016 leave HEAD detached as it makes future updates simpler -- in this case the
1017 user should first create a new branch or switch to an existing branch before
1018 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001019 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001020 # git clone doesn't seem to insert a newline properly before printing
1021 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001022 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +00001023 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +00001024 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001025 if self.cache_dir:
1026 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001027 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001028 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001029 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001030 # If the parent directory does not exist, Git clone on Windows will not
1031 # create it, so we need to do it manually.
1032 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001033 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001034
1035 template_dir = None
1036 if hasattr(options, 'no_history') and options.no_history:
John Budorick882c91e2018-07-12 22:11:41 +00001037 if gclient_utils.IsGitSha(revision):
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001038 # In the case of a subproject, the pinned sha is not necessarily the
1039 # head of the remote branch (so we can't just use --depth=N). Instead,
1040 # we tell git to fetch all the remote objects from SHA..HEAD by means of
1041 # a template git dir which has a 'shallow' file pointing to the sha.
1042 template_dir = tempfile.mkdtemp(
1043 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
1044 dir=parent_dir)
1045 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
1046 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
1047 template_file.write(revision)
1048 clone_cmd.append('--template=' + template_dir)
1049 else:
1050 # Otherwise, we're just interested in the HEAD. Just use --depth.
1051 clone_cmd.append('--depth=1')
1052
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001053 tmp_dir = tempfile.mkdtemp(
1054 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
1055 dir=parent_dir)
1056 try:
1057 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +01001058 if self.print_outbuf:
1059 print_stdout = True
1060 stdout = gclient_utils.WriteToStdout(self.out_fh)
1061 else:
1062 print_stdout = False
1063 stdout = self.out_fh
1064 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
1065 print_stdout=print_stdout, stdout=stdout)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001066 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +00001067 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1068 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001069 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001070 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001071 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001072 finally:
1073 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001074 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001075 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001076 if template_dir:
1077 gclient_utils.rmtree(template_dir)
Edward Lemur579c9862018-07-13 23:17:51 +00001078 self._SetFetchConfig(options)
1079 self._Fetch(options, prune=options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001080 revision = self._AutoFetchRef(options, revision)
1081 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1082 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001083 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001084 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001085 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +00001086 ('Checked out %s to a detached HEAD. Before making any commits\n'
1087 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
1088 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
John Budorick882c91e2018-07-12 22:11:41 +00001089 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001090
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001091 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001092 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001093 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001094 raise gclient_utils.Error("Background task requires input. Rerun "
1095 "gclient with --jobs=1 so that\n"
1096 "interaction is possible.")
1097 try:
1098 return raw_input(prompt)
1099 except KeyboardInterrupt:
1100 # Hide the exception.
1101 sys.exit(1)
1102
1103
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001104 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001105 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001106 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001107 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001108 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001109 revision = upstream
1110 if newbase:
1111 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001112 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001113 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001114 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001115 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001116 printed_path = True
1117 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001118 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001119
1120 if merge:
1121 merge_output = self._Capture(['merge', revision])
1122 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001123 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001124 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001125
1126 # Build the rebase command here using the args
1127 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1128 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001129 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001130 rebase_cmd.append('--verbose')
1131 if newbase:
1132 rebase_cmd.extend(['--onto', newbase])
1133 rebase_cmd.append(upstream)
1134 if branch:
1135 rebase_cmd.append(branch)
1136
1137 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001138 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001139 except subprocess2.CalledProcessError as e:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001140 if (re.match(r'cannot rebase: you have unstaged changes', e.stderr) or
1141 re.match(r'cannot rebase: your index contains uncommitted changes',
1142 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001143 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001144 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001145 'Cannot rebase because of unstaged changes.\n'
1146 '\'git reset --hard HEAD\' ?\n'
1147 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001148 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001149 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001150 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001151 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001152 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001153 break
1154 elif re.match(r'quit|q', rebase_action, re.I):
1155 raise gclient_utils.Error("Please merge or rebase manually\n"
1156 "cd %s && git " % self.checkout_path
1157 + "%s" % ' '.join(rebase_cmd))
1158 elif re.match(r'show|s', rebase_action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001159 self.Print('%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001160 continue
1161 else:
1162 gclient_utils.Error("Input not recognized")
1163 continue
1164 elif re.search(r'^CONFLICT', e.stdout, re.M):
1165 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1166 "Fix the conflict and run gclient again.\n"
1167 "See 'man git-rebase' for details.\n")
1168 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001169 self.Print(e.stdout.strip())
1170 self.Print('Rebase produced error output:\n%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001171 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1172 "manually.\ncd %s && git " %
1173 self.checkout_path
1174 + "%s" % ' '.join(rebase_cmd))
1175
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001176 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001177 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001178 # Make the output a little prettier. It's nice to have some
1179 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001180 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001181
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001182 @staticmethod
1183 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001184 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1185 if not ok:
1186 raise gclient_utils.Error('git version %s < minimum required %s' %
1187 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001188
John Budorick882c91e2018-07-12 22:11:41 +00001189 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001190 # Special case handling if all 3 conditions are met:
1191 # * the mirros have recently changed, but deps destination remains same,
1192 # * the git histories of mirrors are conflicting.
1193 # * git cache is used
1194 # This manifests itself in current checkout having invalid HEAD commit on
1195 # most git operations. Since git cache is used, just deleted the .git
1196 # folder, and re-create it by cloning.
1197 try:
1198 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1199 except subprocess2.CalledProcessError as e:
1200 if ('fatal: bad object HEAD' in e.stderr
1201 and self.cache_dir and self.cache_dir in url):
1202 self.Print((
1203 'Likely due to DEPS change with git cache_dir, '
1204 'the current commit points to no longer existing object.\n'
1205 '%s' % e)
1206 )
1207 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001208 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001209 else:
1210 raise
1211
msb@chromium.org786fb682010-06-02 15:16:23 +00001212 def _IsRebasing(self):
1213 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1214 # have a plumbing command to determine whether a rebase is in progress, so
1215 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1216 g = os.path.join(self.checkout_path, '.git')
1217 return (
1218 os.path.isdir(os.path.join(g, "rebase-merge")) or
1219 os.path.isdir(os.path.join(g, "rebase-apply")))
1220
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001221 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001222 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1223 if os.path.exists(lockfile):
1224 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001225 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001226 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1227 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001228 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001229
msb@chromium.org786fb682010-06-02 15:16:23 +00001230 # Make sure the tree is clean; see git-rebase.sh for reference
1231 try:
1232 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001233 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001234 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001235 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001236 '\tYou have unstaged changes.\n'
1237 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001238 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001239 try:
1240 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001241 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001242 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001243 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001244 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001245 '\tYour index contains uncommitted changes\n'
1246 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001247 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001248
agable83faed02016-10-24 14:37:10 -07001249 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001250 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1251 # reference by a commit). If not, error out -- most likely a rebase is
1252 # in progress, try to detect so we can give a better error.
1253 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001254 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1255 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001256 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001257 # Commit is not contained by any rev. See if the user is rebasing:
1258 if self._IsRebasing():
1259 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001260 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001261 '\tAlready in a conflict, i.e. (no branch).\n'
1262 '\tFix the conflict and run gclient again.\n'
1263 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1264 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001265 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001266 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001267 name = ('saved-by-gclient-' +
1268 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001269 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001270 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001271 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001272
msb@chromium.org5bde4852009-12-14 16:47:12 +00001273 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001274 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001275 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001276 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001277 return None
1278 return branch
1279
borenet@google.comc3e09d22014-04-10 13:58:18 +00001280 def _Capture(self, args, **kwargs):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001281 kwargs.setdefault('cwd', self.checkout_path)
1282 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001283 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001284 env = scm.GIT.ApplyEnvVars(kwargs)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001285 ret = subprocess2.check_output(['git'] + args, env=env, **kwargs)
1286 if strip:
1287 ret = ret.strip()
1288 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001289
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001290 def _Checkout(self, options, ref, force=False, quiet=None):
1291 """Performs a 'git-checkout' operation.
1292
1293 Args:
1294 options: The configured option set
1295 ref: (str) The branch/commit to checkout
1296 quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
1297 'None', the behavior is inferred from 'options.verbose'.
1298 Returns: (str) The output of the checkout operation
1299 """
1300 if quiet is None:
1301 quiet = (not options.verbose)
1302 checkout_args = ['checkout']
1303 if force:
1304 checkout_args.append('--force')
1305 if quiet:
1306 checkout_args.append('--quiet')
1307 checkout_args.append(ref)
1308 return self._Capture(checkout_args)
1309
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001310 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1311 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001312 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001313 # When updating, the ref is modified to be a remote ref .
1314 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1315 # Try to reverse that mapping.
1316 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1317 if original_ref:
1318 refspec = original_ref + ':' + refspec
1319 # When a mirror is configured, it only fetches
1320 # refs/{heads,branch-heads,tags}/*.
1321 # If asked to fetch other refs, we must fetch those directly from the
1322 # repository, and not from the mirror.
1323 if not original_ref.startswith(
1324 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1325 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001326 fetch_cmd = cfg + [
1327 'fetch',
1328 remote or self.remote,
1329 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001330 if refspec:
1331 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001332
1333 if prune:
1334 fetch_cmd.append('--prune')
1335 if options.verbose:
1336 fetch_cmd.append('--verbose')
1337 elif quiet:
1338 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001339 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001340
1341 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1342 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1343
Edward Lemur579c9862018-07-13 23:17:51 +00001344 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001345 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1346 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001347 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001348 try:
1349 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1350 options)
1351 self._Run(['config', 'remote.%s.fetch' % self.remote,
1352 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1353 except subprocess2.CalledProcessError as e:
1354 # If exit code was 5, it means we attempted to unset a config that
1355 # didn't exist. Ignore it.
1356 if e.returncode != 5:
1357 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001358 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001359 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001360 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1361 '^\\+refs/branch-heads/\\*:.*$']
1362 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001363 if hasattr(options, 'with_tags') and options.with_tags:
1364 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1365 '+refs/tags/*:refs/tags/*',
1366 '^\\+refs/tags/\\*:.*$']
1367 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001368
John Budorick882c91e2018-07-12 22:11:41 +00001369 def _AutoFetchRef(self, options, revision):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001370 """Attempts to fetch |revision| if not available in local repo.
1371
1372 Returns possibly updated revision."""
John Budorick882c91e2018-07-12 22:11:41 +00001373 try:
1374 self._Capture(['rev-parse', revision])
1375 except subprocess2.CalledProcessError:
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001376 self._Fetch(options, refspec=revision)
1377 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1378 return revision
1379
dnj@chromium.org680f2172014-06-25 00:39:32 +00001380 def _Run(self, args, options, show_header=True, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001381 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001382 kwargs.setdefault('cwd', self.checkout_path)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001383 kwargs.setdefault('stdout', self.out_fh)
szager@chromium.org848fd492014-04-09 19:06:44 +00001384 kwargs['filter_fn'] = self.filter
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001385 kwargs.setdefault('print_stdout', False)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001386 env = scm.GIT.ApplyEnvVars(kwargs)
agable@chromium.org772efaf2014-04-01 02:35:44 +00001387 cmd = ['git'] + args
dnj@chromium.org680f2172014-06-25 00:39:32 +00001388 if show_header:
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001389 gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs)
1390 else:
1391 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001392
1393
1394class CipdPackage(object):
1395 """A representation of a single CIPD package."""
1396
John Budorickd3ba72b2018-03-20 12:27:42 -07001397 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001398 self._authority_for_subdir = authority_for_subdir
1399 self._name = name
1400 self._version = version
1401
1402 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001403 def authority_for_subdir(self):
1404 """Whether this package has authority to act on behalf of its subdir.
1405
1406 Some operations should only be performed once per subdirectory. A package
1407 that has authority for its subdirectory is the only package that should
1408 perform such operations.
1409
1410 Returns:
1411 bool; whether this package has subdir authority.
1412 """
1413 return self._authority_for_subdir
1414
1415 @property
1416 def name(self):
1417 return self._name
1418
1419 @property
1420 def version(self):
1421 return self._version
1422
1423
1424class CipdRoot(object):
1425 """A representation of a single CIPD root."""
1426 def __init__(self, root_dir, service_url):
1427 self._all_packages = set()
1428 self._mutator_lock = threading.Lock()
1429 self._packages_by_subdir = collections.defaultdict(list)
1430 self._root_dir = root_dir
1431 self._service_url = service_url
1432
1433 def add_package(self, subdir, package, version):
1434 """Adds a package to this CIPD root.
1435
1436 As far as clients are concerned, this grants both root and subdir authority
1437 to packages arbitrarily. (The implementation grants root authority to the
1438 first package added and subdir authority to the first package added for that
1439 subdir, but clients should not depend on or expect that behavior.)
1440
1441 Args:
1442 subdir: str; relative path to where the package should be installed from
1443 the cipd root directory.
1444 package: str; the cipd package name.
1445 version: str; the cipd package version.
1446 Returns:
1447 CipdPackage; the package that was created and added to this root.
1448 """
1449 with self._mutator_lock:
1450 cipd_package = CipdPackage(
1451 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001452 not self._packages_by_subdir[subdir])
1453 self._all_packages.add(cipd_package)
1454 self._packages_by_subdir[subdir].append(cipd_package)
1455 return cipd_package
1456
1457 def packages(self, subdir):
1458 """Get the list of configured packages for the given subdir."""
1459 return list(self._packages_by_subdir[subdir])
1460
1461 def clobber(self):
1462 """Remove the .cipd directory.
1463
1464 This is useful for forcing ensure to redownload and reinitialize all
1465 packages.
1466 """
1467 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001468 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001469 try:
1470 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1471 except OSError:
1472 if os.path.exists(cipd_cache_dir):
1473 raise
1474
1475 @contextlib.contextmanager
1476 def _create_ensure_file(self):
1477 try:
1478 ensure_file = None
1479 with tempfile.NamedTemporaryFile(
Raul Tambreb946b232019-03-26 14:48:46 +00001480 suffix='.ensure', delete=False, mode='w') as ensure_file:
John Budorick302bb842018-07-17 23:49:17 +00001481 ensure_file.write('$ParanoidMode CheckPresence\n\n')
Raul Tambreb946b232019-03-26 14:48:46 +00001482 for subdir, packages in sorted(self._packages_by_subdir.items()):
John Budorick0f7b2002018-01-19 15:46:17 -08001483 ensure_file.write('@Subdir %s\n' % subdir)
Edward Lemurfbb06aa2018-06-11 20:43:06 +00001484 for package in sorted(packages, key=lambda p: p.name):
John Budorick0f7b2002018-01-19 15:46:17 -08001485 ensure_file.write('%s %s\n' % (package.name, package.version))
1486 ensure_file.write('\n')
1487 yield ensure_file.name
1488 finally:
1489 if ensure_file is not None and os.path.exists(ensure_file.name):
1490 os.remove(ensure_file.name)
1491
1492 def ensure(self):
1493 """Run `cipd ensure`."""
1494 with self._mutator_lock:
1495 with self._create_ensure_file() as ensure_file:
1496 cmd = [
1497 'cipd', 'ensure',
1498 '-log-level', 'error',
1499 '-root', self.root_dir,
1500 '-ensure-file', ensure_file,
1501 ]
1502 gclient_utils.CheckCallAndFilterAndHeader(cmd)
1503
John Budorickd3ba72b2018-03-20 12:27:42 -07001504 def run(self, command):
1505 if command == 'update':
1506 self.ensure()
1507 elif command == 'revert':
1508 self.clobber()
1509 self.ensure()
1510
John Budorick0f7b2002018-01-19 15:46:17 -08001511 def created_package(self, package):
1512 """Checks whether this root created the given package.
1513
1514 Args:
1515 package: CipdPackage; the package to check.
1516 Returns:
1517 bool; whether this root created the given package.
1518 """
1519 return package in self._all_packages
1520
1521 @property
1522 def root_dir(self):
1523 return self._root_dir
1524
1525 @property
1526 def service_url(self):
1527 return self._service_url
1528
1529
1530class CipdWrapper(SCMWrapper):
1531 """Wrapper for CIPD.
1532
1533 Currently only supports chrome-infra-packages.appspot.com.
1534 """
John Budorick3929e9e2018-02-04 18:18:07 -08001535 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001536
1537 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1538 out_cb=None, root=None, package=None):
1539 super(CipdWrapper, self).__init__(
1540 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1541 out_cb=out_cb)
1542 assert root.created_package(package)
1543 self._package = package
1544 self._root = root
1545
1546 #override
1547 def GetCacheMirror(self):
1548 return None
1549
1550 #override
1551 def GetActualRemoteURL(self, options):
1552 return self._root.service_url
1553
1554 #override
1555 def DoesRemoteURLMatch(self, options):
1556 del options
1557 return True
1558
1559 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001560 """Does nothing.
1561
1562 CIPD packages should be reverted at the root by running
1563 `CipdRoot.run('revert')`.
1564 """
1565 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001566
1567 def diff(self, options, args, file_list):
1568 """CIPD has no notion of diffing."""
1569 pass
1570
1571 def pack(self, options, args, file_list):
1572 """CIPD has no notion of diffing."""
1573 pass
1574
1575 def revinfo(self, options, args, file_list):
1576 """Grab the instance ID."""
1577 try:
1578 tmpdir = tempfile.mkdtemp()
1579 describe_json_path = os.path.join(tmpdir, 'describe.json')
1580 cmd = [
1581 'cipd', 'describe',
1582 self._package.name,
1583 '-log-level', 'error',
1584 '-version', self._package.version,
1585 '-json-output', describe_json_path
1586 ]
1587 gclient_utils.CheckCallAndFilter(
1588 cmd, filter_fn=lambda _line: None, print_stdout=False)
1589 with open(describe_json_path) as f:
1590 describe_json = json.load(f)
1591 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1592 finally:
1593 gclient_utils.rmtree(tmpdir)
1594
1595 def status(self, options, args, file_list):
1596 pass
1597
1598 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001599 """Does nothing.
1600
1601 CIPD packages should be updated at the root by running
1602 `CipdRoot.run('update')`.
1603 """
1604 pass