blob: f8369d2733f129533bdec67846723a8be937b2a5 [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
Edward Lemurb800fde2020-01-10 23:04:44 +000042if sys.version_info.major == 2:
43 # On Python 3, BrokenPipeError is raised instead.
44 BrokenPipeError = IOError
45
46
agable02b3c982016-06-22 07:51:22 -070047ROOT = os.path.abspath(os.path.dirname(__file__))
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +000048IS_WIN = sys.platform == 'win32'
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000049TEST_MODE = False
50
Dan Jacques209a6812017-07-12 11:40:20 -070051
52def win_find_git():
53 for elem in os.environ.get('PATH', '').split(os.pathsep):
54 for candidate in ('git.exe', 'git.bat'):
55 path = os.path.join(elem, candidate)
56 if os.path.isfile(path):
57 return path
58 raise ValueError('Could not find Git on PATH.')
59
60
61GIT_EXE = 'git' if not IS_WIN else win_find_git()
62
63
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000064FREEZE = 'FREEZE'
65FREEZE_SECTIONS = {
66 'indexed': 'soft',
67 'unindexed': 'mixed'
68}
69FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000070
71
Dan Jacques2f8b0c12017-04-05 12:57:21 -070072# NOTE: This list is DEPRECATED in favor of the Infra Git wrapper:
73# https://chromium.googlesource.com/infra/infra/+/master/go/src/infra/tools/git
74#
75# New entries should be added to the Git wrapper, NOT to this list. "git_retry"
76# is, similarly, being deprecated in favor of the Git wrapper.
77#
78# ---
79#
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000080# Retry a git operation if git returns a error response with any of these
81# messages. It's all observed 'bad' GoB responses so far.
82#
83# This list is inspired/derived from the one in ChromiumOS's Chromite:
84# <CHROMITE>/lib/git.py::GIT_TRANSIENT_ERRORS
85#
86# It was last imported from '7add3ac29564d98ac35ce426bc295e743e7c0c02'.
87GIT_TRANSIENT_ERRORS = (
88 # crbug.com/285832
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000089 r'!.*\[remote rejected\].*\(error in hook\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000090
91 # crbug.com/289932
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000092 r'!.*\[remote rejected\].*\(failed to lock\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000093
94 # crbug.com/307156
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000095 r'!.*\[remote rejected\].*\(error in Gerrit backend\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000096
97 # crbug.com/285832
98 r'remote error: Internal Server Error',
99
100 # crbug.com/294449
101 r'fatal: Couldn\'t find remote ref ',
102
103 # crbug.com/220543
104 r'git fetch_pack: expected ACK/NAK, got',
105
106 # crbug.com/189455
107 r'protocol error: bad pack header',
108
109 # crbug.com/202807
110 r'The remote end hung up unexpectedly',
111
112 # crbug.com/298189
113 r'TLS packet with unexpected length was received',
114
115 # crbug.com/187444
116 r'RPC failed; result=\d+, HTTP code = \d+',
117
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000118 # crbug.com/388876
119 r'Connection timed out',
dnj@chromium.org45cddd62014-11-06 19:36:42 +0000120
121 # crbug.com/430343
122 # TODO(dnj): Resync with Chromite.
123 r'The requested URL returned error: 5\d+',
Arikonb3a21482016-07-22 10:12:24 -0700124
125 r'Connection reset by peer',
126
127 r'Unable to look up',
128
129 r'Couldn\'t resolve host',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000130)
131
132GIT_TRANSIENT_ERRORS_RE = re.compile('|'.join(GIT_TRANSIENT_ERRORS),
133 re.IGNORECASE)
134
raphael.kubo.da.costa@intel.com58d05b02015-06-24 08:54:41 +0000135# git's for-each-ref command first supported the upstream:track token in its
136# format string in version 1.9.0, but some usages were broken until 2.3.0.
137# See git commit b6160d95 for more information.
138MIN_UPSTREAM_TRACK_GIT_VERSION = (2, 3)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000139
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000140class BadCommitRefException(Exception):
141 def __init__(self, refs):
142 msg = ('one of %s does not seem to be a valid commitref.' %
143 str(refs))
144 super(BadCommitRefException, self).__init__(msg)
145
146
147def memoize_one(**kwargs):
148 """Memoizes a single-argument pure function.
149
150 Values of None are not cached.
151
152 Kwargs:
153 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
154 cache manipulation functions. This is a kwarg so that users of memoize_one
155 are forced to explicitly and verbosely pick True or False.
156
157 Adds three methods to the decorated function:
158 * get(key, default=None) - Gets the value for this key from the cache.
159 * set(key, value) - Sets the value for this key from the cache.
160 * clear() - Drops the entire contents of the cache. Useful for unittests.
161 * update(other) - Updates the contents of the cache from another dict.
162 """
163 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
164 threadsafe = kwargs['threadsafe']
165
166 if threadsafe:
167 def withlock(lock, f):
168 def inner(*args, **kwargs):
169 with lock:
170 return f(*args, **kwargs)
171 return inner
172 else:
173 def withlock(_lock, f):
174 return f
175
176 def decorator(f):
177 # Instantiate the lock in decorator, in case users of memoize_one do:
178 #
179 # memoizer = memoize_one(threadsafe=True)
180 #
181 # @memoizer
182 # def fn1(val): ...
183 #
184 # @memoizer
185 # def fn2(val): ...
186
187 lock = threading.Lock() if threadsafe else None
188 cache = {}
189 _get = withlock(lock, cache.get)
190 _set = withlock(lock, cache.__setitem__)
191
192 @functools.wraps(f)
193 def inner(arg):
194 ret = _get(arg)
195 if ret is None:
196 ret = f(arg)
197 if ret is not None:
198 _set(arg, ret)
199 return ret
200 inner.get = _get
201 inner.set = _set
202 inner.clear = withlock(lock, cache.clear)
203 inner.update = withlock(lock, cache.update)
204 return inner
205 return decorator
206
207
208def _ScopedPool_initer(orig, orig_args): # pragma: no cover
209 """Initializer method for ScopedPool's subprocesses.
210
211 This helps ScopedPool handle Ctrl-C's correctly.
212 """
213 signal.signal(signal.SIGINT, signal.SIG_IGN)
214 if orig:
215 orig(*orig_args)
216
217
218@contextlib.contextmanager
219def ScopedPool(*args, **kwargs):
220 """Context Manager which returns a multiprocessing.pool instance which
221 correctly deals with thrown exceptions.
222
223 *args - Arguments to multiprocessing.pool
224
225 Kwargs:
226 kind ('threads', 'procs') - The type of underlying coprocess to use.
227 **etc - Arguments to multiprocessing.pool
228 """
229 if kwargs.pop('kind', None) == 'threads':
230 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
231 else:
232 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
233 kwargs['initializer'] = _ScopedPool_initer
234 kwargs['initargs'] = orig, orig_args
235 pool = multiprocessing.pool.Pool(*args, **kwargs)
236
237 try:
238 yield pool
239 pool.close()
240 except:
241 pool.terminate()
242 raise
243 finally:
244 pool.join()
245
246
247class ProgressPrinter(object):
248 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000249 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000250 """Create a ProgressPrinter.
251
252 Use it as a context manager which produces a simple 'increment' method:
253
254 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
255 for i in xrange(1000):
256 # do stuff
257 if i % 10 == 0:
258 inc(10)
259
260 Args:
261 fmt - String format with a single '%(count)d' where the counter value
262 should go.
263 enabled (bool) - If this is None, will default to True if
264 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000265 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000266 period (float) - The time in seconds for the printer thread to wait
267 between printing.
268 """
269 self.fmt = fmt
270 if enabled is None: # pragma: no cover
271 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
272 else:
273 self.enabled = enabled
274
275 self._count = 0
276 self._dead = False
277 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000278 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000279 self._thread = threading.Thread(target=self._run)
280 self._period = period
281
282 def _emit(self, s):
283 if self.enabled:
284 self._stream.write('\r' + s)
285 self._stream.flush()
286
287 def _run(self):
288 with self._dead_cond:
289 while not self._dead:
290 self._emit(self.fmt % {'count': self._count})
291 self._dead_cond.wait(self._period)
292 self._emit((self.fmt + '\n') % {'count': self._count})
293
294 def inc(self, amount=1):
295 self._count += amount
296
297 def __enter__(self):
298 self._thread.start()
299 return self.inc
300
301 def __exit__(self, _exc_type, _exc_value, _traceback):
302 self._dead = True
303 with self._dead_cond:
304 self._dead_cond.notifyAll()
305 self._thread.join()
306 del self._thread
307
308
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000309def once(function):
310 """@Decorates |function| so that it only performs its action once, no matter
311 how many times the decorated |function| is called."""
Edward Lemur12a537f2019-10-03 21:57:15 +0000312 has_run = [False]
313 def _wrapper(*args, **kwargs):
314 if not has_run[0]:
315 has_run[0] = True
316 function(*args, **kwargs)
317 return _wrapper
318
319
320def unicode_repr(s):
321 result = repr(s)
322 return result[1:] if result.startswith('u') else result
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000323
324
325## Git functions
326
agable7aa2ddd2016-06-21 07:47:00 -0700327def die(message, *args):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000328 print(textwrap.dedent(message % args), file=sys.stderr)
agable7aa2ddd2016-06-21 07:47:00 -0700329 sys.exit(1)
330
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000331
Mark Mentovaif548d082017-03-08 13:32:00 -0500332def blame(filename, revision=None, porcelain=False, abbrev=None, *_args):
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000333 command = ['blame']
334 if porcelain:
335 command.append('-p')
336 if revision is not None:
337 command.append(revision)
Mark Mentovaif548d082017-03-08 13:32:00 -0500338 if abbrev is not None:
339 command.append('--abbrev=%d' % abbrev)
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000340 command.extend(['--', filename])
341 return run(*command)
342
343
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000344def branch_config(branch, option, default=None):
agable7aa2ddd2016-06-21 07:47:00 -0700345 return get_config('branch.%s.%s' % (branch, option), default=default)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000346
347
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000348def branch_config_map(option):
349 """Return {branch: <|option| value>} for all branches."""
350 try:
351 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
agable7aa2ddd2016-06-21 07:47:00 -0700352 lines = get_config_regexp(reg.pattern)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000353 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
354 except subprocess2.CalledProcessError:
355 return {}
356
357
Francois Dorayd42c6812017-05-30 15:10:20 -0400358def branches(use_limit=True, *args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000359 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000360
361 key = 'depot-tools.branch-limit'
agable7aa2ddd2016-06-21 07:47:00 -0700362 limit = get_config_int(key, 20)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000363
364 raw_branches = run('branch', *args).splitlines()
365
366 num = len(raw_branches)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000367
Francois Dorayd42c6812017-05-30 15:10:20 -0400368 if use_limit and num > limit:
agable7aa2ddd2016-06-21 07:47:00 -0700369 die("""\
370 Your git repo has too many branches (%d/%d) for this tool to work well.
371
372 You may adjust this limit by running:
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000373 git config %s <new_limit>
agable7aa2ddd2016-06-21 07:47:00 -0700374
375 You may also try cleaning up your old branches by running:
376 git cl archive
377 """, num, limit, key)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000378
379 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000380 if line.startswith(NO_BRANCH):
381 continue
382 yield line.split()[-1]
383
384
agable7aa2ddd2016-06-21 07:47:00 -0700385def get_config(option, default=None):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000386 try:
387 return run('config', '--get', option) or default
388 except subprocess2.CalledProcessError:
389 return default
390
391
agable7aa2ddd2016-06-21 07:47:00 -0700392def get_config_int(option, default=0):
393 assert isinstance(default, int)
394 try:
395 return int(get_config(option, default))
396 except ValueError:
397 return default
398
399
400def get_config_list(option):
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000401 try:
402 return run('config', '--get-all', option).split()
403 except subprocess2.CalledProcessError:
404 return []
405
406
agable7aa2ddd2016-06-21 07:47:00 -0700407def get_config_regexp(pattern):
408 if IS_WIN: # pragma: no cover
409 # this madness is because we call git.bat which calls git.exe which calls
410 # bash.exe (or something to that effect). Each layer divides the number of
411 # ^'s by 2.
412 pattern = pattern.replace('^', '^' * 8)
413 return run('config', '--get-regexp', pattern).splitlines()
414
415
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000416def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000417 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000418 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000419 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000420 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000421
422
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000423def del_branch_config(branch, option, scope='local'):
424 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000425
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000426
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000427def del_config(option, scope='local'):
428 try:
429 run('config', '--' + scope, '--unset', option)
430 except subprocess2.CalledProcessError:
431 pass
432
433
mgiuca@chromium.org01d2cde2016-02-05 03:25:41 +0000434def diff(oldrev, newrev, *args):
435 return run('diff', oldrev, newrev, *args)
436
437
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000438def freeze():
439 took_action = False
agable02b3c982016-06-22 07:51:22 -0700440 key = 'depot-tools.freeze-size-limit'
441 MB = 2**20
442 limit_mb = get_config_int(key, 100)
443 untracked_bytes = 0
444
iannuccieaca0332016-08-03 16:46:50 -0700445 root_path = repo_root()
446
agable02b3c982016-06-22 07:51:22 -0700447 for f, s in status():
448 if is_unmerged(s):
449 die("Cannot freeze unmerged changes!")
450 if limit_mb > 0:
451 if s.lstat == '?':
iannuccieaca0332016-08-03 16:46:50 -0700452 untracked_bytes += os.stat(os.path.join(root_path, f)).st_size
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800453 if limit_mb > 0 and untracked_bytes > limit_mb * MB:
454 die("""\
455 You appear to have too much untracked+unignored data in your git
456 checkout: %.1f / %d MB.
agable02b3c982016-06-22 07:51:22 -0700457
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800458 Run `git status` to see what it is.
agable02b3c982016-06-22 07:51:22 -0700459
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800460 In addition to making many git commands slower, this will prevent
461 depot_tools from freezing your in-progress changes.
agable02b3c982016-06-22 07:51:22 -0700462
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800463 You should add untracked data that you want to ignore to your repo's
464 .git/info/exclude
465 file. See `git help ignore` for the format of this file.
agable02b3c982016-06-22 07:51:22 -0700466
Bruce Dawson4bff3fd2018-01-04 14:44:23 -0800467 If this data is indended as part of your commit, you may adjust the
468 freeze limit by running:
469 git config %s <new_limit>
470 Where <new_limit> is an integer threshold in megabytes.""",
471 untracked_bytes / (MB * 1.0), limit_mb, key)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000472
473 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000474 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000475 took_action = True
476 except subprocess2.CalledProcessError:
477 pass
478
agable96e179b2016-06-24 10:32:51 -0700479 add_errors = False
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000480 try:
agable96e179b2016-06-24 10:32:51 -0700481 run('add', '-A', '--ignore-errors')
482 except subprocess2.CalledProcessError:
483 add_errors = True
484
485 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000486 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000487 took_action = True
488 except subprocess2.CalledProcessError:
489 pass
490
agable96e179b2016-06-24 10:32:51 -0700491 ret = []
492 if add_errors:
493 ret.append('Failed to index some unindexed files.')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000494 if not took_action:
agable96e179b2016-06-24 10:32:51 -0700495 ret.append('Nothing to freeze.')
496 return ' '.join(ret) or None
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000497
498
499def get_branch_tree():
500 """Get the dictionary of {branch: parent}, compatible with topo_iter.
501
502 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
503 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000504 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000505 skipped = set()
506 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000507
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000508 for branch in branches():
509 parent = upstream(branch)
510 if not parent:
511 skipped.add(branch)
512 continue
513 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000514
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000515 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000516
517
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000518def get_or_create_merge_base(branch, parent=None):
519 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000520
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000521 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000522 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000523 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000524 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000525 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000526 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000527 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000528 actual_merge_base = run('merge-base', parent, branch)
529
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000530 if base_upstream != parent:
531 base = None
532 base_upstream = None
533
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000534 def is_ancestor(a, b):
535 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
536
clemensh@chromium.orgc3fe99d2016-04-19 08:39:55 +0000537 if base and base != actual_merge_base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000538 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000539 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
540 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000541 elif is_ancestor(base, actual_merge_base):
542 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
543 base = None
544 else:
545 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000546
547 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000548 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000549 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000550
551 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000552
553
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000554def hash_multi(*reflike):
555 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000556
557
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000558def hash_one(reflike, short=False):
559 args = ['rev-parse', reflike]
560 if short:
561 args.insert(1, '--short')
562 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000563
564
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000565def in_rebase():
566 git_dir = run('rev-parse', '--git-dir')
567 return (
568 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
569 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000570
571
572def intern_f(f, kind='blob'):
573 """Interns a file object into the git object store.
574
575 Args:
576 f (file-like object) - The file-like object to intern
577 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
578
579 Returns the git hash of the interned object (hex encoded).
580 """
581 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
582 f.close()
583 return ret
584
585
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000586def is_dormant(branch):
587 # TODO(iannucci): Do an oldness check?
588 return branch_config(branch, 'dormant', 'false') != 'false'
589
590
agable02b3c982016-06-22 07:51:22 -0700591def is_unmerged(stat_value):
592 return (
593 'U' in (stat_value.lstat, stat_value.rstat) or
594 ((stat_value.lstat == stat_value.rstat) and stat_value.lstat in 'AD')
595 )
596
597
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000598def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000599 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000600 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000601
602
603def mktree(treedict):
604 """Makes a git tree object and returns its hash.
605
606 See |tree()| for the values of mode, type, and ref.
607
608 Args:
609 treedict - { name: (mode, type, ref) }
610 """
611 with tempfile.TemporaryFile() as f:
Edward Lemur12a537f2019-10-03 21:57:15 +0000612 for name, (mode, typ, ref) in treedict.items():
Edward Lemur71681bf2019-10-09 23:46:20 +0000613 f.write(('%s %s %s\t%s\0' % (mode, typ, ref, name)).encode('utf-8'))
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000614 f.seek(0)
615 return run('mktree', '-z', stdin=f)
616
617
618def parse_commitrefs(*commitrefs):
619 """Returns binary encoded commit hashes for one or more commitrefs.
620
621 A commitref is anything which can resolve to a commit. Popular examples:
622 * 'HEAD'
623 * 'origin/master'
624 * 'cool_branch~2'
625 """
626 try:
Edward Lemur12a537f2019-10-03 21:57:15 +0000627 return [binascii.unhexlify(h) for h in hash_multi(*commitrefs)]
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000628 except subprocess2.CalledProcessError:
629 raise BadCommitRefException(commitrefs)
630
631
sbc@chromium.org384039b2014-10-13 21:01:00 +0000632RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000633
634
635def rebase(parent, start, branch, abort=False):
636 """Rebases |start|..|branch| onto the branch |parent|.
637
638 Args:
639 parent - The new parent ref for the rebased commits.
640 start - The commit to start from
641 branch - The branch to rebase
642 abort - If True, will call git-rebase --abort in the event that the rebase
643 doesn't complete successfully.
644
645 Returns a namedtuple with fields:
646 success - a boolean indicating that the rebase command completed
647 successfully.
648 message - if the rebase failed, this contains the stdout of the failed
649 rebase.
650 """
651 try:
652 args = ['--onto', parent, start, branch]
653 if TEST_MODE:
654 args.insert(0, '--committer-date-is-author-date')
655 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000656 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000657 except subprocess2.CalledProcessError as cpe:
658 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000659 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000660 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000661
662
663def remove_merge_base(branch):
664 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000665 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000666
667
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000668def repo_root():
669 """Returns the absolute path to the repository root."""
670 return run('rev-parse', '--show-toplevel')
671
672
Jeffrey Yasskin6b52dc22019-12-06 18:32:21 +0000673def upstream_default():
674 """Returns the default branch name of the origin repository."""
675 try:
676 return run('rev-parse', '--abbrev-ref', 'origin/HEAD')
677 except subprocess2.CalledProcessError:
678 return 'origin/master'
679
680
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000681def root():
Jeffrey Yasskin6b52dc22019-12-06 18:32:21 +0000682 return get_config('depot-tools.upstream', upstream_default())
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000683
684
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000685@contextlib.contextmanager
686def less(): # pragma: no cover
687 """Runs 'less' as context manager yielding its stdin as a PIPE.
688
689 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
690 running less and just yields sys.stdout.
Edward Lemur0d462e92020-01-08 20:11:31 +0000691
692 The returned PIPE is opened on binary mode.
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000693 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000694 if not setup_color.IS_TTY:
Edward Lemur5e94b802019-11-26 21:44:08 +0000695 # On Python 3, sys.stdout doesn't accept bytes, and sys.stdout.buffer must
696 # be used.
697 yield getattr(sys.stdout, 'buffer', sys.stdout)
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000698 return
699
700 # Run with the same options that git uses (see setup_pager in git repo).
701 # -F: Automatically quit if the output is less than one screen.
702 # -R: Don't escape ANSI color codes.
703 # -X: Don't clear the screen before starting.
704 cmd = ('less', '-FRX')
705 try:
706 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
707 yield proc.stdin
708 finally:
Edward Lemurb800fde2020-01-10 23:04:44 +0000709 try:
710 proc.stdin.close()
711 except BrokenPipeError:
712 # BrokenPipeError is raised if proc has already completed,
713 pass
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000714 proc.wait()
715
716
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000717def run(*cmd, **kwargs):
718 """The same as run_with_stderr, except it only returns stdout."""
719 return run_with_stderr(*cmd, **kwargs)[0]
720
721
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000722def run_with_retcode(*cmd, **kwargs):
723 """Run a command but only return the status code."""
724 try:
725 run(*cmd, **kwargs)
726 return 0
727 except subprocess2.CalledProcessError as cpe:
728 return cpe.returncode
729
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000730def run_stream(*cmd, **kwargs):
731 """Runs a git command. Returns stdout as a PIPE (file-like object).
732
733 stderr is dropped to avoid races if the process outputs to both stdout and
734 stderr.
735 """
736 kwargs.setdefault('stderr', subprocess2.VOID)
737 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000738 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000739 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000740 proc = subprocess2.Popen(cmd, **kwargs)
741 return proc.stdout
742
743
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000744@contextlib.contextmanager
745def run_stream_with_retcode(*cmd, **kwargs):
746 """Runs a git command as context manager yielding stdout as a PIPE.
747
748 stderr is dropped to avoid races if the process outputs to both stdout and
749 stderr.
750
751 Raises subprocess2.CalledProcessError on nonzero return code.
752 """
753 kwargs.setdefault('stderr', subprocess2.VOID)
754 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000755 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000756 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
757 try:
758 proc = subprocess2.Popen(cmd, **kwargs)
759 yield proc.stdout
760 finally:
761 retcode = proc.wait()
762 if retcode != 0:
763 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
764 None, None)
765
766
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000767def run_with_stderr(*cmd, **kwargs):
768 """Runs a git command.
769
770 Returns (stdout, stderr) as a pair of strings.
771
772 kwargs
773 autostrip (bool) - Strip the output. Defaults to True.
774 indata (str) - Specifies stdin data for the process.
775 """
776 kwargs.setdefault('stdin', subprocess2.PIPE)
777 kwargs.setdefault('stdout', subprocess2.PIPE)
778 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000779 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000780 autostrip = kwargs.pop('autostrip', True)
781 indata = kwargs.pop('indata', None)
Edward Lemur12a537f2019-10-03 21:57:15 +0000782 decode = kwargs.pop('decode', True)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000783
iannucci@chromium.org21980022014-04-11 04:51:49 +0000784 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000785 proc = subprocess2.Popen(cmd, **kwargs)
786 ret, err = proc.communicate(indata)
787 retcode = proc.wait()
788 if retcode != 0:
789 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
790
791 if autostrip:
Edward Lemur12a537f2019-10-03 21:57:15 +0000792 ret = (ret or b'').strip()
793 err = (err or b'').strip()
794
795 if decode:
796 ret = ret.decode('utf-8', 'replace')
797 err = err.decode('utf-8', 'replace')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000798
799 return ret, err
800
801
802def set_branch_config(branch, option, value, scope='local'):
803 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
804
805
806def set_config(option, value, scope='local'):
807 run('config', '--' + scope, option, value)
808
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000809
sbc@chromium.org71437c02015-04-09 19:29:40 +0000810def get_dirty_files():
811 # Make sure index is up-to-date before running diff-index.
812 run_with_retcode('update-index', '--refresh', '-q')
Eli Ribble54434e72019-05-24 00:41:15 +0000813 return run('diff-index', '--ignore-submodules', '--name-status', 'HEAD')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000814
815
816def is_dirty_git_tree(cmd):
iannuccie38699b2016-08-15 17:32:31 -0700817 w = lambda s: sys.stderr.write(s+"\n")
818
sbc@chromium.org71437c02015-04-09 19:29:40 +0000819 dirty = get_dirty_files()
820 if dirty:
iannuccie38699b2016-08-15 17:32:31 -0700821 w('Cannot %s with a dirty tree. Commit, freeze or stash your changes first.'
822 % cmd)
823 w('Uncommitted files: (git diff-index --name-status HEAD)')
824 w(dirty[:4096])
sbc@chromium.org71437c02015-04-09 19:29:40 +0000825 if len(dirty) > 4096: # pragma: no cover
iannuccie38699b2016-08-15 17:32:31 -0700826 w('... (run "git diff-index --name-status HEAD" to see full output).')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000827 return True
828 return False
829
830
agable02b3c982016-06-22 07:51:22 -0700831def status():
832 """Returns a parsed version of git-status.
833
834 Returns a generator of (current_name, (lstat, rstat, src)) pairs where:
835 * current_name is the name of the file
836 * lstat is the left status code letter from git-status
837 * rstat is the left status code letter from git-status
838 * src is the current name of the file, or the original name of the file
839 if lstat == 'R'
840 """
841 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src')
842
843 def tokenizer(stream):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000844 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700845 c = None
Edward Lemur12a537f2019-10-03 21:57:15 +0000846 while c != b'':
agable02b3c982016-06-22 07:51:22 -0700847 c = stream.read(1)
Edward Lemur12a537f2019-10-03 21:57:15 +0000848 if c in (None, b'', b'\0'):
Raul Tambrec2f74c12019-03-19 05:55:53 +0000849 if len(acc.getvalue()):
agable02b3c982016-06-22 07:51:22 -0700850 yield acc.getvalue()
Raul Tambrec2f74c12019-03-19 05:55:53 +0000851 acc = BytesIO()
agable02b3c982016-06-22 07:51:22 -0700852 else:
853 acc.write(c)
854
855 def parser(tokens):
856 while True:
Edward Lemur12a537f2019-10-03 21:57:15 +0000857 try:
858 status_dest = next(tokens).decode('utf-8')
859 except StopIteration:
860 return
agable02b3c982016-06-22 07:51:22 -0700861 stat, dest = status_dest[:2], status_dest[3:]
862 lstat, rstat = stat
863 if lstat == 'R':
Edward Lemur12a537f2019-10-03 21:57:15 +0000864 src = next(tokens).decode('utf-8')
agable02b3c982016-06-22 07:51:22 -0700865 else:
866 src = dest
867 yield (dest, stat_entry(lstat, rstat, src))
868
869 return parser(tokenizer(run_stream('status', '-z', bufsize=-1)))
870
871
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000872def squash_current_branch(header=None, merge_base=None):
Alan Cutter00017822016-12-20 17:39:59 +1100873 header = header or 'git squash commit for %s.' % current_branch()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000874 merge_base = merge_base or get_or_create_merge_base(current_branch())
875 log_msg = header + '\n'
876 if log_msg:
877 log_msg += '\n'
878 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
879 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000880
881 if not get_dirty_files():
882 # Sometimes the squash can result in the same tree, meaning that there is
883 # nothing to commit at this point.
Raul Tambrec2f74c12019-03-19 05:55:53 +0000884 print('Nothing to commit; squashed branch is empty')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000885 return False
Edward Lemur71681bf2019-10-09 23:46:20 +0000886 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg.encode('utf-8'))
sbc@chromium.org71437c02015-04-09 19:29:40 +0000887 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000888
889
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000890def tags(*args):
891 return run('tag', *args).splitlines()
892
893
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000894def thaw():
895 took_action = False
Edward Lemur12a537f2019-10-03 21:57:15 +0000896 for sha in run_stream('rev-list', 'HEAD').readlines():
897 sha = sha.strip().decode('utf-8')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000898 msg = run('show', '--format=%f%b', '-s', 'HEAD')
899 match = FREEZE_MATCHER.match(msg)
900 if not match:
901 if not took_action:
902 return 'Nothing to thaw.'
903 break
904
905 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
906 took_action = True
907
908
909def topo_iter(branch_tree, top_down=True):
910 """Generates (branch, parent) in topographical order for a branch tree.
911
912 Given a tree:
913
914 A1
915 B1 B2
916 C1 C2 C3
917 D1
918
919 branch_tree would look like: {
920 'D1': 'C3',
921 'C3': 'B2',
922 'B2': 'A1',
923 'C1': 'B1',
924 'C2': 'B1',
925 'B1': 'A1',
926 }
927
928 It is OK to have multiple 'root' nodes in your graph.
929
930 if top_down is True, items are yielded from A->D. Otherwise they're yielded
931 from D->A. Within a layer the branches will be yielded in sorted order.
932 """
933 branch_tree = branch_tree.copy()
934
935 # TODO(iannucci): There is probably a more efficient way to do these.
936 if top_down:
937 while branch_tree:
Edward Lemur12a537f2019-10-03 21:57:15 +0000938 this_pass = [(b, p) for b, p in branch_tree.items()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000939 if p not in branch_tree]
940 assert this_pass, "Branch tree has cycles: %r" % branch_tree
941 for branch, parent in sorted(this_pass):
942 yield branch, parent
943 del branch_tree[branch]
944 else:
945 parent_to_branches = collections.defaultdict(set)
Edward Lemur12a537f2019-10-03 21:57:15 +0000946 for branch, parent in branch_tree.items():
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000947 parent_to_branches[parent].add(branch)
948
949 while branch_tree:
Edward Lemur12a537f2019-10-03 21:57:15 +0000950 this_pass = [(b, p) for b, p in branch_tree.items()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000951 if not parent_to_branches[b]]
952 assert this_pass, "Branch tree has cycles: %r" % branch_tree
953 for branch, parent in sorted(this_pass):
954 yield branch, parent
955 parent_to_branches[parent].discard(branch)
956 del branch_tree[branch]
957
958
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000959def tree(treeref, recurse=False):
960 """Returns a dict representation of a git tree object.
961
962 Args:
963 treeref (str) - a git ref which resolves to a tree (commits count as trees).
qyearsley12fa6ff2016-08-24 09:18:40 -0700964 recurse (bool) - include all of the tree's descendants too. File names will
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000965 take the form of 'some/path/to/file'.
966
967 Return format:
968 { 'file_name': (mode, type, ref) }
969
970 mode is an integer where:
971 * 0040000 - Directory
972 * 0100644 - Regular non-executable file
973 * 0100664 - Regular non-executable group-writeable file
974 * 0100755 - Regular executable file
975 * 0120000 - Symbolic link
976 * 0160000 - Gitlink
977
978 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
979
980 ref is the hex encoded hash of the entry.
981 """
982 ret = {}
983 opts = ['ls-tree', '--full-tree']
984 if recurse:
985 opts.append('-r')
986 opts.append(treeref)
987 try:
988 for line in run(*opts).splitlines():
989 mode, typ, ref, name = line.split(None, 3)
990 ret[name] = (mode, typ, ref)
991 except subprocess2.CalledProcessError:
992 return None
993 return ret
994
995
Mun Yong Jang781e71e2017-10-25 15:46:20 -0700996def get_remote_url(remote='origin'):
997 try:
998 return run('config', 'remote.%s.url' % remote)
999 except subprocess2.CalledProcessError:
1000 return None
1001
1002
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +00001003def upstream(branch):
1004 try:
1005 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
1006 branch+'@{upstream}')
1007 except subprocess2.CalledProcessError:
1008 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001009
agable@chromium.orgd629fb42014-10-01 09:40:10 +00001010
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001011def get_git_version():
1012 """Returns a tuple that contains the numeric components of the current git
1013 version."""
1014 version_string = run('--version')
1015 version_match = re.search(r'(\d+.)+(\d+)', version_string)
1016 version = version_match.group() if version_match else ''
1017
1018 return tuple(int(x) for x in version.split('.'))
1019
1020
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001021def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001022 format_string = (
1023 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
1024
1025 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001026 if (include_tracking_status and
1027 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001028 format_string += '%(upstream:track)'
1029
1030 info_map = {}
1031 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001032 BranchesInfo = collections.namedtuple(
1033 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001034 for line in data.splitlines():
1035 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
1036
1037 ahead_match = re.search(r'ahead (\d+)', tracking_status)
1038 ahead = int(ahead_match.group(1)) if ahead_match else None
1039
1040 behind_match = re.search(r'behind (\d+)', tracking_status)
1041 behind = int(behind_match.group(1)) if behind_match else None
1042
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001043 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001044 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
1045
1046 # Set None for upstreams which are not branches (e.g empty upstream, remotes
1047 # and deleted upstream branches).
1048 missing_upstreams = {}
1049 for info in info_map.values():
1050 if info.upstream not in info_map and info.upstream not in missing_upstreams:
1051 missing_upstreams[info.upstream] = None
1052
Edward Lemur12a537f2019-10-03 21:57:15 +00001053 result = info_map.copy()
1054 result.update(missing_upstreams)
1055 return result
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001056
1057
1058def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001059 files_to_copy, symlink=None):
1060 if not symlink:
1061 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001062 os.makedirs(new_workdir)
1063 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001064 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001065 for entry in files_to_copy:
1066 clone_file(repository, new_workdir, entry, shutil.copy)
1067
1068
1069def make_workdir(repository, new_workdir):
1070 GIT_DIRECTORY_WHITELIST = [
1071 'config',
1072 'info',
1073 'hooks',
1074 'logs/refs',
1075 'objects',
1076 'packed-refs',
1077 'refs',
1078 'remotes',
1079 'rr-cache',
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001080 ]
1081 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
1082 ['HEAD'])
1083
1084
1085def clone_file(repository, new_workdir, link, operation):
1086 if not os.path.exists(os.path.join(repository, link)):
1087 return
1088 link_dir = os.path.dirname(os.path.join(new_workdir, link))
1089 if not os.path.exists(link_dir):
1090 os.makedirs(link_dir)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001091 src = os.path.join(repository, link)
1092 if os.path.islink(src):
Henrique Ferreiroaea45d22018-02-19 09:48:36 +01001093 src = os.path.realpath(src)
Henrique Ferreirofd4ad242018-01-10 12:19:18 +01001094 operation(src, os.path.join(new_workdir, link))