blob: 182fc0c1648939bcbacd22c716486932844079e0 [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
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
Josip Sokcevic72f991f2020-04-23 18:53:30 +0000665 return RebaseRet(False, cpe.stdout.decode('utf-8', 'replace'),
666 cpe.stderr.decode('utf-8', 'replace'))
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000667
668
669def remove_merge_base(branch):
670 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000671 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000672
673
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000674def repo_root():
675 """Returns the absolute path to the repository root."""
676 return run('rev-parse', '--show-toplevel')
677
678
Jeffrey Yasskin6b52dc22019-12-06 18:32:21 +0000679def upstream_default():
680 """Returns the default branch name of the origin repository."""
681 try:
682 return run('rev-parse', '--abbrev-ref', 'origin/HEAD')
683 except subprocess2.CalledProcessError:
684 return 'origin/master'
685
686
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000687def root():
Jeffrey Yasskin6b52dc22019-12-06 18:32:21 +0000688 return get_config('depot-tools.upstream', upstream_default())
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000689
690
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000691@contextlib.contextmanager
692def less(): # pragma: no cover
693 """Runs 'less' as context manager yielding its stdin as a PIPE.
694
695 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
696 running less and just yields sys.stdout.
Edward Lemur0d462e92020-01-08 20:11:31 +0000697
698 The returned PIPE is opened on binary mode.
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000699 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000700 if not setup_color.IS_TTY:
Edward Lemur5e94b802019-11-26 21:44:08 +0000701 # On Python 3, sys.stdout doesn't accept bytes, and sys.stdout.buffer must
702 # be used.
703 yield getattr(sys.stdout, 'buffer', sys.stdout)
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000704 return
705
706 # Run with the same options that git uses (see setup_pager in git repo).
707 # -F: Automatically quit if the output is less than one screen.
708 # -R: Don't escape ANSI color codes.
709 # -X: Don't clear the screen before starting.
710 cmd = ('less', '-FRX')
711 try:
712 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
713 yield proc.stdin
714 finally:
Edward Lemurb800fde2020-01-10 23:04:44 +0000715 try:
716 proc.stdin.close()
717 except BrokenPipeError:
718 # BrokenPipeError is raised if proc has already completed,
719 pass
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000720 proc.wait()
721
722
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000723def run(*cmd, **kwargs):
724 """The same as run_with_stderr, except it only returns stdout."""
725 return run_with_stderr(*cmd, **kwargs)[0]
726
727
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000728def run_with_retcode(*cmd, **kwargs):
729 """Run a command but only return the status code."""
730 try:
731 run(*cmd, **kwargs)
732 return 0
733 except subprocess2.CalledProcessError as cpe:
734 return cpe.returncode
735
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000736def run_stream(*cmd, **kwargs):
737 """Runs a git command. Returns stdout as a PIPE (file-like object).
738
739 stderr is dropped to avoid races if the process outputs to both stdout and
740 stderr.
741 """
742 kwargs.setdefault('stderr', subprocess2.VOID)
743 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000744 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000745 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000746 proc = subprocess2.Popen(cmd, **kwargs)
747 return proc.stdout
748
749
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000750@contextlib.contextmanager
751def run_stream_with_retcode(*cmd, **kwargs):
752 """Runs a git command as context manager yielding stdout as a PIPE.
753
754 stderr is dropped to avoid races if the process outputs to both stdout and
755 stderr.
756
757 Raises subprocess2.CalledProcessError on nonzero return code.
758 """
759 kwargs.setdefault('stderr', subprocess2.VOID)
760 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000761 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000762 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
763 try:
764 proc = subprocess2.Popen(cmd, **kwargs)
765 yield proc.stdout
766 finally:
767 retcode = proc.wait()
768 if retcode != 0:
769 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
Josip Sokcevic72f991f2020-04-23 18:53:30 +0000770 b'', b'')
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000771
772
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000773def run_with_stderr(*cmd, **kwargs):
774 """Runs a git command.
775
776 Returns (stdout, stderr) as a pair of strings.
777
778 kwargs
779 autostrip (bool) - Strip the output. Defaults to True.
780 indata (str) - Specifies stdin data for the process.
781 """
782 kwargs.setdefault('stdin', subprocess2.PIPE)
783 kwargs.setdefault('stdout', subprocess2.PIPE)
784 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000785 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000786 autostrip = kwargs.pop('autostrip', True)
787 indata = kwargs.pop('indata', None)
Edward Lemur12a537f2019-10-03 21:57:15 +0000788 decode = kwargs.pop('decode', True)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000789
iannucci@chromium.org21980022014-04-11 04:51:49 +0000790 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000791 proc = subprocess2.Popen(cmd, **kwargs)
792 ret, err = proc.communicate(indata)
793 retcode = proc.wait()
794 if retcode != 0:
795 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
796
797 if autostrip:
Edward Lemur12a537f2019-10-03 21:57:15 +0000798 ret = (ret or b'').strip()
799 err = (err or b'').strip()
800
801 if decode:
802 ret = ret.decode('utf-8', 'replace')
803 err = err.decode('utf-8', 'replace')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000804
805 return ret, err
806
807
808def set_branch_config(branch, option, value, scope='local'):
809 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
810
811
812def set_config(option, value, scope='local'):
813 run('config', '--' + scope, option, value)
814
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000815
sbc@chromium.org71437c02015-04-09 19:29:40 +0000816def get_dirty_files():
817 # Make sure index is up-to-date before running diff-index.
818 run_with_retcode('update-index', '--refresh', '-q')
Eli Ribble54434e72019-05-24 00:41:15 +0000819 return run('diff-index', '--ignore-submodules', '--name-status', 'HEAD')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000820
821
822def is_dirty_git_tree(cmd):
iannuccie38699b2016-08-15 17:32:31 -0700823 w = lambda s: sys.stderr.write(s+"\n")
824
sbc@chromium.org71437c02015-04-09 19:29:40 +0000825 dirty = get_dirty_files()
826 if dirty:
iannuccie38699b2016-08-15 17:32:31 -0700827 w('Cannot %s with a dirty tree. Commit, freeze or stash your changes first.'
828 % cmd)
829 w('Uncommitted files: (git diff-index --name-status HEAD)')
830 w(dirty[:4096])
sbc@chromium.org71437c02015-04-09 19:29:40 +0000831 if len(dirty) > 4096: # pragma: no cover
iannuccie38699b2016-08-15 17:32:31 -0700832 w('... (run "git diff-index --name-status HEAD" to see full output).')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000833 return True
834 return False
835
836
agable02b3c982016-06-22 07:51:22 -0700837def status():
838 """Returns a parsed version of git-status.
839
840 Returns a generator of (current_name, (lstat, rstat, src)) pairs where:
841 * current_name is the name of the file
842 * lstat is the left status code letter from git-status
843 * rstat is the left status code letter from git-status
844 * src is the current name of the file, or the original name of the file
845 if lstat == 'R'
846 """
847 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src')
848
849 def tokenizer(stream):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000850 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700851 c = None
Edward Lemur12a537f2019-10-03 21:57:15 +0000852 while c != b'':
agable02b3c982016-06-22 07:51:22 -0700853 c = stream.read(1)
Edward Lemur12a537f2019-10-03 21:57:15 +0000854 if c in (None, b'', b'\0'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000855 if len(acc.getvalue()):
agable02b3c982016-06-22 07:51:22 -0700856 yield acc.getvalue()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000857 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700858 else:
859 acc.write(c)
860
861 def parser(tokens):
862 while True:
Edward Lemur12a537f2019-10-03 21:57:15 +0000863 try:
864 status_dest = next(tokens).decode('utf-8')
865 except StopIteration:
866 return
agable02b3c982016-06-22 07:51:22 -0700867 stat, dest = status_dest[:2], status_dest[3:]
868 lstat, rstat = stat
869 if lstat == 'R':
Edward Lemur12a537f2019-10-03 21:57:15 +0000870 src = next(tokens).decode('utf-8')
agable02b3c982016-06-22 07:51:22 -0700871 else:
872 src = dest
873 yield (dest, stat_entry(lstat, rstat, src))
874
875 return parser(tokenizer(run_stream('status', '-z', bufsize=-1)))
876
877
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000878def squash_current_branch(header=None, merge_base=None):
Alan Cutter00017822016-12-20 17:39:59 +1100879 header = header or 'git squash commit for %s.' % current_branch()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000880 merge_base = merge_base or get_or_create_merge_base(current_branch())
881 log_msg = header + '\n'
882 if log_msg:
883 log_msg += '\n'
884 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
885 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000886
887 if not get_dirty_files():
888 # Sometimes the squash can result in the same tree, meaning that there is
889 # nothing to commit at this point.
Raul Tambrec2f74c12019-03-19 05:55:53 +0000890 print('Nothing to commit; squashed branch is empty')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000891 return False
Edward Lemur71681bf2019-10-09 23:46:20 +0000892 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg.encode('utf-8'))
sbc@chromium.org71437c02015-04-09 19:29:40 +0000893 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000894
895
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000896def tags(*args):
897 return run('tag', *args).splitlines()
898
899
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000900def thaw():
901 took_action = False
Edward Lemur12a537f2019-10-03 21:57:15 +0000902 for sha in run_stream('rev-list', 'HEAD').readlines():
903 sha = sha.strip().decode('utf-8')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000904 msg = run('show', '--format=%f%b', '-s', 'HEAD')
905 match = FREEZE_MATCHER.match(msg)
906 if not match:
907 if not took_action:
908 return 'Nothing to thaw.'
909 break
910
911 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
912 took_action = True
913
914
915def topo_iter(branch_tree, top_down=True):
916 """Generates (branch, parent) in topographical order for a branch tree.
917
918 Given a tree:
919
920 A1
921 B1 B2
922 C1 C2 C3
923 D1
924
925 branch_tree would look like: {
926 'D1': 'C3',
927 'C3': 'B2',
928 'B2': 'A1',
929 'C1': 'B1',
930 'C2': 'B1',
931 'B1': 'A1',
932 }
933
934 It is OK to have multiple 'root' nodes in your graph.
935
936 if top_down is True, items are yielded from A->D. Otherwise they're yielded
937 from D->A. Within a layer the branches will be yielded in sorted order.
938 """
939 branch_tree = branch_tree.copy()
940
941 # TODO(iannucci): There is probably a more efficient way to do these.
942 if top_down:
943 while branch_tree:
Edward Lemur12a537f2019-10-03 21:57:15 +0000944 this_pass = [(b, p) for b, p in branch_tree.items()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000945 if p not in branch_tree]
946 assert this_pass, "Branch tree has cycles: %r" % branch_tree
947 for branch, parent in sorted(this_pass):
948 yield branch, parent
949 del branch_tree[branch]
950 else:
951 parent_to_branches = collections.defaultdict(set)
Edward Lemur12a537f2019-10-03 21:57:15 +0000952 for branch, parent in branch_tree.items():
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000953 parent_to_branches[parent].add(branch)
954
955 while branch_tree:
Edward Lemur12a537f2019-10-03 21:57:15 +0000956 this_pass = [(b, p) for b, p in branch_tree.items()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000957 if not parent_to_branches[b]]
958 assert this_pass, "Branch tree has cycles: %r" % branch_tree
959 for branch, parent in sorted(this_pass):
960 yield branch, parent
961 parent_to_branches[parent].discard(branch)
962 del branch_tree[branch]
963
964
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000965def tree(treeref, recurse=False):
966 """Returns a dict representation of a git tree object.
967
968 Args:
969 treeref (str) - a git ref which resolves to a tree (commits count as trees).
qyearsley12fa6ff2016-08-24 09:18:40 -0700970 recurse (bool) - include all of the tree's descendants too. File names will
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000971 take the form of 'some/path/to/file'.
972
973 Return format:
974 { 'file_name': (mode, type, ref) }
975
976 mode is an integer where:
977 * 0040000 - Directory
978 * 0100644 - Regular non-executable file
979 * 0100664 - Regular non-executable group-writeable file
980 * 0100755 - Regular executable file
981 * 0120000 - Symbolic link
982 * 0160000 - Gitlink
983
984 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
985
986 ref is the hex encoded hash of the entry.
987 """
988 ret = {}
989 opts = ['ls-tree', '--full-tree']
990 if recurse:
991 opts.append('-r')
992 opts.append(treeref)
993 try:
994 for line in run(*opts).splitlines():
995 mode, typ, ref, name = line.split(None, 3)
996 ret[name] = (mode, typ, ref)
997 except subprocess2.CalledProcessError:
998 return None
999 return ret
1000
1001
Mun Yong Jang781e71e2017-10-25 15:46:20 -07001002def get_remote_url(remote='origin'):
1003 try:
1004 return run('config', 'remote.%s.url' % remote)
1005 except subprocess2.CalledProcessError:
1006 return None
1007
1008
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +00001009def upstream(branch):
1010 try:
1011 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
1012 branch+'@{upstream}')
1013 except subprocess2.CalledProcessError:
1014 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001015
agable@chromium.orgd629fb42014-10-01 09:40:10 +00001016
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001017def get_git_version():
1018 """Returns a tuple that contains the numeric components of the current git
1019 version."""
1020 version_string = run('--version')
1021 version_match = re.search(r'(\d+.)+(\d+)', version_string)
1022 version = version_match.group() if version_match else ''
1023
1024 return tuple(int(x) for x in version.split('.'))
1025
1026
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001027def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001028 format_string = (
1029 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
1030
1031 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001032 if (include_tracking_status and
1033 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001034 format_string += '%(upstream:track)'
1035
1036 info_map = {}
1037 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001038 BranchesInfo = collections.namedtuple(
1039 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001040 for line in data.splitlines():
1041 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
1042
1043 ahead_match = re.search(r'ahead (\d+)', tracking_status)
1044 ahead = int(ahead_match.group(1)) if ahead_match else None
1045
1046 behind_match = re.search(r'behind (\d+)', tracking_status)
1047 behind = int(behind_match.group(1)) if behind_match else None
1048
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001049 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001050 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
1051
1052 # Set None for upstreams which are not branches (e.g empty upstream, remotes
1053 # and deleted upstream branches).
1054 missing_upstreams = {}
1055 for info in info_map.values():
1056 if info.upstream not in info_map and info.upstream not in missing_upstreams:
1057 missing_upstreams[info.upstream] = None
1058
Edward Lemur12a537f2019-10-03 21:57:15 +00001059 result = info_map.copy()
1060 result.update(missing_upstreams)
1061 return result
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001062
1063
1064def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001065 files_to_copy, symlink=None):
1066 if not symlink:
1067 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001068 os.makedirs(new_workdir)
1069 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001070 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001071 for entry in files_to_copy:
1072 clone_file(repository, new_workdir, entry, shutil.copy)
1073
1074
1075def make_workdir(repository, new_workdir):
1076 GIT_DIRECTORY_WHITELIST = [
1077 'config',
1078 'info',
1079 'hooks',
1080 'logs/refs',
1081 'objects',
1082 'packed-refs',
1083 'refs',
1084 'remotes',
1085 'rr-cache',
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001086 ]
1087 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
1088 ['HEAD'])
1089
1090
1091def clone_file(repository, new_workdir, link, operation):
1092 if not os.path.exists(os.path.join(repository, link)):
1093 return
1094 link_dir = os.path.dirname(os.path.join(new_workdir, link))
1095 if not os.path.exists(link_dir):
1096 os.makedirs(link_dir)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001097 src = os.path.join(repository, link)
1098 if os.path.islink(src):
Henrique Ferreiroaea45d22018-02-19 09:48:36 +01001099 src = os.path.realpath(src)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001100 operation(src, os.path.join(new_workdir, link))