blob: 24c178c801e584c22c6a7bfb25cf12580f59eaf9 [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
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000668def root():
agable7aa2ddd2016-06-21 07:47:00 -0700669 return get_config('depot-tools.upstream', 'origin/master')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000670
671
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000672@contextlib.contextmanager
673def less(): # pragma: no cover
674 """Runs 'less' as context manager yielding its stdin as a PIPE.
675
676 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
677 running less and just yields sys.stdout.
678 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000679 if not setup_color.IS_TTY:
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000680 yield sys.stdout
681 return
682
683 # Run with the same options that git uses (see setup_pager in git repo).
684 # -F: Automatically quit if the output is less than one screen.
685 # -R: Don't escape ANSI color codes.
686 # -X: Don't clear the screen before starting.
687 cmd = ('less', '-FRX')
688 try:
689 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
690 yield proc.stdin
691 finally:
692 proc.stdin.close()
693 proc.wait()
694
695
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000696def run(*cmd, **kwargs):
697 """The same as run_with_stderr, except it only returns stdout."""
698 return run_with_stderr(*cmd, **kwargs)[0]
699
700
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000701def run_with_retcode(*cmd, **kwargs):
702 """Run a command but only return the status code."""
703 try:
704 run(*cmd, **kwargs)
705 return 0
706 except subprocess2.CalledProcessError as cpe:
707 return cpe.returncode
708
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000709def run_stream(*cmd, **kwargs):
710 """Runs a git command. Returns stdout as a PIPE (file-like object).
711
712 stderr is dropped to avoid races if the process outputs to both stdout and
713 stderr.
714 """
715 kwargs.setdefault('stderr', subprocess2.VOID)
716 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000717 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000718 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000719 proc = subprocess2.Popen(cmd, **kwargs)
720 return proc.stdout
721
722
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000723@contextlib.contextmanager
724def run_stream_with_retcode(*cmd, **kwargs):
725 """Runs a git command as context manager yielding stdout as a PIPE.
726
727 stderr is dropped to avoid races if the process outputs to both stdout and
728 stderr.
729
730 Raises subprocess2.CalledProcessError on nonzero return code.
731 """
732 kwargs.setdefault('stderr', subprocess2.VOID)
733 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000734 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000735 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
736 try:
737 proc = subprocess2.Popen(cmd, **kwargs)
738 yield proc.stdout
739 finally:
740 retcode = proc.wait()
741 if retcode != 0:
742 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
743 None, None)
744
745
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000746def run_with_stderr(*cmd, **kwargs):
747 """Runs a git command.
748
749 Returns (stdout, stderr) as a pair of strings.
750
751 kwargs
752 autostrip (bool) - Strip the output. Defaults to True.
753 indata (str) - Specifies stdin data for the process.
754 """
755 kwargs.setdefault('stdin', subprocess2.PIPE)
756 kwargs.setdefault('stdout', subprocess2.PIPE)
757 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000758 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000759 autostrip = kwargs.pop('autostrip', True)
760 indata = kwargs.pop('indata', None)
Edward Lemur12a537f2019-10-03 21:57:15 +0000761 decode = kwargs.pop('decode', True)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000762
iannucci@chromium.org21980022014-04-11 04:51:49 +0000763 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000764 proc = subprocess2.Popen(cmd, **kwargs)
765 ret, err = proc.communicate(indata)
766 retcode = proc.wait()
767 if retcode != 0:
768 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
769
770 if autostrip:
Edward Lemur12a537f2019-10-03 21:57:15 +0000771 ret = (ret or b'').strip()
772 err = (err or b'').strip()
773
774 if decode:
775 ret = ret.decode('utf-8', 'replace')
776 err = err.decode('utf-8', 'replace')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000777
778 return ret, err
779
780
781def set_branch_config(branch, option, value, scope='local'):
782 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
783
784
785def set_config(option, value, scope='local'):
786 run('config', '--' + scope, option, value)
787
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000788
sbc@chromium.org71437c02015-04-09 19:29:40 +0000789def get_dirty_files():
790 # Make sure index is up-to-date before running diff-index.
791 run_with_retcode('update-index', '--refresh', '-q')
Eli Ribble54434e72019-05-24 00:41:15 +0000792 return run('diff-index', '--ignore-submodules', '--name-status', 'HEAD')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000793
794
795def is_dirty_git_tree(cmd):
iannuccie38699b2016-08-15 17:32:31 -0700796 w = lambda s: sys.stderr.write(s+"\n")
797
sbc@chromium.org71437c02015-04-09 19:29:40 +0000798 dirty = get_dirty_files()
799 if dirty:
iannuccie38699b2016-08-15 17:32:31 -0700800 w('Cannot %s with a dirty tree. Commit, freeze or stash your changes first.'
801 % cmd)
802 w('Uncommitted files: (git diff-index --name-status HEAD)')
803 w(dirty[:4096])
sbc@chromium.org71437c02015-04-09 19:29:40 +0000804 if len(dirty) > 4096: # pragma: no cover
iannuccie38699b2016-08-15 17:32:31 -0700805 w('... (run "git diff-index --name-status HEAD" to see full output).')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000806 return True
807 return False
808
809
agable02b3c982016-06-22 07:51:22 -0700810def status():
811 """Returns a parsed version of git-status.
812
813 Returns a generator of (current_name, (lstat, rstat, src)) pairs where:
814 * current_name is the name of the file
815 * lstat is the left status code letter from git-status
816 * rstat is the left status code letter from git-status
817 * src is the current name of the file, or the original name of the file
818 if lstat == 'R'
819 """
820 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src')
821
822 def tokenizer(stream):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000823 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700824 c = None
Edward Lemur12a537f2019-10-03 21:57:15 +0000825 while c != b'':
agable02b3c982016-06-22 07:51:22 -0700826 c = stream.read(1)
Edward Lemur12a537f2019-10-03 21:57:15 +0000827 if c in (None, b'', b'\0'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000828 if len(acc.getvalue()):
agable02b3c982016-06-22 07:51:22 -0700829 yield acc.getvalue()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000830 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700831 else:
832 acc.write(c)
833
834 def parser(tokens):
835 while True:
Edward Lemur12a537f2019-10-03 21:57:15 +0000836 try:
837 status_dest = next(tokens).decode('utf-8')
838 except StopIteration:
839 return
agable02b3c982016-06-22 07:51:22 -0700840 stat, dest = status_dest[:2], status_dest[3:]
841 lstat, rstat = stat
842 if lstat == 'R':
Edward Lemur12a537f2019-10-03 21:57:15 +0000843 src = next(tokens).decode('utf-8')
agable02b3c982016-06-22 07:51:22 -0700844 else:
845 src = dest
846 yield (dest, stat_entry(lstat, rstat, src))
847
848 return parser(tokenizer(run_stream('status', '-z', bufsize=-1)))
849
850
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000851def squash_current_branch(header=None, merge_base=None):
Alan Cutter00017822016-12-20 17:39:59 +1100852 header = header or 'git squash commit for %s.' % current_branch()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000853 merge_base = merge_base or get_or_create_merge_base(current_branch())
854 log_msg = header + '\n'
855 if log_msg:
856 log_msg += '\n'
857 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
858 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000859
860 if not get_dirty_files():
861 # Sometimes the squash can result in the same tree, meaning that there is
862 # nothing to commit at this point.
Raul Tambrec2f74c12019-03-19 05:55:53 +0000863 print('Nothing to commit; squashed branch is empty')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000864 return False
Edward Lemur71681bf2019-10-09 23:46:20 +0000865 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg.encode('utf-8'))
sbc@chromium.org71437c02015-04-09 19:29:40 +0000866 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000867
868
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000869def tags(*args):
870 return run('tag', *args).splitlines()
871
872
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000873def thaw():
874 took_action = False
Edward Lemur12a537f2019-10-03 21:57:15 +0000875 for sha in run_stream('rev-list', 'HEAD').readlines():
876 sha = sha.strip().decode('utf-8')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000877 msg = run('show', '--format=%f%b', '-s', 'HEAD')
878 match = FREEZE_MATCHER.match(msg)
879 if not match:
880 if not took_action:
881 return 'Nothing to thaw.'
882 break
883
884 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
885 took_action = True
886
887
888def topo_iter(branch_tree, top_down=True):
889 """Generates (branch, parent) in topographical order for a branch tree.
890
891 Given a tree:
892
893 A1
894 B1 B2
895 C1 C2 C3
896 D1
897
898 branch_tree would look like: {
899 'D1': 'C3',
900 'C3': 'B2',
901 'B2': 'A1',
902 'C1': 'B1',
903 'C2': 'B1',
904 'B1': 'A1',
905 }
906
907 It is OK to have multiple 'root' nodes in your graph.
908
909 if top_down is True, items are yielded from A->D. Otherwise they're yielded
910 from D->A. Within a layer the branches will be yielded in sorted order.
911 """
912 branch_tree = branch_tree.copy()
913
914 # TODO(iannucci): There is probably a more efficient way to do these.
915 if top_down:
916 while branch_tree:
Edward Lemur12a537f2019-10-03 21:57:15 +0000917 this_pass = [(b, p) for b, p in branch_tree.items()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000918 if p not in branch_tree]
919 assert this_pass, "Branch tree has cycles: %r" % branch_tree
920 for branch, parent in sorted(this_pass):
921 yield branch, parent
922 del branch_tree[branch]
923 else:
924 parent_to_branches = collections.defaultdict(set)
Edward Lemur12a537f2019-10-03 21:57:15 +0000925 for branch, parent in branch_tree.items():
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000926 parent_to_branches[parent].add(branch)
927
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 not parent_to_branches[b]]
931 assert this_pass, "Branch tree has cycles: %r" % branch_tree
932 for branch, parent in sorted(this_pass):
933 yield branch, parent
934 parent_to_branches[parent].discard(branch)
935 del branch_tree[branch]
936
937
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000938def tree(treeref, recurse=False):
939 """Returns a dict representation of a git tree object.
940
941 Args:
942 treeref (str) - a git ref which resolves to a tree (commits count as trees).
qyearsley12fa6ff2016-08-24 09:18:40 -0700943 recurse (bool) - include all of the tree's descendants too. File names will
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000944 take the form of 'some/path/to/file'.
945
946 Return format:
947 { 'file_name': (mode, type, ref) }
948
949 mode is an integer where:
950 * 0040000 - Directory
951 * 0100644 - Regular non-executable file
952 * 0100664 - Regular non-executable group-writeable file
953 * 0100755 - Regular executable file
954 * 0120000 - Symbolic link
955 * 0160000 - Gitlink
956
957 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
958
959 ref is the hex encoded hash of the entry.
960 """
961 ret = {}
962 opts = ['ls-tree', '--full-tree']
963 if recurse:
964 opts.append('-r')
965 opts.append(treeref)
966 try:
967 for line in run(*opts).splitlines():
968 mode, typ, ref, name = line.split(None, 3)
969 ret[name] = (mode, typ, ref)
970 except subprocess2.CalledProcessError:
971 return None
972 return ret
973
974
Mun Yong Jang781e71e2017-10-25 15:46:20 -0700975def get_remote_url(remote='origin'):
976 try:
977 return run('config', 'remote.%s.url' % remote)
978 except subprocess2.CalledProcessError:
979 return None
980
981
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000982def upstream(branch):
983 try:
984 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
985 branch+'@{upstream}')
986 except subprocess2.CalledProcessError:
987 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000988
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000989
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000990def get_git_version():
991 """Returns a tuple that contains the numeric components of the current git
992 version."""
993 version_string = run('--version')
994 version_match = re.search(r'(\d+.)+(\d+)', version_string)
995 version = version_match.group() if version_match else ''
996
997 return tuple(int(x) for x in version.split('.'))
998
999
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001000def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001001 format_string = (
1002 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
1003
1004 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001005 if (include_tracking_status and
1006 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001007 format_string += '%(upstream:track)'
1008
1009 info_map = {}
1010 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001011 BranchesInfo = collections.namedtuple(
1012 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001013 for line in data.splitlines():
1014 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
1015
1016 ahead_match = re.search(r'ahead (\d+)', tracking_status)
1017 ahead = int(ahead_match.group(1)) if ahead_match else None
1018
1019 behind_match = re.search(r'behind (\d+)', tracking_status)
1020 behind = int(behind_match.group(1)) if behind_match else None
1021
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001022 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001023 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
1024
1025 # Set None for upstreams which are not branches (e.g empty upstream, remotes
1026 # and deleted upstream branches).
1027 missing_upstreams = {}
1028 for info in info_map.values():
1029 if info.upstream not in info_map and info.upstream not in missing_upstreams:
1030 missing_upstreams[info.upstream] = None
1031
Edward Lemur12a537f2019-10-03 21:57:15 +00001032 result = info_map.copy()
1033 result.update(missing_upstreams)
1034 return result
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001035
1036
1037def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001038 files_to_copy, symlink=None):
1039 if not symlink:
1040 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001041 os.makedirs(new_workdir)
1042 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001043 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001044 for entry in files_to_copy:
1045 clone_file(repository, new_workdir, entry, shutil.copy)
1046
1047
1048def make_workdir(repository, new_workdir):
1049 GIT_DIRECTORY_WHITELIST = [
1050 'config',
1051 'info',
1052 'hooks',
1053 'logs/refs',
1054 'objects',
1055 'packed-refs',
1056 'refs',
1057 'remotes',
1058 'rr-cache',
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001059 ]
1060 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
1061 ['HEAD'])
1062
1063
1064def clone_file(repository, new_workdir, link, operation):
1065 if not os.path.exists(os.path.join(repository, link)):
1066 return
1067 link_dir = os.path.dirname(os.path.join(new_workdir, link))
1068 if not os.path.exists(link_dir):
1069 os.makedirs(link_dir)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001070 src = os.path.join(repository, link)
1071 if os.path.islink(src):
Henrique Ferreiroaea45d22018-02-19 09:48:36 +01001072 src = os.path.realpath(src)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001073 operation(src, os.path.join(new_workdir, link))