blob: 0fa07e8e14113479b5bf46b0d098d1c803fa70f4 [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.
686 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000687 if not setup_color.IS_TTY:
Edward Lemur5e94b802019-11-26 21:44:08 +0000688 # On Python 3, sys.stdout doesn't accept bytes, and sys.stdout.buffer must
689 # be used.
690 yield getattr(sys.stdout, 'buffer', sys.stdout)
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000691 return
692
693 # Run with the same options that git uses (see setup_pager in git repo).
694 # -F: Automatically quit if the output is less than one screen.
695 # -R: Don't escape ANSI color codes.
696 # -X: Don't clear the screen before starting.
697 cmd = ('less', '-FRX')
698 try:
699 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
700 yield proc.stdin
701 finally:
702 proc.stdin.close()
703 proc.wait()
704
705
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000706def run(*cmd, **kwargs):
707 """The same as run_with_stderr, except it only returns stdout."""
708 return run_with_stderr(*cmd, **kwargs)[0]
709
710
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000711def run_with_retcode(*cmd, **kwargs):
712 """Run a command but only return the status code."""
713 try:
714 run(*cmd, **kwargs)
715 return 0
716 except subprocess2.CalledProcessError as cpe:
717 return cpe.returncode
718
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000719def run_stream(*cmd, **kwargs):
720 """Runs a git command. Returns stdout as a PIPE (file-like object).
721
722 stderr is dropped to avoid races if the process outputs to both stdout and
723 stderr.
724 """
725 kwargs.setdefault('stderr', subprocess2.VOID)
726 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000727 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000728 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000729 proc = subprocess2.Popen(cmd, **kwargs)
730 return proc.stdout
731
732
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000733@contextlib.contextmanager
734def run_stream_with_retcode(*cmd, **kwargs):
735 """Runs a git command as context manager yielding stdout as a PIPE.
736
737 stderr is dropped to avoid races if the process outputs to both stdout and
738 stderr.
739
740 Raises subprocess2.CalledProcessError on nonzero return code.
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)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000745 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
746 try:
747 proc = subprocess2.Popen(cmd, **kwargs)
748 yield proc.stdout
749 finally:
750 retcode = proc.wait()
751 if retcode != 0:
752 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
753 None, None)
754
755
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000756def run_with_stderr(*cmd, **kwargs):
757 """Runs a git command.
758
759 Returns (stdout, stderr) as a pair of strings.
760
761 kwargs
762 autostrip (bool) - Strip the output. Defaults to True.
763 indata (str) - Specifies stdin data for the process.
764 """
765 kwargs.setdefault('stdin', subprocess2.PIPE)
766 kwargs.setdefault('stdout', subprocess2.PIPE)
767 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000768 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000769 autostrip = kwargs.pop('autostrip', True)
770 indata = kwargs.pop('indata', None)
Edward Lemur12a537f2019-10-03 21:57:15 +0000771 decode = kwargs.pop('decode', True)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000772
iannucci@chromium.org21980022014-04-11 04:51:49 +0000773 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000774 proc = subprocess2.Popen(cmd, **kwargs)
775 ret, err = proc.communicate(indata)
776 retcode = proc.wait()
777 if retcode != 0:
778 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
779
780 if autostrip:
Edward Lemur12a537f2019-10-03 21:57:15 +0000781 ret = (ret or b'').strip()
782 err = (err or b'').strip()
783
784 if decode:
785 ret = ret.decode('utf-8', 'replace')
786 err = err.decode('utf-8', 'replace')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000787
788 return ret, err
789
790
791def set_branch_config(branch, option, value, scope='local'):
792 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
793
794
795def set_config(option, value, scope='local'):
796 run('config', '--' + scope, option, value)
797
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000798
sbc@chromium.org71437c02015-04-09 19:29:40 +0000799def get_dirty_files():
800 # Make sure index is up-to-date before running diff-index.
801 run_with_retcode('update-index', '--refresh', '-q')
Eli Ribble54434e72019-05-24 00:41:15 +0000802 return run('diff-index', '--ignore-submodules', '--name-status', 'HEAD')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000803
804
805def is_dirty_git_tree(cmd):
iannuccie38699b2016-08-15 17:32:31 -0700806 w = lambda s: sys.stderr.write(s+"\n")
807
sbc@chromium.org71437c02015-04-09 19:29:40 +0000808 dirty = get_dirty_files()
809 if dirty:
iannuccie38699b2016-08-15 17:32:31 -0700810 w('Cannot %s with a dirty tree. Commit, freeze or stash your changes first.'
811 % cmd)
812 w('Uncommitted files: (git diff-index --name-status HEAD)')
813 w(dirty[:4096])
sbc@chromium.org71437c02015-04-09 19:29:40 +0000814 if len(dirty) > 4096: # pragma: no cover
iannuccie38699b2016-08-15 17:32:31 -0700815 w('... (run "git diff-index --name-status HEAD" to see full output).')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000816 return True
817 return False
818
819
agable02b3c982016-06-22 07:51:22 -0700820def status():
821 """Returns a parsed version of git-status.
822
823 Returns a generator of (current_name, (lstat, rstat, src)) pairs where:
824 * current_name is the name of the file
825 * lstat is the left status code letter from git-status
826 * rstat is the left status code letter from git-status
827 * src is the current name of the file, or the original name of the file
828 if lstat == 'R'
829 """
830 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src')
831
832 def tokenizer(stream):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000833 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700834 c = None
Edward Lemur12a537f2019-10-03 21:57:15 +0000835 while c != b'':
agable02b3c982016-06-22 07:51:22 -0700836 c = stream.read(1)
Edward Lemur12a537f2019-10-03 21:57:15 +0000837 if c in (None, b'', b'\0'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000838 if len(acc.getvalue()):
agable02b3c982016-06-22 07:51:22 -0700839 yield acc.getvalue()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000840 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700841 else:
842 acc.write(c)
843
844 def parser(tokens):
845 while True:
Edward Lemur12a537f2019-10-03 21:57:15 +0000846 try:
847 status_dest = next(tokens).decode('utf-8')
848 except StopIteration:
849 return
agable02b3c982016-06-22 07:51:22 -0700850 stat, dest = status_dest[:2], status_dest[3:]
851 lstat, rstat = stat
852 if lstat == 'R':
Edward Lemur12a537f2019-10-03 21:57:15 +0000853 src = next(tokens).decode('utf-8')
agable02b3c982016-06-22 07:51:22 -0700854 else:
855 src = dest
856 yield (dest, stat_entry(lstat, rstat, src))
857
858 return parser(tokenizer(run_stream('status', '-z', bufsize=-1)))
859
860
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000861def squash_current_branch(header=None, merge_base=None):
Alan Cutter00017822016-12-20 17:39:59 +1100862 header = header or 'git squash commit for %s.' % current_branch()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000863 merge_base = merge_base or get_or_create_merge_base(current_branch())
864 log_msg = header + '\n'
865 if log_msg:
866 log_msg += '\n'
867 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
868 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000869
870 if not get_dirty_files():
871 # Sometimes the squash can result in the same tree, meaning that there is
872 # nothing to commit at this point.
Raul Tambrec2f74c12019-03-19 05:55:53 +0000873 print('Nothing to commit; squashed branch is empty')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000874 return False
Edward Lemur71681bf2019-10-09 23:46:20 +0000875 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg.encode('utf-8'))
sbc@chromium.org71437c02015-04-09 19:29:40 +0000876 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000877
878
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000879def tags(*args):
880 return run('tag', *args).splitlines()
881
882
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000883def thaw():
884 took_action = False
Edward Lemur12a537f2019-10-03 21:57:15 +0000885 for sha in run_stream('rev-list', 'HEAD').readlines():
886 sha = sha.strip().decode('utf-8')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000887 msg = run('show', '--format=%f%b', '-s', 'HEAD')
888 match = FREEZE_MATCHER.match(msg)
889 if not match:
890 if not took_action:
891 return 'Nothing to thaw.'
892 break
893
894 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
895 took_action = True
896
897
898def topo_iter(branch_tree, top_down=True):
899 """Generates (branch, parent) in topographical order for a branch tree.
900
901 Given a tree:
902
903 A1
904 B1 B2
905 C1 C2 C3
906 D1
907
908 branch_tree would look like: {
909 'D1': 'C3',
910 'C3': 'B2',
911 'B2': 'A1',
912 'C1': 'B1',
913 'C2': 'B1',
914 'B1': 'A1',
915 }
916
917 It is OK to have multiple 'root' nodes in your graph.
918
919 if top_down is True, items are yielded from A->D. Otherwise they're yielded
920 from D->A. Within a layer the branches will be yielded in sorted order.
921 """
922 branch_tree = branch_tree.copy()
923
924 # TODO(iannucci): There is probably a more efficient way to do these.
925 if top_down:
926 while branch_tree:
Edward Lemur12a537f2019-10-03 21:57:15 +0000927 this_pass = [(b, p) for b, p in branch_tree.items()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000928 if p not in branch_tree]
929 assert this_pass, "Branch tree has cycles: %r" % branch_tree
930 for branch, parent in sorted(this_pass):
931 yield branch, parent
932 del branch_tree[branch]
933 else:
934 parent_to_branches = collections.defaultdict(set)
Edward Lemur12a537f2019-10-03 21:57:15 +0000935 for branch, parent in branch_tree.items():
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000936 parent_to_branches[parent].add(branch)
937
938 while branch_tree:
Edward Lemur12a537f2019-10-03 21:57:15 +0000939 this_pass = [(b, p) for b, p in branch_tree.items()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000940 if not parent_to_branches[b]]
941 assert this_pass, "Branch tree has cycles: %r" % branch_tree
942 for branch, parent in sorted(this_pass):
943 yield branch, parent
944 parent_to_branches[parent].discard(branch)
945 del branch_tree[branch]
946
947
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000948def tree(treeref, recurse=False):
949 """Returns a dict representation of a git tree object.
950
951 Args:
952 treeref (str) - a git ref which resolves to a tree (commits count as trees).
qyearsley12fa6ff2016-08-24 09:18:40 -0700953 recurse (bool) - include all of the tree's descendants too. File names will
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000954 take the form of 'some/path/to/file'.
955
956 Return format:
957 { 'file_name': (mode, type, ref) }
958
959 mode is an integer where:
960 * 0040000 - Directory
961 * 0100644 - Regular non-executable file
962 * 0100664 - Regular non-executable group-writeable file
963 * 0100755 - Regular executable file
964 * 0120000 - Symbolic link
965 * 0160000 - Gitlink
966
967 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
968
969 ref is the hex encoded hash of the entry.
970 """
971 ret = {}
972 opts = ['ls-tree', '--full-tree']
973 if recurse:
974 opts.append('-r')
975 opts.append(treeref)
976 try:
977 for line in run(*opts).splitlines():
978 mode, typ, ref, name = line.split(None, 3)
979 ret[name] = (mode, typ, ref)
980 except subprocess2.CalledProcessError:
981 return None
982 return ret
983
984
Mun Yong Jang781e71e2017-10-25 15:46:20 -0700985def get_remote_url(remote='origin'):
986 try:
987 return run('config', 'remote.%s.url' % remote)
988 except subprocess2.CalledProcessError:
989 return None
990
991
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000992def upstream(branch):
993 try:
994 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
995 branch+'@{upstream}')
996 except subprocess2.CalledProcessError:
997 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000998
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000999
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001000def get_git_version():
1001 """Returns a tuple that contains the numeric components of the current git
1002 version."""
1003 version_string = run('--version')
1004 version_match = re.search(r'(\d+.)+(\d+)', version_string)
1005 version = version_match.group() if version_match else ''
1006
1007 return tuple(int(x) for x in version.split('.'))
1008
1009
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001010def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001011 format_string = (
1012 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
1013
1014 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001015 if (include_tracking_status and
1016 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001017 format_string += '%(upstream:track)'
1018
1019 info_map = {}
1020 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001021 BranchesInfo = collections.namedtuple(
1022 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001023 for line in data.splitlines():
1024 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
1025
1026 ahead_match = re.search(r'ahead (\d+)', tracking_status)
1027 ahead = int(ahead_match.group(1)) if ahead_match else None
1028
1029 behind_match = re.search(r'behind (\d+)', tracking_status)
1030 behind = int(behind_match.group(1)) if behind_match else None
1031
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001032 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001033 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
1034
1035 # Set None for upstreams which are not branches (e.g empty upstream, remotes
1036 # and deleted upstream branches).
1037 missing_upstreams = {}
1038 for info in info_map.values():
1039 if info.upstream not in info_map and info.upstream not in missing_upstreams:
1040 missing_upstreams[info.upstream] = None
1041
Edward Lemur12a537f2019-10-03 21:57:15 +00001042 result = info_map.copy()
1043 result.update(missing_upstreams)
1044 return result
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001045
1046
1047def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001048 files_to_copy, symlink=None):
1049 if not symlink:
1050 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001051 os.makedirs(new_workdir)
1052 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001053 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001054 for entry in files_to_copy:
1055 clone_file(repository, new_workdir, entry, shutil.copy)
1056
1057
1058def make_workdir(repository, new_workdir):
1059 GIT_DIRECTORY_WHITELIST = [
1060 'config',
1061 'info',
1062 'hooks',
1063 'logs/refs',
1064 'objects',
1065 'packed-refs',
1066 'refs',
1067 'remotes',
1068 'rr-cache',
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001069 ]
1070 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
1071 ['HEAD'])
1072
1073
1074def clone_file(repository, new_workdir, link, operation):
1075 if not os.path.exists(os.path.join(repository, link)):
1076 return
1077 link_dir = os.path.dirname(os.path.join(new_workdir, link))
1078 if not os.path.exists(link_dir):
1079 os.makedirs(link_dir)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001080 src = os.path.join(repository, link)
1081 if os.path.islink(src):
Henrique Ferreiroaea45d22018-02-19 09:48:36 +01001082 src = os.path.realpath(src)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001083 operation(src, os.path.join(new_workdir, link))