blob: 817f2c30d60e87d0f77cc879cb5053915629326e [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
12from multiprocessing.pool import IMapIterator
13def wrapper(func):
14 def wrap(self, timeout=None):
Edward Lemur12a537f2019-10-03 21:57:15 +000015 return func(self, timeout=timeout or 1 << 31)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000016 return wrap
17IMapIterator.next = wrapper(IMapIterator.next)
18IMapIterator.__next__ = IMapIterator.next
19# TODO(iannucci): Monkeypatch all other 'wait' methods too.
20
21
22import binascii
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000023import collections
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000024import contextlib
25import functools
26import logging
iannucci@chromium.org97345eb2014-03-13 07:55:15 +000027import os
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000028import re
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000029import setup_color
sammc@chromium.org900a33f2015-09-29 06:57:09 +000030import shutil
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000031import signal
32import sys
33import tempfile
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +000034import textwrap
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000035import threading
36
37import subprocess2
38
Raul Tambrec2f74c12019-03-19 05:55:53 +000039from io import BytesIO
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000040
agable02b3c982016-06-22 07:51:22 -070041
42ROOT = os.path.abspath(os.path.dirname(__file__))
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +000043IS_WIN = sys.platform == 'win32'
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000044TEST_MODE = False
45
Dan Jacques209a6812017-07-12 11:40:20 -070046
47def win_find_git():
48 for elem in os.environ.get('PATH', '').split(os.pathsep):
49 for candidate in ('git.exe', 'git.bat'):
50 path = os.path.join(elem, candidate)
51 if os.path.isfile(path):
52 return path
53 raise ValueError('Could not find Git on PATH.')
54
55
56GIT_EXE = 'git' if not IS_WIN else win_find_git()
57
58
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000059FREEZE = 'FREEZE'
60FREEZE_SECTIONS = {
61 'indexed': 'soft',
62 'unindexed': 'mixed'
63}
64FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000065
66
Dan Jacques2f8b0c12017-04-05 12:57:21 -070067# NOTE: This list is DEPRECATED in favor of the Infra Git wrapper:
68# https://chromium.googlesource.com/infra/infra/+/master/go/src/infra/tools/git
69#
70# New entries should be added to the Git wrapper, NOT to this list. "git_retry"
71# is, similarly, being deprecated in favor of the Git wrapper.
72#
73# ---
74#
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000075# Retry a git operation if git returns a error response with any of these
76# messages. It's all observed 'bad' GoB responses so far.
77#
78# This list is inspired/derived from the one in ChromiumOS's Chromite:
79# <CHROMITE>/lib/git.py::GIT_TRANSIENT_ERRORS
80#
81# It was last imported from '7add3ac29564d98ac35ce426bc295e743e7c0c02'.
82GIT_TRANSIENT_ERRORS = (
83 # crbug.com/285832
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000084 r'!.*\[remote rejected\].*\(error in hook\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000085
86 # crbug.com/289932
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000087 r'!.*\[remote rejected\].*\(failed to lock\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000088
89 # crbug.com/307156
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000090 r'!.*\[remote rejected\].*\(error in Gerrit backend\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000091
92 # crbug.com/285832
93 r'remote error: Internal Server Error',
94
95 # crbug.com/294449
96 r'fatal: Couldn\'t find remote ref ',
97
98 # crbug.com/220543
99 r'git fetch_pack: expected ACK/NAK, got',
100
101 # crbug.com/189455
102 r'protocol error: bad pack header',
103
104 # crbug.com/202807
105 r'The remote end hung up unexpectedly',
106
107 # crbug.com/298189
108 r'TLS packet with unexpected length was received',
109
110 # crbug.com/187444
111 r'RPC failed; result=\d+, HTTP code = \d+',
112
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000113 # crbug.com/388876
114 r'Connection timed out',
dnj@chromium.org45cddd62014-11-06 19:36:42 +0000115
116 # crbug.com/430343
117 # TODO(dnj): Resync with Chromite.
118 r'The requested URL returned error: 5\d+',
Arikonb3a21482016-07-22 10:12:24 -0700119
120 r'Connection reset by peer',
121
122 r'Unable to look up',
123
124 r'Couldn\'t resolve host',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000125)
126
127GIT_TRANSIENT_ERRORS_RE = re.compile('|'.join(GIT_TRANSIENT_ERRORS),
128 re.IGNORECASE)
129
raphael.kubo.da.costa@intel.com58d05b02015-06-24 08:54:41 +0000130# git's for-each-ref command first supported the upstream:track token in its
131# format string in version 1.9.0, but some usages were broken until 2.3.0.
132# See git commit b6160d95 for more information.
133MIN_UPSTREAM_TRACK_GIT_VERSION = (2, 3)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000134
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000135class BadCommitRefException(Exception):
136 def __init__(self, refs):
137 msg = ('one of %s does not seem to be a valid commitref.' %
138 str(refs))
139 super(BadCommitRefException, self).__init__(msg)
140
141
142def memoize_one(**kwargs):
143 """Memoizes a single-argument pure function.
144
145 Values of None are not cached.
146
147 Kwargs:
148 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
149 cache manipulation functions. This is a kwarg so that users of memoize_one
150 are forced to explicitly and verbosely pick True or False.
151
152 Adds three methods to the decorated function:
153 * get(key, default=None) - Gets the value for this key from the cache.
154 * set(key, value) - Sets the value for this key from the cache.
155 * clear() - Drops the entire contents of the cache. Useful for unittests.
156 * update(other) - Updates the contents of the cache from another dict.
157 """
158 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
159 threadsafe = kwargs['threadsafe']
160
161 if threadsafe:
162 def withlock(lock, f):
163 def inner(*args, **kwargs):
164 with lock:
165 return f(*args, **kwargs)
166 return inner
167 else:
168 def withlock(_lock, f):
169 return f
170
171 def decorator(f):
172 # Instantiate the lock in decorator, in case users of memoize_one do:
173 #
174 # memoizer = memoize_one(threadsafe=True)
175 #
176 # @memoizer
177 # def fn1(val): ...
178 #
179 # @memoizer
180 # def fn2(val): ...
181
182 lock = threading.Lock() if threadsafe else None
183 cache = {}
184 _get = withlock(lock, cache.get)
185 _set = withlock(lock, cache.__setitem__)
186
187 @functools.wraps(f)
188 def inner(arg):
189 ret = _get(arg)
190 if ret is None:
191 ret = f(arg)
192 if ret is not None:
193 _set(arg, ret)
194 return ret
195 inner.get = _get
196 inner.set = _set
197 inner.clear = withlock(lock, cache.clear)
198 inner.update = withlock(lock, cache.update)
199 return inner
200 return decorator
201
202
203def _ScopedPool_initer(orig, orig_args): # pragma: no cover
204 """Initializer method for ScopedPool's subprocesses.
205
206 This helps ScopedPool handle Ctrl-C's correctly.
207 """
208 signal.signal(signal.SIGINT, signal.SIG_IGN)
209 if orig:
210 orig(*orig_args)
211
212
213@contextlib.contextmanager
214def ScopedPool(*args, **kwargs):
215 """Context Manager which returns a multiprocessing.pool instance which
216 correctly deals with thrown exceptions.
217
218 *args - Arguments to multiprocessing.pool
219
220 Kwargs:
221 kind ('threads', 'procs') - The type of underlying coprocess to use.
222 **etc - Arguments to multiprocessing.pool
223 """
224 if kwargs.pop('kind', None) == 'threads':
225 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
226 else:
227 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
228 kwargs['initializer'] = _ScopedPool_initer
229 kwargs['initargs'] = orig, orig_args
230 pool = multiprocessing.pool.Pool(*args, **kwargs)
231
232 try:
233 yield pool
234 pool.close()
235 except:
236 pool.terminate()
237 raise
238 finally:
239 pool.join()
240
241
242class ProgressPrinter(object):
243 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000244 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000245 """Create a ProgressPrinter.
246
247 Use it as a context manager which produces a simple 'increment' method:
248
249 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
250 for i in xrange(1000):
251 # do stuff
252 if i % 10 == 0:
253 inc(10)
254
255 Args:
256 fmt - String format with a single '%(count)d' where the counter value
257 should go.
258 enabled (bool) - If this is None, will default to True if
259 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000260 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000261 period (float) - The time in seconds for the printer thread to wait
262 between printing.
263 """
264 self.fmt = fmt
265 if enabled is None: # pragma: no cover
266 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
267 else:
268 self.enabled = enabled
269
270 self._count = 0
271 self._dead = False
272 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000273 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000274 self._thread = threading.Thread(target=self._run)
275 self._period = period
276
277 def _emit(self, s):
278 if self.enabled:
279 self._stream.write('\r' + s)
280 self._stream.flush()
281
282 def _run(self):
283 with self._dead_cond:
284 while not self._dead:
285 self._emit(self.fmt % {'count': self._count})
286 self._dead_cond.wait(self._period)
287 self._emit((self.fmt + '\n') % {'count': self._count})
288
289 def inc(self, amount=1):
290 self._count += amount
291
292 def __enter__(self):
293 self._thread.start()
294 return self.inc
295
296 def __exit__(self, _exc_type, _exc_value, _traceback):
297 self._dead = True
298 with self._dead_cond:
299 self._dead_cond.notifyAll()
300 self._thread.join()
301 del self._thread
302
303
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000304def once(function):
305 """@Decorates |function| so that it only performs its action once, no matter
306 how many times the decorated |function| is called."""
Edward Lemur12a537f2019-10-03 21:57:15 +0000307 has_run = [False]
308 def _wrapper(*args, **kwargs):
309 if not has_run[0]:
310 has_run[0] = True
311 function(*args, **kwargs)
312 return _wrapper
313
314
315def unicode_repr(s):
316 result = repr(s)
317 return result[1:] if result.startswith('u') else result
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000318
319
320## Git functions
321
agable7aa2ddd2016-06-21 07:47:00 -0700322def die(message, *args):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000323 print(textwrap.dedent(message % args), file=sys.stderr)
agable7aa2ddd2016-06-21 07:47:00 -0700324 sys.exit(1)
325
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000326
Mark Mentovaif548d082017-03-08 13:32:00 -0500327def blame(filename, revision=None, porcelain=False, abbrev=None, *_args):
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000328 command = ['blame']
329 if porcelain:
330 command.append('-p')
331 if revision is not None:
332 command.append(revision)
Mark Mentovaif548d082017-03-08 13:32:00 -0500333 if abbrev is not None:
334 command.append('--abbrev=%d' % abbrev)
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000335 command.extend(['--', filename])
336 return run(*command)
337
338
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000339def branch_config(branch, option, default=None):
agable7aa2ddd2016-06-21 07:47:00 -0700340 return get_config('branch.%s.%s' % (branch, option), default=default)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000341
342
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000343def branch_config_map(option):
344 """Return {branch: <|option| value>} for all branches."""
345 try:
346 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
agable7aa2ddd2016-06-21 07:47:00 -0700347 lines = get_config_regexp(reg.pattern)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000348 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
349 except subprocess2.CalledProcessError:
350 return {}
351
352
Francois Dorayd42c6812017-05-30 15:10:20 -0400353def branches(use_limit=True, *args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000354 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000355
356 key = 'depot-tools.branch-limit'
agable7aa2ddd2016-06-21 07:47:00 -0700357 limit = get_config_int(key, 20)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000358
359 raw_branches = run('branch', *args).splitlines()
360
361 num = len(raw_branches)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000362
Francois Dorayd42c6812017-05-30 15:10:20 -0400363 if use_limit and num > limit:
agable7aa2ddd2016-06-21 07:47:00 -0700364 die("""\
365 Your git repo has too many branches (%d/%d) for this tool to work well.
366
367 You may adjust this limit by running:
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000368 git config %s <new_limit>
agable7aa2ddd2016-06-21 07:47:00 -0700369
370 You may also try cleaning up your old branches by running:
371 git cl archive
372 """, num, limit, key)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000373
374 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000375 if line.startswith(NO_BRANCH):
376 continue
377 yield line.split()[-1]
378
379
agable7aa2ddd2016-06-21 07:47:00 -0700380def get_config(option, default=None):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000381 try:
382 return run('config', '--get', option) or default
383 except subprocess2.CalledProcessError:
384 return default
385
386
agable7aa2ddd2016-06-21 07:47:00 -0700387def get_config_int(option, default=0):
388 assert isinstance(default, int)
389 try:
390 return int(get_config(option, default))
391 except ValueError:
392 return default
393
394
395def get_config_list(option):
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000396 try:
397 return run('config', '--get-all', option).split()
398 except subprocess2.CalledProcessError:
399 return []
400
401
agable7aa2ddd2016-06-21 07:47:00 -0700402def get_config_regexp(pattern):
403 if IS_WIN: # pragma: no cover
404 # this madness is because we call git.bat which calls git.exe which calls
405 # bash.exe (or something to that effect). Each layer divides the number of
406 # ^'s by 2.
407 pattern = pattern.replace('^', '^' * 8)
408 return run('config', '--get-regexp', pattern).splitlines()
409
410
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000411def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000412 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000413 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000414 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000415 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000416
417
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000418def del_branch_config(branch, option, scope='local'):
419 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000420
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000421
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000422def del_config(option, scope='local'):
423 try:
424 run('config', '--' + scope, '--unset', option)
425 except subprocess2.CalledProcessError:
426 pass
427
428
mgiuca@chromium.org01d2cde2016-02-05 03:25:41 +0000429def diff(oldrev, newrev, *args):
430 return run('diff', oldrev, newrev, *args)
431
432
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000433def freeze():
434 took_action = False
agable02b3c982016-06-22 07:51:22 -0700435 key = 'depot-tools.freeze-size-limit'
436 MB = 2**20
437 limit_mb = get_config_int(key, 100)
438 untracked_bytes = 0
439
iannuccieaca0332016-08-03 16:46:50 -0700440 root_path = repo_root()
441
agable02b3c982016-06-22 07:51:22 -0700442 for f, s in status():
443 if is_unmerged(s):
444 die("Cannot freeze unmerged changes!")
445 if limit_mb > 0:
446 if s.lstat == '?':
iannuccieaca0332016-08-03 16:46:50 -0700447 untracked_bytes += os.stat(os.path.join(root_path, f)).st_size
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800448 if limit_mb > 0 and untracked_bytes > limit_mb * MB:
449 die("""\
450 You appear to have too much untracked+unignored data in your git
451 checkout: %.1f / %d MB.
agable02b3c982016-06-22 07:51:22 -0700452
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800453 Run `git status` to see what it is.
agable02b3c982016-06-22 07:51:22 -0700454
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800455 In addition to making many git commands slower, this will prevent
456 depot_tools from freezing your in-progress changes.
agable02b3c982016-06-22 07:51:22 -0700457
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800458 You should add untracked data that you want to ignore to your repo's
459 .git/info/exclude
460 file. See `git help ignore` for the format of this file.
agable02b3c982016-06-22 07:51:22 -0700461
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800462 If this data is indended as part of your commit, you may adjust the
463 freeze limit by running:
464 git config %s <new_limit>
465 Where <new_limit> is an integer threshold in megabytes.""",
466 untracked_bytes / (MB * 1.0), limit_mb, key)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000467
468 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000469 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000470 took_action = True
471 except subprocess2.CalledProcessError:
472 pass
473
agable96e179b2016-06-24 10:32:51 -0700474 add_errors = False
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000475 try:
agable96e179b2016-06-24 10:32:51 -0700476 run('add', '-A', '--ignore-errors')
477 except subprocess2.CalledProcessError:
478 add_errors = True
479
480 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000481 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000482 took_action = True
483 except subprocess2.CalledProcessError:
484 pass
485
agable96e179b2016-06-24 10:32:51 -0700486 ret = []
487 if add_errors:
488 ret.append('Failed to index some unindexed files.')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000489 if not took_action:
agable96e179b2016-06-24 10:32:51 -0700490 ret.append('Nothing to freeze.')
491 return ' '.join(ret) or None
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000492
493
494def get_branch_tree():
495 """Get the dictionary of {branch: parent}, compatible with topo_iter.
496
497 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
498 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000499 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000500 skipped = set()
501 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000502
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000503 for branch in branches():
504 parent = upstream(branch)
505 if not parent:
506 skipped.add(branch)
507 continue
508 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000509
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000510 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000511
512
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000513def get_or_create_merge_base(branch, parent=None):
514 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000515
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000516 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000517 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000518 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000519 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000520 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000521 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000522 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000523 actual_merge_base = run('merge-base', parent, branch)
524
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000525 if base_upstream != parent:
526 base = None
527 base_upstream = None
528
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000529 def is_ancestor(a, b):
530 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
531
clemensh@chromium.orgc3fe99d2016-04-19 08:39:55 +0000532 if base and base != actual_merge_base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000533 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000534 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
535 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000536 elif is_ancestor(base, actual_merge_base):
537 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
538 base = None
539 else:
540 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000541
542 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000543 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000544 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000545
546 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000547
548
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000549def hash_multi(*reflike):
550 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000551
552
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000553def hash_one(reflike, short=False):
554 args = ['rev-parse', reflike]
555 if short:
556 args.insert(1, '--short')
557 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000558
559
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000560def in_rebase():
561 git_dir = run('rev-parse', '--git-dir')
562 return (
563 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
564 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000565
566
567def intern_f(f, kind='blob'):
568 """Interns a file object into the git object store.
569
570 Args:
571 f (file-like object) - The file-like object to intern
572 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
573
574 Returns the git hash of the interned object (hex encoded).
575 """
576 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
577 f.close()
578 return ret
579
580
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000581def is_dormant(branch):
582 # TODO(iannucci): Do an oldness check?
583 return branch_config(branch, 'dormant', 'false') != 'false'
584
585
agable02b3c982016-06-22 07:51:22 -0700586def is_unmerged(stat_value):
587 return (
588 'U' in (stat_value.lstat, stat_value.rstat) or
589 ((stat_value.lstat == stat_value.rstat) and stat_value.lstat in 'AD')
590 )
591
592
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000593def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000594 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000595 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000596
597
598def mktree(treedict):
599 """Makes a git tree object and returns its hash.
600
601 See |tree()| for the values of mode, type, and ref.
602
603 Args:
604 treedict - { name: (mode, type, ref) }
605 """
606 with tempfile.TemporaryFile() as f:
Edward Lemur12a537f2019-10-03 21:57:15 +0000607 for name, (mode, typ, ref) in treedict.items():
Edward Lemur71681bf2019-10-09 23:46:20 +0000608 f.write(('%s %s %s\t%s\0' % (mode, typ, ref, name)).encode('utf-8'))
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000609 f.seek(0)
610 return run('mktree', '-z', stdin=f)
611
612
613def parse_commitrefs(*commitrefs):
614 """Returns binary encoded commit hashes for one or more commitrefs.
615
616 A commitref is anything which can resolve to a commit. Popular examples:
617 * 'HEAD'
618 * 'origin/master'
619 * 'cool_branch~2'
620 """
621 try:
Edward Lemur12a537f2019-10-03 21:57:15 +0000622 return [binascii.unhexlify(h) for h in hash_multi(*commitrefs)]
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000623 except subprocess2.CalledProcessError:
624 raise BadCommitRefException(commitrefs)
625
626
sbc@chromium.org384039b2014-10-13 21:01:00 +0000627RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000628
629
630def rebase(parent, start, branch, abort=False):
631 """Rebases |start|..|branch| onto the branch |parent|.
632
633 Args:
634 parent - The new parent ref for the rebased commits.
635 start - The commit to start from
636 branch - The branch to rebase
637 abort - If True, will call git-rebase --abort in the event that the rebase
638 doesn't complete successfully.
639
640 Returns a namedtuple with fields:
641 success - a boolean indicating that the rebase command completed
642 successfully.
643 message - if the rebase failed, this contains the stdout of the failed
644 rebase.
645 """
646 try:
647 args = ['--onto', parent, start, branch]
648 if TEST_MODE:
649 args.insert(0, '--committer-date-is-author-date')
650 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000651 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000652 except subprocess2.CalledProcessError as cpe:
653 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000654 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000655 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000656
657
658def remove_merge_base(branch):
659 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000660 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000661
662
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000663def repo_root():
664 """Returns the absolute path to the repository root."""
665 return run('rev-parse', '--show-toplevel')
666
667
Jeffrey Yasskin6b52dc22019-12-06 18:32:21 +0000668def upstream_default():
669 """Returns the default branch name of the origin repository."""
670 try:
671 return run('rev-parse', '--abbrev-ref', 'origin/HEAD')
672 except subprocess2.CalledProcessError:
673 return 'origin/master'
674
675
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000676def root():
Jeffrey Yasskin6b52dc22019-12-06 18:32:21 +0000677 return get_config('depot-tools.upstream', upstream_default())
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000678
679
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000680@contextlib.contextmanager
681def less(): # pragma: no cover
682 """Runs 'less' as context manager yielding its stdin as a PIPE.
683
684 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
685 running less and just yields sys.stdout.
Edward Lemur0d462e92020-01-08 20:11:31 +0000686
687 The returned PIPE is opened on binary mode.
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000688 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000689 if not setup_color.IS_TTY:
Edward Lemur5e94b802019-11-26 21:44:08 +0000690 # On Python 3, sys.stdout doesn't accept bytes, and sys.stdout.buffer must
691 # be used.
692 yield getattr(sys.stdout, 'buffer', sys.stdout)
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000693 return
694
695 # Run with the same options that git uses (see setup_pager in git repo).
696 # -F: Automatically quit if the output is less than one screen.
697 # -R: Don't escape ANSI color codes.
698 # -X: Don't clear the screen before starting.
699 cmd = ('less', '-FRX')
700 try:
701 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
702 yield proc.stdin
703 finally:
704 proc.stdin.close()
705 proc.wait()
706
707
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000708def run(*cmd, **kwargs):
709 """The same as run_with_stderr, except it only returns stdout."""
710 return run_with_stderr(*cmd, **kwargs)[0]
711
712
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000713def run_with_retcode(*cmd, **kwargs):
714 """Run a command but only return the status code."""
715 try:
716 run(*cmd, **kwargs)
717 return 0
718 except subprocess2.CalledProcessError as cpe:
719 return cpe.returncode
720
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000721def run_stream(*cmd, **kwargs):
722 """Runs a git command. Returns stdout as a PIPE (file-like object).
723
724 stderr is dropped to avoid races if the process outputs to both stdout and
725 stderr.
726 """
727 kwargs.setdefault('stderr', subprocess2.VOID)
728 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000729 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000730 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000731 proc = subprocess2.Popen(cmd, **kwargs)
732 return proc.stdout
733
734
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000735@contextlib.contextmanager
736def run_stream_with_retcode(*cmd, **kwargs):
737 """Runs a git command as context manager yielding stdout as a PIPE.
738
739 stderr is dropped to avoid races if the process outputs to both stdout and
740 stderr.
741
742 Raises subprocess2.CalledProcessError on nonzero return code.
743 """
744 kwargs.setdefault('stderr', subprocess2.VOID)
745 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000746 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000747 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
748 try:
749 proc = subprocess2.Popen(cmd, **kwargs)
750 yield proc.stdout
751 finally:
752 retcode = proc.wait()
753 if retcode != 0:
754 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
755 None, None)
756
757
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000758def run_with_stderr(*cmd, **kwargs):
759 """Runs a git command.
760
761 Returns (stdout, stderr) as a pair of strings.
762
763 kwargs
764 autostrip (bool) - Strip the output. Defaults to True.
765 indata (str) - Specifies stdin data for the process.
766 """
767 kwargs.setdefault('stdin', subprocess2.PIPE)
768 kwargs.setdefault('stdout', subprocess2.PIPE)
769 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000770 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000771 autostrip = kwargs.pop('autostrip', True)
772 indata = kwargs.pop('indata', None)
Edward Lemur12a537f2019-10-03 21:57:15 +0000773 decode = kwargs.pop('decode', True)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000774
iannucci@chromium.org21980022014-04-11 04:51:49 +0000775 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000776 proc = subprocess2.Popen(cmd, **kwargs)
777 ret, err = proc.communicate(indata)
778 retcode = proc.wait()
779 if retcode != 0:
780 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
781
782 if autostrip:
Edward Lemur12a537f2019-10-03 21:57:15 +0000783 ret = (ret or b'').strip()
784 err = (err or b'').strip()
785
786 if decode:
787 ret = ret.decode('utf-8', 'replace')
788 err = err.decode('utf-8', 'replace')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000789
790 return ret, err
791
792
793def set_branch_config(branch, option, value, scope='local'):
794 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
795
796
797def set_config(option, value, scope='local'):
798 run('config', '--' + scope, option, value)
799
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000800
sbc@chromium.org71437c02015-04-09 19:29:40 +0000801def get_dirty_files():
802 # Make sure index is up-to-date before running diff-index.
803 run_with_retcode('update-index', '--refresh', '-q')
Eli Ribble54434e72019-05-24 00:41:15 +0000804 return run('diff-index', '--ignore-submodules', '--name-status', 'HEAD')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000805
806
807def is_dirty_git_tree(cmd):
iannuccie38699b2016-08-15 17:32:31 -0700808 w = lambda s: sys.stderr.write(s+"\n")
809
sbc@chromium.org71437c02015-04-09 19:29:40 +0000810 dirty = get_dirty_files()
811 if dirty:
iannuccie38699b2016-08-15 17:32:31 -0700812 w('Cannot %s with a dirty tree. Commit, freeze or stash your changes first.'
813 % cmd)
814 w('Uncommitted files: (git diff-index --name-status HEAD)')
815 w(dirty[:4096])
sbc@chromium.org71437c02015-04-09 19:29:40 +0000816 if len(dirty) > 4096: # pragma: no cover
iannuccie38699b2016-08-15 17:32:31 -0700817 w('... (run "git diff-index --name-status HEAD" to see full output).')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000818 return True
819 return False
820
821
agable02b3c982016-06-22 07:51:22 -0700822def status():
823 """Returns a parsed version of git-status.
824
825 Returns a generator of (current_name, (lstat, rstat, src)) pairs where:
826 * current_name is the name of the file
827 * lstat is the left status code letter from git-status
828 * rstat is the left status code letter from git-status
829 * src is the current name of the file, or the original name of the file
830 if lstat == 'R'
831 """
832 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src')
833
834 def tokenizer(stream):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000835 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700836 c = None
Edward Lemur12a537f2019-10-03 21:57:15 +0000837 while c != b'':
agable02b3c982016-06-22 07:51:22 -0700838 c = stream.read(1)
Edward Lemur12a537f2019-10-03 21:57:15 +0000839 if c in (None, b'', b'\0'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000840 if len(acc.getvalue()):
agable02b3c982016-06-22 07:51:22 -0700841 yield acc.getvalue()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000842 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700843 else:
844 acc.write(c)
845
846 def parser(tokens):
847 while True:
Edward Lemur12a537f2019-10-03 21:57:15 +0000848 try:
849 status_dest = next(tokens).decode('utf-8')
850 except StopIteration:
851 return
agable02b3c982016-06-22 07:51:22 -0700852 stat, dest = status_dest[:2], status_dest[3:]
853 lstat, rstat = stat
854 if lstat == 'R':
Edward Lemur12a537f2019-10-03 21:57:15 +0000855 src = next(tokens).decode('utf-8')
agable02b3c982016-06-22 07:51:22 -0700856 else:
857 src = dest
858 yield (dest, stat_entry(lstat, rstat, src))
859
860 return parser(tokenizer(run_stream('status', '-z', bufsize=-1)))
861
862
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000863def squash_current_branch(header=None, merge_base=None):
Alan Cutter00017822016-12-20 17:39:59 +1100864 header = header or 'git squash commit for %s.' % current_branch()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000865 merge_base = merge_base or get_or_create_merge_base(current_branch())
866 log_msg = header + '\n'
867 if log_msg:
868 log_msg += '\n'
869 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
870 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000871
872 if not get_dirty_files():
873 # Sometimes the squash can result in the same tree, meaning that there is
874 # nothing to commit at this point.
Raul Tambrec2f74c12019-03-19 05:55:53 +0000875 print('Nothing to commit; squashed branch is empty')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000876 return False
Edward Lemur71681bf2019-10-09 23:46:20 +0000877 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg.encode('utf-8'))
sbc@chromium.org71437c02015-04-09 19:29:40 +0000878 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000879
880
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000881def tags(*args):
882 return run('tag', *args).splitlines()
883
884
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000885def thaw():
886 took_action = False
Edward Lemur12a537f2019-10-03 21:57:15 +0000887 for sha in run_stream('rev-list', 'HEAD').readlines():
888 sha = sha.strip().decode('utf-8')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000889 msg = run('show', '--format=%f%b', '-s', 'HEAD')
890 match = FREEZE_MATCHER.match(msg)
891 if not match:
892 if not took_action:
893 return 'Nothing to thaw.'
894 break
895
896 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
897 took_action = True
898
899
900def topo_iter(branch_tree, top_down=True):
901 """Generates (branch, parent) in topographical order for a branch tree.
902
903 Given a tree:
904
905 A1
906 B1 B2
907 C1 C2 C3
908 D1
909
910 branch_tree would look like: {
911 'D1': 'C3',
912 'C3': 'B2',
913 'B2': 'A1',
914 'C1': 'B1',
915 'C2': 'B1',
916 'B1': 'A1',
917 }
918
919 It is OK to have multiple 'root' nodes in your graph.
920
921 if top_down is True, items are yielded from A->D. Otherwise they're yielded
922 from D->A. Within a layer the branches will be yielded in sorted order.
923 """
924 branch_tree = branch_tree.copy()
925
926 # TODO(iannucci): There is probably a more efficient way to do these.
927 if top_down:
928 while branch_tree:
Edward Lemur12a537f2019-10-03 21:57:15 +0000929 this_pass = [(b, p) for b, p in branch_tree.items()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000930 if p not in branch_tree]
931 assert this_pass, "Branch tree has cycles: %r" % branch_tree
932 for branch, parent in sorted(this_pass):
933 yield branch, parent
934 del branch_tree[branch]
935 else:
936 parent_to_branches = collections.defaultdict(set)
Edward Lemur12a537f2019-10-03 21:57:15 +0000937 for branch, parent in branch_tree.items():
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000938 parent_to_branches[parent].add(branch)
939
940 while branch_tree:
Edward Lemur12a537f2019-10-03 21:57:15 +0000941 this_pass = [(b, p) for b, p in branch_tree.items()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000942 if not parent_to_branches[b]]
943 assert this_pass, "Branch tree has cycles: %r" % branch_tree
944 for branch, parent in sorted(this_pass):
945 yield branch, parent
946 parent_to_branches[parent].discard(branch)
947 del branch_tree[branch]
948
949
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000950def tree(treeref, recurse=False):
951 """Returns a dict representation of a git tree object.
952
953 Args:
954 treeref (str) - a git ref which resolves to a tree (commits count as trees).
qyearsley12fa6ff2016-08-24 09:18:40 -0700955 recurse (bool) - include all of the tree's descendants too. File names will
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000956 take the form of 'some/path/to/file'.
957
958 Return format:
959 { 'file_name': (mode, type, ref) }
960
961 mode is an integer where:
962 * 0040000 - Directory
963 * 0100644 - Regular non-executable file
964 * 0100664 - Regular non-executable group-writeable file
965 * 0100755 - Regular executable file
966 * 0120000 - Symbolic link
967 * 0160000 - Gitlink
968
969 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
970
971 ref is the hex encoded hash of the entry.
972 """
973 ret = {}
974 opts = ['ls-tree', '--full-tree']
975 if recurse:
976 opts.append('-r')
977 opts.append(treeref)
978 try:
979 for line in run(*opts).splitlines():
980 mode, typ, ref, name = line.split(None, 3)
981 ret[name] = (mode, typ, ref)
982 except subprocess2.CalledProcessError:
983 return None
984 return ret
985
986
Mun Yong Jang781e71e2017-10-25 15:46:20 -0700987def get_remote_url(remote='origin'):
988 try:
989 return run('config', 'remote.%s.url' % remote)
990 except subprocess2.CalledProcessError:
991 return None
992
993
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000994def upstream(branch):
995 try:
996 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
997 branch+'@{upstream}')
998 except subprocess2.CalledProcessError:
999 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001000
agable@chromium.orgd629fb42014-10-01 09:40:10 +00001001
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001002def get_git_version():
1003 """Returns a tuple that contains the numeric components of the current git
1004 version."""
1005 version_string = run('--version')
1006 version_match = re.search(r'(\d+.)+(\d+)', version_string)
1007 version = version_match.group() if version_match else ''
1008
1009 return tuple(int(x) for x in version.split('.'))
1010
1011
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001012def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001013 format_string = (
1014 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
1015
1016 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001017 if (include_tracking_status and
1018 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001019 format_string += '%(upstream:track)'
1020
1021 info_map = {}
1022 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001023 BranchesInfo = collections.namedtuple(
1024 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001025 for line in data.splitlines():
1026 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
1027
1028 ahead_match = re.search(r'ahead (\d+)', tracking_status)
1029 ahead = int(ahead_match.group(1)) if ahead_match else None
1030
1031 behind_match = re.search(r'behind (\d+)', tracking_status)
1032 behind = int(behind_match.group(1)) if behind_match else None
1033
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001034 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001035 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
1036
1037 # Set None for upstreams which are not branches (e.g empty upstream, remotes
1038 # and deleted upstream branches).
1039 missing_upstreams = {}
1040 for info in info_map.values():
1041 if info.upstream not in info_map and info.upstream not in missing_upstreams:
1042 missing_upstreams[info.upstream] = None
1043
Edward Lemur12a537f2019-10-03 21:57:15 +00001044 result = info_map.copy()
1045 result.update(missing_upstreams)
1046 return result
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001047
1048
1049def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001050 files_to_copy, symlink=None):
1051 if not symlink:
1052 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001053 os.makedirs(new_workdir)
1054 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001055 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001056 for entry in files_to_copy:
1057 clone_file(repository, new_workdir, entry, shutil.copy)
1058
1059
1060def make_workdir(repository, new_workdir):
1061 GIT_DIRECTORY_WHITELIST = [
1062 'config',
1063 'info',
1064 'hooks',
1065 'logs/refs',
1066 'objects',
1067 'packed-refs',
1068 'refs',
1069 'remotes',
1070 'rr-cache',
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001071 ]
1072 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
1073 ['HEAD'])
1074
1075
1076def clone_file(repository, new_workdir, link, operation):
1077 if not os.path.exists(os.path.join(repository, link)):
1078 return
1079 link_dir = os.path.dirname(os.path.join(new_workdir, link))
1080 if not os.path.exists(link_dir):
1081 os.makedirs(link_dir)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001082 src = os.path.join(repository, link)
1083 if os.path.islink(src):
Henrique Ferreiroaea45d22018-02-19 09:48:36 +01001084 src = os.path.realpath(src)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001085 operation(src, os.path.join(new_workdir, link))