blob: 66263fce382b099aaf2832bcdc6a25c45e10eb10 [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
hinoka@google.com2f2ca142014-01-07 03:59:18 +000021import urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000022
hinoka@google.com2f2ca142014-01-07 03:59:18 +000023import download_from_google_storage
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000024import gclient_utils
szager@chromium.org848fd492014-04-09 19:06:44 +000025import git_cache
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000026import scm
borenet@google.comb2256212014-05-07 20:57:28 +000027import shutil
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000028import subprocess2
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000029
30
szager@chromium.org71cbb502013-04-19 23:30:15 +000031THIS_FILE_PATH = os.path.abspath(__file__)
32
hinoka@google.com2f2ca142014-01-07 03:59:18 +000033GSUTIL_DEFAULT_PATH = os.path.join(
hinoka@chromium.orgb091aa52014-12-20 01:47:31 +000034 os.path.dirname(os.path.abspath(__file__)), 'gsutil.py')
hinoka@google.com2f2ca142014-01-07 03:59:18 +000035
maruel@chromium.org79d62372015-06-01 18:50:55 +000036
smutae7ea312016-07-18 11:59:41 -070037class NoUsableRevError(gclient_utils.Error):
38 """Raised if requested revision isn't found in checkout."""
39
40
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000041class DiffFiltererWrapper(object):
42 """Simple base class which tracks which file is being diffed and
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000043 replaces instances of its file name in the original and
agable41e3a6c2016-10-20 11:36:56 -070044 working copy lines of the git diff output."""
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000045 index_string = None
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000046 original_prefix = "--- "
47 working_prefix = "+++ "
48
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000049 def __init__(self, relpath, print_func):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000050 # Note that we always use '/' as the path separator to be
agable41e3a6c2016-10-20 11:36:56 -070051 # consistent with cygwin-style output on Windows
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000052 self._relpath = relpath.replace("\\", "/")
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000053 self._current_file = None
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000054 self._print_func = print_func
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000055
maruel@chromium.org6e29d572010-06-04 17:32:20 +000056 def SetCurrentFile(self, current_file):
57 self._current_file = current_file
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000058
iannucci@chromium.org3830a672013-02-19 20:15:14 +000059 @property
60 def _replacement_file(self):
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000061 return posixpath.join(self._relpath, self._current_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000062
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000063 def _Replace(self, line):
64 return line.replace(self._current_file, self._replacement_file)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000065
66 def Filter(self, line):
67 if (line.startswith(self.index_string)):
68 self.SetCurrentFile(line[len(self.index_string):])
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000069 line = self._Replace(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000070 else:
71 if (line.startswith(self.original_prefix) or
72 line.startswith(self.working_prefix)):
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +000073 line = self._Replace(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000074 self._print_func(line)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000075
76
haitao.feng@intel.com306080c2012-05-04 13:11:29 +000077class GitDiffFilterer(DiffFiltererWrapper):
78 index_string = "diff --git "
79
80 def SetCurrentFile(self, current_file):
81 # Get filename by parsing "a/<filename> b/<filename>"
82 self._current_file = current_file[:(len(current_file)/2)][2:]
83
84 def _Replace(self, line):
85 return re.sub("[a|b]/" + self._current_file, self._replacement_file, line)
86
87
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000088# SCMWrapper base class
89
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000090class SCMWrapper(object):
91 """Add necessary glue between all the supported SCM.
92
msb@chromium.orgd6504212010-01-13 17:34:31 +000093 This is the abstraction layer to bind to different SCM.
94 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000095
96 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
Edward Lemur231f5ea2018-01-31 19:02:36 +010097 out_cb=None, print_outbuf=False):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000098 self.url = url
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +000099 self._root_dir = root_dir
100 if self._root_dir:
101 self._root_dir = self._root_dir.replace('/', os.sep)
102 self.relpath = relpath
103 if self.relpath:
104 self.relpath = self.relpath.replace('/', os.sep)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000105 if self.relpath and self._root_dir:
106 self.checkout_path = os.path.join(self._root_dir, self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000107 if out_fh is None:
108 out_fh = sys.stdout
109 self.out_fh = out_fh
110 self.out_cb = out_cb
Edward Lemur231f5ea2018-01-31 19:02:36 +0100111 self.print_outbuf = print_outbuf
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000112
113 def Print(self, *args, **kwargs):
114 kwargs.setdefault('file', self.out_fh)
115 if kwargs.pop('timestamp', True):
116 self.out_fh.write('[%s] ' % gclient_utils.Elapsed())
117 print(*args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000118
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000119 def RunCommand(self, command, options, args, file_list=None):
agabledebf6c82016-12-21 12:50:12 -0800120 commands = ['update', 'updatesingle', 'revert',
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000121 'revinfo', 'status', 'diff', 'pack', 'runhooks']
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000122
123 if not command in commands:
124 raise gclient_utils.Error('Unknown command %s' % command)
125
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000126 if not command in dir(self):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000127 raise gclient_utils.Error('Command %s not implemented in %s wrapper' % (
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000128 command, self.__class__.__name__))
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000129
130 return getattr(self, command)(options, args, file_list)
131
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000132 @staticmethod
133 def _get_first_remote_url(checkout_path):
134 log = scm.GIT.Capture(
135 ['config', '--local', '--get-regexp', r'remote.*.url'],
136 cwd=checkout_path)
137 # Get the second token of the first line of the log.
138 return log.splitlines()[0].split(' ', 1)[1]
139
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000140 def GetCacheMirror(self):
141 if (getattr(self, 'cache_dir', None)):
142 url, _ = gclient_utils.SplitUrlRevision(self.url)
143 return git_cache.Mirror(url)
144 return None
145
smut@google.comd33eab32014-07-07 19:35:18 +0000146 def GetActualRemoteURL(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000147 """Attempt to determine the remote URL for this SCMWrapper."""
smut@google.comd33eab32014-07-07 19:35:18 +0000148 # Git
borenet@google.combda475e2014-03-24 19:04:45 +0000149 if os.path.exists(os.path.join(self.checkout_path, '.git')):
hinoka@chromium.orgfa2b9b42014-08-22 18:08:53 +0000150 actual_remote_url = self._get_first_remote_url(self.checkout_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000151
levarum@chromium.org27a6f9a2016-05-28 00:21:49 +0000152 mirror = self.GetCacheMirror()
153 # If the cache is used, obtain the actual remote URL from there.
154 if (mirror and mirror.exists() and
155 mirror.mirror_path.replace('\\', '/') ==
156 actual_remote_url.replace('\\', '/')):
157 actual_remote_url = self._get_first_remote_url(mirror.mirror_path)
borenet@google.com4e9be262014-04-08 19:40:30 +0000158 return actual_remote_url
borenet@google.com88d10082014-03-21 17:24:48 +0000159 return None
160
borenet@google.com4e9be262014-04-08 19:40:30 +0000161 def DoesRemoteURLMatch(self, options):
borenet@google.com88d10082014-03-21 17:24:48 +0000162 """Determine whether the remote URL of this checkout is the expected URL."""
163 if not os.path.exists(self.checkout_path):
164 # A checkout which doesn't exist can't be broken.
165 return True
166
smut@google.comd33eab32014-07-07 19:35:18 +0000167 actual_remote_url = self.GetActualRemoteURL(options)
borenet@google.com88d10082014-03-21 17:24:48 +0000168 if actual_remote_url:
borenet@google.com8156c9f2014-04-01 16:41:36 +0000169 return (gclient_utils.SplitUrlRevision(actual_remote_url)[0].rstrip('/')
170 == gclient_utils.SplitUrlRevision(self.url)[0].rstrip('/'))
borenet@google.com88d10082014-03-21 17:24:48 +0000171 else:
172 # This may occur if the self.checkout_path exists but does not contain a
agable41e3a6c2016-10-20 11:36:56 -0700173 # valid git checkout.
borenet@google.com88d10082014-03-21 17:24:48 +0000174 return False
175
borenet@google.comb09097a2014-04-09 19:09:08 +0000176 def _DeleteOrMove(self, force):
177 """Delete the checkout directory or move it out of the way.
178
179 Args:
180 force: bool; if True, delete the directory. Otherwise, just move it.
181 """
borenet@google.comb2256212014-05-07 20:57:28 +0000182 if force and os.environ.get('CHROME_HEADLESS') == '1':
183 self.Print('_____ Conflicting directory found in %s. Removing.'
184 % self.checkout_path)
185 gclient_utils.AddWarning('Conflicting directory %s deleted.'
186 % self.checkout_path)
187 gclient_utils.rmtree(self.checkout_path)
188 else:
189 bad_scm_dir = os.path.join(self._root_dir, '_bad_scm',
190 os.path.dirname(self.relpath))
191
192 try:
193 os.makedirs(bad_scm_dir)
194 except OSError as e:
195 if e.errno != errno.EEXIST:
196 raise
197
198 dest_path = tempfile.mkdtemp(
199 prefix=os.path.basename(self.relpath),
200 dir=bad_scm_dir)
201 self.Print('_____ Conflicting directory found in %s. Moving to %s.'
202 % (self.checkout_path, dest_path))
203 gclient_utils.AddWarning('Conflicting directory %s moved to %s.'
204 % (self.checkout_path, dest_path))
205 shutil.move(self.checkout_path, dest_path)
borenet@google.comb09097a2014-04-09 19:09:08 +0000206
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000207
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000208class GitWrapper(SCMWrapper):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000209 """Wrapper for Git"""
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000210 name = 'git'
maruel@chromium.org1a60dca2013-11-26 14:06:26 +0000211 remote = 'origin'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000212
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000213 cache_dir = None
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000214
John Budorick0f7b2002018-01-19 15:46:17 -0800215 def __init__(self, url=None, *args, **kwargs):
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000216 """Removes 'git+' fake prefix from git URL."""
217 if url.startswith('git+http://') or url.startswith('git+https://'):
218 url = url[4:]
John Budorick0f7b2002018-01-19 15:46:17 -0800219 SCMWrapper.__init__(self, url, *args, **kwargs)
szager@chromium.org848fd492014-04-09 19:06:44 +0000220 filter_kwargs = { 'time_throttle': 1, 'out_fh': self.out_fh }
221 if self.out_cb:
222 filter_kwargs['predicate'] = self.out_cb
223 self.filter = gclient_utils.GitFilter(**filter_kwargs)
igorgatis@gmail.com4e075672011-11-21 16:35:08 +0000224
mukai@chromium.org9e3e82c2012-04-18 12:55:43 +0000225 @staticmethod
226 def BinaryExists():
227 """Returns true if the command exists."""
228 try:
229 # We assume git is newer than 1.7. See: crbug.com/114483
230 result, version = scm.GIT.AssertVersion('1.7')
231 if not result:
232 raise gclient_utils.Error('Git version is older than 1.7: %s' % version)
233 return result
234 except OSError:
235 return False
236
xusydoc@chromium.org885a9602013-05-31 09:54:40 +0000237 def GetCheckoutRoot(self):
238 return scm.GIT.GetCheckoutRoot(self.checkout_path)
239
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000240 def GetRevisionDate(self, _revision):
floitsch@google.comeaab7842011-04-28 09:07:58 +0000241 """Returns the given revision's date in ISO-8601 format (which contains the
242 time zone)."""
243 # TODO(floitsch): get the time-stamp of the given revision and not just the
244 # time-stamp of the currently checked out revision.
245 return self._Capture(['log', '-n', '1', '--format=%ai'])
246
Aaron Gablef4068aa2017-12-12 15:14:09 -0800247 def _GetDiffFilenames(self, base):
248 """Returns the names of files modified since base."""
249 return self._Capture(
250 # Filter to remove base if it is None.
251 filter(bool, ['-c', 'core.quotePath=false', 'diff', '--name-only', base])
252 ).split()
253
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000254 def diff(self, options, _args, _file_list):
Aaron Gable1853f662018-02-12 15:45:56 -0800255 _, revision = gclient_utils.SplitUrlRevision(self.url)
256 if not revision:
257 revision = 'refs/remotes/%s/master' % self.remote
258 self._Run(['-c', 'core.quotePath=false', 'diff', revision], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000259
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000260 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000261 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000262 repository.
263
264 The patch file is generated from a diff of the merge base of HEAD and
265 its upstream branch.
266 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700267 try:
268 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
269 except subprocess2.CalledProcessError:
270 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000271 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700272 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000273 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000274 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000275
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800276 def _Scrub(self, target, options):
277 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000278 quiet = []
279 if not options.verbose:
280 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800281 self._Run(['reset', '--hard', target] + quiet, options)
282 if options.force and options.delete_unversioned_trees:
283 # where `target` is a commit that contains both upper and lower case
284 # versions of the same file on a case insensitive filesystem, we are
285 # actually in a broken state here. The index will have both 'a' and 'A',
286 # but only one of them will exist on the disk. To progress, we delete
287 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800288 output = self._Capture([
289 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800290 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800291 # --porcelain (v1) looks like:
292 # XY filename
293 try:
294 filename = line[3:]
295 self.Print('_____ Deleting residual after reset: %r.' % filename)
296 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800297 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800298 except OSError:
299 pass
300
301 def _FetchAndReset(self, revision, file_list, options):
302 """Equivalent to git fetch; git reset."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000303 self._UpdateBranchHeads(options, fetch=False)
304
dnj@chromium.org680f2172014-06-25 00:39:32 +0000305 self._Fetch(options, prune=True, quiet=options.verbose)
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800306 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000307 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800308 files = self._Capture(
309 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000310 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
311
szager@chromium.org8a139702014-06-20 15:55:01 +0000312 def _DisableHooks(self):
313 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
314 if not os.path.isdir(hook_dir):
315 return
316 for f in os.listdir(hook_dir):
317 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000318 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
319 if os.path.exists(disabled_hook_path):
320 os.remove(disabled_hook_path)
321 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000322
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000323 def _maybe_break_locks(self, options):
324 """This removes all .lock files from this repo's .git directory, if the
325 user passed the --break_repo_locks command line flag.
326
327 In particular, this will cleanup index.lock files, as well as ref lock
328 files.
329 """
330 if options.break_repo_locks:
331 git_dir = os.path.join(self.checkout_path, '.git')
332 for path, _, filenames in os.walk(git_dir):
333 for filename in filenames:
334 if filename.endswith('.lock'):
335 to_break = os.path.join(path, filename)
336 self.Print('breaking lock: %s' % (to_break,))
337 try:
338 os.remove(to_break)
339 except OSError as ex:
340 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
341 raise
342
Edward Lesmesc621b212018-03-21 20:26:56 -0400343 def apply_patch_ref(self, patch_repo, patch_ref, options, file_list):
344 base_rev = self._Capture(['rev-parse', 'HEAD'])
345 self.Print('===Applying patch ref===')
346 self.Print('Repo is %r @ %r, ref is %r, root is %r' % (
347 patch_repo, patch_ref, base_rev, self.checkout_path))
348 self._Capture(['reset', '--hard'])
349 self._Capture(['fetch', patch_repo, patch_ref])
Edward Lesmes8271d222018-03-30 21:27:17 -0400350 if file_list is not None:
351 file_list.extend(self._GetDiffFilenames('FETCH_HEAD'))
Edward Lesmesc621b212018-03-21 20:26:56 -0400352 self._Capture(['checkout', 'FETCH_HEAD'])
353
354 if options.rebase_patch_ref:
355 try:
356 # TODO(ehmaldonado): Look into cherry-picking to avoid an expensive
357 # checkout + rebase.
358 self._Capture(['rebase', base_rev])
359 except subprocess2.CalledProcessError as e:
Edward Lesmesc621b212018-03-21 20:26:56 -0400360 self.Print('Failed to apply %r @ %r to %r at %r' % (
361 patch_repo, patch_ref, base_rev, self.checkout_path))
362 self.Print('git returned non-zero exit status %s:\n%s' % (
363 e.returncode, e.stderr))
Edward Lesmes97ce0582018-04-26 17:58:32 -0400364 self._Capture(['rebase', '--abort'])
Edward Lesmesc621b212018-03-21 20:26:56 -0400365 raise
366 if options.reset_patch_ref:
367 self._Capture(['reset', '--soft', base_rev])
368
msb@chromium.orge28e4982009-09-25 20:51:45 +0000369 def update(self, options, args, file_list):
370 """Runs git to update or transparently checkout the working copy.
371
372 All updated files will be appended to file_list.
373
374 Raises:
375 Error: if can't get URL for relative path.
376 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000377 if args:
378 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
379
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000380 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000381
maruel@chromium.org1a60dca2013-11-26 14:06:26 +0000382 # If a dependency is not pinned, track the default remote branch.
smut@google.comd33eab32014-07-07 19:35:18 +0000383 default_rev = 'refs/remotes/%s/master' % self.remote
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000384 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000385 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000386 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000387 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000388 # Override the revision number.
389 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000390 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000391 # Check again for a revision in case an initial ref was specified
392 # in the url, for example bla.git@refs/heads/custombranch
393 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000394 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000395 if not revision:
396 revision = default_rev
msb@chromium.orge28e4982009-09-25 20:51:45 +0000397
szager@chromium.org8a139702014-06-20 15:55:01 +0000398 if managed:
399 self._DisableHooks()
400
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000401 printed_path = False
402 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000403 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700404 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000405 verbose = ['--verbose']
406 printed_path = True
407
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000408 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
409 if remote_ref:
smut@google.comd33eab32014-07-07 19:35:18 +0000410 # Rewrite remote refs to their local equivalents.
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000411 revision = ''.join(remote_ref)
412 rev_type = "branch"
413 elif revision.startswith('refs/'):
414 # Local branch? We probably don't want to support, since DEPS should
415 # always specify branches as they are in the upstream repo.
smut@google.comd33eab32014-07-07 19:35:18 +0000416 rev_type = "branch"
417 else:
418 # hash is also a tag, only make a distinction at checkout
419 rev_type = "hash"
420
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000421 mirror = self._GetMirror(url, options)
422 if mirror:
423 url = mirror.mirror_path
424
primiano@chromium.org1c127382015-02-17 11:15:40 +0000425 # If we are going to introduce a new project, there is a possibility that
426 # we are syncing back to a state where the project was originally a
427 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
428 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
429 # In such case, we might have a backup of the former .git folder, which can
430 # be used to avoid re-fetching the entire repo again (useful for bisects).
431 backup_dir = self.GetGitBackupDirPath()
432 target_dir = os.path.join(self.checkout_path, '.git')
433 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
434 gclient_utils.safe_makedirs(self.checkout_path)
435 os.rename(backup_dir, target_dir)
436 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800437 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000438
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000439 if (not os.path.exists(self.checkout_path) or
440 (os.path.isdir(self.checkout_path) and
441 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000442 if mirror:
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800443 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000444 try:
smut@google.comd33eab32014-07-07 19:35:18 +0000445 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000446 except subprocess2.CalledProcessError:
447 self._DeleteOrMove(options.force)
smut@google.comd33eab32014-07-07 19:35:18 +0000448 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000449 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800450 files = self._Capture(
451 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000452 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000453 if not verbose:
454 # Make the output a little prettier. It's nice to have some whitespace
455 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000456 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000457 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000458
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000459 if not managed:
460 self._UpdateBranchHeads(options, fetch=False)
461 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
462 return self._Capture(['rev-parse', '--verify', 'HEAD'])
463
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000464 self._maybe_break_locks(options)
465
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000466 if mirror:
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800467 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000468
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000469 # See if the url has changed (the unittests use git://foo for the url, let
470 # that through).
471 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
472 return_early = False
473 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
474 # unit test pass. (and update the comment above)
475 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
476 # This allows devs to use experimental repos which have a different url
477 # but whose branch(s) are the same as official repos.
borenet@google.comb09097a2014-04-09 19:09:08 +0000478 if (current_url.rstrip('/') != url.rstrip('/') and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000479 url != 'git://foo' and
480 subprocess2.capture(
481 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
482 cwd=self.checkout_path).strip() != 'False'):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000483 self.Print('_____ switching %s to a new upstream' % self.relpath)
iannucci@chromium.org78514212014-08-20 23:08:00 +0000484 if not (options.force or options.reset):
485 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700486 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000487 # Switch over to the new upstream
488 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000489 if mirror:
490 with open(os.path.join(
491 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
492 'w') as fh:
493 fh.write(os.path.join(url, 'objects'))
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000494 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
smut@google.comd33eab32014-07-07 19:35:18 +0000495 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000496
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000497 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000498 else:
499 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000500
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000501 if return_early:
502 return self._Capture(['rev-parse', '--verify', 'HEAD'])
503
msb@chromium.org5bde4852009-12-14 16:47:12 +0000504 cur_branch = self._GetCurrentBranch()
505
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000506 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000507 # 0) HEAD is detached. Probably from our initial clone.
508 # - make sure HEAD is contained by a named ref, then update.
509 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700510 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000511 # - try to rebase onto the new hash or branch
512 # 2) current branch is tracking a remote branch with local committed
513 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000514 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000515 # 3) current branch is tracking a remote branch w/or w/out changes, and
516 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000517 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000518 # 4) current branch is tracking a remote branch, but DEPS switches to a
519 # different remote branch, and
520 # a) current branch has no local changes, and --force:
521 # - checkout new branch
522 # b) current branch has local changes, and --force and --reset:
523 # - checkout new branch
524 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000525
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000526 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
527 # a tracking branch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000528 # or 'master' if not a tracking branch (it's based on a specific rev/hash)
529 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000530 if cur_branch is None:
531 upstream_branch = None
532 current_type = "detached"
533 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000534 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000535 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
536 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
537 current_type = "hash"
538 logging.debug("Current branch is not tracking an upstream (remote)"
539 " branch.")
540 elif upstream_branch.startswith('refs/remotes'):
541 current_type = "branch"
542 else:
543 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000544
smut@google.comd33eab32014-07-07 19:35:18 +0000545 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
bauerb@chromium.orgcbd20a42012-06-27 13:49:27 +0000546 # Update the remotes first so we have all the refs.
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000547 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
bauerb@chromium.orgcbd20a42012-06-27 13:49:27 +0000548 cwd=self.checkout_path)
bauerb@chromium.orgcbd20a42012-06-27 13:49:27 +0000549 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000550 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000551
mmoss@chromium.org37ac0e32015-08-18 18:14:38 +0000552 self._UpdateBranchHeads(options, fetch=True)
mmoss@chromium.orge409df62013-04-16 17:28:57 +0000553
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200554 revision = self._AutoFetchRef(options, revision)
555
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000556 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000557 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000558 target = 'HEAD'
559 if options.upstream and upstream_branch:
560 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800561 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000562
msb@chromium.org786fb682010-06-02 15:16:23 +0000563 if current_type == 'detached':
564 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800565 # We just did a Scrub, this is as clean as it's going to get. In
566 # particular if HEAD is a commit that contains two versions of the same
567 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
568 # to actually "Clean" the checkout; that commit is uncheckoutable on this
569 # system. The best we can do is carry forward to the checkout step.
570 if not (options.force or options.reset):
571 self._CheckClean(revision)
agable83faed02016-10-24 14:37:10 -0700572 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000573 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000574 self.Print('Up-to-date; skipping checkout.')
575 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000576 # 'git checkout' may need to overwrite existing untracked files. Allow
577 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000578 self._Checkout(
579 options,
580 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000581 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000582 quiet=True,
583 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000584 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700585 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000586 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000587 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700588 # Can't find a merge-base since we don't know our upstream. That makes
589 # this command VERY likely to produce a rebase failure. For now we
590 # assume origin is our upstream since that's what the old behavior was.
591 upstream_branch = self.remote
592 if options.revision or deps_revision:
593 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700594 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700595 printed_path=printed_path, merge=options.merge)
596 printed_path = True
smut@google.comd33eab32014-07-07 19:35:18 +0000597 elif rev_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000598 # case 2
agable1a8439a2016-10-24 16:36:14 -0700599 self._AttemptRebase(upstream_branch, file_list, options,
smut@google.comd33eab32014-07-07 19:35:18 +0000600 newbase=revision, printed_path=printed_path,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000601 merge=options.merge)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000602 printed_path = True
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000603 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000604 # case 4
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000605 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000606 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700607 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000608 switch_error = ("Could not switch upstream branch from %s to %s\n"
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000609 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000610 "Please use --force or merge or rebase manually:\n" +
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000611 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
612 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000613 force_switch = False
614 if options.force:
615 try:
agable83faed02016-10-24 14:37:10 -0700616 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000617 # case 4a
618 force_switch = True
619 except gclient_utils.Error as e:
620 if options.reset:
621 # case 4b
622 force_switch = True
623 else:
624 switch_error = '%s\n%s' % (e.message, switch_error)
625 if force_switch:
626 self.Print("Switching upstream branch from %s to %s" %
627 (upstream_branch, new_base))
628 switch_branch = 'gclient_' + remote_ref[1]
629 self._Capture(['branch', '-f', switch_branch, new_base])
630 self._Checkout(options, switch_branch, force=True, quiet=True)
631 else:
632 # case 4c
633 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000634 else:
635 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800636 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000637 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000638 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000639 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000640 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000641 if options.merge:
642 merge_args.append('--ff')
643 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000644 merge_args.append('--ff-only')
645 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000646 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000647 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700648 rebase_files = []
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000649 if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr):
650 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700651 self.Print('_____ %s at %s' % (self.relpath, revision),
652 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000653 printed_path = True
654 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000655 if not options.auto_rebase:
656 try:
657 action = self._AskForData(
658 'Cannot %s, attempt to rebase? '
659 '(y)es / (q)uit / (s)kip : ' %
660 ('merge' if options.merge else 'fast-forward merge'),
661 options)
662 except ValueError:
663 raise gclient_utils.Error('Invalid Character')
664 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700665 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000666 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000667 printed_path = True
668 break
669 elif re.match(r'quit|q', action, re.I):
670 raise gclient_utils.Error("Can't fast-forward, please merge or "
671 "rebase manually.\n"
672 "cd %s && git " % self.checkout_path
673 + "rebase %s" % upstream_branch)
674 elif re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000675 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000676 return
677 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000678 self.Print('Input not recognized')
smut@google.com27c9c8a2014-09-11 19:57:55 +0000679 elif re.match("error: Your local changes to '.*' would be "
680 "overwritten by merge. Aborting.\nPlease, commit your "
681 "changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000682 e.stderr):
683 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700684 self.Print('_____ %s at %s' % (self.relpath, revision),
685 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000686 printed_path = True
687 raise gclient_utils.Error(e.stderr)
688 else:
689 # Some other problem happened with the merge
690 logging.error("Error during fast-forward merge in %s!" % self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000691 self.Print(e.stderr)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000692 raise
693 else:
694 # Fast-forward merge was successful
695 if not re.match('Already up-to-date.', merge_output) or verbose:
696 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700697 self.Print('_____ %s at %s' % (self.relpath, revision),
698 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000699 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000700 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000701 if not verbose:
702 # Make the output a little prettier. It's nice to have some
703 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000704 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000705
agablec3937b92016-10-25 10:13:03 -0700706 if file_list is not None:
707 file_list.extend(
708 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000709
710 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000711 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700712 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000713 '\nConflict while rebasing this branch.\n'
714 'Fix the conflict and run gclient again.\n'
715 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700716 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000717
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000718 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000719 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
720 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000721
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000722 # If --reset and --delete_unversioned_trees are specified, remove any
723 # untracked directories.
724 if options.reset and options.delete_unversioned_trees:
725 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
726 # merge-base by default), so doesn't include untracked files. So we use
727 # 'git ls-files --directory --others --exclude-standard' here directly.
728 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800729 ['-c', 'core.quotePath=false', 'ls-files',
730 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000731 self.checkout_path)
732 for path in (p for p in paths.splitlines() if p.endswith('/')):
733 full_path = os.path.join(self.checkout_path, path)
734 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000735 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000736 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000737
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000738 return self._Capture(['rev-parse', '--verify', 'HEAD'])
739
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000740 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000741 """Reverts local modifications.
742
743 All reverted files will be appended to file_list.
744 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000745 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000746 # revert won't work if the directory doesn't exist. It needs to
747 # checkout instead.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000748 self.Print('_____ %s is missing, synching instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000749 # Don't reuse the args.
750 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000751
752 default_rev = "refs/heads/master"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000753 if options.upstream:
754 if self._GetCurrentBranch():
755 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
756 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000757 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000758 if not deps_revision:
759 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000760 if deps_revision.startswith('refs/heads/'):
761 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700762 try:
763 deps_revision = self.GetUsableRev(deps_revision, options)
764 except NoUsableRevError as e:
765 # If the DEPS entry's url and hash changed, try to update the origin.
766 # See also http://crbug.com/520067.
767 logging.warn(
768 'Couldn\'t find usable revision, will retrying to update instead: %s',
769 e.message)
770 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000771
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000772 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800773 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000774
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800775 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000776 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000777
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000778 if file_list is not None:
779 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
780
781 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000782 """Returns revision"""
783 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000784
msb@chromium.orge28e4982009-09-25 20:51:45 +0000785 def runhooks(self, options, args, file_list):
786 self.status(options, args, file_list)
787
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000788 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000789 """Display status information."""
790 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000791 self.Print('________ couldn\'t run status in %s:\n'
792 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000793 else:
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700794 try:
795 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
796 except subprocess2.CalledProcessError:
797 merge_base = []
Aaron Gablef4068aa2017-12-12 15:14:09 -0800798 self._Run(
799 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
800 options, stdout=self.out_fh, always=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000801 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800802 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000803 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000804
smutae7ea312016-07-18 11:59:41 -0700805 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700806 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700807 sha1 = None
808 if not os.path.isdir(self.checkout_path):
809 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800810 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700811
812 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
813 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700814 else:
agable41e3a6c2016-10-20 11:36:56 -0700815 # May exist in origin, but we don't have it yet, so fetch and look
816 # again.
817 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -0700818 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
819 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700820
821 if not sha1:
822 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800823 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -0700824
825 return sha1
826
msb@chromium.orge6f78352010-01-13 17:05:33 +0000827 def FullUrlForRelativeUrl(self, url):
828 # Strip from last '/'
829 # Equivalent to unix basename
830 base_url = self.url
831 return base_url[:base_url.rfind('/')] + url
832
primiano@chromium.org1c127382015-02-17 11:15:40 +0000833 def GetGitBackupDirPath(self):
834 """Returns the path where the .git folder for the current project can be
835 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
836 return os.path.join(self._root_dir,
837 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
838
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000839 def _GetMirror(self, url, options):
840 """Get a git_cache.Mirror object for the argument url."""
841 if not git_cache.Mirror.GetCachePath():
842 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000843 mirror_kwargs = {
844 'print_func': self.filter,
845 'refs': []
846 }
hinoka@google.comb1b54572014-04-16 22:29:23 +0000847 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
848 mirror_kwargs['refs'].append('refs/branch-heads/*')
szager@chromium.org8d3348f2014-08-19 22:49:16 +0000849 if hasattr(options, 'with_tags') and options.with_tags:
850 mirror_kwargs['refs'].append('refs/tags/*')
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000851 return git_cache.Mirror(url, **mirror_kwargs)
852
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800853 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
854 """Update a git mirror by fetching the latest commits from the remote,
855 unless mirror already contains revision whose type is sha1 hash.
856 """
857 if rev_type == 'hash' and mirror.contains_revision(revision):
858 if options.verbose:
859 self.Print('skipping mirror update, it has rev=%s already' % revision,
860 timestamp=False)
861 return
862
szager@chromium.org3ec84f62014-08-22 21:00:22 +0000863 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000864 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000865 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000866 depth = 10
867 else:
868 depth = 10000
869 else:
870 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +0000871 mirror.populate(verbose=options.verbose,
872 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +0000873 depth=depth,
874 ignore_lock=getattr(options, 'ignore_locks', False),
875 lock_timeout=getattr(options, 'lock_timeout', 0))
876 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000877
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000878 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000879 """Clone a git repository from the given URL.
880
msb@chromium.org786fb682010-06-02 15:16:23 +0000881 Once we've cloned the repo, we checkout a working branch if the specified
882 revision is a branch head. If it is a tag or a specific commit, then we
883 leave HEAD detached as it makes future updates simpler -- in this case the
884 user should first create a new branch or switch to an existing branch before
885 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000886 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000887 # git clone doesn't seem to insert a newline properly before printing
888 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000889 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +0000890 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +0000891 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000892 if self.cache_dir:
893 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000894 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000895 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000896 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +0000897 # If the parent directory does not exist, Git clone on Windows will not
898 # create it, so we need to do it manually.
899 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000900 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000901
902 template_dir = None
903 if hasattr(options, 'no_history') and options.no_history:
904 if gclient_utils.IsGitSha(revision):
905 # In the case of a subproject, the pinned sha is not necessarily the
906 # head of the remote branch (so we can't just use --depth=N). Instead,
907 # we tell git to fetch all the remote objects from SHA..HEAD by means of
908 # a template git dir which has a 'shallow' file pointing to the sha.
909 template_dir = tempfile.mkdtemp(
910 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
911 dir=parent_dir)
912 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
913 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
914 template_file.write(revision)
915 clone_cmd.append('--template=' + template_dir)
916 else:
917 # Otherwise, we're just interested in the HEAD. Just use --depth.
918 clone_cmd.append('--depth=1')
919
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000920 tmp_dir = tempfile.mkdtemp(
921 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
922 dir=parent_dir)
923 try:
924 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +0100925 if self.print_outbuf:
926 print_stdout = True
927 stdout = gclient_utils.WriteToStdout(self.out_fh)
928 else:
929 print_stdout = False
930 stdout = self.out_fh
931 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
932 print_stdout=print_stdout, stdout=stdout)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000933 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000934 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
935 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +0000936 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000937 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +0000938 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000939 finally:
940 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000941 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000942 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000943 if template_dir:
944 gclient_utils.rmtree(template_dir)
mmoss@chromium.org1a6bec02014-06-02 21:53:29 +0000945 self._UpdateBranchHeads(options, fetch=True)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200946 revision = self._AutoFetchRef(options, revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000947 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
948 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000949 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +0000950 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000951 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +0000952 ('Checked out %s to a detached HEAD. Before making any commits\n'
953 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
954 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
955 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000956
szager@chromium.org6cd41b62014-04-21 23:55:22 +0000957 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000958 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +0000959 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000960 raise gclient_utils.Error("Background task requires input. Rerun "
961 "gclient with --jobs=1 so that\n"
962 "interaction is possible.")
963 try:
964 return raw_input(prompt)
965 except KeyboardInterrupt:
966 # Hide the exception.
967 sys.exit(1)
968
969
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000970 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000971 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000972 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000973 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800974 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000975 revision = upstream
976 if newbase:
977 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000978 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000979 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000980 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000981 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000982 printed_path = True
983 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000984 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000985
986 if merge:
987 merge_output = self._Capture(['merge', revision])
988 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000989 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000990 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000991
992 # Build the rebase command here using the args
993 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
994 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000995 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000996 rebase_cmd.append('--verbose')
997 if newbase:
998 rebase_cmd.extend(['--onto', newbase])
999 rebase_cmd.append(upstream)
1000 if branch:
1001 rebase_cmd.append(branch)
1002
1003 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001004 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001005 except subprocess2.CalledProcessError, e:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001006 if (re.match(r'cannot rebase: you have unstaged changes', e.stderr) or
1007 re.match(r'cannot rebase: your index contains uncommitted changes',
1008 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001009 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001010 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001011 'Cannot rebase because of unstaged changes.\n'
1012 '\'git reset --hard HEAD\' ?\n'
1013 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001014 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001015 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001016 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001017 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001018 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001019 break
1020 elif re.match(r'quit|q', rebase_action, re.I):
1021 raise gclient_utils.Error("Please merge or rebase manually\n"
1022 "cd %s && git " % self.checkout_path
1023 + "%s" % ' '.join(rebase_cmd))
1024 elif re.match(r'show|s', rebase_action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001025 self.Print('%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001026 continue
1027 else:
1028 gclient_utils.Error("Input not recognized")
1029 continue
1030 elif re.search(r'^CONFLICT', e.stdout, re.M):
1031 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1032 "Fix the conflict and run gclient again.\n"
1033 "See 'man git-rebase' for details.\n")
1034 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001035 self.Print(e.stdout.strip())
1036 self.Print('Rebase produced error output:\n%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001037 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1038 "manually.\ncd %s && git " %
1039 self.checkout_path
1040 + "%s" % ' '.join(rebase_cmd))
1041
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001042 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001043 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001044 # Make the output a little prettier. It's nice to have some
1045 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001046 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001047
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001048 @staticmethod
1049 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001050 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1051 if not ok:
1052 raise gclient_utils.Error('git version %s < minimum required %s' %
1053 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001054
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001055 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
1056 # Special case handling if all 3 conditions are met:
1057 # * the mirros have recently changed, but deps destination remains same,
1058 # * the git histories of mirrors are conflicting.
1059 # * git cache is used
1060 # This manifests itself in current checkout having invalid HEAD commit on
1061 # most git operations. Since git cache is used, just deleted the .git
1062 # folder, and re-create it by cloning.
1063 try:
1064 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1065 except subprocess2.CalledProcessError as e:
1066 if ('fatal: bad object HEAD' in e.stderr
1067 and self.cache_dir and self.cache_dir in url):
1068 self.Print((
1069 'Likely due to DEPS change with git cache_dir, '
1070 'the current commit points to no longer existing object.\n'
1071 '%s' % e)
1072 )
1073 self._DeleteOrMove(options.force)
1074 self._Clone(revision, url, options)
1075 else:
1076 raise
1077
msb@chromium.org786fb682010-06-02 15:16:23 +00001078 def _IsRebasing(self):
1079 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1080 # have a plumbing command to determine whether a rebase is in progress, so
1081 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1082 g = os.path.join(self.checkout_path, '.git')
1083 return (
1084 os.path.isdir(os.path.join(g, "rebase-merge")) or
1085 os.path.isdir(os.path.join(g, "rebase-apply")))
1086
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001087 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001088 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1089 if os.path.exists(lockfile):
1090 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001091 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001092 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1093 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001094 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001095
msb@chromium.org786fb682010-06-02 15:16:23 +00001096 # Make sure the tree is clean; see git-rebase.sh for reference
1097 try:
1098 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001099 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001100 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001101 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001102 '\tYou have unstaged changes.\n'
1103 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001104 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001105 try:
1106 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001107 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001108 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001109 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001110 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001111 '\tYour index contains uncommitted changes\n'
1112 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001113 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001114
agable83faed02016-10-24 14:37:10 -07001115 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001116 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1117 # reference by a commit). If not, error out -- most likely a rebase is
1118 # in progress, try to detect so we can give a better error.
1119 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001120 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1121 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001122 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001123 # Commit is not contained by any rev. See if the user is rebasing:
1124 if self._IsRebasing():
1125 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001126 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001127 '\tAlready in a conflict, i.e. (no branch).\n'
1128 '\tFix the conflict and run gclient again.\n'
1129 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1130 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001131 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001132 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001133 name = ('saved-by-gclient-' +
1134 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001135 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001136 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001137 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001138
msb@chromium.org5bde4852009-12-14 16:47:12 +00001139 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001140 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001141 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001142 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001143 return None
1144 return branch
1145
borenet@google.comc3e09d22014-04-10 13:58:18 +00001146 def _Capture(self, args, **kwargs):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001147 kwargs.setdefault('cwd', self.checkout_path)
1148 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001149 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001150 env = scm.GIT.ApplyEnvVars(kwargs)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001151 ret = subprocess2.check_output(['git'] + args, env=env, **kwargs)
1152 if strip:
1153 ret = ret.strip()
1154 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001155
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001156 def _Checkout(self, options, ref, force=False, quiet=None):
1157 """Performs a 'git-checkout' operation.
1158
1159 Args:
1160 options: The configured option set
1161 ref: (str) The branch/commit to checkout
1162 quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
1163 'None', the behavior is inferred from 'options.verbose'.
1164 Returns: (str) The output of the checkout operation
1165 """
1166 if quiet is None:
1167 quiet = (not options.verbose)
1168 checkout_args = ['checkout']
1169 if force:
1170 checkout_args.append('--force')
1171 if quiet:
1172 checkout_args.append('--quiet')
1173 checkout_args.append(ref)
1174 return self._Capture(checkout_args)
1175
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001176 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1177 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001178 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
1179 fetch_cmd = cfg + [
1180 'fetch',
1181 remote or self.remote,
1182 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001183 if refspec:
1184 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001185
1186 if prune:
1187 fetch_cmd.append('--prune')
1188 if options.verbose:
1189 fetch_cmd.append('--verbose')
1190 elif quiet:
1191 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001192 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001193
1194 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1195 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1196
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001197 def _UpdateBranchHeads(self, options, fetch=False):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001198 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1199 if requested."""
1200 need_fetch = fetch
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001201 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001202 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001203 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1204 '^\\+refs/branch-heads/\\*:.*$']
1205 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001206 need_fetch = True
1207 if hasattr(options, 'with_tags') and options.with_tags:
1208 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1209 '+refs/tags/*:refs/tags/*',
1210 '^\\+refs/tags/\\*:.*$']
1211 self._Run(config_cmd, options)
1212 need_fetch = True
1213 if fetch and need_fetch:
bpastene2a3e9912016-09-07 16:22:25 -07001214 self._Fetch(options, prune=options.force)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001215
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001216 def _AutoFetchRef(self, options, revision):
1217 """Attempts to fetch |revision| if not available in local repo.
1218
1219 Returns possibly updated revision."""
1220 try:
1221 self._Capture(['rev-parse', revision])
1222 except subprocess2.CalledProcessError:
1223 self._Fetch(options, refspec=revision)
1224 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1225 return revision
1226
dnj@chromium.org680f2172014-06-25 00:39:32 +00001227 def _Run(self, args, options, show_header=True, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001228 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001229 kwargs.setdefault('cwd', self.checkout_path)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001230 kwargs.setdefault('stdout', self.out_fh)
szager@chromium.org848fd492014-04-09 19:06:44 +00001231 kwargs['filter_fn'] = self.filter
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001232 kwargs.setdefault('print_stdout', False)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001233 env = scm.GIT.ApplyEnvVars(kwargs)
agable@chromium.org772efaf2014-04-01 02:35:44 +00001234 cmd = ['git'] + args
dnj@chromium.org680f2172014-06-25 00:39:32 +00001235 if show_header:
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001236 gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs)
1237 else:
1238 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001239
1240
1241class CipdPackage(object):
1242 """A representation of a single CIPD package."""
1243
John Budorickd3ba72b2018-03-20 12:27:42 -07001244 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001245 self._authority_for_subdir = authority_for_subdir
1246 self._name = name
1247 self._version = version
1248
1249 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001250 def authority_for_subdir(self):
1251 """Whether this package has authority to act on behalf of its subdir.
1252
1253 Some operations should only be performed once per subdirectory. A package
1254 that has authority for its subdirectory is the only package that should
1255 perform such operations.
1256
1257 Returns:
1258 bool; whether this package has subdir authority.
1259 """
1260 return self._authority_for_subdir
1261
1262 @property
1263 def name(self):
1264 return self._name
1265
1266 @property
1267 def version(self):
1268 return self._version
1269
1270
1271class CipdRoot(object):
1272 """A representation of a single CIPD root."""
1273 def __init__(self, root_dir, service_url):
1274 self._all_packages = set()
1275 self._mutator_lock = threading.Lock()
1276 self._packages_by_subdir = collections.defaultdict(list)
1277 self._root_dir = root_dir
1278 self._service_url = service_url
1279
1280 def add_package(self, subdir, package, version):
1281 """Adds a package to this CIPD root.
1282
1283 As far as clients are concerned, this grants both root and subdir authority
1284 to packages arbitrarily. (The implementation grants root authority to the
1285 first package added and subdir authority to the first package added for that
1286 subdir, but clients should not depend on or expect that behavior.)
1287
1288 Args:
1289 subdir: str; relative path to where the package should be installed from
1290 the cipd root directory.
1291 package: str; the cipd package name.
1292 version: str; the cipd package version.
1293 Returns:
1294 CipdPackage; the package that was created and added to this root.
1295 """
1296 with self._mutator_lock:
1297 cipd_package = CipdPackage(
1298 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001299 not self._packages_by_subdir[subdir])
1300 self._all_packages.add(cipd_package)
1301 self._packages_by_subdir[subdir].append(cipd_package)
1302 return cipd_package
1303
1304 def packages(self, subdir):
1305 """Get the list of configured packages for the given subdir."""
1306 return list(self._packages_by_subdir[subdir])
1307
1308 def clobber(self):
1309 """Remove the .cipd directory.
1310
1311 This is useful for forcing ensure to redownload and reinitialize all
1312 packages.
1313 """
1314 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001315 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001316 try:
1317 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1318 except OSError:
1319 if os.path.exists(cipd_cache_dir):
1320 raise
1321
1322 @contextlib.contextmanager
1323 def _create_ensure_file(self):
1324 try:
1325 ensure_file = None
1326 with tempfile.NamedTemporaryFile(
1327 suffix='.ensure', delete=False) as ensure_file:
1328 for subdir, packages in sorted(self._packages_by_subdir.iteritems()):
1329 ensure_file.write('@Subdir %s\n' % subdir)
1330 for package in packages:
1331 ensure_file.write('%s %s\n' % (package.name, package.version))
1332 ensure_file.write('\n')
1333 yield ensure_file.name
1334 finally:
1335 if ensure_file is not None and os.path.exists(ensure_file.name):
1336 os.remove(ensure_file.name)
1337
1338 def ensure(self):
1339 """Run `cipd ensure`."""
1340 with self._mutator_lock:
1341 with self._create_ensure_file() as ensure_file:
1342 cmd = [
1343 'cipd', 'ensure',
1344 '-log-level', 'error',
1345 '-root', self.root_dir,
1346 '-ensure-file', ensure_file,
1347 ]
1348 gclient_utils.CheckCallAndFilterAndHeader(cmd)
1349
John Budorickd3ba72b2018-03-20 12:27:42 -07001350 def run(self, command):
1351 if command == 'update':
1352 self.ensure()
1353 elif command == 'revert':
1354 self.clobber()
1355 self.ensure()
1356
John Budorick0f7b2002018-01-19 15:46:17 -08001357 def created_package(self, package):
1358 """Checks whether this root created the given package.
1359
1360 Args:
1361 package: CipdPackage; the package to check.
1362 Returns:
1363 bool; whether this root created the given package.
1364 """
1365 return package in self._all_packages
1366
1367 @property
1368 def root_dir(self):
1369 return self._root_dir
1370
1371 @property
1372 def service_url(self):
1373 return self._service_url
1374
1375
1376class CipdWrapper(SCMWrapper):
1377 """Wrapper for CIPD.
1378
1379 Currently only supports chrome-infra-packages.appspot.com.
1380 """
John Budorick3929e9e2018-02-04 18:18:07 -08001381 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001382
1383 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1384 out_cb=None, root=None, package=None):
1385 super(CipdWrapper, self).__init__(
1386 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1387 out_cb=out_cb)
1388 assert root.created_package(package)
1389 self._package = package
1390 self._root = root
1391
1392 #override
1393 def GetCacheMirror(self):
1394 return None
1395
1396 #override
1397 def GetActualRemoteURL(self, options):
1398 return self._root.service_url
1399
1400 #override
1401 def DoesRemoteURLMatch(self, options):
1402 del options
1403 return True
1404
1405 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001406 """Does nothing.
1407
1408 CIPD packages should be reverted at the root by running
1409 `CipdRoot.run('revert')`.
1410 """
1411 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001412
1413 def diff(self, options, args, file_list):
1414 """CIPD has no notion of diffing."""
1415 pass
1416
1417 def pack(self, options, args, file_list):
1418 """CIPD has no notion of diffing."""
1419 pass
1420
1421 def revinfo(self, options, args, file_list):
1422 """Grab the instance ID."""
1423 try:
1424 tmpdir = tempfile.mkdtemp()
1425 describe_json_path = os.path.join(tmpdir, 'describe.json')
1426 cmd = [
1427 'cipd', 'describe',
1428 self._package.name,
1429 '-log-level', 'error',
1430 '-version', self._package.version,
1431 '-json-output', describe_json_path
1432 ]
1433 gclient_utils.CheckCallAndFilter(
1434 cmd, filter_fn=lambda _line: None, print_stdout=False)
1435 with open(describe_json_path) as f:
1436 describe_json = json.load(f)
1437 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1438 finally:
1439 gclient_utils.rmtree(tmpdir)
1440
1441 def status(self, options, args, file_list):
1442 pass
1443
1444 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001445 """Does nothing.
1446
1447 CIPD packages should be updated at the root by running
1448 `CipdRoot.run('update')`.
1449 """
1450 pass