blob: 1f4c077765798bc5e646a7766447286b82d70c6e [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
primiano@chromium.org1c127382015-02-17 11:15:40 +0000827 def GetGitBackupDirPath(self):
828 """Returns the path where the .git folder for the current project can be
829 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
830 return os.path.join(self._root_dir,
831 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
832
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000833 def _GetMirror(self, url, options):
834 """Get a git_cache.Mirror object for the argument url."""
835 if not git_cache.Mirror.GetCachePath():
836 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000837 mirror_kwargs = {
838 'print_func': self.filter,
839 'refs': []
840 }
hinoka@google.comb1b54572014-04-16 22:29:23 +0000841 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
842 mirror_kwargs['refs'].append('refs/branch-heads/*')
szager@chromium.org8d3348f2014-08-19 22:49:16 +0000843 if hasattr(options, 'with_tags') and options.with_tags:
844 mirror_kwargs['refs'].append('refs/tags/*')
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000845 return git_cache.Mirror(url, **mirror_kwargs)
846
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800847 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
848 """Update a git mirror by fetching the latest commits from the remote,
849 unless mirror already contains revision whose type is sha1 hash.
850 """
851 if rev_type == 'hash' and mirror.contains_revision(revision):
852 if options.verbose:
853 self.Print('skipping mirror update, it has rev=%s already' % revision,
854 timestamp=False)
855 return
856
szager@chromium.org3ec84f62014-08-22 21:00:22 +0000857 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000858 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000859 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000860 depth = 10
861 else:
862 depth = 10000
863 else:
864 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +0000865 mirror.populate(verbose=options.verbose,
866 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +0000867 depth=depth,
868 ignore_lock=getattr(options, 'ignore_locks', False),
869 lock_timeout=getattr(options, 'lock_timeout', 0))
870 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000871
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000872 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000873 """Clone a git repository from the given URL.
874
msb@chromium.org786fb682010-06-02 15:16:23 +0000875 Once we've cloned the repo, we checkout a working branch if the specified
876 revision is a branch head. If it is a tag or a specific commit, then we
877 leave HEAD detached as it makes future updates simpler -- in this case the
878 user should first create a new branch or switch to an existing branch before
879 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000880 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000881 # git clone doesn't seem to insert a newline properly before printing
882 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000883 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +0000884 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +0000885 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000886 if self.cache_dir:
887 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000888 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000889 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000890 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +0000891 # If the parent directory does not exist, Git clone on Windows will not
892 # create it, so we need to do it manually.
893 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000894 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000895
896 template_dir = None
897 if hasattr(options, 'no_history') and options.no_history:
898 if gclient_utils.IsGitSha(revision):
899 # In the case of a subproject, the pinned sha is not necessarily the
900 # head of the remote branch (so we can't just use --depth=N). Instead,
901 # we tell git to fetch all the remote objects from SHA..HEAD by means of
902 # a template git dir which has a 'shallow' file pointing to the sha.
903 template_dir = tempfile.mkdtemp(
904 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
905 dir=parent_dir)
906 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
907 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
908 template_file.write(revision)
909 clone_cmd.append('--template=' + template_dir)
910 else:
911 # Otherwise, we're just interested in the HEAD. Just use --depth.
912 clone_cmd.append('--depth=1')
913
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000914 tmp_dir = tempfile.mkdtemp(
915 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
916 dir=parent_dir)
917 try:
918 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +0100919 if self.print_outbuf:
920 print_stdout = True
921 stdout = gclient_utils.WriteToStdout(self.out_fh)
922 else:
923 print_stdout = False
924 stdout = self.out_fh
925 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
926 print_stdout=print_stdout, stdout=stdout)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000927 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000928 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
929 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +0000930 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000931 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +0000932 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000933 finally:
934 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000935 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000936 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000937 if template_dir:
938 gclient_utils.rmtree(template_dir)
mmoss@chromium.org1a6bec02014-06-02 21:53:29 +0000939 self._UpdateBranchHeads(options, fetch=True)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200940 revision = self._AutoFetchRef(options, revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000941 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
942 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000943 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +0000944 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000945 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +0000946 ('Checked out %s to a detached HEAD. Before making any commits\n'
947 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
948 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
949 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000950
szager@chromium.org6cd41b62014-04-21 23:55:22 +0000951 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000952 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +0000953 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000954 raise gclient_utils.Error("Background task requires input. Rerun "
955 "gclient with --jobs=1 so that\n"
956 "interaction is possible.")
957 try:
958 return raw_input(prompt)
959 except KeyboardInterrupt:
960 # Hide the exception.
961 sys.exit(1)
962
963
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000964 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000965 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000966 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000967 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800968 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000969 revision = upstream
970 if newbase:
971 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000972 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000973 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000974 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000975 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000976 printed_path = True
977 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000978 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000979
980 if merge:
981 merge_output = self._Capture(['merge', revision])
982 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000983 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000984 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000985
986 # Build the rebase command here using the args
987 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
988 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000989 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000990 rebase_cmd.append('--verbose')
991 if newbase:
992 rebase_cmd.extend(['--onto', newbase])
993 rebase_cmd.append(upstream)
994 if branch:
995 rebase_cmd.append(branch)
996
997 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000998 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +0000999 except subprocess2.CalledProcessError, e:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001000 if (re.match(r'cannot rebase: you have unstaged changes', e.stderr) or
1001 re.match(r'cannot rebase: your index contains uncommitted changes',
1002 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001003 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +00001004 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +00001005 'Cannot rebase because of unstaged changes.\n'
1006 '\'git reset --hard HEAD\' ?\n'
1007 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +00001008 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001009 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001010 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001011 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001012 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001013 break
1014 elif re.match(r'quit|q', rebase_action, re.I):
1015 raise gclient_utils.Error("Please merge or rebase manually\n"
1016 "cd %s && git " % self.checkout_path
1017 + "%s" % ' '.join(rebase_cmd))
1018 elif re.match(r'show|s', rebase_action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001019 self.Print('%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001020 continue
1021 else:
1022 gclient_utils.Error("Input not recognized")
1023 continue
1024 elif re.search(r'^CONFLICT', e.stdout, re.M):
1025 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1026 "Fix the conflict and run gclient again.\n"
1027 "See 'man git-rebase' for details.\n")
1028 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001029 self.Print(e.stdout.strip())
1030 self.Print('Rebase produced error output:\n%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001031 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1032 "manually.\ncd %s && git " %
1033 self.checkout_path
1034 + "%s" % ' '.join(rebase_cmd))
1035
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001036 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001037 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001038 # Make the output a little prettier. It's nice to have some
1039 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001040 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001041
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001042 @staticmethod
1043 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001044 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1045 if not ok:
1046 raise gclient_utils.Error('git version %s < minimum required %s' %
1047 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001048
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001049 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
1050 # Special case handling if all 3 conditions are met:
1051 # * the mirros have recently changed, but deps destination remains same,
1052 # * the git histories of mirrors are conflicting.
1053 # * git cache is used
1054 # This manifests itself in current checkout having invalid HEAD commit on
1055 # most git operations. Since git cache is used, just deleted the .git
1056 # folder, and re-create it by cloning.
1057 try:
1058 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1059 except subprocess2.CalledProcessError as e:
1060 if ('fatal: bad object HEAD' in e.stderr
1061 and self.cache_dir and self.cache_dir in url):
1062 self.Print((
1063 'Likely due to DEPS change with git cache_dir, '
1064 'the current commit points to no longer existing object.\n'
1065 '%s' % e)
1066 )
1067 self._DeleteOrMove(options.force)
1068 self._Clone(revision, url, options)
1069 else:
1070 raise
1071
msb@chromium.org786fb682010-06-02 15:16:23 +00001072 def _IsRebasing(self):
1073 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1074 # have a plumbing command to determine whether a rebase is in progress, so
1075 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1076 g = os.path.join(self.checkout_path, '.git')
1077 return (
1078 os.path.isdir(os.path.join(g, "rebase-merge")) or
1079 os.path.isdir(os.path.join(g, "rebase-apply")))
1080
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001081 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001082 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1083 if os.path.exists(lockfile):
1084 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001085 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001086 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1087 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001088 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001089
msb@chromium.org786fb682010-06-02 15:16:23 +00001090 # Make sure the tree is clean; see git-rebase.sh for reference
1091 try:
1092 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001093 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001094 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001095 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001096 '\tYou have unstaged changes.\n'
1097 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001098 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001099 try:
1100 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001101 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001102 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001103 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001104 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001105 '\tYour index contains uncommitted changes\n'
1106 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001107 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001108
agable83faed02016-10-24 14:37:10 -07001109 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001110 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1111 # reference by a commit). If not, error out -- most likely a rebase is
1112 # in progress, try to detect so we can give a better error.
1113 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001114 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1115 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001116 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001117 # Commit is not contained by any rev. See if the user is rebasing:
1118 if self._IsRebasing():
1119 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001120 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001121 '\tAlready in a conflict, i.e. (no branch).\n'
1122 '\tFix the conflict and run gclient again.\n'
1123 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1124 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001125 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001126 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001127 name = ('saved-by-gclient-' +
1128 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001129 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001130 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001131 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001132
msb@chromium.org5bde4852009-12-14 16:47:12 +00001133 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001134 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001135 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001136 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001137 return None
1138 return branch
1139
borenet@google.comc3e09d22014-04-10 13:58:18 +00001140 def _Capture(self, args, **kwargs):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001141 kwargs.setdefault('cwd', self.checkout_path)
1142 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001143 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001144 env = scm.GIT.ApplyEnvVars(kwargs)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001145 ret = subprocess2.check_output(['git'] + args, env=env, **kwargs)
1146 if strip:
1147 ret = ret.strip()
1148 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001149
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001150 def _Checkout(self, options, ref, force=False, quiet=None):
1151 """Performs a 'git-checkout' operation.
1152
1153 Args:
1154 options: The configured option set
1155 ref: (str) The branch/commit to checkout
1156 quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
1157 'None', the behavior is inferred from 'options.verbose'.
1158 Returns: (str) The output of the checkout operation
1159 """
1160 if quiet is None:
1161 quiet = (not options.verbose)
1162 checkout_args = ['checkout']
1163 if force:
1164 checkout_args.append('--force')
1165 if quiet:
1166 checkout_args.append('--quiet')
1167 checkout_args.append(ref)
1168 return self._Capture(checkout_args)
1169
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001170 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1171 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001172 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
1173 fetch_cmd = cfg + [
1174 'fetch',
1175 remote or self.remote,
1176 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001177 if refspec:
1178 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001179
1180 if prune:
1181 fetch_cmd.append('--prune')
1182 if options.verbose:
1183 fetch_cmd.append('--verbose')
1184 elif quiet:
1185 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001186 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001187
1188 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1189 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1190
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001191 def _UpdateBranchHeads(self, options, fetch=False):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001192 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1193 if requested."""
1194 need_fetch = fetch
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001195 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001196 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001197 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1198 '^\\+refs/branch-heads/\\*:.*$']
1199 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001200 need_fetch = True
1201 if hasattr(options, 'with_tags') and options.with_tags:
1202 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1203 '+refs/tags/*:refs/tags/*',
1204 '^\\+refs/tags/\\*:.*$']
1205 self._Run(config_cmd, options)
1206 need_fetch = True
1207 if fetch and need_fetch:
bpastene2a3e9912016-09-07 16:22:25 -07001208 self._Fetch(options, prune=options.force)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001209
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001210 def _AutoFetchRef(self, options, revision):
1211 """Attempts to fetch |revision| if not available in local repo.
1212
1213 Returns possibly updated revision."""
1214 try:
1215 self._Capture(['rev-parse', revision])
1216 except subprocess2.CalledProcessError:
1217 self._Fetch(options, refspec=revision)
1218 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1219 return revision
1220
dnj@chromium.org680f2172014-06-25 00:39:32 +00001221 def _Run(self, args, options, show_header=True, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001222 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001223 kwargs.setdefault('cwd', self.checkout_path)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001224 kwargs.setdefault('stdout', self.out_fh)
szager@chromium.org848fd492014-04-09 19:06:44 +00001225 kwargs['filter_fn'] = self.filter
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001226 kwargs.setdefault('print_stdout', False)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001227 env = scm.GIT.ApplyEnvVars(kwargs)
agable@chromium.org772efaf2014-04-01 02:35:44 +00001228 cmd = ['git'] + args
dnj@chromium.org680f2172014-06-25 00:39:32 +00001229 if show_header:
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001230 gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs)
1231 else:
1232 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001233
1234
1235class CipdPackage(object):
1236 """A representation of a single CIPD package."""
1237
John Budorickd3ba72b2018-03-20 12:27:42 -07001238 def __init__(self, name, version, authority_for_subdir):
John Budorick0f7b2002018-01-19 15:46:17 -08001239 self._authority_for_subdir = authority_for_subdir
1240 self._name = name
1241 self._version = version
1242
1243 @property
John Budorick0f7b2002018-01-19 15:46:17 -08001244 def authority_for_subdir(self):
1245 """Whether this package has authority to act on behalf of its subdir.
1246
1247 Some operations should only be performed once per subdirectory. A package
1248 that has authority for its subdirectory is the only package that should
1249 perform such operations.
1250
1251 Returns:
1252 bool; whether this package has subdir authority.
1253 """
1254 return self._authority_for_subdir
1255
1256 @property
1257 def name(self):
1258 return self._name
1259
1260 @property
1261 def version(self):
1262 return self._version
1263
1264
1265class CipdRoot(object):
1266 """A representation of a single CIPD root."""
1267 def __init__(self, root_dir, service_url):
1268 self._all_packages = set()
1269 self._mutator_lock = threading.Lock()
1270 self._packages_by_subdir = collections.defaultdict(list)
1271 self._root_dir = root_dir
1272 self._service_url = service_url
1273
1274 def add_package(self, subdir, package, version):
1275 """Adds a package to this CIPD root.
1276
1277 As far as clients are concerned, this grants both root and subdir authority
1278 to packages arbitrarily. (The implementation grants root authority to the
1279 first package added and subdir authority to the first package added for that
1280 subdir, but clients should not depend on or expect that behavior.)
1281
1282 Args:
1283 subdir: str; relative path to where the package should be installed from
1284 the cipd root directory.
1285 package: str; the cipd package name.
1286 version: str; the cipd package version.
1287 Returns:
1288 CipdPackage; the package that was created and added to this root.
1289 """
1290 with self._mutator_lock:
1291 cipd_package = CipdPackage(
1292 package, version,
John Budorick0f7b2002018-01-19 15:46:17 -08001293 not self._packages_by_subdir[subdir])
1294 self._all_packages.add(cipd_package)
1295 self._packages_by_subdir[subdir].append(cipd_package)
1296 return cipd_package
1297
1298 def packages(self, subdir):
1299 """Get the list of configured packages for the given subdir."""
1300 return list(self._packages_by_subdir[subdir])
1301
1302 def clobber(self):
1303 """Remove the .cipd directory.
1304
1305 This is useful for forcing ensure to redownload and reinitialize all
1306 packages.
1307 """
1308 with self._mutator_lock:
John Budorickd3ba72b2018-03-20 12:27:42 -07001309 cipd_cache_dir = os.path.join(self.root_dir, '.cipd')
John Budorick0f7b2002018-01-19 15:46:17 -08001310 try:
1311 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1312 except OSError:
1313 if os.path.exists(cipd_cache_dir):
1314 raise
1315
1316 @contextlib.contextmanager
1317 def _create_ensure_file(self):
1318 try:
1319 ensure_file = None
1320 with tempfile.NamedTemporaryFile(
1321 suffix='.ensure', delete=False) as ensure_file:
1322 for subdir, packages in sorted(self._packages_by_subdir.iteritems()):
1323 ensure_file.write('@Subdir %s\n' % subdir)
1324 for package in packages:
1325 ensure_file.write('%s %s\n' % (package.name, package.version))
1326 ensure_file.write('\n')
1327 yield ensure_file.name
1328 finally:
1329 if ensure_file is not None and os.path.exists(ensure_file.name):
1330 os.remove(ensure_file.name)
1331
1332 def ensure(self):
1333 """Run `cipd ensure`."""
1334 with self._mutator_lock:
1335 with self._create_ensure_file() as ensure_file:
1336 cmd = [
1337 'cipd', 'ensure',
1338 '-log-level', 'error',
1339 '-root', self.root_dir,
1340 '-ensure-file', ensure_file,
1341 ]
1342 gclient_utils.CheckCallAndFilterAndHeader(cmd)
1343
John Budorickd3ba72b2018-03-20 12:27:42 -07001344 def run(self, command):
1345 if command == 'update':
1346 self.ensure()
1347 elif command == 'revert':
1348 self.clobber()
1349 self.ensure()
1350
John Budorick0f7b2002018-01-19 15:46:17 -08001351 def created_package(self, package):
1352 """Checks whether this root created the given package.
1353
1354 Args:
1355 package: CipdPackage; the package to check.
1356 Returns:
1357 bool; whether this root created the given package.
1358 """
1359 return package in self._all_packages
1360
1361 @property
1362 def root_dir(self):
1363 return self._root_dir
1364
1365 @property
1366 def service_url(self):
1367 return self._service_url
1368
1369
1370class CipdWrapper(SCMWrapper):
1371 """Wrapper for CIPD.
1372
1373 Currently only supports chrome-infra-packages.appspot.com.
1374 """
John Budorick3929e9e2018-02-04 18:18:07 -08001375 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001376
1377 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1378 out_cb=None, root=None, package=None):
1379 super(CipdWrapper, self).__init__(
1380 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1381 out_cb=out_cb)
1382 assert root.created_package(package)
1383 self._package = package
1384 self._root = root
1385
1386 #override
1387 def GetCacheMirror(self):
1388 return None
1389
1390 #override
1391 def GetActualRemoteURL(self, options):
1392 return self._root.service_url
1393
1394 #override
1395 def DoesRemoteURLMatch(self, options):
1396 del options
1397 return True
1398
1399 def revert(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001400 """Does nothing.
1401
1402 CIPD packages should be reverted at the root by running
1403 `CipdRoot.run('revert')`.
1404 """
1405 pass
John Budorick0f7b2002018-01-19 15:46:17 -08001406
1407 def diff(self, options, args, file_list):
1408 """CIPD has no notion of diffing."""
1409 pass
1410
1411 def pack(self, options, args, file_list):
1412 """CIPD has no notion of diffing."""
1413 pass
1414
1415 def revinfo(self, options, args, file_list):
1416 """Grab the instance ID."""
1417 try:
1418 tmpdir = tempfile.mkdtemp()
1419 describe_json_path = os.path.join(tmpdir, 'describe.json')
1420 cmd = [
1421 'cipd', 'describe',
1422 self._package.name,
1423 '-log-level', 'error',
1424 '-version', self._package.version,
1425 '-json-output', describe_json_path
1426 ]
1427 gclient_utils.CheckCallAndFilter(
1428 cmd, filter_fn=lambda _line: None, print_stdout=False)
1429 with open(describe_json_path) as f:
1430 describe_json = json.load(f)
1431 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1432 finally:
1433 gclient_utils.rmtree(tmpdir)
1434
1435 def status(self, options, args, file_list):
1436 pass
1437
1438 def update(self, options, args, file_list):
John Budorickd3ba72b2018-03-20 12:27:42 -07001439 """Does nothing.
1440
1441 CIPD packages should be updated at the root by running
1442 `CipdRoot.run('update')`.
1443 """
1444 pass