blob: b7626e8a3ed4d81434443ef15e427c2cae2bf89a [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
284
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000285def blame(filename, revision=None, porcelain=False, *_args):
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000286 command = ['blame']
287 if porcelain:
288 command.append('-p')
289 if revision is not None:
290 command.append(revision)
291 command.extend(['--', filename])
292 return run(*command)
293
294
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000295def branch_config(branch, option, default=None):
296 return config('branch.%s.%s' % (branch, option), default=default)
297
298
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000299def config_regexp(pattern):
300 if IS_WIN: # pragma: no cover
301 # this madness is because we call git.bat which calls git.exe which calls
302 # bash.exe (or something to that effect). Each layer divides the number of
303 # ^'s by 2.
304 pattern = pattern.replace('^', '^' * 8)
305 return run('config', '--get-regexp', pattern).splitlines()
306
307
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000308def branch_config_map(option):
309 """Return {branch: <|option| value>} for all branches."""
310 try:
311 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000312 lines = config_regexp(reg.pattern)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000313 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
314 except subprocess2.CalledProcessError:
315 return {}
316
317
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000318def branches(*args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000319 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000320
321 key = 'depot-tools.branch-limit'
322 limit = 20
323 try:
324 limit = int(config(key, limit))
325 except ValueError:
326 pass
327
328 raw_branches = run('branch', *args).splitlines()
329
330 num = len(raw_branches)
331 if num > limit:
332 print >> sys.stderr, textwrap.dedent("""\
333 Your git repo has too many branches (%d/%d) for this tool to work well.
334
335 You may adjust this limit by running:
336 git config %s <new_limit>
337 """ % (num, limit, key))
338 sys.exit(1)
339
340 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000341 if line.startswith(NO_BRANCH):
342 continue
343 yield line.split()[-1]
344
345
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000346def config(option, default=None):
347 try:
348 return run('config', '--get', option) or default
349 except subprocess2.CalledProcessError:
350 return default
351
352
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000353def config_list(option):
354 try:
355 return run('config', '--get-all', option).split()
356 except subprocess2.CalledProcessError:
357 return []
358
359
360def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000361 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000362 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000363 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000364 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000365
366
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000367def del_branch_config(branch, option, scope='local'):
368 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000369
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000370
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000371def del_config(option, scope='local'):
372 try:
373 run('config', '--' + scope, '--unset', option)
374 except subprocess2.CalledProcessError:
375 pass
376
377
mgiuca@chromium.org01d2cde2016-02-05 03:25:41 +0000378def diff(oldrev, newrev, *args):
379 return run('diff', oldrev, newrev, *args)
380
381
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000382def freeze():
383 took_action = False
384
385 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000386 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000387 took_action = True
388 except subprocess2.CalledProcessError:
389 pass
390
391 try:
392 run('add', '-A')
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000393 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000394 took_action = True
395 except subprocess2.CalledProcessError:
396 pass
397
398 if not took_action:
399 return 'Nothing to freeze.'
400
401
402def get_branch_tree():
403 """Get the dictionary of {branch: parent}, compatible with topo_iter.
404
405 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
406 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000407 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000408 skipped = set()
409 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000410
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000411 for branch in branches():
412 parent = upstream(branch)
413 if not parent:
414 skipped.add(branch)
415 continue
416 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000417
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000418 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000419
420
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000421def get_or_create_merge_base(branch, parent=None):
422 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000423
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000424 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000425 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000426 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000427 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000428 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000429 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000430 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000431 actual_merge_base = run('merge-base', parent, branch)
432
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000433 if base_upstream != parent:
434 base = None
435 base_upstream = None
436
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000437 def is_ancestor(a, b):
438 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
439
clemensh@chromium.orgc3fe99d2016-04-19 08:39:55 +0000440 if base and base != actual_merge_base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000441 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000442 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
443 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000444 elif is_ancestor(base, actual_merge_base):
445 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
446 base = None
447 else:
448 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000449
450 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000451 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000452 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000453
454 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000455
456
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000457def hash_multi(*reflike):
458 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000459
460
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000461def hash_one(reflike, short=False):
462 args = ['rev-parse', reflike]
463 if short:
464 args.insert(1, '--short')
465 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000466
467
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000468def in_rebase():
469 git_dir = run('rev-parse', '--git-dir')
470 return (
471 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
472 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000473
474
475def intern_f(f, kind='blob'):
476 """Interns a file object into the git object store.
477
478 Args:
479 f (file-like object) - The file-like object to intern
480 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
481
482 Returns the git hash of the interned object (hex encoded).
483 """
484 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
485 f.close()
486 return ret
487
488
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000489def is_dormant(branch):
490 # TODO(iannucci): Do an oldness check?
491 return branch_config(branch, 'dormant', 'false') != 'false'
492
493
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000494def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000495 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000496 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000497
498
499def mktree(treedict):
500 """Makes a git tree object and returns its hash.
501
502 See |tree()| for the values of mode, type, and ref.
503
504 Args:
505 treedict - { name: (mode, type, ref) }
506 """
507 with tempfile.TemporaryFile() as f:
508 for name, (mode, typ, ref) in treedict.iteritems():
509 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
510 f.seek(0)
511 return run('mktree', '-z', stdin=f)
512
513
514def parse_commitrefs(*commitrefs):
515 """Returns binary encoded commit hashes for one or more commitrefs.
516
517 A commitref is anything which can resolve to a commit. Popular examples:
518 * 'HEAD'
519 * 'origin/master'
520 * 'cool_branch~2'
521 """
522 try:
523 return map(binascii.unhexlify, hash_multi(*commitrefs))
524 except subprocess2.CalledProcessError:
525 raise BadCommitRefException(commitrefs)
526
527
sbc@chromium.org384039b2014-10-13 21:01:00 +0000528RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000529
530
531def rebase(parent, start, branch, abort=False):
532 """Rebases |start|..|branch| onto the branch |parent|.
533
534 Args:
535 parent - The new parent ref for the rebased commits.
536 start - The commit to start from
537 branch - The branch to rebase
538 abort - If True, will call git-rebase --abort in the event that the rebase
539 doesn't complete successfully.
540
541 Returns a namedtuple with fields:
542 success - a boolean indicating that the rebase command completed
543 successfully.
544 message - if the rebase failed, this contains the stdout of the failed
545 rebase.
546 """
547 try:
548 args = ['--onto', parent, start, branch]
549 if TEST_MODE:
550 args.insert(0, '--committer-date-is-author-date')
551 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000552 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000553 except subprocess2.CalledProcessError as cpe:
554 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000555 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000556 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000557
558
559def remove_merge_base(branch):
560 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000561 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000562
563
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000564def repo_root():
565 """Returns the absolute path to the repository root."""
566 return run('rev-parse', '--show-toplevel')
567
568
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000569def root():
570 return config('depot-tools.upstream', 'origin/master')
571
572
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000573@contextlib.contextmanager
574def less(): # pragma: no cover
575 """Runs 'less' as context manager yielding its stdin as a PIPE.
576
577 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
578 running less and just yields sys.stdout.
579 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000580 if not setup_color.IS_TTY:
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000581 yield sys.stdout
582 return
583
584 # Run with the same options that git uses (see setup_pager in git repo).
585 # -F: Automatically quit if the output is less than one screen.
586 # -R: Don't escape ANSI color codes.
587 # -X: Don't clear the screen before starting.
588 cmd = ('less', '-FRX')
589 try:
590 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
591 yield proc.stdin
592 finally:
593 proc.stdin.close()
594 proc.wait()
595
596
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000597def run(*cmd, **kwargs):
598 """The same as run_with_stderr, except it only returns stdout."""
599 return run_with_stderr(*cmd, **kwargs)[0]
600
601
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000602def run_with_retcode(*cmd, **kwargs):
603 """Run a command but only return the status code."""
604 try:
605 run(*cmd, **kwargs)
606 return 0
607 except subprocess2.CalledProcessError as cpe:
608 return cpe.returncode
609
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000610def run_stream(*cmd, **kwargs):
611 """Runs a git command. Returns stdout as a PIPE (file-like object).
612
613 stderr is dropped to avoid races if the process outputs to both stdout and
614 stderr.
615 """
616 kwargs.setdefault('stderr', subprocess2.VOID)
617 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000618 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000619 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000620 proc = subprocess2.Popen(cmd, **kwargs)
621 return proc.stdout
622
623
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000624@contextlib.contextmanager
625def run_stream_with_retcode(*cmd, **kwargs):
626 """Runs a git command as context manager yielding stdout as a PIPE.
627
628 stderr is dropped to avoid races if the process outputs to both stdout and
629 stderr.
630
631 Raises subprocess2.CalledProcessError on nonzero return code.
632 """
633 kwargs.setdefault('stderr', subprocess2.VOID)
634 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000635 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000636 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
637 try:
638 proc = subprocess2.Popen(cmd, **kwargs)
639 yield proc.stdout
640 finally:
641 retcode = proc.wait()
642 if retcode != 0:
643 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
644 None, None)
645
646
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000647def run_with_stderr(*cmd, **kwargs):
648 """Runs a git command.
649
650 Returns (stdout, stderr) as a pair of strings.
651
652 kwargs
653 autostrip (bool) - Strip the output. Defaults to True.
654 indata (str) - Specifies stdin data for the process.
655 """
656 kwargs.setdefault('stdin', subprocess2.PIPE)
657 kwargs.setdefault('stdout', subprocess2.PIPE)
658 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000659 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000660 autostrip = kwargs.pop('autostrip', True)
661 indata = kwargs.pop('indata', None)
662
iannucci@chromium.org21980022014-04-11 04:51:49 +0000663 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000664 proc = subprocess2.Popen(cmd, **kwargs)
665 ret, err = proc.communicate(indata)
666 retcode = proc.wait()
667 if retcode != 0:
668 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
669
670 if autostrip:
671 ret = (ret or '').strip()
672 err = (err or '').strip()
673
674 return ret, err
675
676
677def set_branch_config(branch, option, value, scope='local'):
678 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
679
680
681def set_config(option, value, scope='local'):
682 run('config', '--' + scope, option, value)
683
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000684
sbc@chromium.org71437c02015-04-09 19:29:40 +0000685def get_dirty_files():
686 # Make sure index is up-to-date before running diff-index.
687 run_with_retcode('update-index', '--refresh', '-q')
688 return run('diff-index', '--name-status', 'HEAD')
689
690
691def is_dirty_git_tree(cmd):
692 dirty = get_dirty_files()
693 if dirty:
694 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
695 print 'Uncommitted files: (git diff-index --name-status HEAD)'
696 print dirty[:4096]
697 if len(dirty) > 4096: # pragma: no cover
698 print '... (run "git diff-index --name-status HEAD" to see full output).'
699 return True
700 return False
701
702
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000703def squash_current_branch(header=None, merge_base=None):
704 header = header or 'git squash commit.'
705 merge_base = merge_base or get_or_create_merge_base(current_branch())
706 log_msg = header + '\n'
707 if log_msg:
708 log_msg += '\n'
709 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
710 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000711
712 if not get_dirty_files():
713 # Sometimes the squash can result in the same tree, meaning that there is
714 # nothing to commit at this point.
715 print 'Nothing to commit; squashed branch is empty'
716 return False
maruel@chromium.org25b9ab22015-06-18 18:49:03 +0000717 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000718 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000719
720
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000721def tags(*args):
722 return run('tag', *args).splitlines()
723
724
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000725def thaw():
726 took_action = False
727 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
728 msg = run('show', '--format=%f%b', '-s', 'HEAD')
729 match = FREEZE_MATCHER.match(msg)
730 if not match:
731 if not took_action:
732 return 'Nothing to thaw.'
733 break
734
735 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
736 took_action = True
737
738
739def topo_iter(branch_tree, top_down=True):
740 """Generates (branch, parent) in topographical order for a branch tree.
741
742 Given a tree:
743
744 A1
745 B1 B2
746 C1 C2 C3
747 D1
748
749 branch_tree would look like: {
750 'D1': 'C3',
751 'C3': 'B2',
752 'B2': 'A1',
753 'C1': 'B1',
754 'C2': 'B1',
755 'B1': 'A1',
756 }
757
758 It is OK to have multiple 'root' nodes in your graph.
759
760 if top_down is True, items are yielded from A->D. Otherwise they're yielded
761 from D->A. Within a layer the branches will be yielded in sorted order.
762 """
763 branch_tree = branch_tree.copy()
764
765 # TODO(iannucci): There is probably a more efficient way to do these.
766 if top_down:
767 while branch_tree:
768 this_pass = [(b, p) for b, p in branch_tree.iteritems()
769 if p not in branch_tree]
770 assert this_pass, "Branch tree has cycles: %r" % branch_tree
771 for branch, parent in sorted(this_pass):
772 yield branch, parent
773 del branch_tree[branch]
774 else:
775 parent_to_branches = collections.defaultdict(set)
776 for branch, parent in branch_tree.iteritems():
777 parent_to_branches[parent].add(branch)
778
779 while branch_tree:
780 this_pass = [(b, p) for b, p in branch_tree.iteritems()
781 if not parent_to_branches[b]]
782 assert this_pass, "Branch tree has cycles: %r" % branch_tree
783 for branch, parent in sorted(this_pass):
784 yield branch, parent
785 parent_to_branches[parent].discard(branch)
786 del branch_tree[branch]
787
788
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000789def tree(treeref, recurse=False):
790 """Returns a dict representation of a git tree object.
791
792 Args:
793 treeref (str) - a git ref which resolves to a tree (commits count as trees).
794 recurse (bool) - include all of the tree's decendants too. File names will
795 take the form of 'some/path/to/file'.
796
797 Return format:
798 { 'file_name': (mode, type, ref) }
799
800 mode is an integer where:
801 * 0040000 - Directory
802 * 0100644 - Regular non-executable file
803 * 0100664 - Regular non-executable group-writeable file
804 * 0100755 - Regular executable file
805 * 0120000 - Symbolic link
806 * 0160000 - Gitlink
807
808 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
809
810 ref is the hex encoded hash of the entry.
811 """
812 ret = {}
813 opts = ['ls-tree', '--full-tree']
814 if recurse:
815 opts.append('-r')
816 opts.append(treeref)
817 try:
818 for line in run(*opts).splitlines():
819 mode, typ, ref, name = line.split(None, 3)
820 ret[name] = (mode, typ, ref)
821 except subprocess2.CalledProcessError:
822 return None
823 return ret
824
825
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000826def upstream(branch):
827 try:
828 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
829 branch+'@{upstream}')
830 except subprocess2.CalledProcessError:
831 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000832
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000833
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000834def get_git_version():
835 """Returns a tuple that contains the numeric components of the current git
836 version."""
837 version_string = run('--version')
838 version_match = re.search(r'(\d+.)+(\d+)', version_string)
839 version = version_match.group() if version_match else ''
840
841 return tuple(int(x) for x in version.split('.'))
842
843
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000844def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000845 format_string = (
846 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
847
848 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000849 if (include_tracking_status and
850 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000851 format_string += '%(upstream:track)'
852
853 info_map = {}
854 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000855 BranchesInfo = collections.namedtuple(
856 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000857 for line in data.splitlines():
858 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
859
860 ahead_match = re.search(r'ahead (\d+)', tracking_status)
861 ahead = int(ahead_match.group(1)) if ahead_match else None
862
863 behind_match = re.search(r'behind (\d+)', tracking_status)
864 behind = int(behind_match.group(1)) if behind_match else None
865
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000866 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000867 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
868
869 # Set None for upstreams which are not branches (e.g empty upstream, remotes
870 # and deleted upstream branches).
871 missing_upstreams = {}
872 for info in info_map.values():
873 if info.upstream not in info_map and info.upstream not in missing_upstreams:
874 missing_upstreams[info.upstream] = None
875
876 return dict(info_map.items() + missing_upstreams.items())
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000877
878
879def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000880 files_to_copy, symlink=None):
881 if not symlink:
882 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000883 os.makedirs(new_workdir)
884 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000885 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000886 for entry in files_to_copy:
887 clone_file(repository, new_workdir, entry, shutil.copy)
888
889
890def make_workdir(repository, new_workdir):
891 GIT_DIRECTORY_WHITELIST = [
892 'config',
893 'info',
894 'hooks',
895 'logs/refs',
896 'objects',
897 'packed-refs',
898 'refs',
899 'remotes',
900 'rr-cache',
901 'svn'
902 ]
903 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
904 ['HEAD'])
905
906
907def clone_file(repository, new_workdir, link, operation):
908 if not os.path.exists(os.path.join(repository, link)):
909 return
910 link_dir = os.path.dirname(os.path.join(new_workdir, link))
911 if not os.path.exists(link_dir):
912 os.makedirs(link_dir)
913 operation(os.path.join(repository, link), os.path.join(new_workdir, link))