blob: 80470ad57b7adbfc1abb70a2ad18c7206b2b6553 [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):
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700255 try:
256 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
257 except subprocess2.CalledProcessError:
258 merge_base = []
Aaron Gablef4068aa2017-12-12 15:14:09 -0800259 self._Run(['-c', 'core.quotePath=false', 'diff'] + merge_base, options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000260
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000261 def pack(self, _options, _args, _file_list):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000262 """Generates a patch file which can be applied to the root of the
msb@chromium.orgd6504212010-01-13 17:34:31 +0000263 repository.
264
265 The patch file is generated from a diff of the merge base of HEAD and
266 its upstream branch.
267 """
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700268 try:
269 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
270 except subprocess2.CalledProcessError:
271 merge_base = []
maruel@chromium.org17d01792010-09-01 18:07:10 +0000272 gclient_utils.CheckCallAndFilter(
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700273 ['git', 'diff'] + merge_base,
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000274 cwd=self.checkout_path,
avakulenko@google.com255f2be2014-12-05 22:19:55 +0000275 filter_fn=GitDiffFilterer(self.relpath, print_func=self.Print).Filter)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000276
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800277 def _Scrub(self, target, options):
278 """Scrubs out all changes in the local repo, back to the state of target."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000279 quiet = []
280 if not options.verbose:
281 quiet = ['--quiet']
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800282 self._Run(['reset', '--hard', target] + quiet, options)
283 if options.force and options.delete_unversioned_trees:
284 # where `target` is a commit that contains both upper and lower case
285 # versions of the same file on a case insensitive filesystem, we are
286 # actually in a broken state here. The index will have both 'a' and 'A',
287 # but only one of them will exist on the disk. To progress, we delete
288 # everything that status thinks is modified.
Aaron Gable7817f022017-12-12 09:43:17 -0800289 output = self._Capture([
290 '-c', 'core.quotePath=false', 'status', '--porcelain'], strip=False)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800291 for line in output.splitlines():
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800292 # --porcelain (v1) looks like:
293 # XY filename
294 try:
295 filename = line[3:]
296 self.Print('_____ Deleting residual after reset: %r.' % filename)
297 gclient_utils.rm_file_or_tree(
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -0800298 os.path.join(self.checkout_path, filename))
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800299 except OSError:
300 pass
301
302 def _FetchAndReset(self, revision, file_list, options):
303 """Equivalent to git fetch; git reset."""
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000304 self._UpdateBranchHeads(options, fetch=False)
305
dnj@chromium.org680f2172014-06-25 00:39:32 +0000306 self._Fetch(options, prune=True, quiet=options.verbose)
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800307 self._Scrub(revision, options)
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000308 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800309 files = self._Capture(
310 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000311 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
312
szager@chromium.org8a139702014-06-20 15:55:01 +0000313 def _DisableHooks(self):
314 hook_dir = os.path.join(self.checkout_path, '.git', 'hooks')
315 if not os.path.isdir(hook_dir):
316 return
317 for f in os.listdir(hook_dir):
318 if not f.endswith('.sample') and not f.endswith('.disabled'):
primiano@chromium.org41265562015-04-08 09:14:46 +0000319 disabled_hook_path = os.path.join(hook_dir, f + '.disabled')
320 if os.path.exists(disabled_hook_path):
321 os.remove(disabled_hook_path)
322 os.rename(os.path.join(hook_dir, f), disabled_hook_path)
szager@chromium.org8a139702014-06-20 15:55:01 +0000323
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000324 def _maybe_break_locks(self, options):
325 """This removes all .lock files from this repo's .git directory, if the
326 user passed the --break_repo_locks command line flag.
327
328 In particular, this will cleanup index.lock files, as well as ref lock
329 files.
330 """
331 if options.break_repo_locks:
332 git_dir = os.path.join(self.checkout_path, '.git')
333 for path, _, filenames in os.walk(git_dir):
334 for filename in filenames:
335 if filename.endswith('.lock'):
336 to_break = os.path.join(path, filename)
337 self.Print('breaking lock: %s' % (to_break,))
338 try:
339 os.remove(to_break)
340 except OSError as ex:
341 self.Print('FAILED to break lock: %s: %s' % (to_break, ex))
342 raise
343
msb@chromium.orge28e4982009-09-25 20:51:45 +0000344 def update(self, options, args, file_list):
345 """Runs git to update or transparently checkout the working copy.
346
347 All updated files will be appended to file_list.
348
349 Raises:
350 Error: if can't get URL for relative path.
351 """
msb@chromium.orge28e4982009-09-25 20:51:45 +0000352 if args:
353 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
354
nasser@codeaurora.orgece406f2010-02-23 17:29:15 +0000355 self._CheckMinVersion("1.6.6")
msb@chromium.org923a0372009-12-11 20:42:43 +0000356
maruel@chromium.org1a60dca2013-11-26 14:06:26 +0000357 # If a dependency is not pinned, track the default remote branch.
smut@google.comd33eab32014-07-07 19:35:18 +0000358 default_rev = 'refs/remotes/%s/master' % self.remote
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000359 url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.org7080e942010-03-15 15:06:16 +0000360 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000361 managed = True
msb@chromium.orge28e4982009-09-25 20:51:45 +0000362 if options.revision:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000363 # Override the revision number.
364 revision = str(options.revision)
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000365 if revision == 'unmanaged':
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000366 # Check again for a revision in case an initial ref was specified
367 # in the url, for example bla.git@refs/heads/custombranch
368 revision = deps_revision
cmp@chromium.orgeb2756d2011-09-20 20:17:51 +0000369 managed = False
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000370 if not revision:
371 revision = default_rev
msb@chromium.orge28e4982009-09-25 20:51:45 +0000372
szager@chromium.org8a139702014-06-20 15:55:01 +0000373 if managed:
374 self._DisableHooks()
375
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000376 printed_path = False
377 verbose = []
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000378 if options.verbose:
agable83faed02016-10-24 14:37:10 -0700379 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000380 verbose = ['--verbose']
381 printed_path = True
382
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000383 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
384 if remote_ref:
smut@google.comd33eab32014-07-07 19:35:18 +0000385 # Rewrite remote refs to their local equivalents.
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000386 revision = ''.join(remote_ref)
387 rev_type = "branch"
388 elif revision.startswith('refs/'):
389 # Local branch? We probably don't want to support, since DEPS should
390 # always specify branches as they are in the upstream repo.
smut@google.comd33eab32014-07-07 19:35:18 +0000391 rev_type = "branch"
392 else:
393 # hash is also a tag, only make a distinction at checkout
394 rev_type = "hash"
395
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000396 mirror = self._GetMirror(url, options)
397 if mirror:
398 url = mirror.mirror_path
399
primiano@chromium.org1c127382015-02-17 11:15:40 +0000400 # If we are going to introduce a new project, there is a possibility that
401 # we are syncing back to a state where the project was originally a
402 # sub-project rolled by DEPS (realistic case: crossing the Blink merge point
403 # syncing backwards, when Blink was a DEPS entry and not part of src.git).
404 # In such case, we might have a backup of the former .git folder, which can
405 # be used to avoid re-fetching the entire repo again (useful for bisects).
406 backup_dir = self.GetGitBackupDirPath()
407 target_dir = os.path.join(self.checkout_path, '.git')
408 if os.path.exists(backup_dir) and not os.path.exists(target_dir):
409 gclient_utils.safe_makedirs(self.checkout_path)
410 os.rename(backup_dir, target_dir)
411 # Reset to a clean state
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800412 self._Scrub('HEAD', options)
primiano@chromium.org1c127382015-02-17 11:15:40 +0000413
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000414 if (not os.path.exists(self.checkout_path) or
415 (os.path.isdir(self.checkout_path) and
416 not os.path.exists(os.path.join(self.checkout_path, '.git')))):
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000417 if mirror:
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800418 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000419 try:
smut@google.comd33eab32014-07-07 19:35:18 +0000420 self._Clone(revision, url, options)
borenet@google.com90fe58b2014-05-01 18:22:00 +0000421 except subprocess2.CalledProcessError:
422 self._DeleteOrMove(options.force)
smut@google.comd33eab32014-07-07 19:35:18 +0000423 self._Clone(revision, url, options)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000424 if file_list is not None:
Aaron Gable7817f022017-12-12 09:43:17 -0800425 files = self._Capture(
426 ['-c', 'core.quotePath=false', 'ls-files']).splitlines()
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000427 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000428 if not verbose:
429 # Make the output a little prettier. It's nice to have some whitespace
430 # between projects when cloning.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000431 self.Print('')
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000432 return self._Capture(['rev-parse', '--verify', 'HEAD'])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000433
szager@chromium.org3dc5cb72014-06-17 15:06:05 +0000434 if not managed:
435 self._UpdateBranchHeads(options, fetch=False)
436 self.Print('________ unmanaged solution; skipping %s' % self.relpath)
437 return self._Capture(['rev-parse', '--verify', 'HEAD'])
438
iannucci@chromium.org30a07982016-04-07 21:35:19 +0000439 self._maybe_break_locks(options)
440
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000441 if mirror:
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800442 self._UpdateMirrorIfNotContains(mirror, options, rev_type, revision)
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000443
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000444 # See if the url has changed (the unittests use git://foo for the url, let
445 # that through).
446 current_url = self._Capture(['config', 'remote.%s.url' % self.remote])
447 return_early = False
448 # TODO(maruel): Delete url != 'git://foo' since it's just to make the
449 # unit test pass. (and update the comment above)
450 # Skip url auto-correction if remote.origin.gclient-auto-fix-url is set.
451 # This allows devs to use experimental repos which have a different url
452 # but whose branch(s) are the same as official repos.
borenet@google.comb09097a2014-04-09 19:09:08 +0000453 if (current_url.rstrip('/') != url.rstrip('/') and
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000454 url != 'git://foo' and
455 subprocess2.capture(
456 ['git', 'config', 'remote.%s.gclient-auto-fix-url' % self.remote],
457 cwd=self.checkout_path).strip() != 'False'):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000458 self.Print('_____ switching %s to a new upstream' % self.relpath)
iannucci@chromium.org78514212014-08-20 23:08:00 +0000459 if not (options.force or options.reset):
460 # Make sure it's clean
agable83faed02016-10-24 14:37:10 -0700461 self._CheckClean(revision)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000462 # Switch over to the new upstream
463 self._Run(['remote', 'set-url', self.remote, url], options)
szager@chromium.org8dd35462015-06-08 22:56:05 +0000464 if mirror:
465 with open(os.path.join(
466 self.checkout_path, '.git', 'objects', 'info', 'alternates'),
467 'w') as fh:
468 fh.write(os.path.join(url, 'objects'))
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000469 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
smut@google.comd33eab32014-07-07 19:35:18 +0000470 self._FetchAndReset(revision, file_list, options)
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000471
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000472 return_early = True
tandrii@chromium.orgc438c142015-08-24 22:55:55 +0000473 else:
474 self._EnsureValidHeadObjectOrCheckout(revision, options, url)
szager@chromium.org6c2b49d2014-02-26 23:57:38 +0000475
mmoss@chromium.org50fd47f2014-02-13 01:03:19 +0000476 if return_early:
477 return self._Capture(['rev-parse', '--verify', 'HEAD'])
478
msb@chromium.org5bde4852009-12-14 16:47:12 +0000479 cur_branch = self._GetCurrentBranch()
480
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000481 # Cases:
msb@chromium.org786fb682010-06-02 15:16:23 +0000482 # 0) HEAD is detached. Probably from our initial clone.
483 # - make sure HEAD is contained by a named ref, then update.
484 # Cases 1-4. HEAD is a branch.
agable41e3a6c2016-10-20 11:36:56 -0700485 # 1) current branch is not tracking a remote branch
msb@chromium.org786fb682010-06-02 15:16:23 +0000486 # - try to rebase onto the new hash or branch
487 # 2) current branch is tracking a remote branch with local committed
488 # changes, but the DEPS file switched to point to a hash
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000489 # - rebase those changes on top of the hash
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000490 # 3) current branch is tracking a remote branch w/or w/out changes, and
491 # no DEPS switch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000492 # - see if we can FF, if not, prompt the user for rebase, merge, or stop
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000493 # 4) current branch is tracking a remote branch, but DEPS switches to a
494 # different remote branch, and
495 # a) current branch has no local changes, and --force:
496 # - checkout new branch
497 # b) current branch has local changes, and --force and --reset:
498 # - checkout new branch
499 # c) otherwise exit
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000500
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000501 # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
502 # a tracking branch
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000503 # or 'master' if not a tracking branch (it's based on a specific rev/hash)
504 # or it returns None if it couldn't find an upstream
msb@chromium.org786fb682010-06-02 15:16:23 +0000505 if cur_branch is None:
506 upstream_branch = None
507 current_type = "detached"
508 logging.debug("Detached HEAD")
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000509 else:
msb@chromium.org786fb682010-06-02 15:16:23 +0000510 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
511 if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
512 current_type = "hash"
513 logging.debug("Current branch is not tracking an upstream (remote)"
514 " branch.")
515 elif upstream_branch.startswith('refs/remotes'):
516 current_type = "branch"
517 else:
518 raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000519
smut@google.comd33eab32014-07-07 19:35:18 +0000520 if not scm.GIT.IsValidRevision(self.checkout_path, revision, sha_only=True):
bauerb@chromium.orgcbd20a42012-06-27 13:49:27 +0000521 # Update the remotes first so we have all the refs.
ilevy@chromium.orga41249c2013-07-03 00:09:12 +0000522 remote_output = scm.GIT.Capture(['remote'] + verbose + ['update'],
bauerb@chromium.orgcbd20a42012-06-27 13:49:27 +0000523 cwd=self.checkout_path)
bauerb@chromium.orgcbd20a42012-06-27 13:49:27 +0000524 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000525 self.Print(remote_output)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000526
mmoss@chromium.org37ac0e32015-08-18 18:14:38 +0000527 self._UpdateBranchHeads(options, fetch=True)
mmoss@chromium.orge409df62013-04-16 17:28:57 +0000528
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200529 revision = self._AutoFetchRef(options, revision)
530
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000531 # This is a big hammer, debatable if it should even be here...
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000532 if options.force or options.reset:
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000533 target = 'HEAD'
534 if options.upstream and upstream_branch:
535 target = upstream_branch
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800536 self._Scrub(target, options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000537
msb@chromium.org786fb682010-06-02 15:16:23 +0000538 if current_type == 'detached':
539 # case 0
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800540 # We just did a Scrub, this is as clean as it's going to get. In
541 # particular if HEAD is a commit that contains two versions of the same
542 # file on a case-insensitive filesystem (e.g. 'a' and 'A'), there's no way
543 # to actually "Clean" the checkout; that commit is uncheckoutable on this
544 # system. The best we can do is carry forward to the checkout step.
545 if not (options.force or options.reset):
546 self._CheckClean(revision)
agable83faed02016-10-24 14:37:10 -0700547 self._CheckDetachedHead(revision, options)
smut@google.comd33eab32014-07-07 19:35:18 +0000548 if self._Capture(['rev-list', '-n', '1', 'HEAD']) == revision:
szager@chromium.org848fd492014-04-09 19:06:44 +0000549 self.Print('Up-to-date; skipping checkout.')
550 else:
vadimsh@chromium.org2b7d3ed2014-06-20 18:15:37 +0000551 # 'git checkout' may need to overwrite existing untracked files. Allow
552 # it only when nuclear options are enabled.
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000553 self._Checkout(
554 options,
555 revision,
smut@google.com34b4e982016-05-16 19:06:07 +0000556 force=(options.force and options.delete_unversioned_trees),
dnj@chromium.orgbb424c02014-06-23 22:42:51 +0000557 quiet=True,
558 )
msb@chromium.org786fb682010-06-02 15:16:23 +0000559 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700560 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
msb@chromium.org786fb682010-06-02 15:16:23 +0000561 elif current_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000562 # case 1
agable41e3a6c2016-10-20 11:36:56 -0700563 # Can't find a merge-base since we don't know our upstream. That makes
564 # this command VERY likely to produce a rebase failure. For now we
565 # assume origin is our upstream since that's what the old behavior was.
566 upstream_branch = self.remote
567 if options.revision or deps_revision:
568 upstream_branch = revision
agable1a8439a2016-10-24 16:36:14 -0700569 self._AttemptRebase(upstream_branch, file_list, options,
agable41e3a6c2016-10-20 11:36:56 -0700570 printed_path=printed_path, merge=options.merge)
571 printed_path = True
smut@google.comd33eab32014-07-07 19:35:18 +0000572 elif rev_type == 'hash':
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000573 # case 2
agable1a8439a2016-10-24 16:36:14 -0700574 self._AttemptRebase(upstream_branch, file_list, options,
smut@google.comd33eab32014-07-07 19:35:18 +0000575 newbase=revision, printed_path=printed_path,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000576 merge=options.merge)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000577 printed_path = True
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000578 elif remote_ref and ''.join(remote_ref) != upstream_branch:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000579 # case 4
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000580 new_base = ''.join(remote_ref)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000581 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700582 self.Print('_____ %s at %s' % (self.relpath, revision), timestamp=False)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000583 switch_error = ("Could not switch upstream branch from %s to %s\n"
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000584 % (upstream_branch, new_base) +
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000585 "Please use --force or merge or rebase manually:\n" +
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000586 "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
587 "OR git checkout -b <some new branch> %s" % new_base)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000588 force_switch = False
589 if options.force:
590 try:
agable83faed02016-10-24 14:37:10 -0700591 self._CheckClean(revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000592 # case 4a
593 force_switch = True
594 except gclient_utils.Error as e:
595 if options.reset:
596 # case 4b
597 force_switch = True
598 else:
599 switch_error = '%s\n%s' % (e.message, switch_error)
600 if force_switch:
601 self.Print("Switching upstream branch from %s to %s" %
602 (upstream_branch, new_base))
603 switch_branch = 'gclient_' + remote_ref[1]
604 self._Capture(['branch', '-f', switch_branch, new_base])
605 self._Checkout(options, switch_branch, force=True, quiet=True)
606 else:
607 # case 4c
608 raise gclient_utils.Error(switch_error)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000609 else:
610 # case 3 - the default case
Aaron Gablef4068aa2017-12-12 15:14:09 -0800611 rebase_files = self._GetDiffFilenames(upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000612 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000613 self.Print('Trying fast-forward merge to branch : %s' % upstream_branch)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000614 try:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000615 merge_args = ['merge']
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000616 if options.merge:
617 merge_args.append('--ff')
618 else:
bauerb@chromium.org2aad1b22011-07-22 12:00:41 +0000619 merge_args.append('--ff-only')
620 merge_args.append(upstream_branch)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000621 merge_output = self._Capture(merge_args)
bratell@opera.com18fa4542013-05-21 13:30:46 +0000622 except subprocess2.CalledProcessError as e:
agable1a8439a2016-10-24 16:36:14 -0700623 rebase_files = []
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000624 if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr):
625 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700626 self.Print('_____ %s at %s' % (self.relpath, revision),
627 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000628 printed_path = True
629 while True:
dnj@chromium.org5b23e872015-02-20 21:25:57 +0000630 if not options.auto_rebase:
631 try:
632 action = self._AskForData(
633 'Cannot %s, attempt to rebase? '
634 '(y)es / (q)uit / (s)kip : ' %
635 ('merge' if options.merge else 'fast-forward merge'),
636 options)
637 except ValueError:
638 raise gclient_utils.Error('Invalid Character')
639 if options.auto_rebase or re.match(r'yes|y', action, re.I):
agable1a8439a2016-10-24 16:36:14 -0700640 self._AttemptRebase(upstream_branch, rebase_files, options,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000641 printed_path=printed_path, merge=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000642 printed_path = True
643 break
644 elif re.match(r'quit|q', action, re.I):
645 raise gclient_utils.Error("Can't fast-forward, please merge or "
646 "rebase manually.\n"
647 "cd %s && git " % self.checkout_path
648 + "rebase %s" % upstream_branch)
649 elif re.match(r'skip|s', action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000650 self.Print('Skipping %s' % self.relpath)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000651 return
652 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000653 self.Print('Input not recognized')
smut@google.com27c9c8a2014-09-11 19:57:55 +0000654 elif re.match("error: Your local changes to '.*' would be "
655 "overwritten by merge. Aborting.\nPlease, commit your "
656 "changes or stash them before you can merge.\n",
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000657 e.stderr):
658 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700659 self.Print('_____ %s at %s' % (self.relpath, revision),
660 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000661 printed_path = True
662 raise gclient_utils.Error(e.stderr)
663 else:
664 # Some other problem happened with the merge
665 logging.error("Error during fast-forward merge in %s!" % self.relpath)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000666 self.Print(e.stderr)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000667 raise
668 else:
669 # Fast-forward merge was successful
670 if not re.match('Already up-to-date.', merge_output) or verbose:
671 if not printed_path:
agable83faed02016-10-24 14:37:10 -0700672 self.Print('_____ %s at %s' % (self.relpath, revision),
673 timestamp=False)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000674 printed_path = True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000675 self.Print(merge_output.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000676 if not verbose:
677 # Make the output a little prettier. It's nice to have some
678 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000679 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000680
agablec3937b92016-10-25 10:13:03 -0700681 if file_list is not None:
682 file_list.extend(
683 [os.path.join(self.checkout_path, f) for f in rebase_files])
msb@chromium.org5bde4852009-12-14 16:47:12 +0000684
685 # If the rebase generated a conflict, abort and ask user to fix
msb@chromium.org786fb682010-06-02 15:16:23 +0000686 if self._IsRebasing():
agable83faed02016-10-24 14:37:10 -0700687 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org5bde4852009-12-14 16:47:12 +0000688 '\nConflict while rebasing this branch.\n'
689 'Fix the conflict and run gclient again.\n'
690 'See man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -0700691 % (self.relpath, revision))
msb@chromium.org5bde4852009-12-14 16:47:12 +0000692
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000693 if verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000694 self.Print('Checked out revision %s' % self.revinfo(options, (), None),
695 timestamp=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000696
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000697 # If --reset and --delete_unversioned_trees are specified, remove any
698 # untracked directories.
699 if options.reset and options.delete_unversioned_trees:
700 # GIT.CaptureStatus() uses 'dit diff' to compare to a specific SHA1 (the
701 # merge-base by default), so doesn't include untracked files. So we use
702 # 'git ls-files --directory --others --exclude-standard' here directly.
703 paths = scm.GIT.Capture(
Aaron Gable7817f022017-12-12 09:43:17 -0800704 ['-c', 'core.quotePath=false', 'ls-files',
705 '--directory', '--others', '--exclude-standard'],
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000706 self.checkout_path)
707 for path in (p for p in paths.splitlines() if p.endswith('/')):
708 full_path = os.path.join(self.checkout_path, path)
709 if not os.path.islink(full_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000710 self.Print('_____ removing unversioned directory %s' % path)
digit@chromium.orgdc112ac2013-04-24 13:00:19 +0000711 gclient_utils.rmtree(full_path)
steveblock@chromium.org98e69452012-02-16 16:36:43 +0000712
iannucci@chromium.org2702bcd2013-09-24 19:10:07 +0000713 return self._Capture(['rev-parse', '--verify', 'HEAD'])
714
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000715 def revert(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000716 """Reverts local modifications.
717
718 All reverted files will be appended to file_list.
719 """
maruel@chromium.org8469bf92010-09-03 19:03:15 +0000720 if not os.path.isdir(self.checkout_path):
msb@chromium.org260c6532009-10-28 03:22:35 +0000721 # revert won't work if the directory doesn't exist. It needs to
722 # checkout instead.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000723 self.Print('_____ %s is missing, synching instead' % self.relpath)
msb@chromium.org260c6532009-10-28 03:22:35 +0000724 # Don't reuse the args.
725 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000726
727 default_rev = "refs/heads/master"
iannucci@chromium.orgd4fffee2013-06-28 00:35:26 +0000728 if options.upstream:
729 if self._GetCurrentBranch():
730 upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
731 default_rev = upstream_branch or default_rev
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000732 _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000733 if not deps_revision:
734 deps_revision = default_rev
smut@google.comd33eab32014-07-07 19:35:18 +0000735 if deps_revision.startswith('refs/heads/'):
736 deps_revision = deps_revision.replace('refs/heads/', self.remote + '/')
smutae7ea312016-07-18 11:59:41 -0700737 try:
738 deps_revision = self.GetUsableRev(deps_revision, options)
739 except NoUsableRevError as e:
740 # If the DEPS entry's url and hash changed, try to update the origin.
741 # See also http://crbug.com/520067.
742 logging.warn(
743 'Couldn\'t find usable revision, will retrying to update instead: %s',
744 e.message)
745 return self.update(options, [], file_list)
nasser@codeaurora.orgb2b46312010-04-30 20:58:03 +0000746
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000747 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800748 files = self._GetDiffFilenames(deps_revision)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000749
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800750 self._Scrub(deps_revision, options)
lliabraa@chromium.orgade83db2012-09-27 14:06:49 +0000751 self._Run(['clean', '-f', '-d'], options)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000752
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000753 if file_list is not None:
754 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
755
756 def revinfo(self, _options, _args, _file_list):
maruel@chromium.org6cafa132010-09-07 14:17:26 +0000757 """Returns revision"""
758 return self._Capture(['rev-parse', 'HEAD'])
msb@chromium.org0f282062009-11-06 20:14:02 +0000759
msb@chromium.orge28e4982009-09-25 20:51:45 +0000760 def runhooks(self, options, args, file_list):
761 self.status(options, args, file_list)
762
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000763 def status(self, options, _args, file_list):
msb@chromium.orge28e4982009-09-25 20:51:45 +0000764 """Display status information."""
765 if not os.path.isdir(self.checkout_path):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000766 self.Print('________ couldn\'t run status in %s:\n'
767 'The directory does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000768 else:
raphael.kubo.da.costa05c83592016-08-04 08:32:41 -0700769 try:
770 merge_base = [self._Capture(['merge-base', 'HEAD', self.remote])]
771 except subprocess2.CalledProcessError:
772 merge_base = []
Aaron Gablef4068aa2017-12-12 15:14:09 -0800773 self._Run(
774 ['-c', 'core.quotePath=false', 'diff', '--name-status'] + merge_base,
775 options, stdout=self.out_fh, always=options.verbose)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000776 if file_list is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800777 files = self._GetDiffFilenames(merge_base[0] if merge_base else None)
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000778 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orge28e4982009-09-25 20:51:45 +0000779
smutae7ea312016-07-18 11:59:41 -0700780 def GetUsableRev(self, rev, options):
agable41e3a6c2016-10-20 11:36:56 -0700781 """Finds a useful revision for this repository."""
smutae7ea312016-07-18 11:59:41 -0700782 sha1 = None
783 if not os.path.isdir(self.checkout_path):
784 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800785 'This is not a git repo, so we cannot get a usable rev.')
agable41e3a6c2016-10-20 11:36:56 -0700786
787 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
788 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700789 else:
agable41e3a6c2016-10-20 11:36:56 -0700790 # May exist in origin, but we don't have it yet, so fetch and look
791 # again.
792 self._Fetch(options)
smutae7ea312016-07-18 11:59:41 -0700793 if scm.GIT.IsValidRevision(cwd=self.checkout_path, rev=rev):
794 sha1 = rev
smutae7ea312016-07-18 11:59:41 -0700795
796 if not sha1:
797 raise NoUsableRevError(
agablea98a6cd2016-11-15 14:30:10 -0800798 'Hash %s does not appear to be a valid hash in this repo.' % rev)
smutae7ea312016-07-18 11:59:41 -0700799
800 return sha1
801
msb@chromium.orge6f78352010-01-13 17:05:33 +0000802 def FullUrlForRelativeUrl(self, url):
803 # Strip from last '/'
804 # Equivalent to unix basename
805 base_url = self.url
806 return base_url[:base_url.rfind('/')] + url
807
primiano@chromium.org1c127382015-02-17 11:15:40 +0000808 def GetGitBackupDirPath(self):
809 """Returns the path where the .git folder for the current project can be
810 staged/restored. Use case: subproject moved from DEPS <-> outer project."""
811 return os.path.join(self._root_dir,
812 'old_' + self.relpath.replace(os.sep, '_')) + '.git'
813
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000814 def _GetMirror(self, url, options):
815 """Get a git_cache.Mirror object for the argument url."""
816 if not git_cache.Mirror.GetCachePath():
817 return None
hinoka@google.comb1b54572014-04-16 22:29:23 +0000818 mirror_kwargs = {
819 'print_func': self.filter,
820 'refs': []
821 }
hinoka@google.comb1b54572014-04-16 22:29:23 +0000822 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
823 mirror_kwargs['refs'].append('refs/branch-heads/*')
szager@chromium.org8d3348f2014-08-19 22:49:16 +0000824 if hasattr(options, 'with_tags') and options.with_tags:
825 mirror_kwargs['refs'].append('refs/tags/*')
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000826 return git_cache.Mirror(url, **mirror_kwargs)
827
Andrii Shyshkalov46a672b2017-11-24 18:04:43 -0800828 def _UpdateMirrorIfNotContains(self, mirror, options, rev_type, revision):
829 """Update a git mirror by fetching the latest commits from the remote,
830 unless mirror already contains revision whose type is sha1 hash.
831 """
832 if rev_type == 'hash' and mirror.contains_revision(revision):
833 if options.verbose:
834 self.Print('skipping mirror update, it has rev=%s already' % revision,
835 timestamp=False)
836 return
837
szager@chromium.org3ec84f62014-08-22 21:00:22 +0000838 if getattr(options, 'shallow', False):
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000839 # HACK(hinoka): These repositories should be super shallow.
szager@chromium.orgb0a13a22014-06-18 00:52:25 +0000840 if 'flash' in mirror.url:
hinoka@chromium.org46b87412014-05-15 00:42:05 +0000841 depth = 10
842 else:
843 depth = 10000
844 else:
845 depth = None
e.hakkinen@samsung.come8bc1aa2015-04-08 08:00:37 +0000846 mirror.populate(verbose=options.verbose,
847 bootstrap=not getattr(options, 'no_bootstrap', False),
Vadim Shtayura08049e22017-10-11 00:14:52 +0000848 depth=depth,
849 ignore_lock=getattr(options, 'ignore_locks', False),
850 lock_timeout=getattr(options, 'lock_timeout', 0))
851 mirror.unlock()
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000852
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000853 def _Clone(self, revision, url, options):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000854 """Clone a git repository from the given URL.
855
msb@chromium.org786fb682010-06-02 15:16:23 +0000856 Once we've cloned the repo, we checkout a working branch if the specified
857 revision is a branch head. If it is a tag or a specific commit, then we
858 leave HEAD detached as it makes future updates simpler -- in this case the
859 user should first create a new branch or switch to an existing branch before
860 making changes in the repo."""
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000861 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000862 # git clone doesn't seem to insert a newline properly before printing
863 # to stdout
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000864 self.Print('')
szager@chromium.orgf1d73eb2014-04-21 17:07:04 +0000865 cfg = gclient_utils.DefaultIndexPackConfig(url)
szager@chromium.org8a139702014-06-20 15:55:01 +0000866 clone_cmd = cfg + ['clone', '--no-checkout', '--progress']
iannucci@chromium.org53456aa2013-07-03 19:38:34 +0000867 if self.cache_dir:
868 clone_cmd.append('--shared')
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000869 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000870 clone_cmd.append('--verbose')
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000871 clone_cmd.append(url)
nsylvain@chromium.org328c3c72011-06-01 20:50:27 +0000872 # If the parent directory does not exist, Git clone on Windows will not
873 # create it, so we need to do it manually.
874 parent_dir = os.path.dirname(self.checkout_path)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000875 gclient_utils.safe_makedirs(parent_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000876
877 template_dir = None
878 if hasattr(options, 'no_history') and options.no_history:
879 if gclient_utils.IsGitSha(revision):
880 # In the case of a subproject, the pinned sha is not necessarily the
881 # head of the remote branch (so we can't just use --depth=N). Instead,
882 # we tell git to fetch all the remote objects from SHA..HEAD by means of
883 # a template git dir which has a 'shallow' file pointing to the sha.
884 template_dir = tempfile.mkdtemp(
885 prefix='_gclient_gittmp_%s' % os.path.basename(self.checkout_path),
886 dir=parent_dir)
887 self._Run(['init', '--bare', template_dir], options, cwd=self._root_dir)
888 with open(os.path.join(template_dir, 'shallow'), 'w') as template_file:
889 template_file.write(revision)
890 clone_cmd.append('--template=' + template_dir)
891 else:
892 # Otherwise, we're just interested in the HEAD. Just use --depth.
893 clone_cmd.append('--depth=1')
894
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000895 tmp_dir = tempfile.mkdtemp(
896 prefix='_gclient_%s_' % os.path.basename(self.checkout_path),
897 dir=parent_dir)
898 try:
899 clone_cmd.append(tmp_dir)
Edward Lemur231f5ea2018-01-31 19:02:36 +0100900 if self.print_outbuf:
901 print_stdout = True
902 stdout = gclient_utils.WriteToStdout(self.out_fh)
903 else:
904 print_stdout = False
905 stdout = self.out_fh
906 self._Run(clone_cmd, options, cwd=self._root_dir, retry=True,
907 print_stdout=print_stdout, stdout=stdout)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000908 gclient_utils.safe_makedirs(self.checkout_path)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000909 gclient_utils.safe_rename(os.path.join(tmp_dir, '.git'),
910 os.path.join(self.checkout_path, '.git'))
zty@chromium.org6279e8a2014-02-13 01:45:25 +0000911 except:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000912 traceback.print_exc(file=self.out_fh)
zty@chromium.org6279e8a2014-02-13 01:45:25 +0000913 raise
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000914 finally:
915 if os.listdir(tmp_dir):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000916 self.Print('_____ removing non-empty tmp dir %s' % tmp_dir)
ilevy@chromium.org3534aa52013-07-20 01:58:08 +0000917 gclient_utils.rmtree(tmp_dir)
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000918 if template_dir:
919 gclient_utils.rmtree(template_dir)
mmoss@chromium.org1a6bec02014-06-02 21:53:29 +0000920 self._UpdateBranchHeads(options, fetch=True)
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +0200921 revision = self._AutoFetchRef(options, revision)
mmoss@chromium.org6e7202b2014-09-09 18:23:39 +0000922 remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
923 self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
romain.pokrzywka@gmail.com483a0ba2014-05-30 00:06:07 +0000924 if self._GetCurrentBranch() is None:
msb@chromium.org786fb682010-06-02 15:16:23 +0000925 # Squelch git's very verbose detached HEAD warning and use our own
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000926 self.Print(
smut@google.comd33eab32014-07-07 19:35:18 +0000927 ('Checked out %s to a detached HEAD. Before making any commits\n'
928 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
929 'an existing branch or use \'git checkout %s -b <branch>\' to\n'
930 'create a new branch for your work.') % (revision, self.remote))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000931
szager@chromium.org6cd41b62014-04-21 23:55:22 +0000932 def _AskForData(self, prompt, options):
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000933 if options.jobs > 1:
szager@chromium.org6cd41b62014-04-21 23:55:22 +0000934 self.Print(prompt)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000935 raise gclient_utils.Error("Background task requires input. Rerun "
936 "gclient with --jobs=1 so that\n"
937 "interaction is possible.")
938 try:
939 return raw_input(prompt)
940 except KeyboardInterrupt:
941 # Hide the exception.
942 sys.exit(1)
943
944
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000945 def _AttemptRebase(self, upstream, files, options, newbase=None,
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000946 branch=None, printed_path=False, merge=False):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000947 """Attempt to rebase onto either upstream or, if specified, newbase."""
iannucci@chromium.org396e1a62013-07-03 19:41:04 +0000948 if files is not None:
Aaron Gablef4068aa2017-12-12 15:14:09 -0800949 files.extend(self._GetDiffFilenames(upstream))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000950 revision = upstream
951 if newbase:
952 revision = newbase
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000953 action = 'merge' if merge else 'rebase'
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000954 if not printed_path:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000955 self.Print('_____ %s : Attempting %s onto %s...' % (
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000956 self.relpath, action, revision))
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000957 printed_path = True
958 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000959 self.Print('Attempting %s onto %s...' % (action, revision))
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000960
961 if merge:
962 merge_output = self._Capture(['merge', revision])
963 if options.verbose:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000964 self.Print(merge_output)
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000965 return
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000966
967 # Build the rebase command here using the args
968 # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
969 rebase_cmd = ['rebase']
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +0000970 if options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000971 rebase_cmd.append('--verbose')
972 if newbase:
973 rebase_cmd.extend(['--onto', newbase])
974 rebase_cmd.append(upstream)
975 if branch:
976 rebase_cmd.append(branch)
977
978 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000979 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +0000980 except subprocess2.CalledProcessError, e:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000981 if (re.match(r'cannot rebase: you have unstaged changes', e.stderr) or
982 re.match(r'cannot rebase: your index contains uncommitted changes',
983 e.stderr)):
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000984 while True:
bauerb@chromium.org30c46d62014-01-23 12:11:56 +0000985 rebase_action = self._AskForData(
maruel@chromium.org90541732011-04-01 17:54:18 +0000986 'Cannot rebase because of unstaged changes.\n'
987 '\'git reset --hard HEAD\' ?\n'
988 'WARNING: destroys any uncommitted work in your current branch!'
bratell@opera.com18fa4542013-05-21 13:30:46 +0000989 ' (y)es / (q)uit / (s)how : ', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000990 if re.match(r'yes|y', rebase_action, re.I):
Robert Iannuccic41d8b92017-02-16 17:07:37 -0800991 self._Scrub('HEAD', options)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000992 # Should this be recursive?
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000993 rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +0000994 break
995 elif re.match(r'quit|q', rebase_action, re.I):
996 raise gclient_utils.Error("Please merge or rebase manually\n"
997 "cd %s && git " % self.checkout_path
998 + "%s" % ' '.join(rebase_cmd))
999 elif re.match(r'show|s', rebase_action, re.I):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001000 self.Print('%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001001 continue
1002 else:
1003 gclient_utils.Error("Input not recognized")
1004 continue
1005 elif re.search(r'^CONFLICT', e.stdout, re.M):
1006 raise gclient_utils.Error("Conflict while rebasing this branch.\n"
1007 "Fix the conflict and run gclient again.\n"
1008 "See 'man git-rebase' for details.\n")
1009 else:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001010 self.Print(e.stdout.strip())
1011 self.Print('Rebase produced error output:\n%s' % e.stderr.strip())
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001012 raise gclient_utils.Error("Unrecognized error, please merge or rebase "
1013 "manually.\ncd %s && git " %
1014 self.checkout_path
1015 + "%s" % ' '.join(rebase_cmd))
1016
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001017 self.Print(rebase_output.strip())
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001018 if not options.verbose:
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001019 # Make the output a little prettier. It's nice to have some
1020 # whitespace between projects when syncing.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001021 self.Print('')
nasser@codeaurora.orgd90ba3f2010-02-23 14:42:57 +00001022
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001023 @staticmethod
1024 def _CheckMinVersion(min_version):
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +00001025 (ok, current_version) = scm.GIT.AssertVersion(min_version)
1026 if not ok:
1027 raise gclient_utils.Error('git version %s < minimum required %s' %
1028 (current_version, min_version))
msb@chromium.org923a0372009-12-11 20:42:43 +00001029
tandrii@chromium.orgc438c142015-08-24 22:55:55 +00001030 def _EnsureValidHeadObjectOrCheckout(self, revision, options, url):
1031 # Special case handling if all 3 conditions are met:
1032 # * the mirros have recently changed, but deps destination remains same,
1033 # * the git histories of mirrors are conflicting.
1034 # * git cache is used
1035 # This manifests itself in current checkout having invalid HEAD commit on
1036 # most git operations. Since git cache is used, just deleted the .git
1037 # folder, and re-create it by cloning.
1038 try:
1039 self._Capture(['rev-list', '-n', '1', 'HEAD'])
1040 except subprocess2.CalledProcessError as e:
1041 if ('fatal: bad object HEAD' in e.stderr
1042 and self.cache_dir and self.cache_dir in url):
1043 self.Print((
1044 'Likely due to DEPS change with git cache_dir, '
1045 'the current commit points to no longer existing object.\n'
1046 '%s' % e)
1047 )
1048 self._DeleteOrMove(options.force)
1049 self._Clone(revision, url, options)
1050 else:
1051 raise
1052
msb@chromium.org786fb682010-06-02 15:16:23 +00001053 def _IsRebasing(self):
1054 # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
1055 # have a plumbing command to determine whether a rebase is in progress, so
1056 # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
1057 g = os.path.join(self.checkout_path, '.git')
1058 return (
1059 os.path.isdir(os.path.join(g, "rebase-merge")) or
1060 os.path.isdir(os.path.join(g, "rebase-apply")))
1061
Robert Iannuccic41d8b92017-02-16 17:07:37 -08001062 def _CheckClean(self, revision, fixup=False):
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001063 lockfile = os.path.join(self.checkout_path, ".git", "index.lock")
1064 if os.path.exists(lockfile):
1065 raise gclient_utils.Error(
agable83faed02016-10-24 14:37:10 -07001066 '\n____ %s at %s\n'
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001067 '\tYour repo is locked, possibly due to a concurrent git process.\n'
1068 '\tIf no git executable is running, then clean up %r and try again.\n'
agable83faed02016-10-24 14:37:10 -07001069 % (self.relpath, revision, lockfile))
iannucci@chromium.orgd9b318c2015-12-04 20:03:08 +00001070
msb@chromium.org786fb682010-06-02 15:16:23 +00001071 # Make sure the tree is clean; see git-rebase.sh for reference
1072 try:
1073 scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001074 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001075 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001076 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001077 '\tYou have unstaged changes.\n'
1078 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001079 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001080 try:
1081 scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
smut@google.com27c9c8a2014-09-11 19:57:55 +00001082 '--ignore-submodules', 'HEAD', '--'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001083 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001084 except subprocess2.CalledProcessError:
agable83faed02016-10-24 14:37:10 -07001085 raise gclient_utils.Error('\n____ %s at %s\n'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001086 '\tYour index contains uncommitted changes\n'
1087 '\tPlease commit, stash, or reset.\n'
agable83faed02016-10-24 14:37:10 -07001088 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001089
agable83faed02016-10-24 14:37:10 -07001090 def _CheckDetachedHead(self, revision, _options):
msb@chromium.org786fb682010-06-02 15:16:23 +00001091 # HEAD is detached. Make sure it is safe to move away from (i.e., it is
1092 # reference by a commit). If not, error out -- most likely a rebase is
1093 # in progress, try to detect so we can give a better error.
1094 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +00001095 scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
1096 cwd=self.checkout_path)
maruel@chromium.orgbffad372011-09-08 17:54:22 +00001097 except subprocess2.CalledProcessError:
msb@chromium.org786fb682010-06-02 15:16:23 +00001098 # Commit is not contained by any rev. See if the user is rebasing:
1099 if self._IsRebasing():
1100 # Punt to the user
agable83faed02016-10-24 14:37:10 -07001101 raise gclient_utils.Error('\n____ %s at %s\n'
msb@chromium.org786fb682010-06-02 15:16:23 +00001102 '\tAlready in a conflict, i.e. (no branch).\n'
1103 '\tFix the conflict and run gclient again.\n'
1104 '\tOr to abort run:\n\t\tgit-rebase --abort\n'
1105 '\tSee man git-rebase for details.\n'
agable83faed02016-10-24 14:37:10 -07001106 % (self.relpath, revision))
msb@chromium.org786fb682010-06-02 15:16:23 +00001107 # Let's just save off the commit so we can proceed.
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001108 name = ('saved-by-gclient-' +
1109 self._Capture(['rev-parse', '--short', 'HEAD']))
mmoss@chromium.org77bd7362013-09-25 23:46:14 +00001110 self._Capture(['branch', '-f', name])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001111 self.Print('_____ found an unreferenced commit and saved it as \'%s\'' %
maruel@chromium.orgf5d37bf2010-09-02 00:50:34 +00001112 name)
msb@chromium.org786fb682010-06-02 15:16:23 +00001113
msb@chromium.org5bde4852009-12-14 16:47:12 +00001114 def _GetCurrentBranch(self):
msb@chromium.org786fb682010-06-02 15:16:23 +00001115 # Returns name of current branch or None for detached HEAD
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001116 branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
msb@chromium.org786fb682010-06-02 15:16:23 +00001117 if branch == 'HEAD':
msb@chromium.org5bde4852009-12-14 16:47:12 +00001118 return None
1119 return branch
1120
borenet@google.comc3e09d22014-04-10 13:58:18 +00001121 def _Capture(self, args, **kwargs):
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001122 kwargs.setdefault('cwd', self.checkout_path)
1123 kwargs.setdefault('stderr', subprocess2.PIPE)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001124 strip = kwargs.pop('strip', True)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001125 env = scm.GIT.ApplyEnvVars(kwargs)
Robert Iannuccia7a9ceb2017-02-16 17:38:06 -08001126 ret = subprocess2.check_output(['git'] + args, env=env, **kwargs)
1127 if strip:
1128 ret = ret.strip()
1129 return ret
maruel@chromium.org6cafa132010-09-07 14:17:26 +00001130
dnj@chromium.orgbb424c02014-06-23 22:42:51 +00001131 def _Checkout(self, options, ref, force=False, quiet=None):
1132 """Performs a 'git-checkout' operation.
1133
1134 Args:
1135 options: The configured option set
1136 ref: (str) The branch/commit to checkout
1137 quiet: (bool/None) Whether or not the checkout shoud pass '--quiet'; if
1138 'None', the behavior is inferred from 'options.verbose'.
1139 Returns: (str) The output of the checkout operation
1140 """
1141 if quiet is None:
1142 quiet = (not options.verbose)
1143 checkout_args = ['checkout']
1144 if force:
1145 checkout_args.append('--force')
1146 if quiet:
1147 checkout_args.append('--quiet')
1148 checkout_args.append(ref)
1149 return self._Capture(checkout_args)
1150
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001151 def _Fetch(self, options, remote=None, prune=False, quiet=False,
1152 refspec=None):
dnj@chromium.org680f2172014-06-25 00:39:32 +00001153 cfg = gclient_utils.DefaultIndexPackConfig(self.url)
1154 fetch_cmd = cfg + [
1155 'fetch',
1156 remote or self.remote,
1157 ]
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001158 if refspec:
1159 fetch_cmd.append(refspec)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001160
1161 if prune:
1162 fetch_cmd.append('--prune')
1163 if options.verbose:
1164 fetch_cmd.append('--verbose')
1165 elif quiet:
1166 fetch_cmd.append('--quiet')
tandrii64103db2016-10-11 05:30:05 -07001167 self._Run(fetch_cmd, options, show_header=options.verbose, retry=True)
dnj@chromium.org680f2172014-06-25 00:39:32 +00001168
1169 # Return the revision that was fetched; this will be stored in 'FETCH_HEAD'
1170 return self._Capture(['rev-parse', '--verify', 'FETCH_HEAD'])
1171
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001172 def _UpdateBranchHeads(self, options, fetch=False):
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001173 """Adds, and optionally fetches, "branch-heads" and "tags" refspecs
1174 if requested."""
1175 need_fetch = fetch
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001176 if hasattr(options, 'with_branch_heads') and options.with_branch_heads:
maruel@chromium.org1a60dca2013-11-26 14:06:26 +00001177 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +00001178 '+refs/branch-heads/*:refs/remotes/branch-heads/*',
1179 '^\\+refs/branch-heads/\\*:.*$']
1180 self._Run(config_cmd, options)
szager@chromium.org8d3348f2014-08-19 22:49:16 +00001181 need_fetch = True
1182 if hasattr(options, 'with_tags') and options.with_tags:
1183 config_cmd = ['config', 'remote.%s.fetch' % self.remote,
1184 '+refs/tags/*:refs/tags/*',
1185 '^\\+refs/tags/\\*:.*$']
1186 self._Run(config_cmd, options)
1187 need_fetch = True
1188 if fetch and need_fetch:
bpastene2a3e9912016-09-07 16:22:25 -07001189 self._Fetch(options, prune=options.force)
mmoss@chromium.orge409df62013-04-16 17:28:57 +00001190
Paweł Hajdan, Jr63b8c2a2017-09-05 17:59:08 +02001191 def _AutoFetchRef(self, options, revision):
1192 """Attempts to fetch |revision| if not available in local repo.
1193
1194 Returns possibly updated revision."""
1195 try:
1196 self._Capture(['rev-parse', revision])
1197 except subprocess2.CalledProcessError:
1198 self._Fetch(options, refspec=revision)
1199 revision = self._Capture(['rev-parse', 'FETCH_HEAD'])
1200 return revision
1201
dnj@chromium.org680f2172014-06-25 00:39:32 +00001202 def _Run(self, args, options, show_header=True, **kwargs):
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001203 # Disable 'unused options' warning | pylint: disable=unused-argument
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001204 kwargs.setdefault('cwd', self.checkout_path)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001205 kwargs.setdefault('stdout', self.out_fh)
szager@chromium.org848fd492014-04-09 19:06:44 +00001206 kwargs['filter_fn'] = self.filter
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001207 kwargs.setdefault('print_stdout', False)
szager@chromium.org6d8115d2014-04-23 20:59:23 +00001208 env = scm.GIT.ApplyEnvVars(kwargs)
agable@chromium.org772efaf2014-04-01 02:35:44 +00001209 cmd = ['git'] + args
dnj@chromium.org680f2172014-06-25 00:39:32 +00001210 if show_header:
sbc@chromium.org2cd0b8e2014-09-22 21:17:59 +00001211 gclient_utils.CheckCallAndFilterAndHeader(cmd, env=env, **kwargs)
1212 else:
1213 gclient_utils.CheckCallAndFilter(cmd, env=env, **kwargs)
John Budorick0f7b2002018-01-19 15:46:17 -08001214
1215
1216class CipdPackage(object):
1217 """A representation of a single CIPD package."""
1218
1219 def __init__(self, name, version, authority_for_root, authority_for_subdir):
1220 self._authority_for_root = authority_for_root
1221 self._authority_for_subdir = authority_for_subdir
1222 self._name = name
1223 self._version = version
1224
1225 @property
1226 def authority_for_root(self):
1227 """Whether this package has authority to act on behalf of its root.
1228
1229 Some operations should only be performed once per cipd root. A package
1230 that has authority for its cipd root is the only package that should
1231 perform such operations.
1232
1233 Returns:
1234 bool; whether this package has root authority.
1235 """
1236 return self._authority_for_root
1237
1238 @property
1239 def authority_for_subdir(self):
1240 """Whether this package has authority to act on behalf of its subdir.
1241
1242 Some operations should only be performed once per subdirectory. A package
1243 that has authority for its subdirectory is the only package that should
1244 perform such operations.
1245
1246 Returns:
1247 bool; whether this package has subdir authority.
1248 """
1249 return self._authority_for_subdir
1250
1251 @property
1252 def name(self):
1253 return self._name
1254
1255 @property
1256 def version(self):
1257 return self._version
1258
1259
1260class CipdRoot(object):
1261 """A representation of a single CIPD root."""
1262 def __init__(self, root_dir, service_url):
1263 self._all_packages = set()
1264 self._mutator_lock = threading.Lock()
1265 self._packages_by_subdir = collections.defaultdict(list)
1266 self._root_dir = root_dir
1267 self._service_url = service_url
1268
1269 def add_package(self, subdir, package, version):
1270 """Adds a package to this CIPD root.
1271
1272 As far as clients are concerned, this grants both root and subdir authority
1273 to packages arbitrarily. (The implementation grants root authority to the
1274 first package added and subdir authority to the first package added for that
1275 subdir, but clients should not depend on or expect that behavior.)
1276
1277 Args:
1278 subdir: str; relative path to where the package should be installed from
1279 the cipd root directory.
1280 package: str; the cipd package name.
1281 version: str; the cipd package version.
1282 Returns:
1283 CipdPackage; the package that was created and added to this root.
1284 """
1285 with self._mutator_lock:
1286 cipd_package = CipdPackage(
1287 package, version,
1288 not self._packages_by_subdir,
1289 not self._packages_by_subdir[subdir])
1290 self._all_packages.add(cipd_package)
1291 self._packages_by_subdir[subdir].append(cipd_package)
1292 return cipd_package
1293
1294 def packages(self, subdir):
1295 """Get the list of configured packages for the given subdir."""
1296 return list(self._packages_by_subdir[subdir])
1297
1298 def clobber(self):
1299 """Remove the .cipd directory.
1300
1301 This is useful for forcing ensure to redownload and reinitialize all
1302 packages.
1303 """
1304 with self._mutator_lock:
1305 cipd_cache_dir = os.path.join(self._cipd_root, '.cipd')
1306 try:
1307 gclient_utils.rmtree(os.path.join(cipd_cache_dir))
1308 except OSError:
1309 if os.path.exists(cipd_cache_dir):
1310 raise
1311
1312 @contextlib.contextmanager
1313 def _create_ensure_file(self):
1314 try:
1315 ensure_file = None
1316 with tempfile.NamedTemporaryFile(
1317 suffix='.ensure', delete=False) as ensure_file:
1318 for subdir, packages in sorted(self._packages_by_subdir.iteritems()):
1319 ensure_file.write('@Subdir %s\n' % subdir)
1320 for package in packages:
1321 ensure_file.write('%s %s\n' % (package.name, package.version))
1322 ensure_file.write('\n')
1323 yield ensure_file.name
1324 finally:
1325 if ensure_file is not None and os.path.exists(ensure_file.name):
1326 os.remove(ensure_file.name)
1327
1328 def ensure(self):
1329 """Run `cipd ensure`."""
1330 with self._mutator_lock:
1331 with self._create_ensure_file() as ensure_file:
1332 cmd = [
1333 'cipd', 'ensure',
1334 '-log-level', 'error',
1335 '-root', self.root_dir,
1336 '-ensure-file', ensure_file,
1337 ]
1338 gclient_utils.CheckCallAndFilterAndHeader(cmd)
1339
1340 def created_package(self, package):
1341 """Checks whether this root created the given package.
1342
1343 Args:
1344 package: CipdPackage; the package to check.
1345 Returns:
1346 bool; whether this root created the given package.
1347 """
1348 return package in self._all_packages
1349
1350 @property
1351 def root_dir(self):
1352 return self._root_dir
1353
1354 @property
1355 def service_url(self):
1356 return self._service_url
1357
1358
1359class CipdWrapper(SCMWrapper):
1360 """Wrapper for CIPD.
1361
1362 Currently only supports chrome-infra-packages.appspot.com.
1363 """
1364
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()