blob: 2df493663bcf7fae5a155ed6f4067443e383f491 [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
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000568@contextlib.contextmanager
569def run_stream_with_retcode(*cmd, **kwargs):
570 """Runs a git command as context manager yielding stdout as a PIPE.
571
572 stderr is dropped to avoid races if the process outputs to both stdout and
573 stderr.
574
575 Raises subprocess2.CalledProcessError on nonzero return code.
576 """
577 kwargs.setdefault('stderr', subprocess2.VOID)
578 kwargs.setdefault('stdout', subprocess2.PIPE)
579 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
580 try:
581 proc = subprocess2.Popen(cmd, **kwargs)
582 yield proc.stdout
583 finally:
584 retcode = proc.wait()
585 if retcode != 0:
586 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
587 None, None)
588
589
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000590def run_with_stderr(*cmd, **kwargs):
591 """Runs a git command.
592
593 Returns (stdout, stderr) as a pair of strings.
594
595 kwargs
596 autostrip (bool) - Strip the output. Defaults to True.
597 indata (str) - Specifies stdin data for the process.
598 """
599 kwargs.setdefault('stdin', subprocess2.PIPE)
600 kwargs.setdefault('stdout', subprocess2.PIPE)
601 kwargs.setdefault('stderr', subprocess2.PIPE)
602 autostrip = kwargs.pop('autostrip', True)
603 indata = kwargs.pop('indata', None)
604
iannucci@chromium.org21980022014-04-11 04:51:49 +0000605 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000606 proc = subprocess2.Popen(cmd, **kwargs)
607 ret, err = proc.communicate(indata)
608 retcode = proc.wait()
609 if retcode != 0:
610 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
611
612 if autostrip:
613 ret = (ret or '').strip()
614 err = (err or '').strip()
615
616 return ret, err
617
618
619def set_branch_config(branch, option, value, scope='local'):
620 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
621
622
623def set_config(option, value, scope='local'):
624 run('config', '--' + scope, option, value)
625
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000626
sbc@chromium.org71437c02015-04-09 19:29:40 +0000627def get_dirty_files():
628 # Make sure index is up-to-date before running diff-index.
629 run_with_retcode('update-index', '--refresh', '-q')
630 return run('diff-index', '--name-status', 'HEAD')
631
632
633def is_dirty_git_tree(cmd):
634 dirty = get_dirty_files()
635 if dirty:
636 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
637 print 'Uncommitted files: (git diff-index --name-status HEAD)'
638 print dirty[:4096]
639 if len(dirty) > 4096: # pragma: no cover
640 print '... (run "git diff-index --name-status HEAD" to see full output).'
641 return True
642 return False
643
644
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000645def squash_current_branch(header=None, merge_base=None):
646 header = header or 'git squash commit.'
647 merge_base = merge_base or get_or_create_merge_base(current_branch())
648 log_msg = header + '\n'
649 if log_msg:
650 log_msg += '\n'
651 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
652 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000653
654 if not get_dirty_files():
655 # Sometimes the squash can result in the same tree, meaning that there is
656 # nothing to commit at this point.
657 print 'Nothing to commit; squashed branch is empty'
658 return False
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000659 run('commit', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000660 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000661
662
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000663def tags(*args):
664 return run('tag', *args).splitlines()
665
666
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000667def thaw():
668 took_action = False
669 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
670 msg = run('show', '--format=%f%b', '-s', 'HEAD')
671 match = FREEZE_MATCHER.match(msg)
672 if not match:
673 if not took_action:
674 return 'Nothing to thaw.'
675 break
676
677 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
678 took_action = True
679
680
681def topo_iter(branch_tree, top_down=True):
682 """Generates (branch, parent) in topographical order for a branch tree.
683
684 Given a tree:
685
686 A1
687 B1 B2
688 C1 C2 C3
689 D1
690
691 branch_tree would look like: {
692 'D1': 'C3',
693 'C3': 'B2',
694 'B2': 'A1',
695 'C1': 'B1',
696 'C2': 'B1',
697 'B1': 'A1',
698 }
699
700 It is OK to have multiple 'root' nodes in your graph.
701
702 if top_down is True, items are yielded from A->D. Otherwise they're yielded
703 from D->A. Within a layer the branches will be yielded in sorted order.
704 """
705 branch_tree = branch_tree.copy()
706
707 # TODO(iannucci): There is probably a more efficient way to do these.
708 if top_down:
709 while branch_tree:
710 this_pass = [(b, p) for b, p in branch_tree.iteritems()
711 if p not in branch_tree]
712 assert this_pass, "Branch tree has cycles: %r" % branch_tree
713 for branch, parent in sorted(this_pass):
714 yield branch, parent
715 del branch_tree[branch]
716 else:
717 parent_to_branches = collections.defaultdict(set)
718 for branch, parent in branch_tree.iteritems():
719 parent_to_branches[parent].add(branch)
720
721 while branch_tree:
722 this_pass = [(b, p) for b, p in branch_tree.iteritems()
723 if not parent_to_branches[b]]
724 assert this_pass, "Branch tree has cycles: %r" % branch_tree
725 for branch, parent in sorted(this_pass):
726 yield branch, parent
727 parent_to_branches[parent].discard(branch)
728 del branch_tree[branch]
729
730
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000731def tree(treeref, recurse=False):
732 """Returns a dict representation of a git tree object.
733
734 Args:
735 treeref (str) - a git ref which resolves to a tree (commits count as trees).
736 recurse (bool) - include all of the tree's decendants too. File names will
737 take the form of 'some/path/to/file'.
738
739 Return format:
740 { 'file_name': (mode, type, ref) }
741
742 mode is an integer where:
743 * 0040000 - Directory
744 * 0100644 - Regular non-executable file
745 * 0100664 - Regular non-executable group-writeable file
746 * 0100755 - Regular executable file
747 * 0120000 - Symbolic link
748 * 0160000 - Gitlink
749
750 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
751
752 ref is the hex encoded hash of the entry.
753 """
754 ret = {}
755 opts = ['ls-tree', '--full-tree']
756 if recurse:
757 opts.append('-r')
758 opts.append(treeref)
759 try:
760 for line in run(*opts).splitlines():
761 mode, typ, ref, name = line.split(None, 3)
762 ret[name] = (mode, typ, ref)
763 except subprocess2.CalledProcessError:
764 return None
765 return ret
766
767
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000768def upstream(branch):
769 try:
770 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
771 branch+'@{upstream}')
772 except subprocess2.CalledProcessError:
773 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000774
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000775
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000776def get_git_version():
777 """Returns a tuple that contains the numeric components of the current git
778 version."""
779 version_string = run('--version')
780 version_match = re.search(r'(\d+.)+(\d+)', version_string)
781 version = version_match.group() if version_match else ''
782
783 return tuple(int(x) for x in version.split('.'))
784
785
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000786def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000787 format_string = (
788 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
789
790 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000791 if (include_tracking_status and
792 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000793 format_string += '%(upstream:track)'
794
795 info_map = {}
796 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000797 BranchesInfo = collections.namedtuple(
798 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000799 for line in data.splitlines():
800 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
801
802 ahead_match = re.search(r'ahead (\d+)', tracking_status)
803 ahead = int(ahead_match.group(1)) if ahead_match else None
804
805 behind_match = re.search(r'behind (\d+)', tracking_status)
806 behind = int(behind_match.group(1)) if behind_match else None
807
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000808 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000809 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
810
811 # Set None for upstreams which are not branches (e.g empty upstream, remotes
812 # and deleted upstream branches).
813 missing_upstreams = {}
814 for info in info_map.values():
815 if info.upstream not in info_map and info.upstream not in missing_upstreams:
816 missing_upstreams[info.upstream] = None
817
818 return dict(info_map.items() + missing_upstreams.items())