blob: ef7dbae4b919b69c189b1c0c676a67940fa25698 [file] [log] [blame]
steveblock@chromium.org93567042012-02-15 01:02:26 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00005"""Gclient-specific SCM-specific operations."""
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00006
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00007from __future__ import print_function
8
John Budorick0f7b2002018-01-19 15:46:17 -08009import collections
10import contextlib
borenet@google.comb2256212014-05-07 20:57:28 +000011import errno
John Budorick0f7b2002018-01-19 15:46:17 -080012import json
maruel@chromium.org754960e2009-09-21 12:31:05 +000013import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import os
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000015import posixpath
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000016import re
maruel@chromium.org90541732011-04-01 17:54:18 +000017import sys
ilevy@chromium.org3534aa52013-07-20 01:58:08 +000018import tempfile
John Budorick0f7b2002018-01-19 15:46:17 -080019import threading
zty@chromium.org6279e8a2014-02-13 01:45:25 +000020import traceback
Raul Tambreb946b232019-03-26 14:48:46 +000021
22try:
23 import urlparse
24except ImportError: # For Py3 compatibility
25 import urllib.parse as urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000026
hinoka@google.com2f2ca142014-01-07 03:59:18 +000027import download_from_google_storage
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000028import gclient_utils
szager@chromium.org848fd492014-04-09 19:06:44 +000029import git_cache
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000030import scm
borenet@google.comb2256212014-05-07 20:57:28 +000031import shutil
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000032import subprocess2
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000033
34
szager@chromium.org71cbb502013-04-19 23:30:15 +000035THIS_FILE_PATH = os.path.abspath(__file__)
36
hinoka@google.com2f2ca142014-01-07 03:59:18 +000037GSUTIL_DEFAULT_PATH = os.path.join(
hinoka@chromium.orgb091aa52014-12-20 01:47:31 +000038 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
hinoka@google.com2f2ca142014-01-07 03:59:18 +000039
maruel@chromium.org79d62372015-06-01 18:50:55 +000040
smutae7ea312016-07-18 11:59:41 -070041class NoUsableRevError(gclient_utils.Error):
42 """Raised if requested revision isn't found in checkout."""
43
44
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000045class DiffFiltererWrapper(object):
46 """Simple base class which tracks which file is being diffed and
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000047 replaces instances of its file name in the original and
agable41e3a6c2016-10-20 11:36:56 -070048 working copy lines of the git diff output."""
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000049 index_string = None
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000050 original_prefix = "--- "
51 working_prefix = "+++ "
52
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000053 def __init__(self, relpath, print_func):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000054 # Note that we always use '/' as the path separator to be
agable41e3a6c2016-10-20 11:36:56 -070055 # consistent with cygwin-style output on Windows
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000056 self._relpath = relpath.replace("\\", "/")
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000057 self._current_file = None
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000058 self._print_func = print_func
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000059
maruel@chromium.org6e29d572010-06-04 17:32:20 +000060 def SetCurrentFile(self, current_file):
61 self._current_file = current_file
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000062
iannucci@chromium.org3830a672013-02-19 20:15:14 +000063 @property
64 def _replacement_file(self):
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000065 return posixpath.join(self._relpath, self._current_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000066
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000067 def _Replace(self, line):
68 return line.replace(self._current_file, self._replacement_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000069
70 def Filter(self, line):
71 if (line.startswith(self.index_string)):
72 self.SetCurrentFile(line[len(self.index_string):])
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000073 line = self._Replace(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000074 else:
75 if (line.startswith(self.original_prefix) or
76 line.startswith(self.working_prefix)):
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000077 line = self._Replace(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000078 self._print_func(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000079
80
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000081class GitDiffFilterer(DiffFiltererWrapper):
82 index_string = "diff --git "
83
84 def SetCurrentFile(self, current_file):
85 # Get filename by parsing "a/<filename> b/<filename>"
86 self._current_file = current_file[:(len(current_file)/2)][2:]
87
88 def _Replace(self, line):
89 return re.sub("[a|b]/" + self._current_file, self._replacement_file, line)
90
91
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000092# SCMWrapper base class
93
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000094class SCMWrapper(object):
95 """Add necessary glue between all the supported SCM.
96
msb@chromium.orgd6504212010-01-13 17:34:31 +000097 This is the abstraction layer to bind to different SCM.
98 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000099 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
Edward Lemur231f5ea2018-01-31 19:02:36 +0100100 out_cb=None, print_outbuf=False):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000101 self.url = url
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +0000102 self._root_dir = root_dir
103 if self._root_dir:
104 self._root_dir = self._root_dir.replace('/', os.sep)
105 self.relpath = relpath
106 if self.relpath:
107 self.relpath = self.relpath.replace('/', os.sep)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000108 if self.relpath and self._root_dir:
109 self.checkout_path = os.path.join(self._root_dir, self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000110 if out_fh is None:
111 out_fh = sys.stdout
112 self.out_fh = out_fh
113 self.out_cb = out_cb
Edward Lemur231f5ea2018-01-31 19:02:36 +0100114 self.print_outbuf = print_outbuf
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000115
116 def Print(self, *args, **kwargs):
117 kwargs.setdefault('file', self.out_fh)
118 if kwargs.pop('timestamp', True):
119 self.out_fh.write('[%s] ' % gclient_utils.Elapsed())
120 print(*args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000121
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000122 def RunCommand(self, command, options, args, file_list=None):
agabledebf6c82016-12-21 12:50:12 -0800123 commands = ['update', 'updatesingle', 'revert',
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000124 'revinfo', 'status', 'diff', 'pack', 'runhooks']
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000125
126 if not command in commands:
127 raise gclient_utils.Error('Unknown command %s' % command)
128
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000129 if not command in dir(self):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000130 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % (
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000131 command, self.__class__.__name__))
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000132
133 return getattr(self, command)(options, args, file_list)
134
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000135 @staticmethod
136 def _get_first_remote_url(checkout_path):
137 log = scm.GIT.Capture(
138 ['config', '--local', '--get-regexp', r'remote.*.url'],
139 cwd=checkout_path)
140 # Get the second token of the first line of the log.
141 return log.splitlines()[0].split(' ', 1)[1]
142
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000143 def GetCacheMirror(self):
Robert Iannuccia19649b2018-06-29 16:31:45 +0000144 if getattr(self, 'cache_dir', None):
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000145 url, _ = gclient_utils.SplitUrlRevision(self.url)
146 return git_cache.Mirror(url)
147 return None
148
smut@google.comd33eab32014-07-07 19:35:18 +0000149 def GetActualRemoteURL(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000150 """Attempt to determine the remote URL for this SCMWrapper."""
smut@google.comd33eab32014-07-07 19:35:18 +0000151 # Git
borenet@google.combda475e2014-03-24 19:04:45 +0000152 if os.path.exists(os.path.join(self.checkout_path, '.git')):
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000153 actual_remote_url = self._get_first_remote_url(self.checkout_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000154
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000155 mirror = self.GetCacheMirror()
156 # If the cache is used, obtain the actual remote URL from there.
157 if (mirror and mirror.exists() and
158 mirror.mirror_path.replace('\\', '/') ==
159 actual_remote_url.replace('\\', '/')):
160 actual_remote_url = self._get_first_remote_url(mirror.mirror_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000161 return actual_remote_url
borenet@google.com88d10082014-03-21 17:24:48 +0000162 return None
163
borenet@google.com4e9be262014-04-08 19:40:30 +0000164 def DoesRemoteURLMatch(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000165 """Determine whether the remote URL of this checkout is the expected URL."""
166 if not os.path.exists(self.checkout_path):
167 # A checkout which doesn't exist can't be broken.
168 return True
169
smut@google.comd33eab32014-07-07 19:35:18 +0000170 actual_remote_url = self.GetActualRemoteURL(options)
borenet@google.com88d10082014-03-21 17:24:48 +0000171 if actual_remote_url:
borenet@google.com8156c9f2014-04-01 16:41:36 +0000172 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip('/')
173 == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/'))
borenet@google.com88d10082014-03-21 17:24:48 +0000174 else:
175 # This may occur if the self.checkout_path exists but does not contain a
agable41e3a6c2016-10-20 11:36:56 -0700176 # valid git checkout.
borenet@google.com88d10082014-03-21 17:24:48 +0000177 return False
178
borenet@google.comb09097a2014-04-09 19:09:08 +0000179 def _DeleteOrMove(self, force):
180 """Delete the checkout directory or move it out of the way.
181
182 Args:
183 force: bool; if True, delete the directory. Otherwise, just move it.
184 """
borenet@google.comb2256212014-05-07 20:57:28 +0000185 if force and os.environ.get('CHROME_HEADLESS') == '1':
186 self.Print('_____ Conflicting directory found in %s. Removing.'
187 % self.checkout_path)
188 gclient_utils.AddWarning('Conflicting directory %s deleted.'
189 % self.checkout_path)
190 gclient_utils.rmtree(self.checkout_path)
191 else:
192 bad_scm_dir = os.path.join(self._root_dir, '_bad_scm',
193 os.path.dirname(self.relpath))
194
195 try:
196 os.makedirs(bad_scm_dir)
197 except OSError as e:
198 if e.errno != errno.EEXIST:
199 raise
200
201 dest_path = tempfile.mkdtemp(
202 prefix=os.path.basename(self.relpath),
203 dir=bad_scm_dir)
204 self.Print('_____ Conflicting directory found in %s. Moving to %s.'
205 % (self.checkout_path, dest_path))
206 gclient_utils.AddWarning('Conflicting directory %s moved to %s.'
207 % (self.checkout_path, dest_path))
208 shutil.move(self.checkout_path, dest_path)
borenet@google.comb09097a2014-04-09 19:09:08 +0000209
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000210
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000211class GitWrapper(SCMWrapper):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000212 """Wrapper for Git"""
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000213 name = 'git'
maruel@chromium.org1a60dca2013-11-26 14:06:26 +0000214 remote = 'origin'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000215
Robert Iannuccia19649b2018-06-29 16:31:45 +0000216 @property
217 def cache_dir(self):
218 try:
219 return git_cache.Mirror.GetCachePath()
220 except RuntimeError:
221 return None
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000222
John Budorick0f7b2002018-01-19 15:46:17 -0800223 def __init__(self, url=None, *args, **kwargs):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000224 """Removes 'git+' fake prefix from git URL."""
Henrique Ferreiroe72279d2019-04-17 12:01:50 +0000225 if url and (url.startswith('git+http://') or
226 url.startswith('git+https://')):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000227 url = url[4:]
John Budorick0f7b2002018-01-19 15:46:17 -0800228 SCMWrapper.__init__(self, url, *args, **kwargs)
szager@chromium.org848fd492014-04-09 19:06:44 +0000229 filter_kwargs = { 'time_throttle': 1, 'out_fh': self.out_fh }
230 if self.out_cb:
231 filter_kwargs['predicate'] = self.out_cb
232 self.filter = gclient_utils.GitFilter(**filter_kwargs)
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000233
mukai@chromium.org9e3e82c2012-04-18 12:55:43 +0000234 @staticmethod
235 def BinaryExists():
236 """Returns true if the command exists."""
237 try:
238 # We assume git is newer than 1.7. See: crbug.com/114483
239 result, version = scm.GIT.AssertVersion('1.7')
240 if not result:
241 raise gclient_utils.Error('Git version is older than 1.7: %s' % version)
242 return result
243 except OSError:
244 return False
245
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000246 def GetCheckoutRoot(self):
247 return scm.GIT.GetCheckoutRoot(self.checkout_path)
248
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000249 def GetRevisionDate(self, _revision):
floitsch@google.comeaab7842011-04-28 09:07:58 +0000250 """Returns the given revision's date in ISO-8601 format (which contains the
251 time zone)."""
252 # TODO(floitsch): get the time-stamp of the given revision and not just the
253 # time-stamp of the currently checked out revision.
254 return self._Capture(['log', '-n', '1', '--format=%ai'])
255
Aaron Gablef4068aa2017-12-12 15:14:09 -0800256 def _GetDiffFilenames(self, base):
257 """Returns the names of files modified since base."""
258 return self._Capture(
Raul Tambrecd862e32019-05-10 21:19:00 +0000259 # Filter to remove base if it is None.
260 list(filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only',
261 base])
262 )).split()
Aaron Gablef4068aa2017-12-12 15:14:09 -0800263
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000264 def diff(self, options, _args, _file_list):
Aaron Gable1853f662018-02-12 15:45:56 -0800265 _, revision = gclient_utils.SplitUrlRevision(self.url)
266 if not revision:
267 revision = 'refs/remotes/%s/master' % self.remote
268 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000269
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000270 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000271 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000272 repository.
273
274 The patch file is generated from a diff of the merge base of HEAD and
275 its upstream branch.
276 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700277 try:
278 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
279 except subprocess2.CalledProcessError:
280 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000281 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700282 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000283 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000284 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000285
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800286 def _Scrub(self, target, options):
287 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000288 quiet = []
289 if not options.verbose:
290 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800291 self._Run(['reset', '--hard', target] + quiet, options)
292 if options.force and options.delete_unversioned_trees:
293 # where `target` is a commit that contains both upper and lower case
294 # versions of the same file on a case insensitive filesystem, we are
295 # actually in a broken state here. The index will have both 'a' and 'A',
296 # but only one of them will exist on the disk. To progress, we delete
297 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800298 output = self._Capture([
299 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800300 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800301 # --porcelain (v1) looks like:
302 # XY filename
303 try:
304 filename = line[3:]
305 self.Print('_____ Deleting residual after reset: %r.' % filename)
306 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800307 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800308 except OSError:
309 pass
310
John Budorick882c91e2018-07-12 22:11:41 +0000311 def _FetchAndReset(self, revision, file_list, options):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800312 """Equivalent to git fetch; git reset."""
Edward Lemur579c9862018-07-13 23:17:51 +0000313 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000314
dnj@chromium.org680f2172014-06-25 00:39:32 +0000315 self._Fetch(options, prune=True, quiet=options.verbose)
John Budorick882c91e2018-07-12 22:11:41 +0000316 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000317 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800318 files = self._Capture(
319 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambreb946b232019-03-26 14:48:46 +0000320 file_list.extend(
321 [os.path.join(self.checkout_path, f.decode()) for f in files])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000322
szager@chromium.org8a139702014-06-20 15:55:01 +0000323 def _DisableHooks(self):
324 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
325 if not os.path.isdir(hook_dir):
326 return
327 for f in os.listdir(hook_dir):
328 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000329 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
330 if os.path.exists(disabled_hook_path):
331 os.remove(disabled_hook_path)
332 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000333
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000334 def _maybe_break_locks(self, options):
335 """This removes all .lock files from this repo's .git directory, if the
336 user passed the --break_repo_locks command line flag.
337
338 In particular, this will cleanup index.lock files, as well as ref lock
339 files.
340 """
341 if options.break_repo_locks:
342 git_dir = os.path.join(self.checkout_path, '.git')
343 for path, _, filenames in os.walk(git_dir):
344 for filename in filenames:
345 if filename.endswith('.lock'):
346 to_break = os.path.join(path, filename)
347 self.Print('breaking lock: %s' % (to_break,))
348 try:
349 os.remove(to_break)
350 except OSError as ex:
351 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
352 raise
353
Edward Lesmes9a01eb22019-05-22 00:48:01 +0000354 # TODO(ehmaldonado): Remove after bot_update is modified to pass the patch's
355 # branch.
356 def _GetTargetBranchForCommit(self, commit):
357 """Get the remote branch a commit is part of."""
358 _WELL_KNOWN_BRANCHES = [
359 'refs/remotes/origin/master',
360 'refs/remotes/origin/infra/config',
361 'refs/remotes/origin/lkgr',
362 ]
363 for branch in _WELL_KNOWN_BRANCHES:
364 if scm.GIT.IsAncestor(self.checkout_path, commit, branch):
365 return branch
366 remote_refs = self._Capture(
367 ['for-each-ref', 'refs/remotes/%s' % self.remote,
368 '--format=%(refname)']).splitlines()
369 for ref in sorted(remote_refs, reverse=True):
370 if scm.GIT.IsAncestor(self.checkout_path, commit, ref):
371 return ref
372 self.Print('Failed to find a remote ref that contains %s. '
373 'Candidate refs were %s.' % (commit, remote_refs))
374 return None
375
Edward Lemur3acbc742019-05-30 17:57:35 +0000376 def apply_patch_ref(self, patch_repo, patch_rev, target_rev, options,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000377 file_list):
378 """Apply a patch on top of the revision we're synced at.
379
Edward Lemur3acbc742019-05-30 17:57:35 +0000380 The patch ref is given by |patch_repo|@|patch_rev|.
381 |target_rev| is usually the branch that the |patch_rev| was uploaded against
382 (e.g. 'refs/heads/master'), but this is not required.
383
384 We cherry-pick all commits reachable from |patch_rev| on top of the curret
385 HEAD, excluding those reachable from |target_rev|
386 (i.e. git cherry-pick target_rev..patch_rev).
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000387
388 Graphically, it looks like this:
389
Edward Lemur3acbc742019-05-30 17:57:35 +0000390 ... -> o -> [possibly already landed commits] -> target_rev
391 \
392 -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000393
Edward Lemur3acbc742019-05-30 17:57:35 +0000394 The final checkout state is then:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000395
Edward Lemur3acbc742019-05-30 17:57:35 +0000396 ... -> HEAD -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000397
398 After application, if |options.reset_patch_ref| is specified, we soft reset
Edward Lemur3acbc742019-05-30 17:57:35 +0000399 the cherry-picked changes, keeping them in git index only.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000400
401 Args:
Edward Lemur3acbc742019-05-30 17:57:35 +0000402 patch_repo: The patch origin.
403 e.g. 'https://foo.googlesource.com/bar'
404 patch_rev: The revision to patch.
405 e.g. 'refs/changes/1234/34/1'.
406 target_rev: The revision to use when finding the merge base.
407 Typically, the branch that the patch was uploaded against.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000408 e.g. 'refs/heads/master' or 'refs/heads/infra/config'.
409 options: The options passed to gclient.
410 file_list: A list where modified files will be appended.
411 """
412
Edward Lemurca7d8812018-07-24 17:42:45 +0000413 # Abort any cherry-picks in progress.
414 try:
415 self._Capture(['cherry-pick', '--abort'])
416 except subprocess2.CalledProcessError:
417 pass
418
Edward Lesmesc621b212018-03-21 20:26:56 -0400419 base_rev = self._Capture(['rev-parse', 'HEAD'])
Edward Lemura0ffbe42019-05-01 16:52:18 +0000420
Edward Lemur3acbc742019-05-30 17:57:35 +0000421 if not target_rev:
422 # TODO(ehmaldonado): Raise an error once |target_rev| is mandatory.
423 target_rev = self._GetTargetBranchForCommit(base_rev) or base_rev
424 elif target_rev.startswith('refs/heads/'):
425 # If |target_rev| is in refs/heads/**, try first to find the corresponding
426 # remote ref for it, since |target_rev| might point to a local ref which
427 # is not up to date with the corresponding remote ref.
428 remote_ref = ''.join(scm.GIT.RefToRemoteRef(target_rev, self.remote))
429 self.Print('Trying the correspondig remote ref for %r: %r\n' % (
430 target_rev, remote_ref))
431 if scm.GIT.IsValidRevision(self.checkout_path, remote_ref):
432 target_rev = remote_ref
433 elif not scm.GIT.IsValidRevision(self.checkout_path, target_rev):
434 # Fetch |target_rev| if it's not already available.
435 url, _ = gclient_utils.SplitUrlRevision(self.url)
436 mirror = self._GetMirror(url, options, target_rev)
437 if mirror:
438 rev_type = 'branch' if target_rev.startswith('refs/') else 'hash'
439 self._UpdateMirrorIfNotContains(mirror, options, rev_type, target_rev)
440 self._Fetch(options, refspec=target_rev)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000441
Edward Lemur3acbc742019-05-30 17:57:35 +0000442 self.Print('===Applying patch===')
443 self.Print('Revision to patch is %r @ %r.' % (patch_repo, patch_rev))
444 self.Print('Will cherrypick %r .. %r on top of %r.' % (
445 target_rev, patch_rev, base_rev))
446 self.Print('Current dir is %r' % self.checkout_path)
Edward Lesmesc621b212018-03-21 20:26:56 -0400447 self._Capture(['reset', '--hard'])
Edward Lemur3acbc742019-05-30 17:57:35 +0000448 self._Capture(['fetch', patch_repo, patch_rev])
Edward Lemurca7d8812018-07-24 17:42:45 +0000449 patch_rev = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400450
Edward Lemur3acbc742019-05-30 17:57:35 +0000451 if not options.rebase_patch_ref:
452 self._Capture(['checkout', patch_rev])
453 else:
454 try:
455 if scm.GIT.IsAncestor(self.checkout_path, patch_rev, target_rev):
456 # If |patch_rev| is an ancestor of |target_rev|, check it out.
Edward Lemurca7d8812018-07-24 17:42:45 +0000457 self._Capture(['checkout', patch_rev])
458 else:
459 # If a change was uploaded on top of another change, which has already
460 # landed, one of the commits in the cherry-pick range will be
461 # redundant, since it has already landed and its changes incorporated
462 # in the tree.
463 # We pass '--keep-redundant-commits' to ignore those changes.
Edward Lemur3acbc742019-05-30 17:57:35 +0000464 self._Capture(['cherry-pick', target_rev + '..' + patch_rev,
Edward Lemurca7d8812018-07-24 17:42:45 +0000465 '--keep-redundant-commits'])
466
Edward Lemur3acbc742019-05-30 17:57:35 +0000467 except subprocess2.CalledProcessError as e:
468 self.Print('Failed to apply patch.')
469 self.Print('Revision to patch was %r @ %r.' % (patch_repo, patch_rev))
470 self.Print('Tried to cherrypick %r .. %r on top of %r.' % (
471 target_rev, patch_rev, base_rev))
472 self.Print('Current dir is %r' % self.checkout_path)
473 self.Print('git returned non-zero exit status %s:\n%s' % (
474 e.returncode, e.stderr))
475 # Print the current status so that developers know what changes caused
476 # the patch failure, since git cherry-pick doesn't show that
477 # information.
478 self.Print(self._Capture(['status']))
479 try:
480 self._Capture(['cherry-pick', '--abort'])
481 except subprocess2.CalledProcessError:
482 pass
483 raise
Edward Lemurca7d8812018-07-24 17:42:45 +0000484
Edward Lemur3acbc742019-05-30 17:57:35 +0000485 if file_list is not None:
486 file_list.extend(self._GetDiffFilenames(base_rev))
Edward Lemurca7d8812018-07-24 17:42:45 +0000487
Edward Lesmesc621b212018-03-21 20:26:56 -0400488 if options.reset_patch_ref:
489 self._Capture(['reset', '--soft', base_rev])
490
msb@chromium.orge28e4982009-09-25 20:51:45 +0000491 def update(self, options, args, file_list):
492 """Runs git to update or transparently checkout the working copy.
493
494 All updated files will be appended to file_list.
495
496 Raises:
497 Error: if can't get URL for relative path.
498 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000499 if args:
500 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
501
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000502 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000503
John Budorick882c91e2018-07-12 22:11:41 +0000504 # If a dependency is not pinned, track the default remote branch.
505 default_rev = 'refs/remotes/%s/master' % self.remote
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000506 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000507 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000508 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000509 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000510 # Override the revision number.
511 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000512 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000513 # Check again for a revision in case an initial ref was specified
514 # in the url, for example bla.git@refs/heads/custombranch
515 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000516 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000517 if not revision:
518 revision = default_rev
msb@chromium.orge28e4982009-09-25 20:51:45 +0000519
szager@chromium.org8a139702014-06-20 15:55:01 +0000520 if managed:
521 self._DisableHooks()
522
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000523 printed_path = False
524 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000525 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700526 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000527 verbose = ['--verbose']
528 printed_path = True
529
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000530 revision_ref = revision
531 if ':' in revision:
532 revision_ref, _, revision = revision.partition(':')
533
534 mirror = self._GetMirror(url, options, revision_ref)
535 if mirror:
536 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000537
John Budorick882c91e2018-07-12 22:11:41 +0000538 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
539 if remote_ref:
540 # Rewrite remote refs to their local equivalents.
541 revision = ''.join(remote_ref)
542 rev_type = "branch"
543 elif revision.startswith('refs/'):
544 # Local branch? We probably don't want to support, since DEPS should
545 # always specify branches as they are in the upstream repo.
546 rev_type = "branch"
547 else:
548 # hash is also a tag, only make a distinction at checkout
549 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000550
primiano@chromium.org1c127382015-02-17 11:15:40 +0000551 # If we are going to introduce a new project, there is a possibility that
552 # we are syncing back to a state where the project was originally a
553 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
554 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
555 # In such case, we might have a backup of the former .git folder, which can
556 # be used to avoid re-fetching the entire repo again (useful for bisects).
557 backup_dir = self.GetGitBackupDirPath()
558 target_dir = os.path.join(self.checkout_path, '.git')
559 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
560 gclient_utils.safe_makedirs(self.checkout_path)
561 os.rename(backup_dir, target_dir)
562 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800563 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000564
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000565 if (not os.path.exists(self.checkout_path) or
566 (os.path.isdir(self.checkout_path) and
567 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000568 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000569 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000570 try:
John Budorick882c91e2018-07-12 22:11:41 +0000571 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000572 except subprocess2.CalledProcessError:
573 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000574 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000575 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800576 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000577 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000578 file_list.extend(
579 [os.path.join(self.checkout_path, f.decode()) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000580 if mirror:
581 self._Capture(
582 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000583 if not verbose:
584 # Make the output a little prettier. It's nice to have some whitespace
585 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000586 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000587 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000588
John Budorick21a51b32018-09-19 19:39:20 +0000589 if mirror:
590 self._Capture(
591 ['remote', 'set-url', '--push', 'origin', mirror.url])
592
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000593 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000594 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000595 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
596 return self._Capture(['rev-parse', '--verify', 'HEAD'])
597
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000598 self._maybe_break_locks(options)
599
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000600 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000601 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000602
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000603 # See if the url has changed (the unittests use git://foo for the url, let
604 # that through).
605 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
606 return_early = False
607 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
608 # unit test pass. (and update the comment above)
609 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
610 # This allows devs to use experimental repos which have a different url
611 # but whose branch(s) are the same as official repos.
Raul Tambrecd862e32019-05-10 21:19:00 +0000612 if (current_url.rstrip('/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000613 subprocess2.capture(
Aaron Gableac9b0f32019-04-18 17:38:37 +0000614 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000615 cwd=self.checkout_path).strip() != 'False'):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000616 self.Print('_____ switching %s to a new upstream' % self.relpath)
iannucci@chromium.org78514212014-08-20 23:08:00 +0000617 if not (options.force or options.reset):
618 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700619 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000620 # Switch over to the new upstream
621 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000622 if mirror:
623 with open(os.path.join(
624 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
625 'w') as fh:
626 fh.write(os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000627 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
628 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000629
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000630 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000631 else:
John Budorick882c91e2018-07-12 22:11:41 +0000632 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000633
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000634 if return_early:
635 return self._Capture(['rev-parse', '--verify', 'HEAD'])
636
msb@chromium.org5bde4852009-12-14 16:47:12 +0000637 cur_branch = self._GetCurrentBranch()
638
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000639 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000640 # 0) HEAD is detached. Probably from our initial clone.
641 # - make sure HEAD is contained by a named ref, then update.
642 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700643 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000644 # - try to rebase onto the new hash or branch
645 # 2) current branch is tracking a remote branch with local committed
646 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000647 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000648 # 3) current branch is tracking a remote branch w/or w/out changes, and
649 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000650 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000651 # 4) current branch is tracking a remote branch, but DEPS switches to a
652 # different remote branch, and
653 # a) current branch has no local changes, and --force:
654 # - checkout new branch
655 # b) current branch has local changes, and --force and --reset:
656 # - checkout new branch
657 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000658
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000659 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
660 # a tracking branch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000661 # or 'master' if not a tracking branch (it's based on a specific rev/hash)
662 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000663 if cur_branch is None:
664 upstream_branch = None
665 current_type = "detached"
666 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000667 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000668 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
669 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
670 current_type = "hash"
671 logging.debug("Current branch is not tracking an upstream (remote)"
672 " branch.")
673 elif upstream_branch.startswith('refs/remotes'):
674 current_type = "branch"
675 else:
676 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000677
Edward Lemur579c9862018-07-13 23:17:51 +0000678 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000679
Michael Spang73fac912019-03-08 18:44:19 +0000680 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000681 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000682 self._Fetch(options, prune=options.force)
683
684 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
685 sha_only=True):
686 # Update the remotes first so we have all the refs.
687 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
688 cwd=self.checkout_path)
689 if verbose:
690 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000691
John Budorick882c91e2018-07-12 22:11:41 +0000692 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200693
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000694 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000695 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000696 target = 'HEAD'
697 if options.upstream and upstream_branch:
698 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800699 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000700
msb@chromium.org786fb682010-06-02 15:16:23 +0000701 if current_type == 'detached':
702 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800703 # We just did a Scrub, this is as clean as it's going to get. In
704 # particular if HEAD is a commit that contains two versions of the same
705 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
706 # to actually "Clean" the checkout; that commit is uncheckoutable on this
707 # system. The best we can do is carry forward to the checkout step.
708 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000709 self._CheckClean(revision)
710 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000711 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000712 self.Print('Up-to-date; skipping checkout.')
713 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000714 # 'git checkout' may need to overwrite existing untracked files. Allow
715 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000716 self._Checkout(
717 options,
John Budorick882c91e2018-07-12 22:11:41 +0000718 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000719 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000720 quiet=True,
721 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000722 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000723 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000724 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000725 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700726 # Can't find a merge-base since we don't know our upstream. That makes
727 # this command VERY likely to produce a rebase failure. For now we
728 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000729 upstream_branch = self.remote
730 if options.revision or deps_revision:
731 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700732 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700733 printed_path=printed_path, merge=options.merge)
734 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000735 elif rev_type == 'hash':
736 # case 2
737 self._AttemptRebase(upstream_branch, file_list, options,
738 newbase=revision, printed_path=printed_path,
739 merge=options.merge)
740 printed_path = True
741 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000742 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000743 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000744 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000745 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000746 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000747 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000748 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000749 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
750 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000751 force_switch = False
752 if options.force:
753 try:
John Budorick882c91e2018-07-12 22:11:41 +0000754 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000755 # case 4a
756 force_switch = True
757 except gclient_utils.Error as e:
758 if options.reset:
759 # case 4b
760 force_switch = True
761 else:
762 switch_error = '%s\n%s' % (e.message, switch_error)
763 if force_switch:
764 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000765 (upstream_branch, new_base))
766 switch_branch = 'gclient_' + remote_ref[1]
767 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000768 self._Checkout(options, switch_branch, force=True, quiet=True)
769 else:
770 # case 4c
771 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000772 else:
773 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800774 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000775 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000776 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000777 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000778 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000779 if options.merge:
780 merge_args.append('--ff')
781 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000782 merge_args.append('--ff-only')
783 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000784 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000785 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700786 rebase_files = []
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000787 if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr):
788 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000789 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700790 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000791 printed_path = True
792 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000793 if not options.auto_rebase:
794 try:
795 action = self._AskForData(
796 'Cannot %s, attempt to rebase? '
797 '(y)es / (q)uit / (s)kip : ' %
798 ('merge' if options.merge else 'fast-forward merge'),
799 options)
800 except ValueError:
801 raise gclient_utils.Error('Invalid Character')
802 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700803 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000804 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000805 printed_path = True
806 break
807 elif re.match(r'quit|q', action, re.I):
808 raise gclient_utils.Error("Can't fast-forward, please merge or "
809 "rebase manually.\n"
810 "cd %s && git " % self.checkout_path
811 + "rebase %s" % upstream_branch)
812 elif re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000813 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000814 return
815 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000816 self.Print('Input not recognized')
smut@google.com27c9c8a2014-09-11 19:57:55 +0000817 elif re.match("error: Your local changes to '.*' would be "
818 "overwritten by merge. Aborting.\nPlease, commit your "
819 "changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000820 e.stderr):
821 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000822 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700823 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000824 printed_path = True
825 raise gclient_utils.Error(e.stderr)
826 else:
827 # Some other problem happened with the merge
828 logging.error("Error during fast-forward merge in %s!" % self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000829 self.Print(e.stderr)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000830 raise
831 else:
832 # Fast-forward merge was successful
833 if not re.match('Already up-to-date.', merge_output) or verbose:
834 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700835 self.Print('_____ %s at %s' % (self.relpath, revision),
836 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000837 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000838 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000839 if not verbose:
840 # Make the output a little prettier. It's nice to have some
841 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000842 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000843
agablec3937b92016-10-25 10:13:03 -0700844 if file_list is not None:
845 file_list.extend(
846 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000847
848 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000849 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700850 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000851 '\nConflict while rebasing this branch.\n'
852 'Fix the conflict and run gclient again.\n'
853 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700854 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000855
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000856 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000857 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
858 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000859
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000860 # If --reset and --delete_unversioned_trees are specified, remove any
861 # untracked directories.
862 if options.reset and options.delete_unversioned_trees:
863 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
864 # merge-base by default), so doesn't include untracked files. So we use
865 # 'git ls-files --directory --others --exclude-standard' here directly.
866 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800867 ['-c', 'core.quotePath=false', 'ls-files',
868 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000869 self.checkout_path)
870 for path in (p for p in paths.splitlines() if p.endswith('/')):
871 full_path = os.path.join(self.checkout_path, path)
872 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000873 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000874 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000875
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000876 return self._Capture(['rev-parse', '--verify', 'HEAD'])
877
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000878 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000879 """Reverts local modifications.
880
881 All reverted files will be appended to file_list.
882 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000883 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000884 # revert won't work if the directory doesn't exist. It needs to
885 # checkout instead.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000886 self.Print('_____ %s is missing, synching instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000887 # Don't reuse the args.
888 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000889
890 default_rev = "refs/heads/master"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000891 if options.upstream:
892 if self._GetCurrentBranch():
893 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
894 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000895 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000896 if not deps_revision:
897 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000898 if deps_revision.startswith('refs/heads/'):
899 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700900 try:
901 deps_revision = self.GetUsableRev(deps_revision, options)
902 except NoUsableRevError as e:
903 # If the DEPS entry's url and hash changed, try to update the origin.
904 # See also http://crbug.com/520067.
905 logging.warn(
906 'Couldn\'t find usable revision, will retrying to update instead: %s',
907 e.message)
908 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000909
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000910 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800911 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000912
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800913 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000914 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000915
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000916 if file_list is not None:
917 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
918
919 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000920 """Returns revision"""
921 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000922
msb@chromium.orge28e4982009-09-25 20:51:45 +0000923 def runhooks(self, options, args, file_list):
924 self.status(options, args, file_list)
925
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000926 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000927 """Display status information."""
928 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000929 self.Print('________ couldn\'t run status in %s:\n'
930 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000931 else:
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700932 try:
933 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
934 except subprocess2.CalledProcessError:
935 merge_base = []
Aaron Gablef4068aa2017-12-12 15:14:09 -0800936 self._Run(
937 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
938 options, stdout=self.out_fh, always=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000939 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800940 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000941 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000942
smutae7ea312016-07-18 11:59:41 -0700943 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700944 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700945 sha1 = None
946 if not os.path.isdir(self.checkout_path):
947 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800948 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700949
950 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
951 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700952 else:
agable41e3a6c2016-10-20 11:36:56 -0700953 # May exist in origin, but we don't have it yet, so fetch and look
954 # again.
955 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -0700956 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
957 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700958
959 if not sha1:
960 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800961 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -0700962
963 return sha1
964
primiano@chromium.org1c127382015-02-17 11:15:40 +0000965 def GetGitBackupDirPath(self):
966 """Returns the path where the .git folder for the current project can be
967 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
968 return os.path.join(self._root_dir,
969 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
970
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000971 def _GetMirror(self, url, options, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000972 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +0000973 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000974 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000975 mirror_kwargs = {
976 'print_func': self.filter,
John Budorick882c91e2018-07-12 22:11:41 +0000977 'refs': []
hinoka@google.comb1b54572014-04-16 22:29:23 +0000978 }
hinoka@google.comb1b54572014-04-16 22:29:23 +0000979 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
980 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000981 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
982 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +0000983 if hasattr(options, 'with_tags') and options.with_tags:
984 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000985 elif revision_ref and revision_ref.startswith('refs/tags/'):
986 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000987 return git_cache.Mirror(url, **mirror_kwargs)
988
John Budorick882c91e2018-07-12 22:11:41 +0000989 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800990 """Update a git mirror by fetching the latest commits from the remote,
991 unless mirror already contains revision whose type is sha1 hash.
992 """
John Budorick882c91e2018-07-12 22:11:41 +0000993 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800994 if options.verbose:
995 self.Print('skipping mirror update, it has rev=%s already' % revision,
996 timestamp=False)
997 return
998
szager@chromium.org3ec84f62014-08-22 21:00:22 +0000999 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +00001000 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +00001001 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +00001002 depth = 10
1003 else:
1004 depth = 10000
1005 else:
1006 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +00001007 mirror.populate(verbose=options.verbose,
1008 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +00001009 depth=depth,
1010 ignore_lock=getattr(options, 'ignore_locks', False),
1011 lock_timeout=getattr(options, 'lock_timeout', 0))
1012 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001013
John Budorick882c91e2018-07-12 22:11:41 +00001014 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001015 """Clone a git repository from the given URL.
1016
msb@chromium.org786fb682010-06-02 15:16:23 +00001017 Once we've cloned the repo, we checkout a working branch if the specified
1018 revision is a branch head. If it is a tag or a specific commit, then we
1019 leave HEAD detached as it makes future updates simpler -- in this case the
1020 user should first create a new branch or switch to an existing branch before
1021 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001022 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001023 # git clone doesn't seem to insert a newline properly before printing
1024 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001025 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +00001026 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +00001027 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001028 if self.cache_dir:
1029 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001030 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001031 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001032 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001033 # If the parent directory does not exist, Git clone on Windows will not
1034 # create it, so we need to do it manually.
1035 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001036 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001037
1038 template_dir = None
1039 if hasattr(options, 'no_history') and options.no_history:
John Budorick882c91e2018-07-12 22:11:41 +00001040 if gclient_utils.IsGitSha(revision):
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001041 # In the case of a subproject, the pinned sha is not necessarily the
1042 # head of the remote branch (so we can't just use --depth=N). Instead,
1043 # we tell git to fetch all the remote objects from SHA..HEAD by means of
1044 # a template git dir which has a 'shallow' file pointing to the sha.
1045 template_dir = tempfile.mkdtemp(
1046 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
1047 dir=parent_dir)
1048 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
1049 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
1050 template_file.write(revision)
1051 clone_cmd.append('--template=' + template_dir)
1052 else:
1053 # Otherwise, we're just interested in the HEAD. Just use --depth.
1054 clone_cmd.append('--depth=1')
1055
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001056 tmp_dir = tempfile.mkdtemp(
1057 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
1058 dir=parent_dir)
1059 try:
1060 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +01001061 if self.print_outbuf:
1062 print_stdout = True
1063 stdout = gclient_utils.WriteToStdout(self.out_fh)
1064 else:
1065 print_stdout = False
1066 stdout = self.out_fh
1067 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
1068 print_stdout=print_stdout, stdout=stdout)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001069 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +00001070 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1071 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001072 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001073 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001074 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001075 finally:
1076 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001077 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001078 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001079 if template_dir:
1080 gclient_utils.rmtree(template_dir)
Edward Lemur579c9862018-07-13 23:17:51 +00001081 self._SetFetchConfig(options)
1082 self._Fetch(options, prune=options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001083 revision = self._AutoFetchRef(options, revision)
1084 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1085 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001086 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001087 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001088 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +00001089 ('Checked out %s to a detached HEAD. Before making any commits\n'
1090 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
1091 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
John Budorick882c91e2018-07-12 22:11:41 +00001092 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001093
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001094 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001095 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001096 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001097 raise gclient_utils.Error("Background task requires input. Rerun "
1098 "gclient with --jobs=1 so that\n"
1099 "interaction is possible.")
1100 try:
1101 return raw_input(prompt)
1102 except KeyboardInterrupt:
1103 # Hide the exception.
1104 sys.exit(1)
1105
1106
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001107 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001108 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001109 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001110 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001111 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001112 revision = upstream
1113 if newbase:
1114 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001115 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001116 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001117 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001118 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001119 printed_path = True
1120 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001121 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001122
1123 if merge:
1124 merge_output = self._Capture(['merge', revision])
1125 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001126 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001127 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001128
1129 # Build the rebase command here using the args
1130 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1131 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001132 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001133 rebase_cmd.append('--verbose')
1134 if newbase:
1135 rebase_cmd.extend(['--onto', newbase])
1136 rebase_cmd.append(upstream)
1137 if branch:
1138 rebase_cmd.append(branch)
1139
1140 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001141 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001142 except subprocess2.CalledProcessError as e:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001143 if (re.match(r'cannot rebase: you have unstaged changes', e.stderr) or
1144 re.match(r'cannot rebase: your index contains uncommitted changes',
1145 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001146 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001147 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001148 'Cannot rebase because of unstaged changes.\n'
1149 '\'git reset --hard HEAD\' ?\n'
1150 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001151 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001152 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001153 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001154 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001155 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001156 break
1157 elif re.match(r'quit|q', rebase_action, re.I):
1158 raise gclient_utils.Error("Please merge or rebase manually\n"
1159 "cd %s && git " % self.checkout_path
1160 + "%s" % ' '.join(rebase_cmd))
1161 elif re.match(r'show|s', rebase_action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001162 self.Print('%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001163 continue
1164 else:
1165 gclient_utils.Error("Input not recognized")
1166 continue
1167 elif re.search(r'^CONFLICT', e.stdout, re.M):
1168 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1169 "Fix the conflict and run gclient again.\n"
1170 "See 'man git-rebase' for details.\n")
1171 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001172 self.Print(e.stdout.strip())
1173 self.Print('Rebase produced error output:\n%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001174 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1175 "manually.\ncd %s && git " %
1176 self.checkout_path
1177 + "%s" % ' '.join(rebase_cmd))
1178
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001179 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001180 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001181 # Make the output a little prettier. It's nice to have some
1182 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001183 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001184
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001185 @staticmethod
1186 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001187 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1188 if not ok:
1189 raise gclient_utils.Error('git version %s < minimum required %s' %
1190 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001191
John Budorick882c91e2018-07-12 22:11:41 +00001192 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001193 # Special case handling if all 3 conditions are met:
1194 # * the mirros have recently changed, but deps destination remains same,
1195 # * the git histories of mirrors are conflicting.
1196 # * git cache is used
1197 # This manifests itself in current checkout having invalid HEAD commit on
1198 # most git operations. Since git cache is used, just deleted the .git
1199 # folder, and re-create it by cloning.
1200 try:
1201 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1202 except subprocess2.CalledProcessError as e:
1203 if ('fatal: bad object HEAD' in e.stderr
1204 and self.cache_dir and self.cache_dir in url):
1205 self.Print((
1206 'Likely due to DEPS change with git cache_dir, '
1207 'the current commit points to no longer existing object.\n'
1208 '%s' % e)
1209 )
1210 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001211 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001212 else:
1213 raise
1214
msb@chromium.org786fb682010-06-02 15:16:23 +00001215 def _IsRebasing(self):
1216 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1217 # have a plumbing command to determine whether a rebase is in progress, so
1218 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1219 g = os.path.join(self.checkout_path, '.git')
1220 return (
1221 os.path.isdir(os.path.join(g, "rebase-merge")) or
1222 os.path.isdir(os.path.join(g, "rebase-apply")))
1223
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001224 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001225 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1226 if os.path.exists(lockfile):
1227 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001228 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001229 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1230 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001231 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001232
msb@chromium.org786fb682010-06-02 15:16:23 +00001233 # Make sure the tree is clean; see git-rebase.sh for reference
1234 try:
1235 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001236 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001237 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001238 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001239 '\tYou have unstaged changes.\n'
1240 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001241 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001242 try:
1243 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001244 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001245 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001246 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001247 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001248 '\tYour index contains uncommitted changes\n'
1249 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001250 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001251
agable83faed02016-10-24 14:37:10 -07001252 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001253 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1254 # reference by a commit). If not, error out -- most likely a rebase is
1255 # in progress, try to detect so we can give a better error.
1256 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001257 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1258 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001259 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001260 # Commit is not contained by any rev. See if the user is rebasing:
1261 if self._IsRebasing():
1262 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001263 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001264 '\tAlready in a conflict, i.e. (no branch).\n'
1265 '\tFix the conflict and run gclient again.\n'
1266 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1267 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001268 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001269 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001270 name = ('saved-by-gclient-' +
1271 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001272 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001273 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001274 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001275
msb@chromium.org5bde4852009-12-14 16:47:12 +00001276 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001277 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001278 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001279 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001280 return None
1281 return branch
1282
borenet@google.comc3e09d22014-04-10 13:58:18 +00001283 def _Capture(self, args, **kwargs):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001284 kwargs.setdefault('cwd', self.checkout_path)
1285 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001286 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001287 env = scm.GIT.ApplyEnvVars(kwargs)
Raul Tambrecd862e32019-05-10 21:19:00 +00001288 ret = subprocess2.check_output(
1289 ['git'] + args, env=env, **kwargs).decode('utf-8')
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001290 if strip:
1291 ret = ret.strip()
1292 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001293
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001294 def _Checkout(self, options, ref, force=False, quiet=None):
1295 """Performs a 'git-checkout' operation.
1296
1297 Args:
1298 options: The configured option set
1299 ref: (str) The branch/commit to checkout
1300 quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
1301 'None', the behavior is inferred from 'options.verbose'.
1302 Returns: (str) The output of the checkout operation
1303 """
1304 if quiet is None:
1305 quiet = (not options.verbose)
1306 checkout_args = ['checkout']
1307 if force:
1308 checkout_args.append('--force')
1309 if quiet:
1310 checkout_args.append('--quiet')
1311 checkout_args.append(ref)
1312 return self._Capture(checkout_args)
1313
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001314 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1315 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001316 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001317 # When updating, the ref is modified to be a remote ref .
1318 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1319 # Try to reverse that mapping.
1320 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1321 if original_ref:
1322 refspec = original_ref + ':' + refspec
1323 # When a mirror is configured, it only fetches
1324 # refs/{heads,branch-heads,tags}/*.
1325 # If asked to fetch other refs, we must fetch those directly from the
1326 # repository, and not from the mirror.
1327 if not original_ref.startswith(
1328 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1329 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001330 fetch_cmd = cfg + [
1331 'fetch',
1332 remote or self.remote,
1333 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001334 if refspec:
1335 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001336
1337 if prune:
1338 fetch_cmd.append('--prune')
1339 if options.verbose:
1340 fetch_cmd.append('--verbose')
1341 elif quiet:
1342 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001343 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001344
1345 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1346 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1347
Edward Lemur579c9862018-07-13 23:17:51 +00001348 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001349 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1350 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001351 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001352 try:
1353 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1354 options)
1355 self._Run(['config', 'remote.%s.fetch' % self.remote,
1356 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1357 except subprocess2.CalledProcessError as e:
1358 # If exit code was 5, it means we attempted to unset a config that
1359 # didn't exist. Ignore it.
1360 if e.returncode != 5:
1361 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001362 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001363 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001364 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1365 '^\\+refs/branch-heads/\\*:.*$']
1366 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001367 if hasattr(options, 'with_tags') and options.with_tags:
1368 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1369 '+refs/tags/*:refs/tags/*',
1370 '^\\+refs/tags/\\*:.*$']
1371 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001372
John Budorick882c91e2018-07-12 22:11:41 +00001373 def _AutoFetchRef(self, options, revision):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001374 """Attempts to fetch |revision| if not available in local repo.
1375
1376 Returns possibly updated revision."""
John Budorick882c91e2018-07-12 22:11:41 +00001377 try:
1378 self._Capture(['rev-parse', revision])
1379 except subprocess2.CalledProcessError:
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001380 self._Fetch(options, refspec=revision)
1381 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1382 return revision
1383
dnj@chromium.org680f2172014-06-25 00:39:32 +00001384 def _Run(self, args, options, show_header=True, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001385 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001386 kwargs.setdefault('cwd', self.checkout_path)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001387 kwargs.setdefault('stdout', self.out_fh)
szager@chromium.org848fd492014-04-09 19:06:44 +00001388 kwargs['filter_fn'] = self.filter
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001389 kwargs.setdefault('print_stdout', False)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001390 env = scm.GIT.ApplyEnvVars(kwargs)
agable@chromium.org772efaf2014-04-01 02:35:44 +00001391 cmd = ['git'] + args
dnj@chromium.org680f2172014-06-25 00:39:32 +00001392 if show_header:
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001393 gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs)
1394 else:
1395 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001396
1397
1398class CipdPackage(object):
1399 """A representation of a single CIPD package."""
1400
John Budorickd3ba72b2018-03-20 12:27:42 -07001401 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001402 self._authority_for_subdir = authority_for_subdir
1403 self._name = name
1404 self._version = version
1405
1406 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001407 def authority_for_subdir(self):
1408 """Whether this package has authority to act on behalf of its subdir.
1409
1410 Some operations should only be performed once per subdirectory. A package
1411 that has authority for its subdirectory is the only package that should
1412 perform such operations.
1413
1414 Returns:
1415 bool; whether this package has subdir authority.
1416 """
1417 return self._authority_for_subdir
1418
1419 @property
1420 def name(self):
1421 return self._name
1422
1423 @property
1424 def version(self):
1425 return self._version
1426
1427
1428class CipdRoot(object):
1429 """A representation of a single CIPD root."""
1430 def __init__(self, root_dir, service_url):
1431 self._all_packages = set()
1432 self._mutator_lock = threading.Lock()
1433 self._packages_by_subdir = collections.defaultdict(list)
1434 self._root_dir = root_dir
1435 self._service_url = service_url
1436
1437 def add_package(self, subdir, package, version):
1438 """Adds a package to this CIPD root.
1439
1440 As far as clients are concerned, this grants both root and subdir authority
1441 to packages arbitrarily. (The implementation grants root authority to the
1442 first package added and subdir authority to the first package added for that
1443 subdir, but clients should not depend on or expect that behavior.)
1444
1445 Args:
1446 subdir: str; relative path to where the package should be installed from
1447 the cipd root directory.
1448 package: str; the cipd package name.
1449 version: str; the cipd package version.
1450 Returns:
1451 CipdPackage; the package that was created and added to this root.
1452 """
1453 with self._mutator_lock:
1454 cipd_package = CipdPackage(
1455 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001456 not self._packages_by_subdir[subdir])
1457 self._all_packages.add(cipd_package)
1458 self._packages_by_subdir[subdir].append(cipd_package)
1459 return cipd_package
1460
1461 def packages(self, subdir):
1462 """Get the list of configured packages for the given subdir."""
1463 return list(self._packages_by_subdir[subdir])
1464
1465 def clobber(self):
1466 """Remove the .cipd directory.
1467
1468 This is useful for forcing ensure to redownload and reinitialize all
1469 packages.
1470 """
1471 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001472 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001473 try:
1474 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1475 except OSError:
1476 if os.path.exists(cipd_cache_dir):
1477 raise
1478
1479 @contextlib.contextmanager
1480 def _create_ensure_file(self):
1481 try:
1482 ensure_file = None
1483 with tempfile.NamedTemporaryFile(
Raul Tambreb946b232019-03-26 14:48:46 +00001484 suffix='.ensure', delete=False, mode='w') as ensure_file:
John Budorick302bb842018-07-17 23:49:17 +00001485 ensure_file.write('$ParanoidMode CheckPresence\n\n')
Raul Tambreb946b232019-03-26 14:48:46 +00001486 for subdir, packages in sorted(self._packages_by_subdir.items()):
John Budorick0f7b2002018-01-19 15:46:17 -08001487 ensure_file.write('@Subdir %s\n' % subdir)
Edward Lemurfbb06aa2018-06-11 20:43:06 +00001488 for package in sorted(packages, key=lambda p: p.name):
John Budorick0f7b2002018-01-19 15:46:17 -08001489 ensure_file.write('%s %s\n' % (package.name, package.version))
1490 ensure_file.write('\n')
1491 yield ensure_file.name
1492 finally:
1493 if ensure_file is not None and os.path.exists(ensure_file.name):
1494 os.remove(ensure_file.name)
1495
1496 def ensure(self):
1497 """Run `cipd ensure`."""
1498 with self._mutator_lock:
1499 with self._create_ensure_file() as ensure_file:
1500 cmd = [
1501 'cipd', 'ensure',
1502 '-log-level', 'error',
1503 '-root', self.root_dir,
1504 '-ensure-file', ensure_file,
1505 ]
1506 gclient_utils.CheckCallAndFilterAndHeader(cmd)
1507
John Budorickd3ba72b2018-03-20 12:27:42 -07001508 def run(self, command):
1509 if command == 'update':
1510 self.ensure()
1511 elif command == 'revert':
1512 self.clobber()
1513 self.ensure()
1514
John Budorick0f7b2002018-01-19 15:46:17 -08001515 def created_package(self, package):
1516 """Checks whether this root created the given package.
1517
1518 Args:
1519 package: CipdPackage; the package to check.
1520 Returns:
1521 bool; whether this root created the given package.
1522 """
1523 return package in self._all_packages
1524
1525 @property
1526 def root_dir(self):
1527 return self._root_dir
1528
1529 @property
1530 def service_url(self):
1531 return self._service_url
1532
1533
1534class CipdWrapper(SCMWrapper):
1535 """Wrapper for CIPD.
1536
1537 Currently only supports chrome-infra-packages.appspot.com.
1538 """
John Budorick3929e9e2018-02-04 18:18:07 -08001539 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001540
1541 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1542 out_cb=None, root=None, package=None):
1543 super(CipdWrapper, self).__init__(
1544 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1545 out_cb=out_cb)
1546 assert root.created_package(package)
1547 self._package = package
1548 self._root = root
1549
1550 #override
1551 def GetCacheMirror(self):
1552 return None
1553
1554 #override
1555 def GetActualRemoteURL(self, options):
1556 return self._root.service_url
1557
1558 #override
1559 def DoesRemoteURLMatch(self, options):
1560 del options
1561 return True
1562
1563 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001564 """Does nothing.
1565
1566 CIPD packages should be reverted at the root by running
1567 `CipdRoot.run('revert')`.
1568 """
1569 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001570
1571 def diff(self, options, args, file_list):
1572 """CIPD has no notion of diffing."""
1573 pass
1574
1575 def pack(self, options, args, file_list):
1576 """CIPD has no notion of diffing."""
1577 pass
1578
1579 def revinfo(self, options, args, file_list):
1580 """Grab the instance ID."""
1581 try:
1582 tmpdir = tempfile.mkdtemp()
1583 describe_json_path = os.path.join(tmpdir, 'describe.json')
1584 cmd = [
1585 'cipd', 'describe',
1586 self._package.name,
1587 '-log-level', 'error',
1588 '-version', self._package.version,
1589 '-json-output', describe_json_path
1590 ]
1591 gclient_utils.CheckCallAndFilter(
1592 cmd, filter_fn=lambda _line: None, print_stdout=False)
1593 with open(describe_json_path) as f:
1594 describe_json = json.load(f)
1595 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1596 finally:
1597 gclient_utils.rmtree(tmpdir)
1598
1599 def status(self, options, args, file_list):
1600 pass
1601
1602 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001603 """Does nothing.
1604
1605 CIPD packages should be updated at the root by running
1606 `CipdRoot.run('update')`.
1607 """
1608 pass