blob: e7244790aceaad29e04f9a8ff43349c7fe5aaab0 [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
7import multiprocessing.pool
8from multiprocessing.pool import IMapIterator
9def wrapper(func):
10 def wrap(self, timeout=None):
11 return func(self, timeout=timeout or 1e100)
12 return wrap
13IMapIterator.next = wrapper(IMapIterator.next)
14IMapIterator.__next__ = IMapIterator.next
15# TODO(iannucci): Monkeypatch all other 'wait' methods too.
16
17
18import binascii
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000019import collections
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000020import contextlib
21import functools
22import logging
iannucci@chromium.org97345eb2014-03-13 07:55:15 +000023import os
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000024import re
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000025import setup_color
sammc@chromium.org900a33f2015-09-29 06:57:09 +000026import shutil
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000027import signal
28import sys
29import tempfile
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +000030import textwrap
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000031import threading
32
33import subprocess2
34
techtonik@gmail.coma5a945a2014-08-15 20:01:53 +000035ROOT = os.path.abspath(os.path.dirname(__file__))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000036
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +000037IS_WIN = sys.platform == 'win32'
38GIT_EXE = ROOT+'\\git.bat' if IS_WIN else 'git'
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000039TEST_MODE = False
40
41FREEZE = 'FREEZE'
42FREEZE_SECTIONS = {
43 'indexed': 'soft',
44 'unindexed': 'mixed'
45}
46FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000047
48
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000049# Retry a git operation if git returns a error response with any of these
50# messages. It's all observed 'bad' GoB responses so far.
51#
52# This list is inspired/derived from the one in ChromiumOS's Chromite:
53# <CHROMITE>/lib/git.py::GIT_TRANSIENT_ERRORS
54#
55# It was last imported from '7add3ac29564d98ac35ce426bc295e743e7c0c02'.
56GIT_TRANSIENT_ERRORS = (
57 # crbug.com/285832
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000058 r'!.*\[remote rejected\].*\(error in hook\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000059
60 # crbug.com/289932
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000061 r'!.*\[remote rejected\].*\(failed to lock\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000062
63 # crbug.com/307156
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000064 r'!.*\[remote rejected\].*\(error in Gerrit backend\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000065
66 # crbug.com/285832
67 r'remote error: Internal Server Error',
68
69 # crbug.com/294449
70 r'fatal: Couldn\'t find remote ref ',
71
72 # crbug.com/220543
73 r'git fetch_pack: expected ACK/NAK, got',
74
75 # crbug.com/189455
76 r'protocol error: bad pack header',
77
78 # crbug.com/202807
79 r'The remote end hung up unexpectedly',
80
81 # crbug.com/298189
82 r'TLS packet with unexpected length was received',
83
84 # crbug.com/187444
85 r'RPC failed; result=\d+, HTTP code = \d+',
86
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000087 # crbug.com/388876
88 r'Connection timed out',
dnj@chromium.org45cddd62014-11-06 19:36:42 +000089
90 # crbug.com/430343
91 # TODO(dnj): Resync with Chromite.
92 r'The requested URL returned error: 5\d+',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000093)
94
95GIT_TRANSIENT_ERRORS_RE = re.compile('|'.join(GIT_TRANSIENT_ERRORS),
96 re.IGNORECASE)
97
raphael.kubo.da.costa@intel.com58d05b02015-06-24 08:54:41 +000098# git's for-each-ref command first supported the upstream:track token in its
99# format string in version 1.9.0, but some usages were broken until 2.3.0.
100# See git commit b6160d95 for more information.
101MIN_UPSTREAM_TRACK_GIT_VERSION = (2, 3)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000102
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000103class BadCommitRefException(Exception):
104 def __init__(self, refs):
105 msg = ('one of %s does not seem to be a valid commitref.' %
106 str(refs))
107 super(BadCommitRefException, self).__init__(msg)
108
109
110def memoize_one(**kwargs):
111 """Memoizes a single-argument pure function.
112
113 Values of None are not cached.
114
115 Kwargs:
116 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
117 cache manipulation functions. This is a kwarg so that users of memoize_one
118 are forced to explicitly and verbosely pick True or False.
119
120 Adds three methods to the decorated function:
121 * get(key, default=None) - Gets the value for this key from the cache.
122 * set(key, value) - Sets the value for this key from the cache.
123 * clear() - Drops the entire contents of the cache. Useful for unittests.
124 * update(other) - Updates the contents of the cache from another dict.
125 """
126 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
127 threadsafe = kwargs['threadsafe']
128
129 if threadsafe:
130 def withlock(lock, f):
131 def inner(*args, **kwargs):
132 with lock:
133 return f(*args, **kwargs)
134 return inner
135 else:
136 def withlock(_lock, f):
137 return f
138
139 def decorator(f):
140 # Instantiate the lock in decorator, in case users of memoize_one do:
141 #
142 # memoizer = memoize_one(threadsafe=True)
143 #
144 # @memoizer
145 # def fn1(val): ...
146 #
147 # @memoizer
148 # def fn2(val): ...
149
150 lock = threading.Lock() if threadsafe else None
151 cache = {}
152 _get = withlock(lock, cache.get)
153 _set = withlock(lock, cache.__setitem__)
154
155 @functools.wraps(f)
156 def inner(arg):
157 ret = _get(arg)
158 if ret is None:
159 ret = f(arg)
160 if ret is not None:
161 _set(arg, ret)
162 return ret
163 inner.get = _get
164 inner.set = _set
165 inner.clear = withlock(lock, cache.clear)
166 inner.update = withlock(lock, cache.update)
167 return inner
168 return decorator
169
170
171def _ScopedPool_initer(orig, orig_args): # pragma: no cover
172 """Initializer method for ScopedPool's subprocesses.
173
174 This helps ScopedPool handle Ctrl-C's correctly.
175 """
176 signal.signal(signal.SIGINT, signal.SIG_IGN)
177 if orig:
178 orig(*orig_args)
179
180
181@contextlib.contextmanager
182def ScopedPool(*args, **kwargs):
183 """Context Manager which returns a multiprocessing.pool instance which
184 correctly deals with thrown exceptions.
185
186 *args - Arguments to multiprocessing.pool
187
188 Kwargs:
189 kind ('threads', 'procs') - The type of underlying coprocess to use.
190 **etc - Arguments to multiprocessing.pool
191 """
192 if kwargs.pop('kind', None) == 'threads':
193 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
194 else:
195 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
196 kwargs['initializer'] = _ScopedPool_initer
197 kwargs['initargs'] = orig, orig_args
198 pool = multiprocessing.pool.Pool(*args, **kwargs)
199
200 try:
201 yield pool
202 pool.close()
203 except:
204 pool.terminate()
205 raise
206 finally:
207 pool.join()
208
209
210class ProgressPrinter(object):
211 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000212 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000213 """Create a ProgressPrinter.
214
215 Use it as a context manager which produces a simple 'increment' method:
216
217 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
218 for i in xrange(1000):
219 # do stuff
220 if i % 10 == 0:
221 inc(10)
222
223 Args:
224 fmt - String format with a single '%(count)d' where the counter value
225 should go.
226 enabled (bool) - If this is None, will default to True if
227 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000228 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000229 period (float) - The time in seconds for the printer thread to wait
230 between printing.
231 """
232 self.fmt = fmt
233 if enabled is None: # pragma: no cover
234 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
235 else:
236 self.enabled = enabled
237
238 self._count = 0
239 self._dead = False
240 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000241 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000242 self._thread = threading.Thread(target=self._run)
243 self._period = period
244
245 def _emit(self, s):
246 if self.enabled:
247 self._stream.write('\r' + s)
248 self._stream.flush()
249
250 def _run(self):
251 with self._dead_cond:
252 while not self._dead:
253 self._emit(self.fmt % {'count': self._count})
254 self._dead_cond.wait(self._period)
255 self._emit((self.fmt + '\n') % {'count': self._count})
256
257 def inc(self, amount=1):
258 self._count += amount
259
260 def __enter__(self):
261 self._thread.start()
262 return self.inc
263
264 def __exit__(self, _exc_type, _exc_value, _traceback):
265 self._dead = True
266 with self._dead_cond:
267 self._dead_cond.notifyAll()
268 self._thread.join()
269 del self._thread
270
271
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000272def once(function):
273 """@Decorates |function| so that it only performs its action once, no matter
274 how many times the decorated |function| is called."""
275 def _inner_gen():
276 yield function()
277 while True:
278 yield
279 return _inner_gen().next
280
281
282## Git functions
283
agable7aa2ddd2016-06-21 07:47:00 -0700284def die(message, *args):
285 print >> sys.stderr, textwrap.dedent(message % args)
286 sys.exit(1)
287
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000288
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000289def blame(filename, revision=None, porcelain=False, *_args):
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000290 command = ['blame']
291 if porcelain:
292 command.append('-p')
293 if revision is not None:
294 command.append(revision)
295 command.extend(['--', filename])
296 return run(*command)
297
298
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000299def branch_config(branch, option, default=None):
agable7aa2ddd2016-06-21 07:47:00 -0700300 return get_config('branch.%s.%s' % (branch, option), default=default)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000301
302
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000303def branch_config_map(option):
304 """Return {branch: <|option| value>} for all branches."""
305 try:
306 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
agable7aa2ddd2016-06-21 07:47:00 -0700307 lines = get_config_regexp(reg.pattern)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000308 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
309 except subprocess2.CalledProcessError:
310 return {}
311
312
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000313def branches(*args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000314 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000315
316 key = 'depot-tools.branch-limit'
agable7aa2ddd2016-06-21 07:47:00 -0700317 limit = get_config_int(key, 20)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000318
319 raw_branches = run('branch', *args).splitlines()
320
321 num = len(raw_branches)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000322
agable7aa2ddd2016-06-21 07:47:00 -0700323 if num > limit:
324 die("""\
325 Your git repo has too many branches (%d/%d) for this tool to work well.
326
327 You may adjust this limit by running:
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000328 git config %s <new_limit>
agable7aa2ddd2016-06-21 07:47:00 -0700329
330 You may also try cleaning up your old branches by running:
331 git cl archive
332 """, num, limit, key)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000333
334 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000335 if line.startswith(NO_BRANCH):
336 continue
337 yield line.split()[-1]
338
339
agable7aa2ddd2016-06-21 07:47:00 -0700340def get_config(option, default=None):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000341 try:
342 return run('config', '--get', option) or default
343 except subprocess2.CalledProcessError:
344 return default
345
346
agable7aa2ddd2016-06-21 07:47:00 -0700347def get_config_int(option, default=0):
348 assert isinstance(default, int)
349 try:
350 return int(get_config(option, default))
351 except ValueError:
352 return default
353
354
355def get_config_list(option):
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000356 try:
357 return run('config', '--get-all', option).split()
358 except subprocess2.CalledProcessError:
359 return []
360
361
agable7aa2ddd2016-06-21 07:47:00 -0700362def get_config_regexp(pattern):
363 if IS_WIN: # pragma: no cover
364 # this madness is because we call git.bat which calls git.exe which calls
365 # bash.exe (or something to that effect). Each layer divides the number of
366 # ^'s by 2.
367 pattern = pattern.replace('^', '^' * 8)
368 return run('config', '--get-regexp', pattern).splitlines()
369
370
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000371def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000372 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000373 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000374 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000375 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000376
377
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000378def del_branch_config(branch, option, scope='local'):
379 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000380
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000381
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000382def del_config(option, scope='local'):
383 try:
384 run('config', '--' + scope, '--unset', option)
385 except subprocess2.CalledProcessError:
386 pass
387
388
mgiuca@chromium.org01d2cde2016-02-05 03:25:41 +0000389def diff(oldrev, newrev, *args):
390 return run('diff', oldrev, newrev, *args)
391
392
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000393def freeze():
394 took_action = False
395
396 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000397 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000398 took_action = True
399 except subprocess2.CalledProcessError:
400 pass
401
402 try:
403 run('add', '-A')
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000404 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000405 took_action = True
406 except subprocess2.CalledProcessError:
407 pass
408
409 if not took_action:
410 return 'Nothing to freeze.'
411
412
413def get_branch_tree():
414 """Get the dictionary of {branch: parent}, compatible with topo_iter.
415
416 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
417 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000418 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000419 skipped = set()
420 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000421
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000422 for branch in branches():
423 parent = upstream(branch)
424 if not parent:
425 skipped.add(branch)
426 continue
427 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000428
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000429 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000430
431
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000432def get_or_create_merge_base(branch, parent=None):
433 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000434
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000435 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000436 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000437 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000438 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000439 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000440 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000441 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000442 actual_merge_base = run('merge-base', parent, branch)
443
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000444 if base_upstream != parent:
445 base = None
446 base_upstream = None
447
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000448 def is_ancestor(a, b):
449 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
450
clemensh@chromium.orgc3fe99d2016-04-19 08:39:55 +0000451 if base and base != actual_merge_base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000452 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000453 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
454 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000455 elif is_ancestor(base, actual_merge_base):
456 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
457 base = None
458 else:
459 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000460
461 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000462 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000463 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000464
465 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000466
467
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000468def hash_multi(*reflike):
469 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000470
471
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000472def hash_one(reflike, short=False):
473 args = ['rev-parse', reflike]
474 if short:
475 args.insert(1, '--short')
476 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000477
478
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000479def in_rebase():
480 git_dir = run('rev-parse', '--git-dir')
481 return (
482 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
483 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000484
485
486def intern_f(f, kind='blob'):
487 """Interns a file object into the git object store.
488
489 Args:
490 f (file-like object) - The file-like object to intern
491 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
492
493 Returns the git hash of the interned object (hex encoded).
494 """
495 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
496 f.close()
497 return ret
498
499
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000500def is_dormant(branch):
501 # TODO(iannucci): Do an oldness check?
502 return branch_config(branch, 'dormant', 'false') != 'false'
503
504
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000505def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000506 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000507 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000508
509
510def mktree(treedict):
511 """Makes a git tree object and returns its hash.
512
513 See |tree()| for the values of mode, type, and ref.
514
515 Args:
516 treedict - { name: (mode, type, ref) }
517 """
518 with tempfile.TemporaryFile() as f:
519 for name, (mode, typ, ref) in treedict.iteritems():
520 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
521 f.seek(0)
522 return run('mktree', '-z', stdin=f)
523
524
525def parse_commitrefs(*commitrefs):
526 """Returns binary encoded commit hashes for one or more commitrefs.
527
528 A commitref is anything which can resolve to a commit. Popular examples:
529 * 'HEAD'
530 * 'origin/master'
531 * 'cool_branch~2'
532 """
533 try:
534 return map(binascii.unhexlify, hash_multi(*commitrefs))
535 except subprocess2.CalledProcessError:
536 raise BadCommitRefException(commitrefs)
537
538
sbc@chromium.org384039b2014-10-13 21:01:00 +0000539RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000540
541
542def rebase(parent, start, branch, abort=False):
543 """Rebases |start|..|branch| onto the branch |parent|.
544
545 Args:
546 parent - The new parent ref for the rebased commits.
547 start - The commit to start from
548 branch - The branch to rebase
549 abort - If True, will call git-rebase --abort in the event that the rebase
550 doesn't complete successfully.
551
552 Returns a namedtuple with fields:
553 success - a boolean indicating that the rebase command completed
554 successfully.
555 message - if the rebase failed, this contains the stdout of the failed
556 rebase.
557 """
558 try:
559 args = ['--onto', parent, start, branch]
560 if TEST_MODE:
561 args.insert(0, '--committer-date-is-author-date')
562 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000563 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000564 except subprocess2.CalledProcessError as cpe:
565 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000566 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000567 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000568
569
570def remove_merge_base(branch):
571 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000572 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000573
574
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000575def repo_root():
576 """Returns the absolute path to the repository root."""
577 return run('rev-parse', '--show-toplevel')
578
579
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000580def root():
agable7aa2ddd2016-06-21 07:47:00 -0700581 return get_config('depot-tools.upstream', 'origin/master')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000582
583
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000584@contextlib.contextmanager
585def less(): # pragma: no cover
586 """Runs 'less' as context manager yielding its stdin as a PIPE.
587
588 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
589 running less and just yields sys.stdout.
590 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000591 if not setup_color.IS_TTY:
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000592 yield sys.stdout
593 return
594
595 # Run with the same options that git uses (see setup_pager in git repo).
596 # -F: Automatically quit if the output is less than one screen.
597 # -R: Don't escape ANSI color codes.
598 # -X: Don't clear the screen before starting.
599 cmd = ('less', '-FRX')
600 try:
601 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
602 yield proc.stdin
603 finally:
604 proc.stdin.close()
605 proc.wait()
606
607
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000608def run(*cmd, **kwargs):
609 """The same as run_with_stderr, except it only returns stdout."""
610 return run_with_stderr(*cmd, **kwargs)[0]
611
612
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000613def run_with_retcode(*cmd, **kwargs):
614 """Run a command but only return the status code."""
615 try:
616 run(*cmd, **kwargs)
617 return 0
618 except subprocess2.CalledProcessError as cpe:
619 return cpe.returncode
620
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000621def run_stream(*cmd, **kwargs):
622 """Runs a git command. Returns stdout as a PIPE (file-like object).
623
624 stderr is dropped to avoid races if the process outputs to both stdout and
625 stderr.
626 """
627 kwargs.setdefault('stderr', subprocess2.VOID)
628 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000629 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000630 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000631 proc = subprocess2.Popen(cmd, **kwargs)
632 return proc.stdout
633
634
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000635@contextlib.contextmanager
636def run_stream_with_retcode(*cmd, **kwargs):
637 """Runs a git command as context manager yielding stdout as a PIPE.
638
639 stderr is dropped to avoid races if the process outputs to both stdout and
640 stderr.
641
642 Raises subprocess2.CalledProcessError on nonzero return code.
643 """
644 kwargs.setdefault('stderr', subprocess2.VOID)
645 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000646 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000647 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
648 try:
649 proc = subprocess2.Popen(cmd, **kwargs)
650 yield proc.stdout
651 finally:
652 retcode = proc.wait()
653 if retcode != 0:
654 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
655 None, None)
656
657
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000658def run_with_stderr(*cmd, **kwargs):
659 """Runs a git command.
660
661 Returns (stdout, stderr) as a pair of strings.
662
663 kwargs
664 autostrip (bool) - Strip the output. Defaults to True.
665 indata (str) - Specifies stdin data for the process.
666 """
667 kwargs.setdefault('stdin', subprocess2.PIPE)
668 kwargs.setdefault('stdout', subprocess2.PIPE)
669 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000670 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000671 autostrip = kwargs.pop('autostrip', True)
672 indata = kwargs.pop('indata', None)
673
iannucci@chromium.org21980022014-04-11 04:51:49 +0000674 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000675 proc = subprocess2.Popen(cmd, **kwargs)
676 ret, err = proc.communicate(indata)
677 retcode = proc.wait()
678 if retcode != 0:
679 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
680
681 if autostrip:
682 ret = (ret or '').strip()
683 err = (err or '').strip()
684
685 return ret, err
686
687
688def set_branch_config(branch, option, value, scope='local'):
689 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
690
691
692def set_config(option, value, scope='local'):
693 run('config', '--' + scope, option, value)
694
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000695
sbc@chromium.org71437c02015-04-09 19:29:40 +0000696def get_dirty_files():
697 # Make sure index is up-to-date before running diff-index.
698 run_with_retcode('update-index', '--refresh', '-q')
699 return run('diff-index', '--name-status', 'HEAD')
700
701
702def is_dirty_git_tree(cmd):
703 dirty = get_dirty_files()
704 if dirty:
705 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
706 print 'Uncommitted files: (git diff-index --name-status HEAD)'
707 print dirty[:4096]
708 if len(dirty) > 4096: # pragma: no cover
709 print '... (run "git diff-index --name-status HEAD" to see full output).'
710 return True
711 return False
712
713
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000714def squash_current_branch(header=None, merge_base=None):
715 header = header or 'git squash commit.'
716 merge_base = merge_base or get_or_create_merge_base(current_branch())
717 log_msg = header + '\n'
718 if log_msg:
719 log_msg += '\n'
720 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
721 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000722
723 if not get_dirty_files():
724 # Sometimes the squash can result in the same tree, meaning that there is
725 # nothing to commit at this point.
726 print 'Nothing to commit; squashed branch is empty'
727 return False
maruel@chromium.org25b9ab22015-06-18 18:49:03 +0000728 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000729 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000730
731
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000732def tags(*args):
733 return run('tag', *args).splitlines()
734
735
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000736def thaw():
737 took_action = False
738 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
739 msg = run('show', '--format=%f%b', '-s', 'HEAD')
740 match = FREEZE_MATCHER.match(msg)
741 if not match:
742 if not took_action:
743 return 'Nothing to thaw.'
744 break
745
746 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
747 took_action = True
748
749
750def topo_iter(branch_tree, top_down=True):
751 """Generates (branch, parent) in topographical order for a branch tree.
752
753 Given a tree:
754
755 A1
756 B1 B2
757 C1 C2 C3
758 D1
759
760 branch_tree would look like: {
761 'D1': 'C3',
762 'C3': 'B2',
763 'B2': 'A1',
764 'C1': 'B1',
765 'C2': 'B1',
766 'B1': 'A1',
767 }
768
769 It is OK to have multiple 'root' nodes in your graph.
770
771 if top_down is True, items are yielded from A->D. Otherwise they're yielded
772 from D->A. Within a layer the branches will be yielded in sorted order.
773 """
774 branch_tree = branch_tree.copy()
775
776 # TODO(iannucci): There is probably a more efficient way to do these.
777 if top_down:
778 while branch_tree:
779 this_pass = [(b, p) for b, p in branch_tree.iteritems()
780 if p not in branch_tree]
781 assert this_pass, "Branch tree has cycles: %r" % branch_tree
782 for branch, parent in sorted(this_pass):
783 yield branch, parent
784 del branch_tree[branch]
785 else:
786 parent_to_branches = collections.defaultdict(set)
787 for branch, parent in branch_tree.iteritems():
788 parent_to_branches[parent].add(branch)
789
790 while branch_tree:
791 this_pass = [(b, p) for b, p in branch_tree.iteritems()
792 if not parent_to_branches[b]]
793 assert this_pass, "Branch tree has cycles: %r" % branch_tree
794 for branch, parent in sorted(this_pass):
795 yield branch, parent
796 parent_to_branches[parent].discard(branch)
797 del branch_tree[branch]
798
799
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000800def tree(treeref, recurse=False):
801 """Returns a dict representation of a git tree object.
802
803 Args:
804 treeref (str) - a git ref which resolves to a tree (commits count as trees).
805 recurse (bool) - include all of the tree's decendants too. File names will
806 take the form of 'some/path/to/file'.
807
808 Return format:
809 { 'file_name': (mode, type, ref) }
810
811 mode is an integer where:
812 * 0040000 - Directory
813 * 0100644 - Regular non-executable file
814 * 0100664 - Regular non-executable group-writeable file
815 * 0100755 - Regular executable file
816 * 0120000 - Symbolic link
817 * 0160000 - Gitlink
818
819 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
820
821 ref is the hex encoded hash of the entry.
822 """
823 ret = {}
824 opts = ['ls-tree', '--full-tree']
825 if recurse:
826 opts.append('-r')
827 opts.append(treeref)
828 try:
829 for line in run(*opts).splitlines():
830 mode, typ, ref, name = line.split(None, 3)
831 ret[name] = (mode, typ, ref)
832 except subprocess2.CalledProcessError:
833 return None
834 return ret
835
836
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000837def upstream(branch):
838 try:
839 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
840 branch+'@{upstream}')
841 except subprocess2.CalledProcessError:
842 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000843
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000844
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000845def get_git_version():
846 """Returns a tuple that contains the numeric components of the current git
847 version."""
848 version_string = run('--version')
849 version_match = re.search(r'(\d+.)+(\d+)', version_string)
850 version = version_match.group() if version_match else ''
851
852 return tuple(int(x) for x in version.split('.'))
853
854
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000855def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000856 format_string = (
857 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
858
859 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000860 if (include_tracking_status and
861 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000862 format_string += '%(upstream:track)'
863
864 info_map = {}
865 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000866 BranchesInfo = collections.namedtuple(
867 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000868 for line in data.splitlines():
869 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
870
871 ahead_match = re.search(r'ahead (\d+)', tracking_status)
872 ahead = int(ahead_match.group(1)) if ahead_match else None
873
874 behind_match = re.search(r'behind (\d+)', tracking_status)
875 behind = int(behind_match.group(1)) if behind_match else None
876
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000877 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000878 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
879
880 # Set None for upstreams which are not branches (e.g empty upstream, remotes
881 # and deleted upstream branches).
882 missing_upstreams = {}
883 for info in info_map.values():
884 if info.upstream not in info_map and info.upstream not in missing_upstreams:
885 missing_upstreams[info.upstream] = None
886
887 return dict(info_map.items() + missing_upstreams.items())
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000888
889
890def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000891 files_to_copy, symlink=None):
892 if not symlink:
893 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000894 os.makedirs(new_workdir)
895 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000896 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000897 for entry in files_to_copy:
898 clone_file(repository, new_workdir, entry, shutil.copy)
899
900
901def make_workdir(repository, new_workdir):
902 GIT_DIRECTORY_WHITELIST = [
903 'config',
904 'info',
905 'hooks',
906 'logs/refs',
907 'objects',
908 'packed-refs',
909 'refs',
910 'remotes',
911 'rr-cache',
912 'svn'
913 ]
914 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
915 ['HEAD'])
916
917
918def clone_file(repository, new_workdir, link, operation):
919 if not os.path.exists(os.path.join(repository, link)):
920 return
921 link_dir = os.path.dirname(os.path.join(new_workdir, link))
922 if not os.path.exists(link_dir):
923 os.makedirs(link_dir)
924 operation(os.path.join(repository, link), os.path.join(new_workdir, link))