blob: 12ea7b62520f16cd07d3e439f94af2c55311b026 [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
msb@chromium.orge28e4982009-09-25 20:51:45 +0000343 def update(self, options, args, file_list):
344 """Runs git to update or transparently checkout the working copy.
345
346 All updated files will be appended to file_list.
347
348 Raises:
349 Error: if can't get URL for relative path.
350 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000351 if args:
352 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
353
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000354 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000355
maruel@chromium.org1a60dca2013-11-26 14:06:26 +0000356 # If a dependency is not pinned, track the default remote branch.
smut@google.comd33eab32014-07-07 19:35:18 +0000357 default_rev = 'refs/remotes/%s/master' % self.remote
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000358 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000359 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000360 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000361 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000362 # Override the revision number.
363 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000364 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000365 # Check again for a revision in case an initial ref was specified
366 # in the url, for example bla.git@refs/heads/custombranch
367 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000368 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000369 if not revision:
370 revision = default_rev
msb@chromium.orge28e4982009-09-25 20:51:45 +0000371
szager@chromium.org8a139702014-06-20 15:55:01 +0000372 if managed:
373 self._DisableHooks()
374
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000375 printed_path = False
376 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000377 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700378 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000379 verbose = ['--verbose']
380 printed_path = True
381
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000382 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
383 if remote_ref:
smut@google.comd33eab32014-07-07 19:35:18 +0000384 # Rewrite remote refs to their local equivalents.
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000385 revision = ''.join(remote_ref)
386 rev_type = "branch"
387 elif revision.startswith('refs/'):
388 # Local branch? We probably don't want to support, since DEPS should
389 # always specify branches as they are in the upstream repo.
smut@google.comd33eab32014-07-07 19:35:18 +0000390 rev_type = "branch"
391 else:
392 # hash is also a tag, only make a distinction at checkout
393 rev_type = "hash"
394
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000395 mirror = self._GetMirror(url, options)
396 if mirror:
397 url = mirror.mirror_path
398
primiano@chromium.org1c127382015-02-17 11:15:40 +0000399 # If we are going to introduce a new project, there is a possibility that
400 # we are syncing back to a state where the project was originally a
401 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
402 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
403 # In such case, we might have a backup of the former .git folder, which can
404 # be used to avoid re-fetching the entire repo again (useful for bisects).
405 backup_dir = self.GetGitBackupDirPath()
406 target_dir = os.path.join(self.checkout_path, '.git')
407 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
408 gclient_utils.safe_makedirs(self.checkout_path)
409 os.rename(backup_dir, target_dir)
410 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800411 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000412
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000413 if (not os.path.exists(self.checkout_path) or
414 (os.path.isdir(self.checkout_path) and
415 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000416 if mirror:
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800417 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000418 try:
smut@google.comd33eab32014-07-07 19:35:18 +0000419 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000420 except subprocess2.CalledProcessError:
421 self._DeleteOrMove(options.force)
smut@google.comd33eab32014-07-07 19:35:18 +0000422 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000423 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800424 files = self._Capture(
425 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000426 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000427 if not verbose:
428 # Make the output a little prettier. It's nice to have some whitespace
429 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000430 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000431 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000432
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000433 if not managed:
434 self._UpdateBranchHeads(options, fetch=False)
435 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
436 return self._Capture(['rev-parse', '--verify', 'HEAD'])
437
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000438 self._maybe_break_locks(options)
439
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000440 if mirror:
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800441 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000442
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000443 # See if the url has changed (the unittests use git://foo for the url, let
444 # that through).
445 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
446 return_early = False
447 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
448 # unit test pass. (and update the comment above)
449 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
450 # This allows devs to use experimental repos which have a different url
451 # but whose branch(s) are the same as official repos.
borenet@google.comb09097a2014-04-09 19:09:08 +0000452 if (current_url.rstrip('/') != url.rstrip('/') and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000453 url != 'git://foo' and
454 subprocess2.capture(
455 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
456 cwd=self.checkout_path).strip() != 'False'):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000457 self.Print('_____ switching %s to a new upstream' % self.relpath)
iannucci@chromium.org78514212014-08-20 23:08:00 +0000458 if not (options.force or options.reset):
459 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700460 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000461 # Switch over to the new upstream
462 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000463 if mirror:
464 with open(os.path.join(
465 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
466 'w') as fh:
467 fh.write(os.path.join(url, 'objects'))
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000468 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
smut@google.comd33eab32014-07-07 19:35:18 +0000469 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000470
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000471 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000472 else:
473 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000474
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000475 if return_early:
476 return self._Capture(['rev-parse', '--verify', 'HEAD'])
477
msb@chromium.org5bde4852009-12-14 16:47:12 +0000478 cur_branch = self._GetCurrentBranch()
479
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000480 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000481 # 0) HEAD is detached. Probably from our initial clone.
482 # - make sure HEAD is contained by a named ref, then update.
483 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700484 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000485 # - try to rebase onto the new hash or branch
486 # 2) current branch is tracking a remote branch with local committed
487 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000488 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000489 # 3) current branch is tracking a remote branch w/or w/out changes, and
490 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000491 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000492 # 4) current branch is tracking a remote branch, but DEPS switches to a
493 # different remote branch, and
494 # a) current branch has no local changes, and --force:
495 # - checkout new branch
496 # b) current branch has local changes, and --force and --reset:
497 # - checkout new branch
498 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000499
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000500 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
501 # a tracking branch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000502 # or 'master' if not a tracking branch (it's based on a specific rev/hash)
503 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000504 if cur_branch is None:
505 upstream_branch = None
506 current_type = "detached"
507 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000508 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000509 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
510 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
511 current_type = "hash"
512 logging.debug("Current branch is not tracking an upstream (remote)"
513 " branch.")
514 elif upstream_branch.startswith('refs/remotes'):
515 current_type = "branch"
516 else:
517 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000518
smut@google.comd33eab32014-07-07 19:35:18 +0000519 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
bauerb@chromium.orgcbd20a42012-06-27 13:49:27 +0000520 # Update the remotes first so we have all the refs.
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000521 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
bauerb@chromium.orgcbd20a42012-06-27 13:49:27 +0000522 cwd=self.checkout_path)
bauerb@chromium.orgcbd20a42012-06-27 13:49:27 +0000523 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000524 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000525
mmoss@chromium.org37ac0e32015-08-18 18:14:38 +0000526 self._UpdateBranchHeads(options, fetch=True)
mmoss@chromium.orge409df62013-04-16 17:28:57 +0000527
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200528 revision = self._AutoFetchRef(options, revision)
529
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000530 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000531 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000532 target = 'HEAD'
533 if options.upstream and upstream_branch:
534 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800535 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000536
msb@chromium.org786fb682010-06-02 15:16:23 +0000537 if current_type == 'detached':
538 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800539 # We just did a Scrub, this is as clean as it's going to get. In
540 # particular if HEAD is a commit that contains two versions of the same
541 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
542 # to actually "Clean" the checkout; that commit is uncheckoutable on this
543 # system. The best we can do is carry forward to the checkout step.
544 if not (options.force or options.reset):
545 self._CheckClean(revision)
agable83faed02016-10-24 14:37:10 -0700546 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000547 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000548 self.Print('Up-to-date; skipping checkout.')
549 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000550 # 'git checkout' may need to overwrite existing untracked files. Allow
551 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000552 self._Checkout(
553 options,
554 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000555 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000556 quiet=True,
557 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000558 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700559 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000560 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000561 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700562 # Can't find a merge-base since we don't know our upstream. That makes
563 # this command VERY likely to produce a rebase failure. For now we
564 # assume origin is our upstream since that's what the old behavior was.
565 upstream_branch = self.remote
566 if options.revision or deps_revision:
567 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700568 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700569 printed_path=printed_path, merge=options.merge)
570 printed_path = True
smut@google.comd33eab32014-07-07 19:35:18 +0000571 elif rev_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000572 # case 2
agable1a8439a2016-10-24 16:36:14 -0700573 self._AttemptRebase(upstream_branch, file_list, options,
smut@google.comd33eab32014-07-07 19:35:18 +0000574 newbase=revision, printed_path=printed_path,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000575 merge=options.merge)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000576 printed_path = True
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000577 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000578 # case 4
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000579 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000580 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700581 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000582 switch_error = ("Could not switch upstream branch from %s to %s\n"
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000583 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000584 "Please use --force or merge or rebase manually:\n" +
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000585 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
586 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000587 force_switch = False
588 if options.force:
589 try:
agable83faed02016-10-24 14:37:10 -0700590 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000591 # case 4a
592 force_switch = True
593 except gclient_utils.Error as e:
594 if options.reset:
595 # case 4b
596 force_switch = True
597 else:
598 switch_error = '%s\n%s' % (e.message, switch_error)
599 if force_switch:
600 self.Print("Switching upstream branch from %s to %s" %
601 (upstream_branch, new_base))
602 switch_branch = 'gclient_' + remote_ref[1]
603 self._Capture(['branch', '-f', switch_branch, new_base])
604 self._Checkout(options, switch_branch, force=True, quiet=True)
605 else:
606 # case 4c
607 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000608 else:
609 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800610 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000611 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000612 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000613 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000614 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000615 if options.merge:
616 merge_args.append('--ff')
617 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000618 merge_args.append('--ff-only')
619 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000620 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000621 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700622 rebase_files = []
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000623 if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr):
624 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700625 self.Print('_____ %s at %s' % (self.relpath, revision),
626 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000627 printed_path = True
628 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000629 if not options.auto_rebase:
630 try:
631 action = self._AskForData(
632 'Cannot %s, attempt to rebase? '
633 '(y)es / (q)uit / (s)kip : ' %
634 ('merge' if options.merge else 'fast-forward merge'),
635 options)
636 except ValueError:
637 raise gclient_utils.Error('Invalid Character')
638 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700639 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000640 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000641 printed_path = True
642 break
643 elif re.match(r'quit|q', action, re.I):
644 raise gclient_utils.Error("Can't fast-forward, please merge or "
645 "rebase manually.\n"
646 "cd %s && git " % self.checkout_path
647 + "rebase %s" % upstream_branch)
648 elif re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000649 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000650 return
651 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000652 self.Print('Input not recognized')
smut@google.com27c9c8a2014-09-11 19:57:55 +0000653 elif re.match("error: Your local changes to '.*' would be "
654 "overwritten by merge. Aborting.\nPlease, commit your "
655 "changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000656 e.stderr):
657 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700658 self.Print('_____ %s at %s' % (self.relpath, revision),
659 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000660 printed_path = True
661 raise gclient_utils.Error(e.stderr)
662 else:
663 # Some other problem happened with the merge
664 logging.error("Error during fast-forward merge in %s!" % self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000665 self.Print(e.stderr)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000666 raise
667 else:
668 # Fast-forward merge was successful
669 if not re.match('Already up-to-date.', merge_output) or verbose:
670 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700671 self.Print('_____ %s at %s' % (self.relpath, revision),
672 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000673 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000674 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000675 if not verbose:
676 # Make the output a little prettier. It's nice to have some
677 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000678 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000679
agablec3937b92016-10-25 10:13:03 -0700680 if file_list is not None:
681 file_list.extend(
682 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000683
684 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000685 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700686 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000687 '\nConflict while rebasing this branch.\n'
688 'Fix the conflict and run gclient again.\n'
689 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700690 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000691
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000692 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000693 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
694 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000695
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000696 # If --reset and --delete_unversioned_trees are specified, remove any
697 # untracked directories.
698 if options.reset and options.delete_unversioned_trees:
699 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
700 # merge-base by default), so doesn't include untracked files. So we use
701 # 'git ls-files --directory --others --exclude-standard' here directly.
702 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800703 ['-c', 'core.quotePath=false', 'ls-files',
704 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000705 self.checkout_path)
706 for path in (p for p in paths.splitlines() if p.endswith('/')):
707 full_path = os.path.join(self.checkout_path, path)
708 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000709 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000710 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000711
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000712 return self._Capture(['rev-parse', '--verify', 'HEAD'])
713
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000714 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000715 """Reverts local modifications.
716
717 All reverted files will be appended to file_list.
718 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000719 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000720 # revert won't work if the directory doesn't exist. It needs to
721 # checkout instead.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000722 self.Print('_____ %s is missing, synching instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000723 # Don't reuse the args.
724 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000725
726 default_rev = "refs/heads/master"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000727 if options.upstream:
728 if self._GetCurrentBranch():
729 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
730 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000731 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000732 if not deps_revision:
733 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000734 if deps_revision.startswith('refs/heads/'):
735 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700736 try:
737 deps_revision = self.GetUsableRev(deps_revision, options)
738 except NoUsableRevError as e:
739 # If the DEPS entry's url and hash changed, try to update the origin.
740 # See also http://crbug.com/520067.
741 logging.warn(
742 'Couldn\'t find usable revision, will retrying to update instead: %s',
743 e.message)
744 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000745
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000746 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800747 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000748
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800749 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000750 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000751
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000752 if file_list is not None:
753 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
754
755 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000756 """Returns revision"""
757 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000758
msb@chromium.orge28e4982009-09-25 20:51:45 +0000759 def runhooks(self, options, args, file_list):
760 self.status(options, args, file_list)
761
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000762 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000763 """Display status information."""
764 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000765 self.Print('________ couldn\'t run status in %s:\n'
766 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000767 else:
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700768 try:
769 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
770 except subprocess2.CalledProcessError:
771 merge_base = []
Aaron Gablef4068aa2017-12-12 15:14:09 -0800772 self._Run(
773 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
774 options, stdout=self.out_fh, always=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000775 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800776 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000777 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000778
smutae7ea312016-07-18 11:59:41 -0700779 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700780 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700781 sha1 = None
782 if not os.path.isdir(self.checkout_path):
783 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800784 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700785
786 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
787 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700788 else:
agable41e3a6c2016-10-20 11:36:56 -0700789 # May exist in origin, but we don't have it yet, so fetch and look
790 # again.
791 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -0700792 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
793 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700794
795 if not sha1:
796 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800797 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -0700798
799 return sha1
800
msb@chromium.orge6f78352010-01-13 17:05:33 +0000801 def FullUrlForRelativeUrl(self, url):
802 # Strip from last '/'
803 # Equivalent to unix basename
804 base_url = self.url
805 return base_url[:base_url.rfind('/')] + url
806
primiano@chromium.org1c127382015-02-17 11:15:40 +0000807 def GetGitBackupDirPath(self):
808 """Returns the path where the .git folder for the current project can be
809 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
810 return os.path.join(self._root_dir,
811 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
812
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000813 def _GetMirror(self, url, options):
814 """Get a git_cache.Mirror object for the argument url."""
815 if not git_cache.Mirror.GetCachePath():
816 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000817 mirror_kwargs = {
818 'print_func': self.filter,
819 'refs': []
820 }
hinoka@google.comb1b54572014-04-16 22:29:23 +0000821 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
822 mirror_kwargs['refs'].append('refs/branch-heads/*')
szager@chromium.org8d3348f2014-08-19 22:49:16 +0000823 if hasattr(options, 'with_tags') and options.with_tags:
824 mirror_kwargs['refs'].append('refs/tags/*')
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000825 return git_cache.Mirror(url, **mirror_kwargs)
826
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800827 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
828 """Update a git mirror by fetching the latest commits from the remote,
829 unless mirror already contains revision whose type is sha1 hash.
830 """
831 if rev_type == 'hash' and mirror.contains_revision(revision):
832 if options.verbose:
833 self.Print('skipping mirror update, it has rev=%s already' % revision,
834 timestamp=False)
835 return
836
szager@chromium.org3ec84f62014-08-22 21:00:22 +0000837 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000838 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000839 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000840 depth = 10
841 else:
842 depth = 10000
843 else:
844 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +0000845 mirror.populate(verbose=options.verbose,
846 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +0000847 depth=depth,
848 ignore_lock=getattr(options, 'ignore_locks', False),
849 lock_timeout=getattr(options, 'lock_timeout', 0))
850 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000851
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000852 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000853 """Clone a git repository from the given URL.
854
msb@chromium.org786fb682010-06-02 15:16:23 +0000855 Once we've cloned the repo, we checkout a working branch if the specified
856 revision is a branch head. If it is a tag or a specific commit, then we
857 leave HEAD detached as it makes future updates simpler -- in this case the
858 user should first create a new branch or switch to an existing branch before
859 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000860 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000861 # git clone doesn't seem to insert a newline properly before printing
862 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000863 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +0000864 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +0000865 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000866 if self.cache_dir:
867 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000868 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000869 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000870 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +0000871 # If the parent directory does not exist, Git clone on Windows will not
872 # create it, so we need to do it manually.
873 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000874 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000875
876 template_dir = None
877 if hasattr(options, 'no_history') and options.no_history:
878 if gclient_utils.IsGitSha(revision):
879 # In the case of a subproject, the pinned sha is not necessarily the
880 # head of the remote branch (so we can't just use --depth=N). Instead,
881 # we tell git to fetch all the remote objects from SHA..HEAD by means of
882 # a template git dir which has a 'shallow' file pointing to the sha.
883 template_dir = tempfile.mkdtemp(
884 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
885 dir=parent_dir)
886 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
887 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
888 template_file.write(revision)
889 clone_cmd.append('--template=' + template_dir)
890 else:
891 # Otherwise, we're just interested in the HEAD. Just use --depth.
892 clone_cmd.append('--depth=1')
893
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000894 tmp_dir = tempfile.mkdtemp(
895 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
896 dir=parent_dir)
897 try:
898 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +0100899 if self.print_outbuf:
900 print_stdout = True
901 stdout = gclient_utils.WriteToStdout(self.out_fh)
902 else:
903 print_stdout = False
904 stdout = self.out_fh
905 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
906 print_stdout=print_stdout, stdout=stdout)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000907 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000908 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
909 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +0000910 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000911 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +0000912 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000913 finally:
914 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000915 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000916 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000917 if template_dir:
918 gclient_utils.rmtree(template_dir)
mmoss@chromium.org1a6bec02014-06-02 21:53:29 +0000919 self._UpdateBranchHeads(options, fetch=True)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200920 revision = self._AutoFetchRef(options, revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000921 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
922 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000923 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +0000924 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000925 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +0000926 ('Checked out %s to a detached HEAD. Before making any commits\n'
927 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
928 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
929 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000930
szager@chromium.org6cd41b62014-04-21 23:55:22 +0000931 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000932 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +0000933 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000934 raise gclient_utils.Error("Background task requires input. Rerun "
935 "gclient with --jobs=1 so that\n"
936 "interaction is possible.")
937 try:
938 return raw_input(prompt)
939 except KeyboardInterrupt:
940 # Hide the exception.
941 sys.exit(1)
942
943
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000944 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000945 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000946 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000947 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800948 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000949 revision = upstream
950 if newbase:
951 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000952 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000953 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000954 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000955 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000956 printed_path = True
957 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000958 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000959
960 if merge:
961 merge_output = self._Capture(['merge', revision])
962 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000963 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000964 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000965
966 # Build the rebase command here using the args
967 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
968 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000969 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000970 rebase_cmd.append('--verbose')
971 if newbase:
972 rebase_cmd.extend(['--onto', newbase])
973 rebase_cmd.append(upstream)
974 if branch:
975 rebase_cmd.append(branch)
976
977 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000978 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +0000979 except subprocess2.CalledProcessError, e:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000980 if (re.match(r'cannot rebase: you have unstaged changes', e.stderr) or
981 re.match(r'cannot rebase: your index contains uncommitted changes',
982 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000983 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000984 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +0000985 'Cannot rebase because of unstaged changes.\n'
986 '\'git reset --hard HEAD\' ?\n'
987 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +0000988 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000989 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800990 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000991 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000992 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000993 break
994 elif re.match(r'quit|q', rebase_action, re.I):
995 raise gclient_utils.Error("Please merge or rebase manually\n"
996 "cd %s && git " % self.checkout_path
997 + "%s" % ' '.join(rebase_cmd))
998 elif re.match(r'show|s', rebase_action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000999 self.Print('%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001000 continue
1001 else:
1002 gclient_utils.Error("Input not recognized")
1003 continue
1004 elif re.search(r'^CONFLICT', e.stdout, re.M):
1005 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1006 "Fix the conflict and run gclient again.\n"
1007 "See 'man git-rebase' for details.\n")
1008 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001009 self.Print(e.stdout.strip())
1010 self.Print('Rebase produced error output:\n%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001011 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1012 "manually.\ncd %s && git " %
1013 self.checkout_path
1014 + "%s" % ' '.join(rebase_cmd))
1015
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001016 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001017 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001018 # Make the output a little prettier. It's nice to have some
1019 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001020 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001021
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001022 @staticmethod
1023 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001024 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1025 if not ok:
1026 raise gclient_utils.Error('git version %s < minimum required %s' %
1027 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001028
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001029 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
1030 # Special case handling if all 3 conditions are met:
1031 # * the mirros have recently changed, but deps destination remains same,
1032 # * the git histories of mirrors are conflicting.
1033 # * git cache is used
1034 # This manifests itself in current checkout having invalid HEAD commit on
1035 # most git operations. Since git cache is used, just deleted the .git
1036 # folder, and re-create it by cloning.
1037 try:
1038 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1039 except subprocess2.CalledProcessError as e:
1040 if ('fatal: bad object HEAD' in e.stderr
1041 and self.cache_dir and self.cache_dir in url):
1042 self.Print((
1043 'Likely due to DEPS change with git cache_dir, '
1044 'the current commit points to no longer existing object.\n'
1045 '%s' % e)
1046 )
1047 self._DeleteOrMove(options.force)
1048 self._Clone(revision, url, options)
1049 else:
1050 raise
1051
msb@chromium.org786fb682010-06-02 15:16:23 +00001052 def _IsRebasing(self):
1053 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1054 # have a plumbing command to determine whether a rebase is in progress, so
1055 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1056 g = os.path.join(self.checkout_path, '.git')
1057 return (
1058 os.path.isdir(os.path.join(g, "rebase-merge")) or
1059 os.path.isdir(os.path.join(g, "rebase-apply")))
1060
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001061 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001062 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1063 if os.path.exists(lockfile):
1064 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001065 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001066 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1067 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001068 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001069
msb@chromium.org786fb682010-06-02 15:16:23 +00001070 # Make sure the tree is clean; see git-rebase.sh for reference
1071 try:
1072 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001073 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001074 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001075 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001076 '\tYou have unstaged changes.\n'
1077 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001078 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001079 try:
1080 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001081 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001082 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001083 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001084 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001085 '\tYour index contains uncommitted changes\n'
1086 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001087 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001088
agable83faed02016-10-24 14:37:10 -07001089 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001090 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1091 # reference by a commit). If not, error out -- most likely a rebase is
1092 # in progress, try to detect so we can give a better error.
1093 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001094 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1095 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001096 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001097 # Commit is not contained by any rev. See if the user is rebasing:
1098 if self._IsRebasing():
1099 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001100 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001101 '\tAlready in a conflict, i.e. (no branch).\n'
1102 '\tFix the conflict and run gclient again.\n'
1103 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1104 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001105 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001106 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001107 name = ('saved-by-gclient-' +
1108 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001109 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001110 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001111 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001112
msb@chromium.org5bde4852009-12-14 16:47:12 +00001113 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001114 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001115 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001116 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001117 return None
1118 return branch
1119
borenet@google.comc3e09d22014-04-10 13:58:18 +00001120 def _Capture(self, args, **kwargs):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001121 kwargs.setdefault('cwd', self.checkout_path)
1122 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001123 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001124 env = scm.GIT.ApplyEnvVars(kwargs)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001125 ret = subprocess2.check_output(['git'] + args, env=env, **kwargs)
1126 if strip:
1127 ret = ret.strip()
1128 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001129
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001130 def _Checkout(self, options, ref, force=False, quiet=None):
1131 """Performs a 'git-checkout' operation.
1132
1133 Args:
1134 options: The configured option set
1135 ref: (str) The branch/commit to checkout
1136 quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
1137 'None', the behavior is inferred from 'options.verbose'.
1138 Returns: (str) The output of the checkout operation
1139 """
1140 if quiet is None:
1141 quiet = (not options.verbose)
1142 checkout_args = ['checkout']
1143 if force:
1144 checkout_args.append('--force')
1145 if quiet:
1146 checkout_args.append('--quiet')
1147 checkout_args.append(ref)
1148 return self._Capture(checkout_args)
1149
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001150 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1151 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001152 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
1153 fetch_cmd = cfg + [
1154 'fetch',
1155 remote or self.remote,
1156 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001157 if refspec:
1158 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001159
1160 if prune:
1161 fetch_cmd.append('--prune')
1162 if options.verbose:
1163 fetch_cmd.append('--verbose')
1164 elif quiet:
1165 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001166 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001167
1168 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1169 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1170
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001171 def _UpdateBranchHeads(self, options, fetch=False):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001172 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1173 if requested."""
1174 need_fetch = fetch
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001175 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001176 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001177 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1178 '^\\+refs/branch-heads/\\*:.*$']
1179 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001180 need_fetch = True
1181 if hasattr(options, 'with_tags') and options.with_tags:
1182 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1183 '+refs/tags/*:refs/tags/*',
1184 '^\\+refs/tags/\\*:.*$']
1185 self._Run(config_cmd, options)
1186 need_fetch = True
1187 if fetch and need_fetch:
bpastene2a3e9912016-09-07 16:22:25 -07001188 self._Fetch(options, prune=options.force)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001189
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001190 def _AutoFetchRef(self, options, revision):
1191 """Attempts to fetch |revision| if not available in local repo.
1192
1193 Returns possibly updated revision."""
1194 try:
1195 self._Capture(['rev-parse', revision])
1196 except subprocess2.CalledProcessError:
1197 self._Fetch(options, refspec=revision)
1198 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1199 return revision
1200
dnj@chromium.org680f2172014-06-25 00:39:32 +00001201 def _Run(self, args, options, show_header=True, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001202 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001203 kwargs.setdefault('cwd', self.checkout_path)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001204 kwargs.setdefault('stdout', self.out_fh)
szager@chromium.org848fd492014-04-09 19:06:44 +00001205 kwargs['filter_fn'] = self.filter
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001206 kwargs.setdefault('print_stdout', False)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001207 env = scm.GIT.ApplyEnvVars(kwargs)
agable@chromium.org772efaf2014-04-01 02:35:44 +00001208 cmd = ['git'] + args
dnj@chromium.org680f2172014-06-25 00:39:32 +00001209 if show_header:
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001210 gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs)
1211 else:
1212 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001213
1214
1215class CipdPackage(object):
1216 """A representation of a single CIPD package."""
1217
1218 def __init__(self, name, version, authority_for_root, authority_for_subdir):
1219 self._authority_for_root = authority_for_root
1220 self._authority_for_subdir = authority_for_subdir
1221 self._name = name
1222 self._version = version
1223
1224 @property
1225 def authority_for_root(self):
1226 """Whether this package has authority to act on behalf of its root.
1227
1228 Some operations should only be performed once per cipd root. A package
1229 that has authority for its cipd root is the only package that should
1230 perform such operations.
1231
1232 Returns:
1233 bool; whether this package has root authority.
1234 """
1235 return self._authority_for_root
1236
1237 @property
1238 def authority_for_subdir(self):
1239 """Whether this package has authority to act on behalf of its subdir.
1240
1241 Some operations should only be performed once per subdirectory. A package
1242 that has authority for its subdirectory is the only package that should
1243 perform such operations.
1244
1245 Returns:
1246 bool; whether this package has subdir authority.
1247 """
1248 return self._authority_for_subdir
1249
1250 @property
1251 def name(self):
1252 return self._name
1253
1254 @property
1255 def version(self):
1256 return self._version
1257
1258
1259class CipdRoot(object):
1260 """A representation of a single CIPD root."""
1261 def __init__(self, root_dir, service_url):
1262 self._all_packages = set()
1263 self._mutator_lock = threading.Lock()
1264 self._packages_by_subdir = collections.defaultdict(list)
1265 self._root_dir = root_dir
1266 self._service_url = service_url
1267
1268 def add_package(self, subdir, package, version):
1269 """Adds a package to this CIPD root.
1270
1271 As far as clients are concerned, this grants both root and subdir authority
1272 to packages arbitrarily. (The implementation grants root authority to the
1273 first package added and subdir authority to the first package added for that
1274 subdir, but clients should not depend on or expect that behavior.)
1275
1276 Args:
1277 subdir: str; relative path to where the package should be installed from
1278 the cipd root directory.
1279 package: str; the cipd package name.
1280 version: str; the cipd package version.
1281 Returns:
1282 CipdPackage; the package that was created and added to this root.
1283 """
1284 with self._mutator_lock:
1285 cipd_package = CipdPackage(
1286 package, version,
1287 not self._packages_by_subdir,
1288 not self._packages_by_subdir[subdir])
1289 self._all_packages.add(cipd_package)
1290 self._packages_by_subdir[subdir].append(cipd_package)
1291 return cipd_package
1292
1293 def packages(self, subdir):
1294 """Get the list of configured packages for the given subdir."""
1295 return list(self._packages_by_subdir[subdir])
1296
1297 def clobber(self):
1298 """Remove the .cipd directory.
1299
1300 This is useful for forcing ensure to redownload and reinitialize all
1301 packages.
1302 """
1303 with self._mutator_lock:
1304 cipd_cache_dir = os.path.join(self._cipd_root, '.cipd')
1305 try:
1306 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1307 except OSError:
1308 if os.path.exists(cipd_cache_dir):
1309 raise
1310
1311 @contextlib.contextmanager
1312 def _create_ensure_file(self):
1313 try:
1314 ensure_file = None
1315 with tempfile.NamedTemporaryFile(
1316 suffix='.ensure', delete=False) as ensure_file:
1317 for subdir, packages in sorted(self._packages_by_subdir.iteritems()):
1318 ensure_file.write('@Subdir %s\n' % subdir)
1319 for package in packages:
1320 ensure_file.write('%s %s\n' % (package.name, package.version))
1321 ensure_file.write('\n')
1322 yield ensure_file.name
1323 finally:
1324 if ensure_file is not None and os.path.exists(ensure_file.name):
1325 os.remove(ensure_file.name)
1326
1327 def ensure(self):
1328 """Run `cipd ensure`."""
1329 with self._mutator_lock:
1330 with self._create_ensure_file() as ensure_file:
1331 cmd = [
1332 'cipd', 'ensure',
1333 '-log-level', 'error',
1334 '-root', self.root_dir,
1335 '-ensure-file', ensure_file,
1336 ]
1337 gclient_utils.CheckCallAndFilterAndHeader(cmd)
1338
1339 def created_package(self, package):
1340 """Checks whether this root created the given package.
1341
1342 Args:
1343 package: CipdPackage; the package to check.
1344 Returns:
1345 bool; whether this root created the given package.
1346 """
1347 return package in self._all_packages
1348
1349 @property
1350 def root_dir(self):
1351 return self._root_dir
1352
1353 @property
1354 def service_url(self):
1355 return self._service_url
1356
1357
1358class CipdWrapper(SCMWrapper):
1359 """Wrapper for CIPD.
1360
1361 Currently only supports chrome-infra-packages.appspot.com.
1362 """
John Budorick3929e9e2018-02-04 18:18:07 -08001363 name = 'cipd'
John Budorick0f7b2002018-01-19 15:46:17 -08001364
1365 def __init__(self, url=None, root_dir=None, relpath=None, out_fh=None,
1366 out_cb=None, root=None, package=None):
1367 super(CipdWrapper, self).__init__(
1368 url=url, root_dir=root_dir, relpath=relpath, out_fh=out_fh,
1369 out_cb=out_cb)
1370 assert root.created_package(package)
1371 self._package = package
1372 self._root = root
1373
1374 #override
1375 def GetCacheMirror(self):
1376 return None
1377
1378 #override
1379 def GetActualRemoteURL(self, options):
1380 return self._root.service_url
1381
1382 #override
1383 def DoesRemoteURLMatch(self, options):
1384 del options
1385 return True
1386
1387 def revert(self, options, args, file_list):
1388 """Deletes .cipd and reruns ensure."""
1389 if self._package.authority_for_root:
1390 self._root.clobber()
1391 self._root.ensure()
1392
1393 def diff(self, options, args, file_list):
1394 """CIPD has no notion of diffing."""
1395 pass
1396
1397 def pack(self, options, args, file_list):
1398 """CIPD has no notion of diffing."""
1399 pass
1400
1401 def revinfo(self, options, args, file_list):
1402 """Grab the instance ID."""
1403 try:
1404 tmpdir = tempfile.mkdtemp()
1405 describe_json_path = os.path.join(tmpdir, 'describe.json')
1406 cmd = [
1407 'cipd', 'describe',
1408 self._package.name,
1409 '-log-level', 'error',
1410 '-version', self._package.version,
1411 '-json-output', describe_json_path
1412 ]
1413 gclient_utils.CheckCallAndFilter(
1414 cmd, filter_fn=lambda _line: None, print_stdout=False)
1415 with open(describe_json_path) as f:
1416 describe_json = json.load(f)
1417 return describe_json.get('result', {}).get('pin', {}).get('instance_id')
1418 finally:
1419 gclient_utils.rmtree(tmpdir)
1420
1421 def status(self, options, args, file_list):
1422 pass
1423
1424 def update(self, options, args, file_list):
1425 """Runs ensure."""
1426 if self._package.authority_for_root:
1427 self._root.ensure()