blob: a01f8e5694465219af083d5c4b12a5cbaca97a4b [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.orgaa74cf62013-11-19 20:00:49 +000025import signal
26import sys
27import tempfile
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +000028import textwrap
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000029import threading
30
31import subprocess2
32
techtonik@gmail.coma5a945a2014-08-15 20:01:53 +000033ROOT = os.path.abspath(os.path.dirname(__file__))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000034
techtonik@gmail.coma5a945a2014-08-15 20:01:53 +000035GIT_EXE = ROOT+'\\git.bat' if sys.platform.startswith('win') else 'git'
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000036TEST_MODE = False
37
38FREEZE = 'FREEZE'
39FREEZE_SECTIONS = {
40 'indexed': 'soft',
41 'unindexed': 'mixed'
42}
43FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000044
45
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000046# Retry a git operation if git returns a error response with any of these
47# messages. It's all observed 'bad' GoB responses so far.
48#
49# This list is inspired/derived from the one in ChromiumOS's Chromite:
50# <CHROMITE>/lib/git.py::GIT_TRANSIENT_ERRORS
51#
52# It was last imported from '7add3ac29564d98ac35ce426bc295e743e7c0c02'.
53GIT_TRANSIENT_ERRORS = (
54 # crbug.com/285832
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000055 r'!.*\[remote rejected\].*\(error in hook\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000056
57 # crbug.com/289932
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000058 r'!.*\[remote rejected\].*\(failed to lock\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000059
60 # crbug.com/307156
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000061 r'!.*\[remote rejected\].*\(error in Gerrit backend\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000062
63 # crbug.com/285832
64 r'remote error: Internal Server Error',
65
66 # crbug.com/294449
67 r'fatal: Couldn\'t find remote ref ',
68
69 # crbug.com/220543
70 r'git fetch_pack: expected ACK/NAK, got',
71
72 # crbug.com/189455
73 r'protocol error: bad pack header',
74
75 # crbug.com/202807
76 r'The remote end hung up unexpectedly',
77
78 # crbug.com/298189
79 r'TLS packet with unexpected length was received',
80
81 # crbug.com/187444
82 r'RPC failed; result=\d+, HTTP code = \d+',
83
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000084 # crbug.com/388876
85 r'Connection timed out',
dnj@chromium.org45cddd62014-11-06 19:36:42 +000086
87 # crbug.com/430343
88 # TODO(dnj): Resync with Chromite.
89 r'The requested URL returned error: 5\d+',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000090)
91
92GIT_TRANSIENT_ERRORS_RE = re.compile('|'.join(GIT_TRANSIENT_ERRORS),
93 re.IGNORECASE)
94
calamity@chromium.org9d2c8802014-09-03 02:04:46 +000095# First version where the for-each-ref command's format string supported the
96# upstream:track token.
97MIN_UPSTREAM_TRACK_GIT_VERSION = (1, 9)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000098
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000099class BadCommitRefException(Exception):
100 def __init__(self, refs):
101 msg = ('one of %s does not seem to be a valid commitref.' %
102 str(refs))
103 super(BadCommitRefException, self).__init__(msg)
104
105
106def memoize_one(**kwargs):
107 """Memoizes a single-argument pure function.
108
109 Values of None are not cached.
110
111 Kwargs:
112 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
113 cache manipulation functions. This is a kwarg so that users of memoize_one
114 are forced to explicitly and verbosely pick True or False.
115
116 Adds three methods to the decorated function:
117 * get(key, default=None) - Gets the value for this key from the cache.
118 * set(key, value) - Sets the value for this key from the cache.
119 * clear() - Drops the entire contents of the cache. Useful for unittests.
120 * update(other) - Updates the contents of the cache from another dict.
121 """
122 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
123 threadsafe = kwargs['threadsafe']
124
125 if threadsafe:
126 def withlock(lock, f):
127 def inner(*args, **kwargs):
128 with lock:
129 return f(*args, **kwargs)
130 return inner
131 else:
132 def withlock(_lock, f):
133 return f
134
135 def decorator(f):
136 # Instantiate the lock in decorator, in case users of memoize_one do:
137 #
138 # memoizer = memoize_one(threadsafe=True)
139 #
140 # @memoizer
141 # def fn1(val): ...
142 #
143 # @memoizer
144 # def fn2(val): ...
145
146 lock = threading.Lock() if threadsafe else None
147 cache = {}
148 _get = withlock(lock, cache.get)
149 _set = withlock(lock, cache.__setitem__)
150
151 @functools.wraps(f)
152 def inner(arg):
153 ret = _get(arg)
154 if ret is None:
155 ret = f(arg)
156 if ret is not None:
157 _set(arg, ret)
158 return ret
159 inner.get = _get
160 inner.set = _set
161 inner.clear = withlock(lock, cache.clear)
162 inner.update = withlock(lock, cache.update)
163 return inner
164 return decorator
165
166
167def _ScopedPool_initer(orig, orig_args): # pragma: no cover
168 """Initializer method for ScopedPool's subprocesses.
169
170 This helps ScopedPool handle Ctrl-C's correctly.
171 """
172 signal.signal(signal.SIGINT, signal.SIG_IGN)
173 if orig:
174 orig(*orig_args)
175
176
177@contextlib.contextmanager
178def ScopedPool(*args, **kwargs):
179 """Context Manager which returns a multiprocessing.pool instance which
180 correctly deals with thrown exceptions.
181
182 *args - Arguments to multiprocessing.pool
183
184 Kwargs:
185 kind ('threads', 'procs') - The type of underlying coprocess to use.
186 **etc - Arguments to multiprocessing.pool
187 """
188 if kwargs.pop('kind', None) == 'threads':
189 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
190 else:
191 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
192 kwargs['initializer'] = _ScopedPool_initer
193 kwargs['initargs'] = orig, orig_args
194 pool = multiprocessing.pool.Pool(*args, **kwargs)
195
196 try:
197 yield pool
198 pool.close()
199 except:
200 pool.terminate()
201 raise
202 finally:
203 pool.join()
204
205
206class ProgressPrinter(object):
207 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000208 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000209 """Create a ProgressPrinter.
210
211 Use it as a context manager which produces a simple 'increment' method:
212
213 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
214 for i in xrange(1000):
215 # do stuff
216 if i % 10 == 0:
217 inc(10)
218
219 Args:
220 fmt - String format with a single '%(count)d' where the counter value
221 should go.
222 enabled (bool) - If this is None, will default to True if
223 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000224 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000225 period (float) - The time in seconds for the printer thread to wait
226 between printing.
227 """
228 self.fmt = fmt
229 if enabled is None: # pragma: no cover
230 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
231 else:
232 self.enabled = enabled
233
234 self._count = 0
235 self._dead = False
236 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000237 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000238 self._thread = threading.Thread(target=self._run)
239 self._period = period
240
241 def _emit(self, s):
242 if self.enabled:
243 self._stream.write('\r' + s)
244 self._stream.flush()
245
246 def _run(self):
247 with self._dead_cond:
248 while not self._dead:
249 self._emit(self.fmt % {'count': self._count})
250 self._dead_cond.wait(self._period)
251 self._emit((self.fmt + '\n') % {'count': self._count})
252
253 def inc(self, amount=1):
254 self._count += amount
255
256 def __enter__(self):
257 self._thread.start()
258 return self.inc
259
260 def __exit__(self, _exc_type, _exc_value, _traceback):
261 self._dead = True
262 with self._dead_cond:
263 self._dead_cond.notifyAll()
264 self._thread.join()
265 del self._thread
266
267
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000268def once(function):
269 """@Decorates |function| so that it only performs its action once, no matter
270 how many times the decorated |function| is called."""
271 def _inner_gen():
272 yield function()
273 while True:
274 yield
275 return _inner_gen().next
276
277
278## Git functions
279
280
281def branch_config(branch, option, default=None):
282 return config('branch.%s.%s' % (branch, option), default=default)
283
284
285def branch_config_map(option):
286 """Return {branch: <|option| value>} for all branches."""
287 try:
288 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
289 lines = run('config', '--get-regexp', reg.pattern).splitlines()
290 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
291 except subprocess2.CalledProcessError:
292 return {}
293
294
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000295def branches(*args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000296 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000297
298 key = 'depot-tools.branch-limit'
299 limit = 20
300 try:
301 limit = int(config(key, limit))
302 except ValueError:
303 pass
304
305 raw_branches = run('branch', *args).splitlines()
306
307 num = len(raw_branches)
308 if num > limit:
309 print >> sys.stderr, textwrap.dedent("""\
310 Your git repo has too many branches (%d/%d) for this tool to work well.
311
312 You may adjust this limit by running:
313 git config %s <new_limit>
314 """ % (num, limit, key))
315 sys.exit(1)
316
317 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000318 if line.startswith(NO_BRANCH):
319 continue
320 yield line.split()[-1]
321
322
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000323def config(option, default=None):
324 try:
325 return run('config', '--get', option) or default
326 except subprocess2.CalledProcessError:
327 return default
328
329
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000330def config_list(option):
331 try:
332 return run('config', '--get-all', option).split()
333 except subprocess2.CalledProcessError:
334 return []
335
336
337def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000338 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000339 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000340 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000341 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000342
343
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000344def del_branch_config(branch, option, scope='local'):
345 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000346
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000347
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000348def del_config(option, scope='local'):
349 try:
350 run('config', '--' + scope, '--unset', option)
351 except subprocess2.CalledProcessError:
352 pass
353
354
355def freeze():
356 took_action = False
357
358 try:
359 run('commit', '-m', FREEZE + '.indexed')
360 took_action = True
361 except subprocess2.CalledProcessError:
362 pass
363
364 try:
365 run('add', '-A')
366 run('commit', '-m', FREEZE + '.unindexed')
367 took_action = True
368 except subprocess2.CalledProcessError:
369 pass
370
371 if not took_action:
372 return 'Nothing to freeze.'
373
374
375def get_branch_tree():
376 """Get the dictionary of {branch: parent}, compatible with topo_iter.
377
378 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
379 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000380 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000381 skipped = set()
382 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000383
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000384 for branch in branches():
385 parent = upstream(branch)
386 if not parent:
387 skipped.add(branch)
388 continue
389 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000390
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000391 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000392
393
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000394def get_or_create_merge_base(branch, parent=None):
395 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000396
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000397 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000398 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000399 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000400 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000401 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000402 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000403 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000404 actual_merge_base = run('merge-base', parent, branch)
405
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000406 if base_upstream != parent:
407 base = None
408 base_upstream = None
409
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000410 def is_ancestor(a, b):
411 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
412
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000413 if base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000414 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000415 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
416 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000417 elif is_ancestor(base, actual_merge_base):
418 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
419 base = None
420 else:
421 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000422
423 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000424 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000425 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000426
427 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000428
429
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000430def hash_multi(*reflike):
431 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000432
433
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000434def hash_one(reflike, short=False):
435 args = ['rev-parse', reflike]
436 if short:
437 args.insert(1, '--short')
438 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000439
440
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000441def in_rebase():
442 git_dir = run('rev-parse', '--git-dir')
443 return (
444 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
445 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000446
447
448def intern_f(f, kind='blob'):
449 """Interns a file object into the git object store.
450
451 Args:
452 f (file-like object) - The file-like object to intern
453 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
454
455 Returns the git hash of the interned object (hex encoded).
456 """
457 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
458 f.close()
459 return ret
460
461
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000462def is_dormant(branch):
463 # TODO(iannucci): Do an oldness check?
464 return branch_config(branch, 'dormant', 'false') != 'false'
465
466
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000467def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000468 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000469 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000470
471
472def mktree(treedict):
473 """Makes a git tree object and returns its hash.
474
475 See |tree()| for the values of mode, type, and ref.
476
477 Args:
478 treedict - { name: (mode, type, ref) }
479 """
480 with tempfile.TemporaryFile() as f:
481 for name, (mode, typ, ref) in treedict.iteritems():
482 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
483 f.seek(0)
484 return run('mktree', '-z', stdin=f)
485
486
487def parse_commitrefs(*commitrefs):
488 """Returns binary encoded commit hashes for one or more commitrefs.
489
490 A commitref is anything which can resolve to a commit. Popular examples:
491 * 'HEAD'
492 * 'origin/master'
493 * 'cool_branch~2'
494 """
495 try:
496 return map(binascii.unhexlify, hash_multi(*commitrefs))
497 except subprocess2.CalledProcessError:
498 raise BadCommitRefException(commitrefs)
499
500
sbc@chromium.org384039b2014-10-13 21:01:00 +0000501RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000502
503
504def rebase(parent, start, branch, abort=False):
505 """Rebases |start|..|branch| onto the branch |parent|.
506
507 Args:
508 parent - The new parent ref for the rebased commits.
509 start - The commit to start from
510 branch - The branch to rebase
511 abort - If True, will call git-rebase --abort in the event that the rebase
512 doesn't complete successfully.
513
514 Returns a namedtuple with fields:
515 success - a boolean indicating that the rebase command completed
516 successfully.
517 message - if the rebase failed, this contains the stdout of the failed
518 rebase.
519 """
520 try:
521 args = ['--onto', parent, start, branch]
522 if TEST_MODE:
523 args.insert(0, '--committer-date-is-author-date')
524 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000525 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000526 except subprocess2.CalledProcessError as cpe:
527 if abort:
528 run('rebase', '--abort')
sbc@chromium.org384039b2014-10-13 21:01:00 +0000529 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000530
531
532def remove_merge_base(branch):
533 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000534 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000535
536
537def root():
538 return config('depot-tools.upstream', 'origin/master')
539
540
541def run(*cmd, **kwargs):
542 """The same as run_with_stderr, except it only returns stdout."""
543 return run_with_stderr(*cmd, **kwargs)[0]
544
545
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000546def run_with_retcode(*cmd, **kwargs):
547 """Run a command but only return the status code."""
548 try:
549 run(*cmd, **kwargs)
550 return 0
551 except subprocess2.CalledProcessError as cpe:
552 return cpe.returncode
553
554
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000555def run_stream(*cmd, **kwargs):
556 """Runs a git command. Returns stdout as a PIPE (file-like object).
557
558 stderr is dropped to avoid races if the process outputs to both stdout and
559 stderr.
560 """
561 kwargs.setdefault('stderr', subprocess2.VOID)
562 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000563 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000564 proc = subprocess2.Popen(cmd, **kwargs)
565 return proc.stdout
566
567
568def run_with_stderr(*cmd, **kwargs):
569 """Runs a git command.
570
571 Returns (stdout, stderr) as a pair of strings.
572
573 kwargs
574 autostrip (bool) - Strip the output. Defaults to True.
575 indata (str) - Specifies stdin data for the process.
576 """
577 kwargs.setdefault('stdin', subprocess2.PIPE)
578 kwargs.setdefault('stdout', subprocess2.PIPE)
579 kwargs.setdefault('stderr', subprocess2.PIPE)
580 autostrip = kwargs.pop('autostrip', True)
581 indata = kwargs.pop('indata', None)
582
iannucci@chromium.org21980022014-04-11 04:51:49 +0000583 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000584 proc = subprocess2.Popen(cmd, **kwargs)
585 ret, err = proc.communicate(indata)
586 retcode = proc.wait()
587 if retcode != 0:
588 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
589
590 if autostrip:
591 ret = (ret or '').strip()
592 err = (err or '').strip()
593
594 return ret, err
595
596
597def set_branch_config(branch, option, value, scope='local'):
598 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
599
600
601def set_config(option, value, scope='local'):
602 run('config', '--' + scope, option, value)
603
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000604
sbc@chromium.org71437c02015-04-09 19:29:40 +0000605def get_dirty_files():
606 # Make sure index is up-to-date before running diff-index.
607 run_with_retcode('update-index', '--refresh', '-q')
608 return run('diff-index', '--name-status', 'HEAD')
609
610
611def is_dirty_git_tree(cmd):
612 dirty = get_dirty_files()
613 if dirty:
614 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
615 print 'Uncommitted files: (git diff-index --name-status HEAD)'
616 print dirty[:4096]
617 if len(dirty) > 4096: # pragma: no cover
618 print '... (run "git diff-index --name-status HEAD" to see full output).'
619 return True
620 return False
621
622
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000623def squash_current_branch(header=None, merge_base=None):
624 header = header or 'git squash commit.'
625 merge_base = merge_base or get_or_create_merge_base(current_branch())
626 log_msg = header + '\n'
627 if log_msg:
628 log_msg += '\n'
629 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
630 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000631
632 if not get_dirty_files():
633 # Sometimes the squash can result in the same tree, meaning that there is
634 # nothing to commit at this point.
635 print 'Nothing to commit; squashed branch is empty'
636 return False
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000637 run('commit', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000638 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000639
640
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000641def tags(*args):
642 return run('tag', *args).splitlines()
643
644
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000645def thaw():
646 took_action = False
647 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
648 msg = run('show', '--format=%f%b', '-s', 'HEAD')
649 match = FREEZE_MATCHER.match(msg)
650 if not match:
651 if not took_action:
652 return 'Nothing to thaw.'
653 break
654
655 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
656 took_action = True
657
658
659def topo_iter(branch_tree, top_down=True):
660 """Generates (branch, parent) in topographical order for a branch tree.
661
662 Given a tree:
663
664 A1
665 B1 B2
666 C1 C2 C3
667 D1
668
669 branch_tree would look like: {
670 'D1': 'C3',
671 'C3': 'B2',
672 'B2': 'A1',
673 'C1': 'B1',
674 'C2': 'B1',
675 'B1': 'A1',
676 }
677
678 It is OK to have multiple 'root' nodes in your graph.
679
680 if top_down is True, items are yielded from A->D. Otherwise they're yielded
681 from D->A. Within a layer the branches will be yielded in sorted order.
682 """
683 branch_tree = branch_tree.copy()
684
685 # TODO(iannucci): There is probably a more efficient way to do these.
686 if top_down:
687 while branch_tree:
688 this_pass = [(b, p) for b, p in branch_tree.iteritems()
689 if p not in branch_tree]
690 assert this_pass, "Branch tree has cycles: %r" % branch_tree
691 for branch, parent in sorted(this_pass):
692 yield branch, parent
693 del branch_tree[branch]
694 else:
695 parent_to_branches = collections.defaultdict(set)
696 for branch, parent in branch_tree.iteritems():
697 parent_to_branches[parent].add(branch)
698
699 while branch_tree:
700 this_pass = [(b, p) for b, p in branch_tree.iteritems()
701 if not parent_to_branches[b]]
702 assert this_pass, "Branch tree has cycles: %r" % branch_tree
703 for branch, parent in sorted(this_pass):
704 yield branch, parent
705 parent_to_branches[parent].discard(branch)
706 del branch_tree[branch]
707
708
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000709def tree(treeref, recurse=False):
710 """Returns a dict representation of a git tree object.
711
712 Args:
713 treeref (str) - a git ref which resolves to a tree (commits count as trees).
714 recurse (bool) - include all of the tree's decendants too. File names will
715 take the form of 'some/path/to/file'.
716
717 Return format:
718 { 'file_name': (mode, type, ref) }
719
720 mode is an integer where:
721 * 0040000 - Directory
722 * 0100644 - Regular non-executable file
723 * 0100664 - Regular non-executable group-writeable file
724 * 0100755 - Regular executable file
725 * 0120000 - Symbolic link
726 * 0160000 - Gitlink
727
728 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
729
730 ref is the hex encoded hash of the entry.
731 """
732 ret = {}
733 opts = ['ls-tree', '--full-tree']
734 if recurse:
735 opts.append('-r')
736 opts.append(treeref)
737 try:
738 for line in run(*opts).splitlines():
739 mode, typ, ref, name = line.split(None, 3)
740 ret[name] = (mode, typ, ref)
741 except subprocess2.CalledProcessError:
742 return None
743 return ret
744
745
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000746def upstream(branch):
747 try:
748 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
749 branch+'@{upstream}')
750 except subprocess2.CalledProcessError:
751 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000752
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000753
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000754def get_git_version():
755 """Returns a tuple that contains the numeric components of the current git
756 version."""
757 version_string = run('--version')
758 version_match = re.search(r'(\d+.)+(\d+)', version_string)
759 version = version_match.group() if version_match else ''
760
761 return tuple(int(x) for x in version.split('.'))
762
763
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000764def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000765 format_string = (
766 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
767
768 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000769 if (include_tracking_status and
770 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000771 format_string += '%(upstream:track)'
772
773 info_map = {}
774 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000775 BranchesInfo = collections.namedtuple(
776 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000777 for line in data.splitlines():
778 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
779
780 ahead_match = re.search(r'ahead (\d+)', tracking_status)
781 ahead = int(ahead_match.group(1)) if ahead_match else None
782
783 behind_match = re.search(r'behind (\d+)', tracking_status)
784 behind = int(behind_match.group(1)) if behind_match else None
785
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000786 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000787 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
788
789 # Set None for upstreams which are not branches (e.g empty upstream, remotes
790 # and deleted upstream branches).
791 missing_upstreams = {}
792 for info in info_map.values():
793 if info.upstream not in info_map and info.upstream not in missing_upstreams:
794 missing_upstreams[info.upstream] = None
795
796 return dict(info_map.items() + missing_upstreams.items())