blob: c91d592fa2342da07c4c030d990d9ef377f193b3 [file] [log] [blame]
steveblock@chromium.org93567042012-02-15 01:02:26 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00005"""Gclient-specific SCM-specific operations."""
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00006
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00007from __future__ import print_function
8
John Budorick0f7b2002018-01-19 15:46:17 -08009import collections
10import contextlib
borenet@google.comb2256212014-05-07 20:57:28 +000011import errno
John Budorick0f7b2002018-01-19 15:46:17 -080012import json
maruel@chromium.org754960e2009-09-21 12:31:05 +000013import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000014import os
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000015import posixpath
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000016import re
maruel@chromium.org90541732011-04-01 17:54:18 +000017import sys
ilevy@chromium.org3534aa52013-07-20 01:58:08 +000018import tempfile
John Budorick0f7b2002018-01-19 15:46:17 -080019import threading
zty@chromium.org6279e8a2014-02-13 01:45:25 +000020import traceback
Raul Tambreb946b232019-03-26 14:48:46 +000021
22try:
23 import urlparse
24except ImportError: # For Py3 compatibility
25 import urllib.parse as urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000026
27import gclient_utils
szager@chromium.org848fd492014-04-09 19:06:44 +000028import git_cache
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000029import scm
borenet@google.comb2256212014-05-07 20:57:28 +000030import shutil
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000031import subprocess2
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000032
33
szager@chromium.org71cbb502013-04-19 23:30:15 +000034THIS_FILE_PATH = os.path.abspath(__file__)
35
hinoka@google.com2f2ca142014-01-07 03:59:18 +000036GSUTIL_DEFAULT_PATH = os.path.join(
hinoka@chromium.orgb091aa52014-12-20 01:47:31 +000037 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
hinoka@google.com2f2ca142014-01-07 03:59:18 +000038
maruel@chromium.org79d62372015-06-01 18:50:55 +000039
smutae7ea312016-07-18 11:59:41 -070040class NoUsableRevError(gclient_utils.Error):
41 """Raised if requested revision isn't found in checkout."""
42
43
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000044class DiffFiltererWrapper(object):
45 """Simple base class which tracks which file is being diffed and
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000046 replaces instances of its file name in the original and
agable41e3a6c2016-10-20 11:36:56 -070047 working copy lines of the git diff output."""
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000048 index_string = None
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000049 original_prefix = "--- "
50 working_prefix = "+++ "
51
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000052 def __init__(self, relpath, print_func):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000053 # Note that we always use '/' as the path separator to be
agable41e3a6c2016-10-20 11:36:56 -070054 # consistent with cygwin-style output on Windows
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000055 self._relpath = relpath.replace("\\", "/")
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000056 self._current_file = None
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000057 self._print_func = print_func
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000058
maruel@chromium.org6e29d572010-06-04 17:32:20 +000059 def SetCurrentFile(self, current_file):
60 self._current_file = current_file
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000061
iannucci@chromium.org3830a672013-02-19 20:15:14 +000062 @property
63 def _replacement_file(self):
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000064 return posixpath.join(self._relpath, self._current_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000065
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000066 def _Replace(self, line):
67 return line.replace(self._current_file, self._replacement_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000068
69 def Filter(self, line):
70 if (line.startswith(self.index_string)):
71 self.SetCurrentFile(line[len(self.index_string):])
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000072 line = self._Replace(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000073 else:
74 if (line.startswith(self.original_prefix) or
75 line.startswith(self.working_prefix)):
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000076 line = self._Replace(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000077 self._print_func(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000078
79
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000080class GitDiffFilterer(DiffFiltererWrapper):
81 index_string = "diff --git "
82
83 def SetCurrentFile(self, current_file):
84 # Get filename by parsing "a/<filename> b/<filename>"
85 self._current_file = current_file[:(len(current_file)/2)][2:]
86
87 def _Replace(self, line):
88 return re.sub("[a|b]/" + self._current_file, self._replacement_file, line)
89
90
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000091# SCMWrapper base class
92
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000093class SCMWrapper(object):
94 """Add necessary glue between all the supported SCM.
95
msb@chromium.orgd6504212010-01-13 17:34:31 +000096 This is the abstraction layer to bind to different SCM.
97 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000098 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
Edward Lemur231f5ea2018-01-31 19:02:36 +010099 out_cb=None, print_outbuf=False):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000100 self.url = url
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +0000101 self._root_dir = root_dir
102 if self._root_dir:
103 self._root_dir = self._root_dir.replace('/', os.sep)
104 self.relpath = relpath
105 if self.relpath:
106 self.relpath = self.relpath.replace('/', os.sep)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000107 if self.relpath and self._root_dir:
108 self.checkout_path = os.path.join(self._root_dir, self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000109 if out_fh is None:
110 out_fh = sys.stdout
111 self.out_fh = out_fh
112 self.out_cb = out_cb
Edward Lemur231f5ea2018-01-31 19:02:36 +0100113 self.print_outbuf = print_outbuf
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000114
115 def Print(self, *args, **kwargs):
116 kwargs.setdefault('file', self.out_fh)
117 if kwargs.pop('timestamp', True):
118 self.out_fh.write('[%s] ' % gclient_utils.Elapsed())
119 print(*args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000120
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000121 def RunCommand(self, command, options, args, file_list=None):
agabledebf6c82016-12-21 12:50:12 -0800122 commands = ['update', 'updatesingle', 'revert',
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000123 'revinfo', 'status', 'diff', 'pack', 'runhooks']
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000124
125 if not command in commands:
126 raise gclient_utils.Error('Unknown command %s' % command)
127
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000128 if not command in dir(self):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000129 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % (
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000130 command, self.__class__.__name__))
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000131
132 return getattr(self, command)(options, args, file_list)
133
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000134 @staticmethod
135 def _get_first_remote_url(checkout_path):
136 log = scm.GIT.Capture(
137 ['config', '--local', '--get-regexp', r'remote.*.url'],
138 cwd=checkout_path)
139 # Get the second token of the first line of the log.
140 return log.splitlines()[0].split(' ', 1)[1]
141
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000142 def GetCacheMirror(self):
Robert Iannuccia19649b2018-06-29 16:31:45 +0000143 if getattr(self, 'cache_dir', None):
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000144 url, _ = gclient_utils.SplitUrlRevision(self.url)
145 return git_cache.Mirror(url)
146 return None
147
smut@google.comd33eab32014-07-07 19:35:18 +0000148 def GetActualRemoteURL(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000149 """Attempt to determine the remote URL for this SCMWrapper."""
smut@google.comd33eab32014-07-07 19:35:18 +0000150 # Git
borenet@google.combda475e2014-03-24 19:04:45 +0000151 if os.path.exists(os.path.join(self.checkout_path, '.git')):
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000152 actual_remote_url = self._get_first_remote_url(self.checkout_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000153
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000154 mirror = self.GetCacheMirror()
155 # If the cache is used, obtain the actual remote URL from there.
156 if (mirror and mirror.exists() and
157 mirror.mirror_path.replace('\\', '/') ==
158 actual_remote_url.replace('\\', '/')):
159 actual_remote_url = self._get_first_remote_url(mirror.mirror_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000160 return actual_remote_url
borenet@google.com88d10082014-03-21 17:24:48 +0000161 return None
162
borenet@google.com4e9be262014-04-08 19:40:30 +0000163 def DoesRemoteURLMatch(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000164 """Determine whether the remote URL of this checkout is the expected URL."""
165 if not os.path.exists(self.checkout_path):
166 # A checkout which doesn't exist can't be broken.
167 return True
168
smut@google.comd33eab32014-07-07 19:35:18 +0000169 actual_remote_url = self.GetActualRemoteURL(options)
borenet@google.com88d10082014-03-21 17:24:48 +0000170 if actual_remote_url:
borenet@google.com8156c9f2014-04-01 16:41:36 +0000171 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip('/')
172 == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/'))
borenet@google.com88d10082014-03-21 17:24:48 +0000173 else:
174 # This may occur if the self.checkout_path exists but does not contain a
agable41e3a6c2016-10-20 11:36:56 -0700175 # valid git checkout.
borenet@google.com88d10082014-03-21 17:24:48 +0000176 return False
177
borenet@google.comb09097a2014-04-09 19:09:08 +0000178 def _DeleteOrMove(self, force):
179 """Delete the checkout directory or move it out of the way.
180
181 Args:
182 force: bool; if True, delete the directory. Otherwise, just move it.
183 """
borenet@google.comb2256212014-05-07 20:57:28 +0000184 if force and os.environ.get('CHROME_HEADLESS') == '1':
185 self.Print('_____ Conflicting directory found in %s. Removing.'
186 % self.checkout_path)
187 gclient_utils.AddWarning('Conflicting directory %s deleted.'
188 % self.checkout_path)
189 gclient_utils.rmtree(self.checkout_path)
190 else:
191 bad_scm_dir = os.path.join(self._root_dir, '_bad_scm',
192 os.path.dirname(self.relpath))
193
194 try:
195 os.makedirs(bad_scm_dir)
196 except OSError as e:
197 if e.errno != errno.EEXIST:
198 raise
199
200 dest_path = tempfile.mkdtemp(
201 prefix=os.path.basename(self.relpath),
202 dir=bad_scm_dir)
203 self.Print('_____ Conflicting directory found in %s. Moving to %s.'
204 % (self.checkout_path, dest_path))
205 gclient_utils.AddWarning('Conflicting directory %s moved to %s.'
206 % (self.checkout_path, dest_path))
207 shutil.move(self.checkout_path, dest_path)
borenet@google.comb09097a2014-04-09 19:09:08 +0000208
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000209
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000210class GitWrapper(SCMWrapper):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000211 """Wrapper for Git"""
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000212 name = 'git'
maruel@chromium.org1a60dca2013-11-26 14:06:26 +0000213 remote = 'origin'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000214
Robert Iannuccia19649b2018-06-29 16:31:45 +0000215 @property
216 def cache_dir(self):
217 try:
218 return git_cache.Mirror.GetCachePath()
219 except RuntimeError:
220 return None
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000221
John Budorick0f7b2002018-01-19 15:46:17 -0800222 def __init__(self, url=None, *args, **kwargs):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000223 """Removes 'git+' fake prefix from git URL."""
Henrique Ferreiroe72279d2019-04-17 12:01:50 +0000224 if url and (url.startswith('git+http://') or
225 url.startswith('git+https://')):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000226 url = url[4:]
John Budorick0f7b2002018-01-19 15:46:17 -0800227 SCMWrapper.__init__(self, url, *args, **kwargs)
szager@chromium.org848fd492014-04-09 19:06:44 +0000228 filter_kwargs = { 'time_throttle': 1, 'out_fh': self.out_fh }
229 if self.out_cb:
230 filter_kwargs['predicate'] = self.out_cb
231 self.filter = gclient_utils.GitFilter(**filter_kwargs)
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000232
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000233 def GetCheckoutRoot(self):
234 return scm.GIT.GetCheckoutRoot(self.checkout_path)
235
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000236 def GetRevisionDate(self, _revision):
floitsch@google.comeaab7842011-04-28 09:07:58 +0000237 """Returns the given revision's date in ISO-8601 format (which contains the
238 time zone)."""
239 # TODO(floitsch): get the time-stamp of the given revision and not just the
240 # time-stamp of the currently checked out revision.
241 return self._Capture(['log', '-n', '1', '--format=%ai'])
242
Aaron Gablef4068aa2017-12-12 15:14:09 -0800243 def _GetDiffFilenames(self, base):
244 """Returns the names of files modified since base."""
245 return self._Capture(
Raul Tambrecd862e32019-05-10 21:19:00 +0000246 # Filter to remove base if it is None.
247 list(filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only',
248 base])
249 )).split()
Aaron Gablef4068aa2017-12-12 15:14:09 -0800250
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000251 def diff(self, options, _args, _file_list):
Aaron Gable1853f662018-02-12 15:45:56 -0800252 _, revision = gclient_utils.SplitUrlRevision(self.url)
253 if not revision:
254 revision = 'refs/remotes/%s/master' % self.remote
255 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000256
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000257 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000258 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000259 repository.
260
261 The patch file is generated from a diff of the merge base of HEAD and
262 its upstream branch.
263 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700264 try:
265 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
266 except subprocess2.CalledProcessError:
267 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000268 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700269 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000270 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000271 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000272
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800273 def _Scrub(self, target, options):
274 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000275 quiet = []
276 if not options.verbose:
277 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800278 self._Run(['reset', '--hard', target] + quiet, options)
279 if options.force and options.delete_unversioned_trees:
280 # where `target` is a commit that contains both upper and lower case
281 # versions of the same file on a case insensitive filesystem, we are
282 # actually in a broken state here. The index will have both 'a' and 'A',
283 # but only one of them will exist on the disk. To progress, we delete
284 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800285 output = self._Capture([
286 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800287 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800288 # --porcelain (v1) looks like:
289 # XY filename
290 try:
291 filename = line[3:]
292 self.Print('_____ Deleting residual after reset: %r.' % filename)
293 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800294 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800295 except OSError:
296 pass
297
John Budorick882c91e2018-07-12 22:11:41 +0000298 def _FetchAndReset(self, revision, file_list, options):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800299 """Equivalent to git fetch; git reset."""
Edward Lemur579c9862018-07-13 23:17:51 +0000300 self._SetFetchConfig(options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000301
dnj@chromium.org680f2172014-06-25 00:39:32 +0000302 self._Fetch(options, prune=True, quiet=options.verbose)
John Budorick882c91e2018-07-12 22:11:41 +0000303 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000304 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800305 files = self._Capture(
306 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambreb946b232019-03-26 14:48:46 +0000307 file_list.extend(
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000308 [os.path.join(self.checkout_path, f) for f in files])
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000309
szager@chromium.org8a139702014-06-20 15:55:01 +0000310 def _DisableHooks(self):
311 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
312 if not os.path.isdir(hook_dir):
313 return
314 for f in os.listdir(hook_dir):
315 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000316 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
317 if os.path.exists(disabled_hook_path):
318 os.remove(disabled_hook_path)
319 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000320
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000321 def _maybe_break_locks(self, options):
322 """This removes all .lock files from this repo's .git directory, if the
323 user passed the --break_repo_locks command line flag.
324
325 In particular, this will cleanup index.lock files, as well as ref lock
326 files.
327 """
328 if options.break_repo_locks:
329 git_dir = os.path.join(self.checkout_path, '.git')
330 for path, _, filenames in os.walk(git_dir):
331 for filename in filenames:
332 if filename.endswith('.lock'):
333 to_break = os.path.join(path, filename)
334 self.Print('breaking lock: %s' % (to_break,))
335 try:
336 os.remove(to_break)
337 except OSError as ex:
338 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
339 raise
340
Edward Lemur3acbc742019-05-30 17:57:35 +0000341 def apply_patch_ref(self, patch_repo, patch_rev, target_rev, options,
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000342 file_list):
343 """Apply a patch on top of the revision we're synced at.
344
Edward Lemur3acbc742019-05-30 17:57:35 +0000345 The patch ref is given by |patch_repo|@|patch_rev|.
346 |target_rev| is usually the branch that the |patch_rev| was uploaded against
347 (e.g. 'refs/heads/master'), but this is not required.
348
349 We cherry-pick all commits reachable from |patch_rev| on top of the curret
350 HEAD, excluding those reachable from |target_rev|
351 (i.e. git cherry-pick target_rev..patch_rev).
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000352
353 Graphically, it looks like this:
354
Edward Lemur3acbc742019-05-30 17:57:35 +0000355 ... -> o -> [possibly already landed commits] -> target_rev
356 \
357 -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000358
Edward Lemur3acbc742019-05-30 17:57:35 +0000359 The final checkout state is then:
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000360
Edward Lemur3acbc742019-05-30 17:57:35 +0000361 ... -> HEAD -> [possibly not yet landed dependent CLs] -> patch_rev
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000362
363 After application, if |options.reset_patch_ref| is specified, we soft reset
Edward Lemur3acbc742019-05-30 17:57:35 +0000364 the cherry-picked changes, keeping them in git index only.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000365
366 Args:
Edward Lemur3acbc742019-05-30 17:57:35 +0000367 patch_repo: The patch origin.
368 e.g. 'https://foo.googlesource.com/bar'
369 patch_rev: The revision to patch.
370 e.g. 'refs/changes/1234/34/1'.
371 target_rev: The revision to use when finding the merge base.
372 Typically, the branch that the patch was uploaded against.
Edward Lemur6a4e31b2018-08-10 19:59:02 +0000373 e.g. 'refs/heads/master' or 'refs/heads/infra/config'.
374 options: The options passed to gclient.
375 file_list: A list where modified files will be appended.
376 """
377
Edward Lemurca7d8812018-07-24 17:42:45 +0000378 # Abort any cherry-picks in progress.
379 try:
380 self._Capture(['cherry-pick', '--abort'])
381 except subprocess2.CalledProcessError:
382 pass
383
Edward Lesmesc621b212018-03-21 20:26:56 -0400384 base_rev = self._Capture(['rev-parse', 'HEAD'])
Edward Lemura0ffbe42019-05-01 16:52:18 +0000385
Edward Lemur3acbc742019-05-30 17:57:35 +0000386 if not target_rev:
Edward Lemur4c5c8ab2019-06-07 15:58:13 +0000387 raise gclient_utils.Error('A target revision for the patch must be given')
Edward Lemur3acbc742019-05-30 17:57:35 +0000388 elif target_rev.startswith('refs/heads/'):
389 # If |target_rev| is in refs/heads/**, try first to find the corresponding
390 # remote ref for it, since |target_rev| might point to a local ref which
391 # is not up to date with the corresponding remote ref.
392 remote_ref = ''.join(scm.GIT.RefToRemoteRef(target_rev, self.remote))
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000393 self.Print('Trying the corresponding remote ref for %r: %r\n' % (
Edward Lemur3acbc742019-05-30 17:57:35 +0000394 target_rev, remote_ref))
395 if scm.GIT.IsValidRevision(self.checkout_path, remote_ref):
396 target_rev = remote_ref
397 elif not scm.GIT.IsValidRevision(self.checkout_path, target_rev):
398 # Fetch |target_rev| if it's not already available.
399 url, _ = gclient_utils.SplitUrlRevision(self.url)
400 mirror = self._GetMirror(url, options, target_rev)
401 if mirror:
402 rev_type = 'branch' if target_rev.startswith('refs/') else 'hash'
403 self._UpdateMirrorIfNotContains(mirror, options, rev_type, target_rev)
404 self._Fetch(options, refspec=target_rev)
Edward Lemura0ffbe42019-05-01 16:52:18 +0000405
Edward Lemur3acbc742019-05-30 17:57:35 +0000406 self.Print('===Applying patch===')
407 self.Print('Revision to patch is %r @ %r.' % (patch_repo, patch_rev))
Edward Lemur3acbc742019-05-30 17:57:35 +0000408 self.Print('Current dir is %r' % self.checkout_path)
Edward Lesmesc621b212018-03-21 20:26:56 -0400409 self._Capture(['reset', '--hard'])
danakjd5c0b562019-11-08 17:27:47 +0000410 self._Capture(['fetch', '--no-tags', patch_repo, patch_rev])
Edward Lemurca7d8812018-07-24 17:42:45 +0000411 patch_rev = self._Capture(['rev-parse', 'FETCH_HEAD'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400412
Edward Lemur3acbc742019-05-30 17:57:35 +0000413 if not options.rebase_patch_ref:
414 self._Capture(['checkout', patch_rev])
Robert Iannuccic39a7782019-11-01 18:30:33 +0000415 # Adjust base_rev to be the first parent of our checked out patch ref;
416 # This will allow us to correctly extend `file_list`, and will show the
417 # correct file-list to programs which do `git diff --cached` expecting to
418 # see the patch diff.
419 base_rev = self._Capture(['rev-parse', patch_rev+'~'])
420
Edward Lemur3acbc742019-05-30 17:57:35 +0000421 else:
Robert Iannuccic39a7782019-11-01 18:30:33 +0000422 self.Print('Will cherrypick %r .. %r on top of %r.' % (
423 target_rev, patch_rev, base_rev))
Edward Lemur3acbc742019-05-30 17:57:35 +0000424 try:
425 if scm.GIT.IsAncestor(self.checkout_path, patch_rev, target_rev):
426 # If |patch_rev| is an ancestor of |target_rev|, check it out.
Edward Lemurca7d8812018-07-24 17:42:45 +0000427 self._Capture(['checkout', patch_rev])
428 else:
429 # If a change was uploaded on top of another change, which has already
430 # landed, one of the commits in the cherry-pick range will be
431 # redundant, since it has already landed and its changes incorporated
432 # in the tree.
433 # We pass '--keep-redundant-commits' to ignore those changes.
Edward Lemur3acbc742019-05-30 17:57:35 +0000434 self._Capture(['cherry-pick', target_rev + '..' + patch_rev,
Edward Lemurca7d8812018-07-24 17:42:45 +0000435 '--keep-redundant-commits'])
436
Edward Lemur3acbc742019-05-30 17:57:35 +0000437 except subprocess2.CalledProcessError as e:
438 self.Print('Failed to apply patch.')
439 self.Print('Revision to patch was %r @ %r.' % (patch_repo, patch_rev))
440 self.Print('Tried to cherrypick %r .. %r on top of %r.' % (
441 target_rev, patch_rev, base_rev))
442 self.Print('Current dir is %r' % self.checkout_path)
443 self.Print('git returned non-zero exit status %s:\n%s' % (
Edward Lemur979fa782019-08-13 22:44:05 +0000444 e.returncode, e.stderr.decode('utf-8')))
Edward Lemur3acbc742019-05-30 17:57:35 +0000445 # Print the current status so that developers know what changes caused
446 # the patch failure, since git cherry-pick doesn't show that
447 # information.
448 self.Print(self._Capture(['status']))
449 try:
450 self._Capture(['cherry-pick', '--abort'])
451 except subprocess2.CalledProcessError:
452 pass
453 raise
Edward Lemurca7d8812018-07-24 17:42:45 +0000454
Edward Lemur3acbc742019-05-30 17:57:35 +0000455 if file_list is not None:
456 file_list.extend(self._GetDiffFilenames(base_rev))
Edward Lemurca7d8812018-07-24 17:42:45 +0000457
Edward Lesmesc621b212018-03-21 20:26:56 -0400458 if options.reset_patch_ref:
459 self._Capture(['reset', '--soft', base_rev])
460
msb@chromium.orge28e4982009-09-25 20:51:45 +0000461 def update(self, options, args, file_list):
462 """Runs git to update or transparently checkout the working copy.
463
464 All updated files will be appended to file_list.
465
466 Raises:
467 Error: if can't get URL for relative path.
468 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000469 if args:
470 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
471
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000472 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000473
John Budorick882c91e2018-07-12 22:11:41 +0000474 # If a dependency is not pinned, track the default remote branch.
475 default_rev = 'refs/remotes/%s/master' % self.remote
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000476 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000477 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000478 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000479 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000480 # Override the revision number.
481 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000482 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000483 # Check again for a revision in case an initial ref was specified
484 # in the url, for example bla.git@refs/heads/custombranch
485 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000486 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000487 if not revision:
488 revision = default_rev
msb@chromium.orge28e4982009-09-25 20:51:45 +0000489
szager@chromium.org8a139702014-06-20 15:55:01 +0000490 if managed:
491 self._DisableHooks()
492
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000493 printed_path = False
494 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000495 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700496 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000497 verbose = ['--verbose']
498 printed_path = True
499
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000500 revision_ref = revision
501 if ':' in revision:
502 revision_ref, _, revision = revision.partition(':')
503
Edward Lesmes8073a502020-04-15 02:11:14 +0000504 if revision_ref.startswith('refs/branch-heads'):
505 options.with_branch_heads = True
506
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000507 mirror = self._GetMirror(url, options, revision_ref)
508 if mirror:
509 url = mirror.mirror_path
Edward Lemurdb5c5ad2019-03-07 16:00:24 +0000510
John Budorick882c91e2018-07-12 22:11:41 +0000511 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
512 if remote_ref:
513 # Rewrite remote refs to their local equivalents.
514 revision = ''.join(remote_ref)
515 rev_type = "branch"
516 elif revision.startswith('refs/'):
517 # Local branch? We probably don't want to support, since DEPS should
518 # always specify branches as they are in the upstream repo.
519 rev_type = "branch"
520 else:
521 # hash is also a tag, only make a distinction at checkout
522 rev_type = "hash"
smut@google.comd33eab32014-07-07 19:35:18 +0000523
primiano@chromium.org1c127382015-02-17 11:15:40 +0000524 # If we are going to introduce a new project, there is a possibility that
525 # we are syncing back to a state where the project was originally a
526 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
527 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
528 # In such case, we might have a backup of the former .git folder, which can
529 # be used to avoid re-fetching the entire repo again (useful for bisects).
530 backup_dir = self.GetGitBackupDirPath()
531 target_dir = os.path.join(self.checkout_path, '.git')
532 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
533 gclient_utils.safe_makedirs(self.checkout_path)
534 os.rename(backup_dir, target_dir)
535 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800536 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000537
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000538 if (not os.path.exists(self.checkout_path) or
539 (os.path.isdir(self.checkout_path) and
540 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000541 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000542 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000543 try:
John Budorick882c91e2018-07-12 22:11:41 +0000544 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000545 except subprocess2.CalledProcessError:
546 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +0000547 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000548 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800549 files = self._Capture(
John Budorick21a51b32018-09-19 19:39:20 +0000550 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000551 file_list.extend(
Edward Lemur979fa782019-08-13 22:44:05 +0000552 [os.path.join(self.checkout_path, f) for f in files])
John Budorick21a51b32018-09-19 19:39:20 +0000553 if mirror:
554 self._Capture(
555 ['remote', 'set-url', '--push', 'origin', mirror.url])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000556 if not verbose:
557 # Make the output a little prettier. It's nice to have some whitespace
558 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000559 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000560 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000561
John Budorick21a51b32018-09-19 19:39:20 +0000562 if mirror:
563 self._Capture(
564 ['remote', 'set-url', '--push', 'origin', mirror.url])
565
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000566 if not managed:
Edward Lemur579c9862018-07-13 23:17:51 +0000567 self._SetFetchConfig(options)
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000568 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
569 return self._Capture(['rev-parse', '--verify', 'HEAD'])
570
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000571 self._maybe_break_locks(options)
572
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000573 if mirror:
John Budorick882c91e2018-07-12 22:11:41 +0000574 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000575
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000576 # See if the url has changed (the unittests use git://foo for the url, let
577 # that through).
578 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
579 return_early = False
580 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
581 # unit test pass. (and update the comment above)
582 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
583 # This allows devs to use experimental repos which have a different url
584 # but whose branch(s) are the same as official repos.
Raul Tambrecd862e32019-05-10 21:19:00 +0000585 if (current_url.rstrip('/') != url.rstrip('/') and url != 'git://foo' and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000586 subprocess2.capture(
Aaron Gableac9b0f32019-04-18 17:38:37 +0000587 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000588 cwd=self.checkout_path).strip() != 'False'):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000589 self.Print('_____ switching %s to a new upstream' % self.relpath)
iannucci@chromium.org78514212014-08-20 23:08:00 +0000590 if not (options.force or options.reset):
591 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700592 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000593 # Switch over to the new upstream
594 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000595 if mirror:
596 with open(os.path.join(
597 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
598 'w') as fh:
599 fh.write(os.path.join(url, 'objects'))
John Budorick882c91e2018-07-12 22:11:41 +0000600 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
601 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000602
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000603 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000604 else:
John Budorick882c91e2018-07-12 22:11:41 +0000605 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000606
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000607 if return_early:
608 return self._Capture(['rev-parse', '--verify', 'HEAD'])
609
msb@chromium.org5bde4852009-12-14 16:47:12 +0000610 cur_branch = self._GetCurrentBranch()
611
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000612 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000613 # 0) HEAD is detached. Probably from our initial clone.
614 # - make sure HEAD is contained by a named ref, then update.
615 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700616 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000617 # - try to rebase onto the new hash or branch
618 # 2) current branch is tracking a remote branch with local committed
619 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000620 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000621 # 3) current branch is tracking a remote branch w/or w/out changes, and
622 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000623 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000624 # 4) current branch is tracking a remote branch, but DEPS switches to a
625 # different remote branch, and
626 # a) current branch has no local changes, and --force:
627 # - checkout new branch
628 # b) current branch has local changes, and --force and --reset:
629 # - checkout new branch
630 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000631
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000632 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
633 # a tracking branch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000634 # or 'master' if not a tracking branch (it's based on a specific rev/hash)
635 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000636 if cur_branch is None:
637 upstream_branch = None
638 current_type = "detached"
639 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000640 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000641 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
642 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
643 current_type = "hash"
644 logging.debug("Current branch is not tracking an upstream (remote)"
645 " branch.")
646 elif upstream_branch.startswith('refs/remotes'):
647 current_type = "branch"
648 else:
649 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000650
Edward Lemur579c9862018-07-13 23:17:51 +0000651 self._SetFetchConfig(options)
Edward Lemur579c9862018-07-13 23:17:51 +0000652
Michael Spang73fac912019-03-08 18:44:19 +0000653 # Fetch upstream if we don't already have |revision|.
John Budorick882c91e2018-07-12 22:11:41 +0000654 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
Michael Spang73fac912019-03-08 18:44:19 +0000655 self._Fetch(options, prune=options.force)
656
657 if not scm.GIT.IsValidRevision(self.checkout_path, revision,
658 sha_only=True):
659 # Update the remotes first so we have all the refs.
660 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
661 cwd=self.checkout_path)
662 if verbose:
663 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000664
John Budorick882c91e2018-07-12 22:11:41 +0000665 revision = self._AutoFetchRef(options, revision)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200666
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000667 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000668 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000669 target = 'HEAD'
670 if options.upstream and upstream_branch:
671 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800672 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000673
msb@chromium.org786fb682010-06-02 15:16:23 +0000674 if current_type == 'detached':
675 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800676 # We just did a Scrub, this is as clean as it's going to get. In
677 # particular if HEAD is a commit that contains two versions of the same
678 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
679 # to actually "Clean" the checkout; that commit is uncheckoutable on this
680 # system. The best we can do is carry forward to the checkout step.
681 if not (options.force or options.reset):
John Budorick882c91e2018-07-12 22:11:41 +0000682 self._CheckClean(revision)
683 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000684 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000685 self.Print('Up-to-date; skipping checkout.')
686 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000687 # 'git checkout' may need to overwrite existing untracked files. Allow
688 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000689 self._Checkout(
690 options,
John Budorick882c91e2018-07-12 22:11:41 +0000691 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000692 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000693 quiet=True,
694 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000695 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000696 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000697 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000698 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700699 # Can't find a merge-base since we don't know our upstream. That makes
700 # this command VERY likely to produce a rebase failure. For now we
701 # assume origin is our upstream since that's what the old behavior was.
John Budorick882c91e2018-07-12 22:11:41 +0000702 upstream_branch = self.remote
703 if options.revision or deps_revision:
704 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700705 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700706 printed_path=printed_path, merge=options.merge)
707 printed_path = True
John Budorick882c91e2018-07-12 22:11:41 +0000708 elif rev_type == 'hash':
709 # case 2
710 self._AttemptRebase(upstream_branch, file_list, options,
711 newbase=revision, printed_path=printed_path,
712 merge=options.merge)
713 printed_path = True
714 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000715 # case 4
John Budorick882c91e2018-07-12 22:11:41 +0000716 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000717 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000718 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000719 switch_error = ("Could not switch upstream branch from %s to %s\n"
John Budorick882c91e2018-07-12 22:11:41 +0000720 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000721 "Please use --force or merge or rebase manually:\n" +
John Budorick882c91e2018-07-12 22:11:41 +0000722 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
723 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000724 force_switch = False
725 if options.force:
726 try:
John Budorick882c91e2018-07-12 22:11:41 +0000727 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000728 # case 4a
729 force_switch = True
730 except gclient_utils.Error as e:
731 if options.reset:
732 # case 4b
733 force_switch = True
734 else:
735 switch_error = '%s\n%s' % (e.message, switch_error)
736 if force_switch:
737 self.Print("Switching upstream branch from %s to %s" %
John Budorick882c91e2018-07-12 22:11:41 +0000738 (upstream_branch, new_base))
739 switch_branch = 'gclient_' + remote_ref[1]
740 self._Capture(['branch', '-f', switch_branch, new_base])
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000741 self._Checkout(options, switch_branch, force=True, quiet=True)
742 else:
743 # case 4c
744 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000745 else:
746 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800747 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000748 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000749 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000750 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000751 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000752 if options.merge:
753 merge_args.append('--ff')
754 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000755 merge_args.append('--ff-only')
756 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000757 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000758 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700759 rebase_files = []
Edward Lemur979fa782019-08-13 22:44:05 +0000760 if re.match(b'fatal: Not possible to fast-forward, aborting.',
761 e.stderr):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000762 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000763 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700764 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000765 printed_path = True
766 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000767 if not options.auto_rebase:
768 try:
769 action = self._AskForData(
770 'Cannot %s, attempt to rebase? '
771 '(y)es / (q)uit / (s)kip : ' %
772 ('merge' if options.merge else 'fast-forward merge'),
773 options)
774 except ValueError:
775 raise gclient_utils.Error('Invalid Character')
776 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700777 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000778 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000779 printed_path = True
780 break
781 elif re.match(r'quit|q', action, re.I):
782 raise gclient_utils.Error("Can't fast-forward, please merge or "
783 "rebase manually.\n"
784 "cd %s && git " % self.checkout_path
785 + "rebase %s" % upstream_branch)
786 elif re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000787 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000788 return
789 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000790 self.Print('Input not recognized')
Edward Lemur979fa782019-08-13 22:44:05 +0000791 elif re.match(b"error: Your local changes to '.*' would be "
792 b"overwritten by merge. Aborting.\nPlease, commit your "
793 b"changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000794 e.stderr):
795 if not printed_path:
John Budorick882c91e2018-07-12 22:11:41 +0000796 self.Print('_____ %s at %s' % (self.relpath, revision),
agable83faed02016-10-24 14:37:10 -0700797 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000798 printed_path = True
Edward Lemur979fa782019-08-13 22:44:05 +0000799 raise gclient_utils.Error(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000800 else:
801 # Some other problem happened with the merge
802 logging.error("Error during fast-forward merge in %s!" % self.relpath)
Edward Lemur979fa782019-08-13 22:44:05 +0000803 self.Print(e.stderr.decode('utf-8'))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000804 raise
805 else:
806 # Fast-forward merge was successful
807 if not re.match('Already up-to-date.', merge_output) or verbose:
808 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700809 self.Print('_____ %s at %s' % (self.relpath, revision),
810 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000811 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000812 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000813 if not verbose:
814 # Make the output a little prettier. It's nice to have some
815 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000816 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000817
agablec3937b92016-10-25 10:13:03 -0700818 if file_list is not None:
819 file_list.extend(
820 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000821
822 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000823 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700824 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000825 '\nConflict while rebasing this branch.\n'
826 'Fix the conflict and run gclient again.\n'
827 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700828 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000829
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000830 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000831 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
832 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000833
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000834 # If --reset and --delete_unversioned_trees are specified, remove any
835 # untracked directories.
836 if options.reset and options.delete_unversioned_trees:
837 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
838 # merge-base by default), so doesn't include untracked files. So we use
839 # 'git ls-files --directory --others --exclude-standard' here directly.
840 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800841 ['-c', 'core.quotePath=false', 'ls-files',
842 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000843 self.checkout_path)
844 for path in (p for p in paths.splitlines() if p.endswith('/')):
845 full_path = os.path.join(self.checkout_path, path)
846 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000847 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000848 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000849
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000850 return self._Capture(['rev-parse', '--verify', 'HEAD'])
851
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000852 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000853 """Reverts local modifications.
854
855 All reverted files will be appended to file_list.
856 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000857 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000858 # revert won't work if the directory doesn't exist. It needs to
859 # checkout instead.
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000860 self.Print('_____ %s is missing, syncing instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000861 # Don't reuse the args.
862 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000863
864 default_rev = "refs/heads/master"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000865 if options.upstream:
866 if self._GetCurrentBranch():
867 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
868 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000869 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000870 if not deps_revision:
871 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000872 if deps_revision.startswith('refs/heads/'):
873 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700874 try:
875 deps_revision = self.GetUsableRev(deps_revision, options)
876 except NoUsableRevError as e:
877 # If the DEPS entry's url and hash changed, try to update the origin.
878 # See also http://crbug.com/520067.
John Budorickd94f8ea2020-03-27 15:55:24 +0000879 logging.warning(
880 "Couldn't find usable revision, will retrying to update instead: %s",
smutae7ea312016-07-18 11:59:41 -0700881 e.message)
882 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000883
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000884 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800885 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000886
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800887 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000888 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000889
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000890 if file_list is not None:
891 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
892
893 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000894 """Returns revision"""
895 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000896
msb@chromium.orge28e4982009-09-25 20:51:45 +0000897 def runhooks(self, options, args, file_list):
898 self.status(options, args, file_list)
899
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000900 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000901 """Display status information."""
902 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000903 self.Print('________ couldn\'t run status in %s:\n'
904 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000905 else:
Anthony Politobb457342019-11-15 22:26:01 +0000906 merge_base = []
907 if self.url:
908 _, base_rev = gclient_utils.SplitUrlRevision(self.url)
909 if base_rev:
910 merge_base = [base_rev]
Aaron Gablef4068aa2017-12-12 15:14:09 -0800911 self._Run(
912 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
Edward Lemur24146be2019-08-01 21:44:52 +0000913 options, always_show_header=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000914 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800915 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000916 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000917
smutae7ea312016-07-18 11:59:41 -0700918 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700919 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700920 sha1 = None
921 if not os.path.isdir(self.checkout_path):
922 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800923 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700924
925 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
926 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700927 else:
agable41e3a6c2016-10-20 11:36:56 -0700928 # May exist in origin, but we don't have it yet, so fetch and look
929 # again.
930 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -0700931 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
932 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700933
934 if not sha1:
935 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800936 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -0700937
938 return sha1
939
primiano@chromium.org1c127382015-02-17 11:15:40 +0000940 def GetGitBackupDirPath(self):
941 """Returns the path where the .git folder for the current project can be
942 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
943 return os.path.join(self._root_dir,
944 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
945
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000946 def _GetMirror(self, url, options, revision_ref=None):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000947 """Get a git_cache.Mirror object for the argument url."""
Robert Iannuccia19649b2018-06-29 16:31:45 +0000948 if not self.cache_dir:
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000949 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000950 mirror_kwargs = {
951 'print_func': self.filter,
John Budorick882c91e2018-07-12 22:11:41 +0000952 'refs': []
hinoka@google.comb1b54572014-04-16 22:29:23 +0000953 }
hinoka@google.comb1b54572014-04-16 22:29:23 +0000954 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
955 mirror_kwargs['refs'].append('refs/branch-heads/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000956 elif revision_ref and revision_ref.startswith('refs/branch-heads/'):
957 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.org8d3348f2014-08-19 22:49:16 +0000958 if hasattr(options, 'with_tags') and options.with_tags:
959 mirror_kwargs['refs'].append('refs/tags/*')
Edward Lemurbdbe07f2019-03-28 22:14:45 +0000960 elif revision_ref and revision_ref.startswith('refs/tags/'):
961 mirror_kwargs['refs'].append(revision_ref)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000962 return git_cache.Mirror(url, **mirror_kwargs)
963
John Budorick882c91e2018-07-12 22:11:41 +0000964 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800965 """Update a git mirror by fetching the latest commits from the remote,
966 unless mirror already contains revision whose type is sha1 hash.
967 """
John Budorick882c91e2018-07-12 22:11:41 +0000968 if rev_type == 'hash' and mirror.contains_revision(revision):
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800969 if options.verbose:
970 self.Print('skipping mirror update, it has rev=%s already' % revision,
971 timestamp=False)
972 return
973
szager@chromium.org3ec84f62014-08-22 21:00:22 +0000974 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000975 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000976 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000977 depth = 10
978 else:
979 depth = 10000
980 else:
981 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +0000982 mirror.populate(verbose=options.verbose,
983 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +0000984 depth=depth,
985 ignore_lock=getattr(options, 'ignore_locks', False),
986 lock_timeout=getattr(options, 'lock_timeout', 0))
987 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000988
John Budorick882c91e2018-07-12 22:11:41 +0000989 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000990 """Clone a git repository from the given URL.
991
msb@chromium.org786fb682010-06-02 15:16:23 +0000992 Once we've cloned the repo, we checkout a working branch if the specified
993 revision is a branch head. If it is a tag or a specific commit, then we
994 leave HEAD detached as it makes future updates simpler -- in this case the
995 user should first create a new branch or switch to an existing branch before
996 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000997 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000998 # git clone doesn't seem to insert a newline properly before printing
999 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001000 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +00001001 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +00001002 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +00001003 if self.cache_dir:
1004 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001005 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001006 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001007 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +00001008 # If the parent directory does not exist, Git clone on Windows will not
1009 # create it, so we need to do it manually.
1010 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001011 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001012
1013 template_dir = None
1014 if hasattr(options, 'no_history') and options.no_history:
John Budorick882c91e2018-07-12 22:11:41 +00001015 if gclient_utils.IsGitSha(revision):
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001016 # In the case of a subproject, the pinned sha is not necessarily the
1017 # head of the remote branch (so we can't just use --depth=N). Instead,
1018 # we tell git to fetch all the remote objects from SHA..HEAD by means of
1019 # a template git dir which has a 'shallow' file pointing to the sha.
1020 template_dir = tempfile.mkdtemp(
1021 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
1022 dir=parent_dir)
1023 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
1024 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
1025 template_file.write(revision)
1026 clone_cmd.append('--template=' + template_dir)
1027 else:
1028 # Otherwise, we're just interested in the HEAD. Just use --depth.
1029 clone_cmd.append('--depth=1')
1030
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001031 tmp_dir = tempfile.mkdtemp(
1032 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
1033 dir=parent_dir)
1034 try:
1035 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +01001036 if self.print_outbuf:
1037 print_stdout = True
Edward Lemur24146be2019-08-01 21:44:52 +00001038 filter_fn = None
Edward Lemur231f5ea2018-01-31 19:02:36 +01001039 else:
1040 print_stdout = False
Edward Lemur24146be2019-08-01 21:44:52 +00001041 filter_fn = self.filter
Edward Lemur231f5ea2018-01-31 19:02:36 +01001042 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
Edward Lemur24146be2019-08-01 21:44:52 +00001043 print_stdout=print_stdout, filter_fn=filter_fn)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001044 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +00001045 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
1046 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001047 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001048 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +00001049 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001050 finally:
1051 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001052 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +00001053 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +00001054 if template_dir:
1055 gclient_utils.rmtree(template_dir)
Edward Lemur579c9862018-07-13 23:17:51 +00001056 self._SetFetchConfig(options)
1057 self._Fetch(options, prune=options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001058 revision = self._AutoFetchRef(options, revision)
1059 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
1060 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +00001061 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +00001062 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001063 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +00001064 ('Checked out %s to a detached HEAD. Before making any commits\n'
1065 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
1066 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
John Budorick882c91e2018-07-12 22:11:41 +00001067 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001068
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001069 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001070 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +00001071 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001072 raise gclient_utils.Error("Background task requires input. Rerun "
1073 "gclient with --jobs=1 so that\n"
1074 "interaction is possible.")
Edward Lesmesae3586b2020-03-23 21:21:14 +00001075 return gclient_utils.AskForData(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001076
1077
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001078 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001079 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001080 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +00001081 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -08001082 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001083 revision = upstream
1084 if newbase:
1085 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001086 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001087 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001088 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001089 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001090 printed_path = True
1091 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001092 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001093
1094 if merge:
1095 merge_output = self._Capture(['merge', revision])
1096 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001097 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001098 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001099
1100 # Build the rebase command here using the args
1101 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
1102 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001103 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001104 rebase_cmd.append('--verbose')
1105 if newbase:
1106 rebase_cmd.extend(['--onto', newbase])
1107 rebase_cmd.append(upstream)
1108 if branch:
1109 rebase_cmd.append(branch)
1110
1111 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001112 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
Raul Tambreb946b232019-03-26 14:48:46 +00001113 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001114 if (re.match(br'cannot rebase: you have unstaged changes', e.stderr) or
1115 re.match(br'cannot rebase: your index contains uncommitted changes',
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001116 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001117 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001118 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001119 'Cannot rebase because of unstaged changes.\n'
1120 '\'git reset --hard HEAD\' ?\n'
1121 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001122 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001123 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001124 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001125 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001126 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001127 break
1128 elif re.match(r'quit|q', rebase_action, re.I):
1129 raise gclient_utils.Error("Please merge or rebase manually\n"
1130 "cd %s && git " % self.checkout_path
1131 + "%s" % ' '.join(rebase_cmd))
1132 elif re.match(r'show|s', rebase_action, re.I):
Edward Lemur979fa782019-08-13 22:44:05 +00001133 self.Print('%s' % e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001134 continue
1135 else:
1136 gclient_utils.Error("Input not recognized")
1137 continue
Edward Lemur979fa782019-08-13 22:44:05 +00001138 elif re.search(br'^CONFLICT', e.stdout, re.M):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001139 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1140 "Fix the conflict and run gclient again.\n"
1141 "See 'man git-rebase' for details.\n")
1142 else:
Edward Lemur979fa782019-08-13 22:44:05 +00001143 self.Print(e.stdout.decode('utf-8').strip())
1144 self.Print('Rebase produced error output:\n%s' %
1145 e.stderr.decode('utf-8').strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001146 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1147 "manually.\ncd %s && git " %
1148 self.checkout_path
1149 + "%s" % ' '.join(rebase_cmd))
1150
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001151 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001152 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001153 # Make the output a little prettier. It's nice to have some
1154 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001155 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001156
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001157 @staticmethod
1158 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001159 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1160 if not ok:
1161 raise gclient_utils.Error('git version %s < minimum required %s' %
1162 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001163
John Budorick882c91e2018-07-12 22:11:41 +00001164 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001165 # Special case handling if all 3 conditions are met:
1166 # * the mirros have recently changed, but deps destination remains same,
1167 # * the git histories of mirrors are conflicting.
1168 # * git cache is used
1169 # This manifests itself in current checkout having invalid HEAD commit on
1170 # most git operations. Since git cache is used, just deleted the .git
1171 # folder, and re-create it by cloning.
1172 try:
1173 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1174 except subprocess2.CalledProcessError as e:
Edward Lemur979fa782019-08-13 22:44:05 +00001175 if (b'fatal: bad object HEAD' in e.stderr
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001176 and self.cache_dir and self.cache_dir in url):
1177 self.Print((
1178 'Likely due to DEPS change with git cache_dir, '
1179 'the current commit points to no longer existing object.\n'
1180 '%s' % e)
1181 )
1182 self._DeleteOrMove(options.force)
John Budorick882c91e2018-07-12 22:11:41 +00001183 self._Clone(revision, url, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001184 else:
1185 raise
1186
msb@chromium.org786fb682010-06-02 15:16:23 +00001187 def _IsRebasing(self):
1188 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1189 # have a plumbing command to determine whether a rebase is in progress, so
1190 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1191 g = os.path.join(self.checkout_path, '.git')
1192 return (
1193 os.path.isdir(os.path.join(g, "rebase-merge")) or
1194 os.path.isdir(os.path.join(g, "rebase-apply")))
1195
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001196 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001197 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1198 if os.path.exists(lockfile):
1199 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001200 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001201 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1202 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001203 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001204
msb@chromium.org786fb682010-06-02 15:16:23 +00001205 # Make sure the tree is clean; see git-rebase.sh for reference
1206 try:
1207 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001208 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001209 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001210 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001211 '\tYou have unstaged changes.\n'
1212 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001213 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001214 try:
1215 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001216 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001217 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001218 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001219 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001220 '\tYour index contains uncommitted changes\n'
1221 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001222 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001223
agable83faed02016-10-24 14:37:10 -07001224 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001225 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1226 # reference by a commit). If not, error out -- most likely a rebase is
1227 # in progress, try to detect so we can give a better error.
1228 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001229 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1230 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001231 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001232 # Commit is not contained by any rev. See if the user is rebasing:
1233 if self._IsRebasing():
1234 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001235 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001236 '\tAlready in a conflict, i.e. (no branch).\n'
1237 '\tFix the conflict and run gclient again.\n'
1238 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1239 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001240 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001241 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001242 name = ('saved-by-gclient-' +
1243 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001244 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001245 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001246 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001247
msb@chromium.org5bde4852009-12-14 16:47:12 +00001248 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001249 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001250 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001251 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001252 return None
1253 return branch
1254
borenet@google.comc3e09d22014-04-10 13:58:18 +00001255 def _Capture(self, args, **kwargs):
Mike Frysinger286fb162019-09-30 03:14:10 +00001256 set_git_dir = 'cwd' not in kwargs
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001257 kwargs.setdefault('cwd', self.checkout_path)
1258 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001259 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001260 env = scm.GIT.ApplyEnvVars(kwargs)
Mike Frysinger286fb162019-09-30 03:14:10 +00001261 # If an explicit cwd isn't set, then default to the .git/ subdir so we get
1262 # stricter behavior. This can be useful in cases of slight corruption --
1263 # we don't accidentally go corrupting parent git checks too. See
1264 # https://crbug.com/1000825 for an example.
1265 if set_git_dir:
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001266 git_dir = os.path.abspath(os.path.join(self.checkout_path, '.git'))
Dirk Prankedb1e79c2019-10-23 01:46:32 +00001267 # Depending on how the .gclient file was defined, self.checkout_path
1268 # might be set to a unicode string, not a regular string; on Windows
1269 # Python2, we can't set env vars to be unicode strings, so we
1270 # forcibly cast the value to a string before setting it.
1271 env.setdefault('GIT_DIR', str(git_dir))
Raul Tambrecd862e32019-05-10 21:19:00 +00001272 ret = subprocess2.check_output(
1273 ['git'] + args, env=env, **kwargs).decode('utf-8')
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001274 if strip:
1275 ret = ret.strip()
Erik Chene16ffff2019-10-14 20:35:53 +00001276 self.Print('Finished running: %s %s' % ('git', ' '.join(args)))
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001277 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001278
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001279 def _Checkout(self, options, ref, force=False, quiet=None):
1280 """Performs a 'git-checkout' operation.
1281
1282 Args:
1283 options: The configured option set
1284 ref: (str) The branch/commit to checkout
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001285 quiet: (bool/None) Whether or not the checkout should pass '--quiet'; if
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001286 'None', the behavior is inferred from 'options.verbose'.
1287 Returns: (str) The output of the checkout operation
1288 """
1289 if quiet is None:
1290 quiet = (not options.verbose)
1291 checkout_args = ['checkout']
1292 if force:
1293 checkout_args.append('--force')
1294 if quiet:
1295 checkout_args.append('--quiet')
1296 checkout_args.append(ref)
1297 return self._Capture(checkout_args)
1298
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001299 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1300 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001301 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
Edward Lemur9a5e3bd2019-04-02 23:37:45 +00001302 # When updating, the ref is modified to be a remote ref .
1303 # (e.g. refs/heads/NAME becomes refs/remotes/REMOTE/NAME).
1304 # Try to reverse that mapping.
1305 original_ref = scm.GIT.RemoteRefToRef(refspec, self.remote)
1306 if original_ref:
1307 refspec = original_ref + ':' + refspec
1308 # When a mirror is configured, it only fetches
1309 # refs/{heads,branch-heads,tags}/*.
1310 # If asked to fetch other refs, we must fetch those directly from the
1311 # repository, and not from the mirror.
1312 if not original_ref.startswith(
1313 ('refs/heads/', 'refs/branch-heads/', 'refs/tags/')):
1314 remote, _ = gclient_utils.SplitUrlRevision(self.url)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001315 fetch_cmd = cfg + [
1316 'fetch',
1317 remote or self.remote,
1318 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001319 if refspec:
1320 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001321
1322 if prune:
1323 fetch_cmd.append('--prune')
1324 if options.verbose:
1325 fetch_cmd.append('--verbose')
danakjd5c0b562019-11-08 17:27:47 +00001326 if not hasattr(options, 'with_tags') or not options.with_tags:
1327 fetch_cmd.append('--no-tags')
dnj@chromium.org680f2172014-06-25 00:39:32 +00001328 elif quiet:
1329 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001330 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001331
1332 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1333 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1334
Edward Lemur579c9862018-07-13 23:17:51 +00001335 def _SetFetchConfig(self, options):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001336 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1337 if requested."""
Edward Lemur2f38df62018-07-14 02:13:21 +00001338 if options.force or options.reset:
Edward Lemur579c9862018-07-13 23:17:51 +00001339 try:
1340 self._Run(['config', '--unset-all', 'remote.%s.fetch' % self.remote],
1341 options)
1342 self._Run(['config', 'remote.%s.fetch' % self.remote,
1343 '+refs/heads/*:refs/remotes/%s/*' % self.remote], options)
1344 except subprocess2.CalledProcessError as e:
1345 # If exit code was 5, it means we attempted to unset a config that
1346 # didn't exist. Ignore it.
1347 if e.returncode != 5:
1348 raise
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001349 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001350 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001351 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1352 '^\\+refs/branch-heads/\\*:.*$']
1353 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001354 if hasattr(options, 'with_tags') and options.with_tags:
1355 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1356 '+refs/tags/*:refs/tags/*',
1357 '^\\+refs/tags/\\*:.*$']
1358 self._Run(config_cmd, options)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001359
John Budorick882c91e2018-07-12 22:11:41 +00001360 def _AutoFetchRef(self, options, revision):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001361 """Attempts to fetch |revision| if not available in local repo.
1362
1363 Returns possibly updated revision."""
Edward Lemure0ba7b82020-03-11 20:31:32 +00001364 if not scm.GIT.IsValidRevision(self.checkout_path, revision):
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001365 self._Fetch(options, refspec=revision)
1366 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1367 return revision
1368
Edward Lemur24146be2019-08-01 21:44:52 +00001369 def _Run(self, args, options, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001370 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001371 kwargs.setdefault('cwd', self.checkout_path)
Edward Lemur24146be2019-08-01 21:44:52 +00001372 kwargs.setdefault('filter_fn', self.filter)
1373 kwargs.setdefault('show_header', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001374 env = scm.GIT.ApplyEnvVars(kwargs)
agable@chromium.org772efaf2014-04-01 02:35:44 +00001375 cmd = ['git'] + args
Edward Lemur24146be2019-08-01 21:44:52 +00001376 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001377
1378
1379class CipdPackage(object):
1380 """A representation of a single CIPD package."""
1381
John Budorickd3ba72b2018-03-20 12:27:42 -07001382 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001383 self._authority_for_subdir = authority_for_subdir
1384 self._name = name
1385 self._version = version
1386
1387 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001388 def authority_for_subdir(self):
1389 """Whether this package has authority to act on behalf of its subdir.
1390
1391 Some operations should only be performed once per subdirectory. A package
1392 that has authority for its subdirectory is the only package that should
1393 perform such operations.
1394
1395 Returns:
1396 bool; whether this package has subdir authority.
1397 """
1398 return self._authority_for_subdir
1399
1400 @property
1401 def name(self):
1402 return self._name
1403
1404 @property
1405 def version(self):
1406 return self._version
1407
1408
1409class CipdRoot(object):
1410 """A representation of a single CIPD root."""
1411 def __init__(self, root_dir, service_url):
1412 self._all_packages = set()
1413 self._mutator_lock = threading.Lock()
1414 self._packages_by_subdir = collections.defaultdict(list)
1415 self._root_dir = root_dir
1416 self._service_url = service_url
1417
1418 def add_package(self, subdir, package, version):
1419 """Adds a package to this CIPD root.
1420
1421 As far as clients are concerned, this grants both root and subdir authority
1422 to packages arbitrarily. (The implementation grants root authority to the
1423 first package added and subdir authority to the first package added for that
1424 subdir, but clients should not depend on or expect that behavior.)
1425
1426 Args:
1427 subdir: str; relative path to where the package should be installed from
1428 the cipd root directory.
1429 package: str; the cipd package name.
1430 version: str; the cipd package version.
1431 Returns:
1432 CipdPackage; the package that was created and added to this root.
1433 """
1434 with self._mutator_lock:
1435 cipd_package = CipdPackage(
1436 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001437 not self._packages_by_subdir[subdir])
1438 self._all_packages.add(cipd_package)
1439 self._packages_by_subdir[subdir].append(cipd_package)
1440 return cipd_package
1441
1442 def packages(self, subdir):
1443 """Get the list of configured packages for the given subdir."""
1444 return list(self._packages_by_subdir[subdir])
1445
1446 def clobber(self):
1447 """Remove the .cipd directory.
1448
1449 This is useful for forcing ensure to redownload and reinitialize all
1450 packages.
1451 """
1452 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001453 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001454 try:
1455 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1456 except OSError:
1457 if os.path.exists(cipd_cache_dir):
1458 raise
1459
1460 @contextlib.contextmanager
1461 def _create_ensure_file(self):
1462 try:
Edward Lesmes05934952019-12-19 20:38:09 +00001463 contents = '$ParanoidMode CheckPresence\n\n'
1464 for subdir, packages in sorted(self._packages_by_subdir.items()):
1465 contents += '@Subdir %s\n' % subdir
1466 for package in sorted(packages, key=lambda p: p.name):
1467 contents += '%s %s\n' % (package.name, package.version)
1468 contents += '\n'
John Budorick0f7b2002018-01-19 15:46:17 -08001469 ensure_file = None
1470 with tempfile.NamedTemporaryFile(
Edward Lesmes05934952019-12-19 20:38:09 +00001471 suffix='.ensure', delete=False, mode='wb') as ensure_file:
1472 ensure_file.write(contents.encode('utf-8', 'replace'))
John Budorick0f7b2002018-01-19 15:46:17 -08001473 yield ensure_file.name
1474 finally:
1475 if ensure_file is not None and os.path.exists(ensure_file.name):
1476 os.remove(ensure_file.name)
1477
1478 def ensure(self):
1479 """Run `cipd ensure`."""
1480 with self._mutator_lock:
1481 with self._create_ensure_file() as ensure_file:
1482 cmd = [
1483 'cipd', 'ensure',
1484 '-log-level', 'error',
1485 '-root', self.root_dir,
1486 '-ensure-file', ensure_file,
1487 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001488 gclient_utils.CheckCallAndFilter(
1489 cmd, print_stdout=True, show_header=True)
John Budorick0f7b2002018-01-19 15:46:17 -08001490
John Budorickd3ba72b2018-03-20 12:27:42 -07001491 def run(self, command):
1492 if command == 'update':
1493 self.ensure()
1494 elif command == 'revert':
1495 self.clobber()
1496 self.ensure()
1497
John Budorick0f7b2002018-01-19 15:46:17 -08001498 def created_package(self, package):
1499 """Checks whether this root created the given package.
1500
1501 Args:
1502 package: CipdPackage; the package to check.
1503 Returns:
1504 bool; whether this root created the given package.
1505 """
1506 return package in self._all_packages
1507
1508 @property
1509 def root_dir(self):
1510 return self._root_dir
1511
1512 @property
1513 def service_url(self):
1514 return self._service_url
1515
1516
1517class CipdWrapper(SCMWrapper):
1518 """Wrapper for CIPD.
1519
1520 Currently only supports chrome-infra-packages.appspot.com.
1521 """
John Budorick3929e9e2018-02-04 18:18:07 -08001522 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001523
1524 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1525 out_cb=None, root=None, package=None):
1526 super(CipdWrapper, self).__init__(
1527 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1528 out_cb=out_cb)
1529 assert root.created_package(package)
1530 self._package = package
1531 self._root = root
1532
1533 #override
1534 def GetCacheMirror(self):
1535 return None
1536
1537 #override
1538 def GetActualRemoteURL(self, options):
1539 return self._root.service_url
1540
1541 #override
1542 def DoesRemoteURLMatch(self, options):
1543 del options
1544 return True
1545
1546 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001547 """Does nothing.
1548
1549 CIPD packages should be reverted at the root by running
1550 `CipdRoot.run('revert')`.
1551 """
1552 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001553
1554 def diff(self, options, args, file_list):
1555 """CIPD has no notion of diffing."""
1556 pass
1557
1558 def pack(self, options, args, file_list):
1559 """CIPD has no notion of diffing."""
1560 pass
1561
1562 def revinfo(self, options, args, file_list):
1563 """Grab the instance ID."""
1564 try:
1565 tmpdir = tempfile.mkdtemp()
1566 describe_json_path = os.path.join(tmpdir, 'describe.json')
1567 cmd = [
1568 'cipd', 'describe',
1569 self._package.name,
1570 '-log-level', 'error',
1571 '-version', self._package.version,
1572 '-json-output', describe_json_path
1573 ]
Edward Lemur24146be2019-08-01 21:44:52 +00001574 gclient_utils.CheckCallAndFilter(cmd)
John Budorick0f7b2002018-01-19 15:46:17 -08001575 with open(describe_json_path) as f:
1576 describe_json = json.load(f)
1577 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1578 finally:
1579 gclient_utils.rmtree(tmpdir)
1580
1581 def status(self, options, args, file_list):
1582 pass
1583
1584 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001585 """Does nothing.
1586
1587 CIPD packages should be updated at the root by running
1588 `CipdRoot.run('update')`.
1589 """
1590 pass