blob: 16e4ce8a127855446019f007b30816b043d0c1c4 [file] [log] [blame]
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +00001# Copyright 2014 The Chromium Authors. All rights reserved.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5# Monkeypatch IMapIterator so that Ctrl-C can kill everything properly.
6# Derived from https://gist.github.com/aljungberg/626518
Raul Tambrec2f74c12019-03-19 05:55:53 +00007
8from __future__ import print_function
Edward Lemur12a537f2019-10-03 21:57:15 +00009from __future__ import unicode_literals
Raul Tambrec2f74c12019-03-19 05:55:53 +000010
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000011import multiprocessing.pool
Josip Sokcevicde6c4562020-03-26 00:39:42 +000012import sys
13import threading
14
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000015from multiprocessing.pool import IMapIterator
Josip Sokcevicde6c4562020-03-26 00:39:42 +000016
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000017def wrapper(func):
18 def wrap(self, timeout=None):
Josip Sokcevicde6c4562020-03-26 00:39:42 +000019 default_timeout = (1 << 31 if sys.version_info.major == 2 else
20 threading.TIMEOUT_MAX)
21 return func(self, timeout=timeout or default_timeout)
22
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000023 return wrap
24IMapIterator.next = wrapper(IMapIterator.next)
25IMapIterator.__next__ = IMapIterator.next
26# TODO(iannucci): Monkeypatch all other 'wait' methods too.
27
28
29import binascii
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000030import collections
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000031import contextlib
32import functools
33import logging
iannucci@chromium.org97345eb2014-03-13 07:55:15 +000034import os
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000035import re
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000036import setup_color
sammc@chromium.org900a33f2015-09-29 06:57:09 +000037import shutil
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000038import signal
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000039import tempfile
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +000040import textwrap
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000041
42import subprocess2
43
Raul Tambrec2f74c12019-03-19 05:55:53 +000044from io import BytesIO
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000045
agable02b3c982016-06-22 07:51:22 -070046
Edward Lemurb800fde2020-01-10 23:04:44 +000047if sys.version_info.major == 2:
48 # On Python 3, BrokenPipeError is raised instead.
49 BrokenPipeError = IOError
50
51
agable02b3c982016-06-22 07:51:22 -070052ROOT = os.path.abspath(os.path.dirname(__file__))
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +000053IS_WIN = sys.platform == 'win32'
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000054TEST_MODE = False
55
Dan Jacques209a6812017-07-12 11:40:20 -070056
57def win_find_git():
58 for elem in os.environ.get('PATH', '').split(os.pathsep):
59 for candidate in ('git.exe', 'git.bat'):
60 path = os.path.join(elem, candidate)
61 if os.path.isfile(path):
62 return path
63 raise ValueError('Could not find Git on PATH.')
64
65
66GIT_EXE = 'git' if not IS_WIN else win_find_git()
67
68
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000069FREEZE = 'FREEZE'
70FREEZE_SECTIONS = {
71 'indexed': 'soft',
72 'unindexed': 'mixed'
73}
74FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000075
76
Dan Jacques2f8b0c12017-04-05 12:57:21 -070077# NOTE: This list is DEPRECATED in favor of the Infra Git wrapper:
78# https://chromium.googlesource.com/infra/infra/+/master/go/src/infra/tools/git
79#
80# New entries should be added to the Git wrapper, NOT to this list. "git_retry"
81# is, similarly, being deprecated in favor of the Git wrapper.
82#
83# ---
84#
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000085# Retry a git operation if git returns a error response with any of these
86# messages. It's all observed 'bad' GoB responses so far.
87#
88# This list is inspired/derived from the one in ChromiumOS's Chromite:
89# <CHROMITE>/lib/git.py::GIT_TRANSIENT_ERRORS
90#
91# It was last imported from '7add3ac29564d98ac35ce426bc295e743e7c0c02'.
92GIT_TRANSIENT_ERRORS = (
93 # crbug.com/285832
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000094 r'!.*\[remote rejected\].*\(error in hook\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000095
96 # crbug.com/289932
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000097 r'!.*\[remote rejected\].*\(failed to lock\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000098
99 # crbug.com/307156
iannucci@chromium.org6e95d402014-08-29 22:10:55 +0000100 r'!.*\[remote rejected\].*\(error in Gerrit backend\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000101
102 # crbug.com/285832
103 r'remote error: Internal Server Error',
104
105 # crbug.com/294449
106 r'fatal: Couldn\'t find remote ref ',
107
108 # crbug.com/220543
109 r'git fetch_pack: expected ACK/NAK, got',
110
111 # crbug.com/189455
112 r'protocol error: bad pack header',
113
114 # crbug.com/202807
115 r'The remote end hung up unexpectedly',
116
117 # crbug.com/298189
118 r'TLS packet with unexpected length was received',
119
120 # crbug.com/187444
121 r'RPC failed; result=\d+, HTTP code = \d+',
122
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000123 # crbug.com/388876
124 r'Connection timed out',
dnj@chromium.org45cddd62014-11-06 19:36:42 +0000125
126 # crbug.com/430343
127 # TODO(dnj): Resync with Chromite.
128 r'The requested URL returned error: 5\d+',
Arikonb3a21482016-07-22 10:12:24 -0700129
130 r'Connection reset by peer',
131
132 r'Unable to look up',
133
134 r'Couldn\'t resolve host',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000135)
136
137GIT_TRANSIENT_ERRORS_RE = re.compile('|'.join(GIT_TRANSIENT_ERRORS),
138 re.IGNORECASE)
139
raphael.kubo.da.costa@intel.com58d05b02015-06-24 08:54:41 +0000140# git's for-each-ref command first supported the upstream:track token in its
141# format string in version 1.9.0, but some usages were broken until 2.3.0.
142# See git commit b6160d95 for more information.
143MIN_UPSTREAM_TRACK_GIT_VERSION = (2, 3)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000144
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000145class BadCommitRefException(Exception):
146 def __init__(self, refs):
147 msg = ('one of %s does not seem to be a valid commitref.' %
148 str(refs))
149 super(BadCommitRefException, self).__init__(msg)
150
151
152def memoize_one(**kwargs):
153 """Memoizes a single-argument pure function.
154
155 Values of None are not cached.
156
157 Kwargs:
158 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
159 cache manipulation functions. This is a kwarg so that users of memoize_one
160 are forced to explicitly and verbosely pick True or False.
161
162 Adds three methods to the decorated function:
163 * get(key, default=None) - Gets the value for this key from the cache.
164 * set(key, value) - Sets the value for this key from the cache.
165 * clear() - Drops the entire contents of the cache. Useful for unittests.
166 * update(other) - Updates the contents of the cache from another dict.
167 """
168 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
169 threadsafe = kwargs['threadsafe']
170
171 if threadsafe:
172 def withlock(lock, f):
173 def inner(*args, **kwargs):
174 with lock:
175 return f(*args, **kwargs)
176 return inner
177 else:
178 def withlock(_lock, f):
179 return f
180
181 def decorator(f):
182 # Instantiate the lock in decorator, in case users of memoize_one do:
183 #
184 # memoizer = memoize_one(threadsafe=True)
185 #
186 # @memoizer
187 # def fn1(val): ...
188 #
189 # @memoizer
190 # def fn2(val): ...
191
192 lock = threading.Lock() if threadsafe else None
193 cache = {}
194 _get = withlock(lock, cache.get)
195 _set = withlock(lock, cache.__setitem__)
196
197 @functools.wraps(f)
198 def inner(arg):
199 ret = _get(arg)
200 if ret is None:
201 ret = f(arg)
202 if ret is not None:
203 _set(arg, ret)
204 return ret
205 inner.get = _get
206 inner.set = _set
207 inner.clear = withlock(lock, cache.clear)
208 inner.update = withlock(lock, cache.update)
209 return inner
210 return decorator
211
212
213def _ScopedPool_initer(orig, orig_args): # pragma: no cover
214 """Initializer method for ScopedPool's subprocesses.
215
216 This helps ScopedPool handle Ctrl-C's correctly.
217 """
218 signal.signal(signal.SIGINT, signal.SIG_IGN)
219 if orig:
220 orig(*orig_args)
221
222
223@contextlib.contextmanager
224def ScopedPool(*args, **kwargs):
225 """Context Manager which returns a multiprocessing.pool instance which
226 correctly deals with thrown exceptions.
227
228 *args - Arguments to multiprocessing.pool
229
230 Kwargs:
231 kind ('threads', 'procs') - The type of underlying coprocess to use.
232 **etc - Arguments to multiprocessing.pool
233 """
234 if kwargs.pop('kind', None) == 'threads':
235 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
236 else:
237 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
238 kwargs['initializer'] = _ScopedPool_initer
239 kwargs['initargs'] = orig, orig_args
240 pool = multiprocessing.pool.Pool(*args, **kwargs)
241
242 try:
243 yield pool
244 pool.close()
245 except:
246 pool.terminate()
247 raise
248 finally:
249 pool.join()
250
251
252class ProgressPrinter(object):
253 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000254 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000255 """Create a ProgressPrinter.
256
257 Use it as a context manager which produces a simple 'increment' method:
258
259 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
260 for i in xrange(1000):
261 # do stuff
262 if i % 10 == 0:
263 inc(10)
264
265 Args:
266 fmt - String format with a single '%(count)d' where the counter value
267 should go.
268 enabled (bool) - If this is None, will default to True if
269 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000270 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000271 period (float) - The time in seconds for the printer thread to wait
272 between printing.
273 """
274 self.fmt = fmt
275 if enabled is None: # pragma: no cover
276 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
277 else:
278 self.enabled = enabled
279
280 self._count = 0
281 self._dead = False
282 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000283 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000284 self._thread = threading.Thread(target=self._run)
285 self._period = period
286
287 def _emit(self, s):
288 if self.enabled:
289 self._stream.write('\r' + s)
290 self._stream.flush()
291
292 def _run(self):
293 with self._dead_cond:
294 while not self._dead:
295 self._emit(self.fmt % {'count': self._count})
296 self._dead_cond.wait(self._period)
297 self._emit((self.fmt + '\n') % {'count': self._count})
298
299 def inc(self, amount=1):
300 self._count += amount
301
302 def __enter__(self):
303 self._thread.start()
304 return self.inc
305
306 def __exit__(self, _exc_type, _exc_value, _traceback):
307 self._dead = True
308 with self._dead_cond:
309 self._dead_cond.notifyAll()
310 self._thread.join()
311 del self._thread
312
313
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000314def once(function):
315 """@Decorates |function| so that it only performs its action once, no matter
316 how many times the decorated |function| is called."""
Edward Lemur12a537f2019-10-03 21:57:15 +0000317 has_run = [False]
318 def _wrapper(*args, **kwargs):
319 if not has_run[0]:
320 has_run[0] = True
321 function(*args, **kwargs)
322 return _wrapper
323
324
325def unicode_repr(s):
326 result = repr(s)
327 return result[1:] if result.startswith('u') else result
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000328
329
330## Git functions
331
agable7aa2ddd2016-06-21 07:47:00 -0700332def die(message, *args):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000333 print(textwrap.dedent(message % args), file=sys.stderr)
agable7aa2ddd2016-06-21 07:47:00 -0700334 sys.exit(1)
335
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000336
Mark Mentovaif548d082017-03-08 13:32:00 -0500337def blame(filename, revision=None, porcelain=False, abbrev=None, *_args):
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000338 command = ['blame']
339 if porcelain:
340 command.append('-p')
341 if revision is not None:
342 command.append(revision)
Mark Mentovaif548d082017-03-08 13:32:00 -0500343 if abbrev is not None:
344 command.append('--abbrev=%d' % abbrev)
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000345 command.extend(['--', filename])
346 return run(*command)
347
348
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000349def branch_config(branch, option, default=None):
agable7aa2ddd2016-06-21 07:47:00 -0700350 return get_config('branch.%s.%s' % (branch, option), default=default)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000351
352
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000353def branch_config_map(option):
354 """Return {branch: <|option| value>} for all branches."""
355 try:
356 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
agable7aa2ddd2016-06-21 07:47:00 -0700357 lines = get_config_regexp(reg.pattern)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000358 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
359 except subprocess2.CalledProcessError:
360 return {}
361
362
Francois Dorayd42c6812017-05-30 15:10:20 -0400363def branches(use_limit=True, *args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000364 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000365
366 key = 'depot-tools.branch-limit'
agable7aa2ddd2016-06-21 07:47:00 -0700367 limit = get_config_int(key, 20)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000368
369 raw_branches = run('branch', *args).splitlines()
370
371 num = len(raw_branches)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000372
Francois Dorayd42c6812017-05-30 15:10:20 -0400373 if use_limit and num > limit:
agable7aa2ddd2016-06-21 07:47:00 -0700374 die("""\
375 Your git repo has too many branches (%d/%d) for this tool to work well.
376
377 You may adjust this limit by running:
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000378 git config %s <new_limit>
agable7aa2ddd2016-06-21 07:47:00 -0700379
380 You may also try cleaning up your old branches by running:
381 git cl archive
382 """, num, limit, key)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000383
384 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000385 if line.startswith(NO_BRANCH):
386 continue
387 yield line.split()[-1]
388
389
agable7aa2ddd2016-06-21 07:47:00 -0700390def get_config(option, default=None):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000391 try:
392 return run('config', '--get', option) or default
393 except subprocess2.CalledProcessError:
394 return default
395
396
agable7aa2ddd2016-06-21 07:47:00 -0700397def get_config_int(option, default=0):
398 assert isinstance(default, int)
399 try:
400 return int(get_config(option, default))
401 except ValueError:
402 return default
403
404
405def get_config_list(option):
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000406 try:
407 return run('config', '--get-all', option).split()
408 except subprocess2.CalledProcessError:
409 return []
410
411
agable7aa2ddd2016-06-21 07:47:00 -0700412def get_config_regexp(pattern):
413 if IS_WIN: # pragma: no cover
414 # this madness is because we call git.bat which calls git.exe which calls
415 # bash.exe (or something to that effect). Each layer divides the number of
416 # ^'s by 2.
417 pattern = pattern.replace('^', '^' * 8)
418 return run('config', '--get-regexp', pattern).splitlines()
419
420
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000421def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000422 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000423 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000424 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000425 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000426
427
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000428def del_branch_config(branch, option, scope='local'):
429 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000430
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000431
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000432def del_config(option, scope='local'):
433 try:
434 run('config', '--' + scope, '--unset', option)
435 except subprocess2.CalledProcessError:
436 pass
437
438
mgiuca@chromium.org01d2cde2016-02-05 03:25:41 +0000439def diff(oldrev, newrev, *args):
440 return run('diff', oldrev, newrev, *args)
441
442
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000443def freeze():
444 took_action = False
agable02b3c982016-06-22 07:51:22 -0700445 key = 'depot-tools.freeze-size-limit'
446 MB = 2**20
447 limit_mb = get_config_int(key, 100)
448 untracked_bytes = 0
449
iannuccieaca0332016-08-03 16:46:50 -0700450 root_path = repo_root()
451
agable02b3c982016-06-22 07:51:22 -0700452 for f, s in status():
453 if is_unmerged(s):
454 die("Cannot freeze unmerged changes!")
455 if limit_mb > 0:
456 if s.lstat == '?':
iannuccieaca0332016-08-03 16:46:50 -0700457 untracked_bytes += os.stat(os.path.join(root_path, f)).st_size
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800458 if limit_mb > 0 and untracked_bytes > limit_mb * MB:
459 die("""\
460 You appear to have too much untracked+unignored data in your git
461 checkout: %.1f / %d MB.
agable02b3c982016-06-22 07:51:22 -0700462
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800463 Run `git status` to see what it is.
agable02b3c982016-06-22 07:51:22 -0700464
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800465 In addition to making many git commands slower, this will prevent
466 depot_tools from freezing your in-progress changes.
agable02b3c982016-06-22 07:51:22 -0700467
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800468 You should add untracked data that you want to ignore to your repo's
469 .git/info/exclude
470 file. See `git help ignore` for the format of this file.
agable02b3c982016-06-22 07:51:22 -0700471
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000472 If this data is intended as part of your commit, you may adjust the
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800473 freeze limit by running:
474 git config %s <new_limit>
475 Where <new_limit> is an integer threshold in megabytes.""",
476 untracked_bytes / (MB * 1.0), limit_mb, key)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000477
478 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000479 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000480 took_action = True
481 except subprocess2.CalledProcessError:
482 pass
483
agable96e179b2016-06-24 10:32:51 -0700484 add_errors = False
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000485 try:
agable96e179b2016-06-24 10:32:51 -0700486 run('add', '-A', '--ignore-errors')
487 except subprocess2.CalledProcessError:
488 add_errors = True
489
490 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000491 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000492 took_action = True
493 except subprocess2.CalledProcessError:
494 pass
495
agable96e179b2016-06-24 10:32:51 -0700496 ret = []
497 if add_errors:
498 ret.append('Failed to index some unindexed files.')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000499 if not took_action:
agable96e179b2016-06-24 10:32:51 -0700500 ret.append('Nothing to freeze.')
501 return ' '.join(ret) or None
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000502
503
504def get_branch_tree():
505 """Get the dictionary of {branch: parent}, compatible with topo_iter.
506
507 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
508 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000509 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000510 skipped = set()
511 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000512
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000513 for branch in branches():
514 parent = upstream(branch)
515 if not parent:
516 skipped.add(branch)
517 continue
518 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000519
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000520 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000521
522
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000523def get_or_create_merge_base(branch, parent=None):
524 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000525
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000526 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000527 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000528 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000529 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000530 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000531 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000532 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000533 actual_merge_base = run('merge-base', parent, branch)
534
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000535 if base_upstream != parent:
536 base = None
537 base_upstream = None
538
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000539 def is_ancestor(a, b):
540 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
541
clemensh@chromium.orgc3fe99d2016-04-19 08:39:55 +0000542 if base and base != actual_merge_base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000543 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000544 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
545 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000546 elif is_ancestor(base, actual_merge_base):
547 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
548 base = None
549 else:
550 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000551
552 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000553 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000554 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000555
556 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000557
558
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000559def hash_multi(*reflike):
560 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000561
562
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000563def hash_one(reflike, short=False):
564 args = ['rev-parse', reflike]
565 if short:
566 args.insert(1, '--short')
567 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000568
569
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000570def in_rebase():
571 git_dir = run('rev-parse', '--git-dir')
572 return (
573 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
574 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000575
576
577def intern_f(f, kind='blob'):
578 """Interns a file object into the git object store.
579
580 Args:
581 f (file-like object) - The file-like object to intern
582 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
583
584 Returns the git hash of the interned object (hex encoded).
585 """
586 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
587 f.close()
588 return ret
589
590
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000591def is_dormant(branch):
592 # TODO(iannucci): Do an oldness check?
593 return branch_config(branch, 'dormant', 'false') != 'false'
594
595
agable02b3c982016-06-22 07:51:22 -0700596def is_unmerged(stat_value):
597 return (
598 'U' in (stat_value.lstat, stat_value.rstat) or
599 ((stat_value.lstat == stat_value.rstat) and stat_value.lstat in 'AD')
600 )
601
602
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000603def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000604 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000605 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000606
607
608def mktree(treedict):
609 """Makes a git tree object and returns its hash.
610
611 See |tree()| for the values of mode, type, and ref.
612
613 Args:
614 treedict - { name: (mode, type, ref) }
615 """
616 with tempfile.TemporaryFile() as f:
Edward Lemur12a537f2019-10-03 21:57:15 +0000617 for name, (mode, typ, ref) in treedict.items():
Edward Lemur71681bf2019-10-09 23:46:20 +0000618 f.write(('%s %s %s\t%s\0' % (mode, typ, ref, name)).encode('utf-8'))
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000619 f.seek(0)
620 return run('mktree', '-z', stdin=f)
621
622
623def parse_commitrefs(*commitrefs):
624 """Returns binary encoded commit hashes for one or more commitrefs.
625
626 A commitref is anything which can resolve to a commit. Popular examples:
627 * 'HEAD'
628 * 'origin/master'
629 * 'cool_branch~2'
630 """
631 try:
Edward Lemur12a537f2019-10-03 21:57:15 +0000632 return [binascii.unhexlify(h) for h in hash_multi(*commitrefs)]
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000633 except subprocess2.CalledProcessError:
634 raise BadCommitRefException(commitrefs)
635
636
sbc@chromium.org384039b2014-10-13 21:01:00 +0000637RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000638
639
640def rebase(parent, start, branch, abort=False):
641 """Rebases |start|..|branch| onto the branch |parent|.
642
643 Args:
644 parent - The new parent ref for the rebased commits.
645 start - The commit to start from
646 branch - The branch to rebase
647 abort - If True, will call git-rebase --abort in the event that the rebase
648 doesn't complete successfully.
649
650 Returns a namedtuple with fields:
651 success - a boolean indicating that the rebase command completed
652 successfully.
653 message - if the rebase failed, this contains the stdout of the failed
654 rebase.
655 """
656 try:
657 args = ['--onto', parent, start, branch]
658 if TEST_MODE:
659 args.insert(0, '--committer-date-is-author-date')
660 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000661 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000662 except subprocess2.CalledProcessError as cpe:
663 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000664 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000665 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000666
667
668def remove_merge_base(branch):
669 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000670 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000671
672
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000673def repo_root():
674 """Returns the absolute path to the repository root."""
675 return run('rev-parse', '--show-toplevel')
676
677
Jeffrey Yasskin6b52dc22019-12-06 18:32:21 +0000678def upstream_default():
679 """Returns the default branch name of the origin repository."""
680 try:
681 return run('rev-parse', '--abbrev-ref', 'origin/HEAD')
682 except subprocess2.CalledProcessError:
683 return 'origin/master'
684
685
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000686def root():
Jeffrey Yasskin6b52dc22019-12-06 18:32:21 +0000687 return get_config('depot-tools.upstream', upstream_default())
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000688
689
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000690@contextlib.contextmanager
691def less(): # pragma: no cover
692 """Runs 'less' as context manager yielding its stdin as a PIPE.
693
694 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
695 running less and just yields sys.stdout.
Edward Lemur0d462e92020-01-08 20:11:31 +0000696
697 The returned PIPE is opened on binary mode.
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000698 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000699 if not setup_color.IS_TTY:
Edward Lemur5e94b802019-11-26 21:44:08 +0000700 # On Python 3, sys.stdout doesn't accept bytes, and sys.stdout.buffer must
701 # be used.
702 yield getattr(sys.stdout, 'buffer', sys.stdout)
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000703 return
704
705 # Run with the same options that git uses (see setup_pager in git repo).
706 # -F: Automatically quit if the output is less than one screen.
707 # -R: Don't escape ANSI color codes.
708 # -X: Don't clear the screen before starting.
709 cmd = ('less', '-FRX')
710 try:
711 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
712 yield proc.stdin
713 finally:
Edward Lemurb800fde2020-01-10 23:04:44 +0000714 try:
715 proc.stdin.close()
716 except BrokenPipeError:
717 # BrokenPipeError is raised if proc has already completed,
718 pass
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000719 proc.wait()
720
721
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000722def run(*cmd, **kwargs):
723 """The same as run_with_stderr, except it only returns stdout."""
724 return run_with_stderr(*cmd, **kwargs)[0]
725
726
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000727def run_with_retcode(*cmd, **kwargs):
728 """Run a command but only return the status code."""
729 try:
730 run(*cmd, **kwargs)
731 return 0
732 except subprocess2.CalledProcessError as cpe:
733 return cpe.returncode
734
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000735def run_stream(*cmd, **kwargs):
736 """Runs a git command. Returns stdout as a PIPE (file-like object).
737
738 stderr is dropped to avoid races if the process outputs to both stdout and
739 stderr.
740 """
741 kwargs.setdefault('stderr', subprocess2.VOID)
742 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000743 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000744 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000745 proc = subprocess2.Popen(cmd, **kwargs)
746 return proc.stdout
747
748
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000749@contextlib.contextmanager
750def run_stream_with_retcode(*cmd, **kwargs):
751 """Runs a git command as context manager yielding stdout as a PIPE.
752
753 stderr is dropped to avoid races if the process outputs to both stdout and
754 stderr.
755
756 Raises subprocess2.CalledProcessError on nonzero return code.
757 """
758 kwargs.setdefault('stderr', subprocess2.VOID)
759 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000760 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000761 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
762 try:
763 proc = subprocess2.Popen(cmd, **kwargs)
764 yield proc.stdout
765 finally:
766 retcode = proc.wait()
767 if retcode != 0:
768 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
769 None, None)
770
771
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000772def run_with_stderr(*cmd, **kwargs):
773 """Runs a git command.
774
775 Returns (stdout, stderr) as a pair of strings.
776
777 kwargs
778 autostrip (bool) - Strip the output. Defaults to True.
779 indata (str) - Specifies stdin data for the process.
780 """
781 kwargs.setdefault('stdin', subprocess2.PIPE)
782 kwargs.setdefault('stdout', subprocess2.PIPE)
783 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000784 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000785 autostrip = kwargs.pop('autostrip', True)
786 indata = kwargs.pop('indata', None)
Edward Lemur12a537f2019-10-03 21:57:15 +0000787 decode = kwargs.pop('decode', True)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000788
iannucci@chromium.org21980022014-04-11 04:51:49 +0000789 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000790 proc = subprocess2.Popen(cmd, **kwargs)
791 ret, err = proc.communicate(indata)
792 retcode = proc.wait()
793 if retcode != 0:
794 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
795
796 if autostrip:
Edward Lemur12a537f2019-10-03 21:57:15 +0000797 ret = (ret or b'').strip()
798 err = (err or b'').strip()
799
800 if decode:
801 ret = ret.decode('utf-8', 'replace')
802 err = err.decode('utf-8', 'replace')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000803
804 return ret, err
805
806
807def set_branch_config(branch, option, value, scope='local'):
808 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
809
810
811def set_config(option, value, scope='local'):
812 run('config', '--' + scope, option, value)
813
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000814
sbc@chromium.org71437c02015-04-09 19:29:40 +0000815def get_dirty_files():
816 # Make sure index is up-to-date before running diff-index.
817 run_with_retcode('update-index', '--refresh', '-q')
Eli Ribble54434e72019-05-24 00:41:15 +0000818 return run('diff-index', '--ignore-submodules', '--name-status', 'HEAD')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000819
820
821def is_dirty_git_tree(cmd):
iannuccie38699b2016-08-15 17:32:31 -0700822 w = lambda s: sys.stderr.write(s+"\n")
823
sbc@chromium.org71437c02015-04-09 19:29:40 +0000824 dirty = get_dirty_files()
825 if dirty:
iannuccie38699b2016-08-15 17:32:31 -0700826 w('Cannot %s with a dirty tree. Commit, freeze or stash your changes first.'
827 % cmd)
828 w('Uncommitted files: (git diff-index --name-status HEAD)')
829 w(dirty[:4096])
sbc@chromium.org71437c02015-04-09 19:29:40 +0000830 if len(dirty) > 4096: # pragma: no cover
iannuccie38699b2016-08-15 17:32:31 -0700831 w('... (run "git diff-index --name-status HEAD" to see full output).')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000832 return True
833 return False
834
835
agable02b3c982016-06-22 07:51:22 -0700836def status():
837 """Returns a parsed version of git-status.
838
839 Returns a generator of (current_name, (lstat, rstat, src)) pairs where:
840 * current_name is the name of the file
841 * lstat is the left status code letter from git-status
842 * rstat is the left status code letter from git-status
843 * src is the current name of the file, or the original name of the file
844 if lstat == 'R'
845 """
846 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src')
847
848 def tokenizer(stream):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000849 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700850 c = None
Edward Lemur12a537f2019-10-03 21:57:15 +0000851 while c != b'':
agable02b3c982016-06-22 07:51:22 -0700852 c = stream.read(1)
Edward Lemur12a537f2019-10-03 21:57:15 +0000853 if c in (None, b'', b'\0'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000854 if len(acc.getvalue()):
agable02b3c982016-06-22 07:51:22 -0700855 yield acc.getvalue()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000856 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700857 else:
858 acc.write(c)
859
860 def parser(tokens):
861 while True:
Edward Lemur12a537f2019-10-03 21:57:15 +0000862 try:
863 status_dest = next(tokens).decode('utf-8')
864 except StopIteration:
865 return
agable02b3c982016-06-22 07:51:22 -0700866 stat, dest = status_dest[:2], status_dest[3:]
867 lstat, rstat = stat
868 if lstat == 'R':
Edward Lemur12a537f2019-10-03 21:57:15 +0000869 src = next(tokens).decode('utf-8')
agable02b3c982016-06-22 07:51:22 -0700870 else:
871 src = dest
872 yield (dest, stat_entry(lstat, rstat, src))
873
874 return parser(tokenizer(run_stream('status', '-z', bufsize=-1)))
875
876
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000877def squash_current_branch(header=None, merge_base=None):
Alan Cutter00017822016-12-20 17:39:59 +1100878 header = header or 'git squash commit for %s.' % current_branch()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000879 merge_base = merge_base or get_or_create_merge_base(current_branch())
880 log_msg = header + '\n'
881 if log_msg:
882 log_msg += '\n'
883 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
884 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000885
886 if not get_dirty_files():
887 # Sometimes the squash can result in the same tree, meaning that there is
888 # nothing to commit at this point.
Raul Tambrec2f74c12019-03-19 05:55:53 +0000889 print('Nothing to commit; squashed branch is empty')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000890 return False
Edward Lemur71681bf2019-10-09 23:46:20 +0000891 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg.encode('utf-8'))
sbc@chromium.org71437c02015-04-09 19:29:40 +0000892 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000893
894
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000895def tags(*args):
896 return run('tag', *args).splitlines()
897
898
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000899def thaw():
900 took_action = False
Edward Lemur12a537f2019-10-03 21:57:15 +0000901 for sha in run_stream('rev-list', 'HEAD').readlines():
902 sha = sha.strip().decode('utf-8')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000903 msg = run('show', '--format=%f%b', '-s', 'HEAD')
904 match = FREEZE_MATCHER.match(msg)
905 if not match:
906 if not took_action:
907 return 'Nothing to thaw.'
908 break
909
910 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
911 took_action = True
912
913
914def topo_iter(branch_tree, top_down=True):
915 """Generates (branch, parent) in topographical order for a branch tree.
916
917 Given a tree:
918
919 A1
920 B1 B2
921 C1 C2 C3
922 D1
923
924 branch_tree would look like: {
925 'D1': 'C3',
926 'C3': 'B2',
927 'B2': 'A1',
928 'C1': 'B1',
929 'C2': 'B1',
930 'B1': 'A1',
931 }
932
933 It is OK to have multiple 'root' nodes in your graph.
934
935 if top_down is True, items are yielded from A->D. Otherwise they're yielded
936 from D->A. Within a layer the branches will be yielded in sorted order.
937 """
938 branch_tree = branch_tree.copy()
939
940 # TODO(iannucci): There is probably a more efficient way to do these.
941 if top_down:
942 while branch_tree:
Edward Lemur12a537f2019-10-03 21:57:15 +0000943 this_pass = [(b, p) for b, p in branch_tree.items()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000944 if p not in branch_tree]
945 assert this_pass, "Branch tree has cycles: %r" % branch_tree
946 for branch, parent in sorted(this_pass):
947 yield branch, parent
948 del branch_tree[branch]
949 else:
950 parent_to_branches = collections.defaultdict(set)
Edward Lemur12a537f2019-10-03 21:57:15 +0000951 for branch, parent in branch_tree.items():
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000952 parent_to_branches[parent].add(branch)
953
954 while branch_tree:
Edward Lemur12a537f2019-10-03 21:57:15 +0000955 this_pass = [(b, p) for b, p in branch_tree.items()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000956 if not parent_to_branches[b]]
957 assert this_pass, "Branch tree has cycles: %r" % branch_tree
958 for branch, parent in sorted(this_pass):
959 yield branch, parent
960 parent_to_branches[parent].discard(branch)
961 del branch_tree[branch]
962
963
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000964def tree(treeref, recurse=False):
965 """Returns a dict representation of a git tree object.
966
967 Args:
968 treeref (str) - a git ref which resolves to a tree (commits count as trees).
qyearsley12fa6ff2016-08-24 09:18:40 -0700969 recurse (bool) - include all of the tree's descendants too. File names will
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000970 take the form of 'some/path/to/file'.
971
972 Return format:
973 { 'file_name': (mode, type, ref) }
974
975 mode is an integer where:
976 * 0040000 - Directory
977 * 0100644 - Regular non-executable file
978 * 0100664 - Regular non-executable group-writeable file
979 * 0100755 - Regular executable file
980 * 0120000 - Symbolic link
981 * 0160000 - Gitlink
982
983 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
984
985 ref is the hex encoded hash of the entry.
986 """
987 ret = {}
988 opts = ['ls-tree', '--full-tree']
989 if recurse:
990 opts.append('-r')
991 opts.append(treeref)
992 try:
993 for line in run(*opts).splitlines():
994 mode, typ, ref, name = line.split(None, 3)
995 ret[name] = (mode, typ, ref)
996 except subprocess2.CalledProcessError:
997 return None
998 return ret
999
1000
Mun Yong Jang781e71e2017-10-25 15:46:20 -07001001def get_remote_url(remote='origin'):
1002 try:
1003 return run('config', 'remote.%s.url' % remote)
1004 except subprocess2.CalledProcessError:
1005 return None
1006
1007
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +00001008def upstream(branch):
1009 try:
1010 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
1011 branch+'@{upstream}')
1012 except subprocess2.CalledProcessError:
1013 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001014
agable@chromium.orgd629fb42014-10-01 09:40:10 +00001015
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001016def get_git_version():
1017 """Returns a tuple that contains the numeric components of the current git
1018 version."""
1019 version_string = run('--version')
1020 version_match = re.search(r'(\d+.)+(\d+)', version_string)
1021 version = version_match.group() if version_match else ''
1022
1023 return tuple(int(x) for x in version.split('.'))
1024
1025
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001026def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001027 format_string = (
1028 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
1029
1030 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001031 if (include_tracking_status and
1032 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001033 format_string += '%(upstream:track)'
1034
1035 info_map = {}
1036 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001037 BranchesInfo = collections.namedtuple(
1038 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001039 for line in data.splitlines():
1040 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
1041
1042 ahead_match = re.search(r'ahead (\d+)', tracking_status)
1043 ahead = int(ahead_match.group(1)) if ahead_match else None
1044
1045 behind_match = re.search(r'behind (\d+)', tracking_status)
1046 behind = int(behind_match.group(1)) if behind_match else None
1047
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001048 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001049 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
1050
1051 # Set None for upstreams which are not branches (e.g empty upstream, remotes
1052 # and deleted upstream branches).
1053 missing_upstreams = {}
1054 for info in info_map.values():
1055 if info.upstream not in info_map and info.upstream not in missing_upstreams:
1056 missing_upstreams[info.upstream] = None
1057
Edward Lemur12a537f2019-10-03 21:57:15 +00001058 result = info_map.copy()
1059 result.update(missing_upstreams)
1060 return result
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001061
1062
1063def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001064 files_to_copy, symlink=None):
1065 if not symlink:
1066 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001067 os.makedirs(new_workdir)
1068 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001069 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001070 for entry in files_to_copy:
1071 clone_file(repository, new_workdir, entry, shutil.copy)
1072
1073
1074def make_workdir(repository, new_workdir):
1075 GIT_DIRECTORY_WHITELIST = [
1076 'config',
1077 'info',
1078 'hooks',
1079 'logs/refs',
1080 'objects',
1081 'packed-refs',
1082 'refs',
1083 'remotes',
1084 'rr-cache',
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001085 ]
1086 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
1087 ['HEAD'])
1088
1089
1090def clone_file(repository, new_workdir, link, operation):
1091 if not os.path.exists(os.path.join(repository, link)):
1092 return
1093 link_dir = os.path.dirname(os.path.join(new_workdir, link))
1094 if not os.path.exists(link_dir):
1095 os.makedirs(link_dir)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001096 src = os.path.join(repository, link)
1097 if os.path.islink(src):
Henrique Ferreiroaea45d22018-02-19 09:48:36 +01001098 src = os.path.realpath(src)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001099 operation(src, os.path.join(new_workdir, link))