blob: e9b32200451f03546542adddcf207b2668f3583c [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 == '?':
Andrew Grievefc5e1032020-04-15 18:16:08 +0000457 untracked_bytes += os.lstat(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
Gavin Mak934ac6e2020-11-09 18:26:44 +0000533
534 try:
535 actual_merge_base = run('merge-base', '--fork-point', parent, branch)
536 except subprocess2.CalledProcessError:
537 actual_merge_base = run('merge-base', parent, branch)
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000538
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000539 if base_upstream != parent:
540 base = None
541 base_upstream = None
542
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000543 def is_ancestor(a, b):
544 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
545
clemensh@chromium.orgc3fe99d2016-04-19 08:39:55 +0000546 if base and base != actual_merge_base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000547 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000548 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
549 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000550 elif is_ancestor(base, actual_merge_base):
551 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
552 base = None
553 else:
554 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000555
556 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000557 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000558 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000559
560 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000561
562
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000563def hash_multi(*reflike):
564 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000565
566
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000567def hash_one(reflike, short=False):
568 args = ['rev-parse', reflike]
569 if short:
570 args.insert(1, '--short')
571 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000572
573
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000574def in_rebase():
575 git_dir = run('rev-parse', '--git-dir')
576 return (
577 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
578 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000579
580
581def intern_f(f, kind='blob'):
582 """Interns a file object into the git object store.
583
584 Args:
585 f (file-like object) - The file-like object to intern
586 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
587
588 Returns the git hash of the interned object (hex encoded).
589 """
590 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
591 f.close()
592 return ret
593
594
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000595def is_dormant(branch):
596 # TODO(iannucci): Do an oldness check?
597 return branch_config(branch, 'dormant', 'false') != 'false'
598
599
agable02b3c982016-06-22 07:51:22 -0700600def is_unmerged(stat_value):
601 return (
602 'U' in (stat_value.lstat, stat_value.rstat) or
603 ((stat_value.lstat == stat_value.rstat) and stat_value.lstat in 'AD')
604 )
605
606
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000607def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000608 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000609 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000610
611
612def mktree(treedict):
613 """Makes a git tree object and returns its hash.
614
615 See |tree()| for the values of mode, type, and ref.
616
617 Args:
618 treedict - { name: (mode, type, ref) }
619 """
620 with tempfile.TemporaryFile() as f:
Edward Lemur12a537f2019-10-03 21:57:15 +0000621 for name, (mode, typ, ref) in treedict.items():
Edward Lemur71681bf2019-10-09 23:46:20 +0000622 f.write(('%s %s %s\t%s\0' % (mode, typ, ref, name)).encode('utf-8'))
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000623 f.seek(0)
624 return run('mktree', '-z', stdin=f)
625
626
627def parse_commitrefs(*commitrefs):
628 """Returns binary encoded commit hashes for one or more commitrefs.
629
630 A commitref is anything which can resolve to a commit. Popular examples:
631 * 'HEAD'
632 * 'origin/master'
633 * 'cool_branch~2'
634 """
635 try:
Edward Lemur12a537f2019-10-03 21:57:15 +0000636 return [binascii.unhexlify(h) for h in hash_multi(*commitrefs)]
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000637 except subprocess2.CalledProcessError:
638 raise BadCommitRefException(commitrefs)
639
640
sbc@chromium.org384039b2014-10-13 21:01:00 +0000641RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000642
643
644def rebase(parent, start, branch, abort=False):
645 """Rebases |start|..|branch| onto the branch |parent|.
646
647 Args:
648 parent - The new parent ref for the rebased commits.
649 start - The commit to start from
650 branch - The branch to rebase
651 abort - If True, will call git-rebase --abort in the event that the rebase
652 doesn't complete successfully.
653
654 Returns a namedtuple with fields:
655 success - a boolean indicating that the rebase command completed
656 successfully.
657 message - if the rebase failed, this contains the stdout of the failed
658 rebase.
659 """
660 try:
661 args = ['--onto', parent, start, branch]
662 if TEST_MODE:
663 args.insert(0, '--committer-date-is-author-date')
664 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000665 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000666 except subprocess2.CalledProcessError as cpe:
667 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000668 run_with_retcode('rebase', '--abort') # ignore failure
Josip Sokcevic72f991f2020-04-23 18:53:30 +0000669 return RebaseRet(False, cpe.stdout.decode('utf-8', 'replace'),
670 cpe.stderr.decode('utf-8', 'replace'))
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000671
672
673def remove_merge_base(branch):
674 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000675 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000676
677
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000678def repo_root():
679 """Returns the absolute path to the repository root."""
680 return run('rev-parse', '--show-toplevel')
681
682
Jeffrey Yasskin6b52dc22019-12-06 18:32:21 +0000683def upstream_default():
684 """Returns the default branch name of the origin repository."""
685 try:
686 return run('rev-parse', '--abbrev-ref', 'origin/HEAD')
687 except subprocess2.CalledProcessError:
688 return 'origin/master'
689
690
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000691def root():
Jeffrey Yasskin6b52dc22019-12-06 18:32:21 +0000692 return get_config('depot-tools.upstream', upstream_default())
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000693
694
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000695@contextlib.contextmanager
696def less(): # pragma: no cover
697 """Runs 'less' as context manager yielding its stdin as a PIPE.
698
699 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
700 running less and just yields sys.stdout.
Edward Lemur0d462e92020-01-08 20:11:31 +0000701
702 The returned PIPE is opened on binary mode.
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000703 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000704 if not setup_color.IS_TTY:
Edward Lemur5e94b802019-11-26 21:44:08 +0000705 # On Python 3, sys.stdout doesn't accept bytes, and sys.stdout.buffer must
706 # be used.
707 yield getattr(sys.stdout, 'buffer', sys.stdout)
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000708 return
709
710 # Run with the same options that git uses (see setup_pager in git repo).
711 # -F: Automatically quit if the output is less than one screen.
712 # -R: Don't escape ANSI color codes.
713 # -X: Don't clear the screen before starting.
714 cmd = ('less', '-FRX')
715 try:
716 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
717 yield proc.stdin
718 finally:
Edward Lemurb800fde2020-01-10 23:04:44 +0000719 try:
720 proc.stdin.close()
721 except BrokenPipeError:
722 # BrokenPipeError is raised if proc has already completed,
723 pass
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000724 proc.wait()
725
726
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000727def run(*cmd, **kwargs):
728 """The same as run_with_stderr, except it only returns stdout."""
729 return run_with_stderr(*cmd, **kwargs)[0]
730
731
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000732def run_with_retcode(*cmd, **kwargs):
733 """Run a command but only return the status code."""
734 try:
735 run(*cmd, **kwargs)
736 return 0
737 except subprocess2.CalledProcessError as cpe:
738 return cpe.returncode
739
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000740def run_stream(*cmd, **kwargs):
741 """Runs a git command. Returns stdout as a PIPE (file-like object).
742
743 stderr is dropped to avoid races if the process outputs to both stdout and
744 stderr.
745 """
746 kwargs.setdefault('stderr', subprocess2.VOID)
747 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000748 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000749 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000750 proc = subprocess2.Popen(cmd, **kwargs)
751 return proc.stdout
752
753
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000754@contextlib.contextmanager
755def run_stream_with_retcode(*cmd, **kwargs):
756 """Runs a git command as context manager yielding stdout as a PIPE.
757
758 stderr is dropped to avoid races if the process outputs to both stdout and
759 stderr.
760
761 Raises subprocess2.CalledProcessError on nonzero return code.
762 """
763 kwargs.setdefault('stderr', subprocess2.VOID)
764 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000765 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000766 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
767 try:
768 proc = subprocess2.Popen(cmd, **kwargs)
769 yield proc.stdout
770 finally:
771 retcode = proc.wait()
772 if retcode != 0:
773 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
Josip Sokcevic72f991f2020-04-23 18:53:30 +0000774 b'', b'')
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000775
776
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000777def run_with_stderr(*cmd, **kwargs):
778 """Runs a git command.
779
780 Returns (stdout, stderr) as a pair of strings.
781
782 kwargs
783 autostrip (bool) - Strip the output. Defaults to True.
784 indata (str) - Specifies stdin data for the process.
785 """
786 kwargs.setdefault('stdin', subprocess2.PIPE)
787 kwargs.setdefault('stdout', subprocess2.PIPE)
788 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000789 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000790 autostrip = kwargs.pop('autostrip', True)
791 indata = kwargs.pop('indata', None)
Edward Lemur12a537f2019-10-03 21:57:15 +0000792 decode = kwargs.pop('decode', True)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000793
iannucci@chromium.org21980022014-04-11 04:51:49 +0000794 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000795 proc = subprocess2.Popen(cmd, **kwargs)
796 ret, err = proc.communicate(indata)
797 retcode = proc.wait()
798 if retcode != 0:
799 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
800
801 if autostrip:
Edward Lemur12a537f2019-10-03 21:57:15 +0000802 ret = (ret or b'').strip()
803 err = (err or b'').strip()
804
805 if decode:
806 ret = ret.decode('utf-8', 'replace')
807 err = err.decode('utf-8', 'replace')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000808
809 return ret, err
810
811
812def set_branch_config(branch, option, value, scope='local'):
813 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
814
815
816def set_config(option, value, scope='local'):
817 run('config', '--' + scope, option, value)
818
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000819
sbc@chromium.org71437c02015-04-09 19:29:40 +0000820def get_dirty_files():
821 # Make sure index is up-to-date before running diff-index.
822 run_with_retcode('update-index', '--refresh', '-q')
Eli Ribble54434e72019-05-24 00:41:15 +0000823 return run('diff-index', '--ignore-submodules', '--name-status', 'HEAD')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000824
825
826def is_dirty_git_tree(cmd):
iannuccie38699b2016-08-15 17:32:31 -0700827 w = lambda s: sys.stderr.write(s+"\n")
828
sbc@chromium.org71437c02015-04-09 19:29:40 +0000829 dirty = get_dirty_files()
830 if dirty:
iannuccie38699b2016-08-15 17:32:31 -0700831 w('Cannot %s with a dirty tree. Commit, freeze or stash your changes first.'
832 % cmd)
833 w('Uncommitted files: (git diff-index --name-status HEAD)')
834 w(dirty[:4096])
sbc@chromium.org71437c02015-04-09 19:29:40 +0000835 if len(dirty) > 4096: # pragma: no cover
iannuccie38699b2016-08-15 17:32:31 -0700836 w('... (run "git diff-index --name-status HEAD" to see full output).')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000837 return True
838 return False
839
840
agable02b3c982016-06-22 07:51:22 -0700841def status():
842 """Returns a parsed version of git-status.
843
844 Returns a generator of (current_name, (lstat, rstat, src)) pairs where:
845 * current_name is the name of the file
846 * lstat is the left status code letter from git-status
847 * rstat is the left status code letter from git-status
848 * src is the current name of the file, or the original name of the file
849 if lstat == 'R'
850 """
851 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src')
852
853 def tokenizer(stream):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000854 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700855 c = None
Edward Lemur12a537f2019-10-03 21:57:15 +0000856 while c != b'':
agable02b3c982016-06-22 07:51:22 -0700857 c = stream.read(1)
Edward Lemur12a537f2019-10-03 21:57:15 +0000858 if c in (None, b'', b'\0'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000859 if len(acc.getvalue()):
agable02b3c982016-06-22 07:51:22 -0700860 yield acc.getvalue()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000861 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700862 else:
863 acc.write(c)
864
865 def parser(tokens):
866 while True:
Edward Lemur12a537f2019-10-03 21:57:15 +0000867 try:
868 status_dest = next(tokens).decode('utf-8')
869 except StopIteration:
870 return
agable02b3c982016-06-22 07:51:22 -0700871 stat, dest = status_dest[:2], status_dest[3:]
872 lstat, rstat = stat
873 if lstat == 'R':
Edward Lemur12a537f2019-10-03 21:57:15 +0000874 src = next(tokens).decode('utf-8')
agable02b3c982016-06-22 07:51:22 -0700875 else:
876 src = dest
877 yield (dest, stat_entry(lstat, rstat, src))
878
879 return parser(tokenizer(run_stream('status', '-z', bufsize=-1)))
880
881
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000882def squash_current_branch(header=None, merge_base=None):
Alan Cutter00017822016-12-20 17:39:59 +1100883 header = header or 'git squash commit for %s.' % current_branch()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000884 merge_base = merge_base or get_or_create_merge_base(current_branch())
885 log_msg = header + '\n'
886 if log_msg:
887 log_msg += '\n'
888 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
889 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000890
891 if not get_dirty_files():
892 # Sometimes the squash can result in the same tree, meaning that there is
893 # nothing to commit at this point.
Raul Tambrec2f74c12019-03-19 05:55:53 +0000894 print('Nothing to commit; squashed branch is empty')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000895 return False
Edward Lemur71681bf2019-10-09 23:46:20 +0000896 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg.encode('utf-8'))
sbc@chromium.org71437c02015-04-09 19:29:40 +0000897 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000898
899
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000900def tags(*args):
901 return run('tag', *args).splitlines()
902
903
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000904def thaw():
905 took_action = False
Edward Lemur12a537f2019-10-03 21:57:15 +0000906 for sha in run_stream('rev-list', 'HEAD').readlines():
907 sha = sha.strip().decode('utf-8')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000908 msg = run('show', '--format=%f%b', '-s', 'HEAD')
909 match = FREEZE_MATCHER.match(msg)
910 if not match:
911 if not took_action:
912 return 'Nothing to thaw.'
913 break
914
915 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
916 took_action = True
917
918
919def topo_iter(branch_tree, top_down=True):
920 """Generates (branch, parent) in topographical order for a branch tree.
921
922 Given a tree:
923
924 A1
925 B1 B2
926 C1 C2 C3
927 D1
928
929 branch_tree would look like: {
930 'D1': 'C3',
931 'C3': 'B2',
932 'B2': 'A1',
933 'C1': 'B1',
934 'C2': 'B1',
935 'B1': 'A1',
936 }
937
938 It is OK to have multiple 'root' nodes in your graph.
939
940 if top_down is True, items are yielded from A->D. Otherwise they're yielded
941 from D->A. Within a layer the branches will be yielded in sorted order.
942 """
943 branch_tree = branch_tree.copy()
944
945 # TODO(iannucci): There is probably a more efficient way to do these.
946 if top_down:
947 while branch_tree:
Edward Lemur12a537f2019-10-03 21:57:15 +0000948 this_pass = [(b, p) for b, p in branch_tree.items()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000949 if p not in branch_tree]
950 assert this_pass, "Branch tree has cycles: %r" % branch_tree
951 for branch, parent in sorted(this_pass):
952 yield branch, parent
953 del branch_tree[branch]
954 else:
955 parent_to_branches = collections.defaultdict(set)
Edward Lemur12a537f2019-10-03 21:57:15 +0000956 for branch, parent in branch_tree.items():
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000957 parent_to_branches[parent].add(branch)
958
959 while branch_tree:
Edward Lemur12a537f2019-10-03 21:57:15 +0000960 this_pass = [(b, p) for b, p in branch_tree.items()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000961 if not parent_to_branches[b]]
962 assert this_pass, "Branch tree has cycles: %r" % branch_tree
963 for branch, parent in sorted(this_pass):
964 yield branch, parent
965 parent_to_branches[parent].discard(branch)
966 del branch_tree[branch]
967
968
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000969def tree(treeref, recurse=False):
970 """Returns a dict representation of a git tree object.
971
972 Args:
973 treeref (str) - a git ref which resolves to a tree (commits count as trees).
qyearsley12fa6ff2016-08-24 09:18:40 -0700974 recurse (bool) - include all of the tree's descendants too. File names will
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000975 take the form of 'some/path/to/file'.
976
977 Return format:
978 { 'file_name': (mode, type, ref) }
979
980 mode is an integer where:
981 * 0040000 - Directory
982 * 0100644 - Regular non-executable file
983 * 0100664 - Regular non-executable group-writeable file
984 * 0100755 - Regular executable file
985 * 0120000 - Symbolic link
986 * 0160000 - Gitlink
987
988 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
989
990 ref is the hex encoded hash of the entry.
991 """
992 ret = {}
993 opts = ['ls-tree', '--full-tree']
994 if recurse:
995 opts.append('-r')
996 opts.append(treeref)
997 try:
998 for line in run(*opts).splitlines():
999 mode, typ, ref, name = line.split(None, 3)
1000 ret[name] = (mode, typ, ref)
1001 except subprocess2.CalledProcessError:
1002 return None
1003 return ret
1004
1005
Mun Yong Jang781e71e2017-10-25 15:46:20 -07001006def get_remote_url(remote='origin'):
1007 try:
1008 return run('config', 'remote.%s.url' % remote)
1009 except subprocess2.CalledProcessError:
1010 return None
1011
1012
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +00001013def upstream(branch):
1014 try:
1015 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
1016 branch+'@{upstream}')
1017 except subprocess2.CalledProcessError:
1018 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001019
agable@chromium.orgd629fb42014-10-01 09:40:10 +00001020
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001021def get_git_version():
1022 """Returns a tuple that contains the numeric components of the current git
1023 version."""
1024 version_string = run('--version')
1025 version_match = re.search(r'(\d+.)+(\d+)', version_string)
1026 version = version_match.group() if version_match else ''
1027
1028 return tuple(int(x) for x in version.split('.'))
1029
1030
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001031def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001032 format_string = (
1033 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
1034
1035 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001036 if (include_tracking_status and
1037 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001038 format_string += '%(upstream:track)'
1039
1040 info_map = {}
1041 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001042 BranchesInfo = collections.namedtuple(
Gavin Mak8d7201b2020-09-17 19:21:38 +00001043 'BranchesInfo', 'hash upstream commits behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001044 for line in data.splitlines():
1045 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
1046
Gavin Mak8d7201b2020-09-17 19:21:38 +00001047 commits = None
1048 base = get_or_create_merge_base(branch)
1049 if base:
Sylvain Defresne39d870e2020-10-15 16:27:45 +00001050 commits_list = run('rev-list', '--count', branch, '^%s' % base, '--')
1051 commits = int(commits_list) or None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001052
1053 behind_match = re.search(r'behind (\d+)', tracking_status)
1054 behind = int(behind_match.group(1)) if behind_match else None
1055
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001056 info_map[branch] = BranchesInfo(
Gavin Mak8d7201b2020-09-17 19:21:38 +00001057 hash=branch_hash, upstream=upstream_branch, commits=commits,
1058 behind=behind)
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001059
1060 # Set None for upstreams which are not branches (e.g empty upstream, remotes
1061 # and deleted upstream branches).
1062 missing_upstreams = {}
1063 for info in info_map.values():
1064 if info.upstream not in info_map and info.upstream not in missing_upstreams:
1065 missing_upstreams[info.upstream] = None
1066
Edward Lemur12a537f2019-10-03 21:57:15 +00001067 result = info_map.copy()
1068 result.update(missing_upstreams)
1069 return result
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001070
1071
1072def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001073 files_to_copy, symlink=None):
1074 if not symlink:
1075 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001076 os.makedirs(new_workdir)
1077 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001078 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001079 for entry in files_to_copy:
1080 clone_file(repository, new_workdir, entry, shutil.copy)
1081
1082
1083def make_workdir(repository, new_workdir):
1084 GIT_DIRECTORY_WHITELIST = [
1085 'config',
1086 'info',
1087 'hooks',
1088 'logs/refs',
1089 'objects',
1090 'packed-refs',
1091 'refs',
1092 'remotes',
1093 'rr-cache',
Richard He42033b22020-05-22 03:03:45 +00001094 'shallow',
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001095 ]
1096 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
1097 ['HEAD'])
1098
1099
1100def clone_file(repository, new_workdir, link, operation):
1101 if not os.path.exists(os.path.join(repository, link)):
1102 return
1103 link_dir = os.path.dirname(os.path.join(new_workdir, link))
1104 if not os.path.exists(link_dir):
1105 os.makedirs(link_dir)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001106 src = os.path.join(repository, link)
1107 if os.path.islink(src):
Henrique Ferreiroaea45d22018-02-19 09:48:36 +01001108 src = os.path.realpath(src)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001109 operation(src, os.path.join(new_workdir, link))