blob: 2b7d258f60cad2c0cbfae55bd42cfa66e562a138 [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
raphael.kubo.da.costa@intel.com58d05b02015-06-24 08:54:41 +000095# git's for-each-ref command first supported the upstream:track token in its
96# format string in version 1.9.0, but some usages were broken until 2.3.0.
97# See git commit b6160d95 for more information.
98MIN_UPSTREAM_TRACK_GIT_VERSION = (2, 3)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000099
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000100class BadCommitRefException(Exception):
101 def __init__(self, refs):
102 msg = ('one of %s does not seem to be a valid commitref.' %
103 str(refs))
104 super(BadCommitRefException, self).__init__(msg)
105
106
107def memoize_one(**kwargs):
108 """Memoizes a single-argument pure function.
109
110 Values of None are not cached.
111
112 Kwargs:
113 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
114 cache manipulation functions. This is a kwarg so that users of memoize_one
115 are forced to explicitly and verbosely pick True or False.
116
117 Adds three methods to the decorated function:
118 * get(key, default=None) - Gets the value for this key from the cache.
119 * set(key, value) - Sets the value for this key from the cache.
120 * clear() - Drops the entire contents of the cache. Useful for unittests.
121 * update(other) - Updates the contents of the cache from another dict.
122 """
123 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
124 threadsafe = kwargs['threadsafe']
125
126 if threadsafe:
127 def withlock(lock, f):
128 def inner(*args, **kwargs):
129 with lock:
130 return f(*args, **kwargs)
131 return inner
132 else:
133 def withlock(_lock, f):
134 return f
135
136 def decorator(f):
137 # Instantiate the lock in decorator, in case users of memoize_one do:
138 #
139 # memoizer = memoize_one(threadsafe=True)
140 #
141 # @memoizer
142 # def fn1(val): ...
143 #
144 # @memoizer
145 # def fn2(val): ...
146
147 lock = threading.Lock() if threadsafe else None
148 cache = {}
149 _get = withlock(lock, cache.get)
150 _set = withlock(lock, cache.__setitem__)
151
152 @functools.wraps(f)
153 def inner(arg):
154 ret = _get(arg)
155 if ret is None:
156 ret = f(arg)
157 if ret is not None:
158 _set(arg, ret)
159 return ret
160 inner.get = _get
161 inner.set = _set
162 inner.clear = withlock(lock, cache.clear)
163 inner.update = withlock(lock, cache.update)
164 return inner
165 return decorator
166
167
168def _ScopedPool_initer(orig, orig_args): # pragma: no cover
169 """Initializer method for ScopedPool's subprocesses.
170
171 This helps ScopedPool handle Ctrl-C's correctly.
172 """
173 signal.signal(signal.SIGINT, signal.SIG_IGN)
174 if orig:
175 orig(*orig_args)
176
177
178@contextlib.contextmanager
179def ScopedPool(*args, **kwargs):
180 """Context Manager which returns a multiprocessing.pool instance which
181 correctly deals with thrown exceptions.
182
183 *args - Arguments to multiprocessing.pool
184
185 Kwargs:
186 kind ('threads', 'procs') - The type of underlying coprocess to use.
187 **etc - Arguments to multiprocessing.pool
188 """
189 if kwargs.pop('kind', None) == 'threads':
190 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
191 else:
192 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
193 kwargs['initializer'] = _ScopedPool_initer
194 kwargs['initargs'] = orig, orig_args
195 pool = multiprocessing.pool.Pool(*args, **kwargs)
196
197 try:
198 yield pool
199 pool.close()
200 except:
201 pool.terminate()
202 raise
203 finally:
204 pool.join()
205
206
207class ProgressPrinter(object):
208 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000209 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000210 """Create a ProgressPrinter.
211
212 Use it as a context manager which produces a simple 'increment' method:
213
214 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
215 for i in xrange(1000):
216 # do stuff
217 if i % 10 == 0:
218 inc(10)
219
220 Args:
221 fmt - String format with a single '%(count)d' where the counter value
222 should go.
223 enabled (bool) - If this is None, will default to True if
224 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000225 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000226 period (float) - The time in seconds for the printer thread to wait
227 between printing.
228 """
229 self.fmt = fmt
230 if enabled is None: # pragma: no cover
231 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
232 else:
233 self.enabled = enabled
234
235 self._count = 0
236 self._dead = False
237 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000238 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000239 self._thread = threading.Thread(target=self._run)
240 self._period = period
241
242 def _emit(self, s):
243 if self.enabled:
244 self._stream.write('\r' + s)
245 self._stream.flush()
246
247 def _run(self):
248 with self._dead_cond:
249 while not self._dead:
250 self._emit(self.fmt % {'count': self._count})
251 self._dead_cond.wait(self._period)
252 self._emit((self.fmt + '\n') % {'count': self._count})
253
254 def inc(self, amount=1):
255 self._count += amount
256
257 def __enter__(self):
258 self._thread.start()
259 return self.inc
260
261 def __exit__(self, _exc_type, _exc_value, _traceback):
262 self._dead = True
263 with self._dead_cond:
264 self._dead_cond.notifyAll()
265 self._thread.join()
266 del self._thread
267
268
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000269def once(function):
270 """@Decorates |function| so that it only performs its action once, no matter
271 how many times the decorated |function| is called."""
272 def _inner_gen():
273 yield function()
274 while True:
275 yield
276 return _inner_gen().next
277
278
279## Git functions
280
281
282def branch_config(branch, option, default=None):
283 return config('branch.%s.%s' % (branch, option), default=default)
284
285
286def branch_config_map(option):
287 """Return {branch: <|option| value>} for all branches."""
288 try:
289 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
290 lines = run('config', '--get-regexp', reg.pattern).splitlines()
291 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
292 except subprocess2.CalledProcessError:
293 return {}
294
295
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000296def branches(*args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000297 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000298
299 key = 'depot-tools.branch-limit'
300 limit = 20
301 try:
302 limit = int(config(key, limit))
303 except ValueError:
304 pass
305
306 raw_branches = run('branch', *args).splitlines()
307
308 num = len(raw_branches)
309 if num > limit:
310 print >> sys.stderr, textwrap.dedent("""\
311 Your git repo has too many branches (%d/%d) for this tool to work well.
312
313 You may adjust this limit by running:
314 git config %s <new_limit>
315 """ % (num, limit, key))
316 sys.exit(1)
317
318 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000319 if line.startswith(NO_BRANCH):
320 continue
321 yield line.split()[-1]
322
323
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000324def config(option, default=None):
325 try:
326 return run('config', '--get', option) or default
327 except subprocess2.CalledProcessError:
328 return default
329
330
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000331def config_list(option):
332 try:
333 return run('config', '--get-all', option).split()
334 except subprocess2.CalledProcessError:
335 return []
336
337
338def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000339 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000340 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000341 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000342 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000343
344
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000345def del_branch_config(branch, option, scope='local'):
346 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000347
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000348
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000349def del_config(option, scope='local'):
350 try:
351 run('config', '--' + scope, '--unset', option)
352 except subprocess2.CalledProcessError:
353 pass
354
355
356def freeze():
357 took_action = False
358
359 try:
360 run('commit', '-m', FREEZE + '.indexed')
361 took_action = True
362 except subprocess2.CalledProcessError:
363 pass
364
365 try:
366 run('add', '-A')
367 run('commit', '-m', FREEZE + '.unindexed')
368 took_action = True
369 except subprocess2.CalledProcessError:
370 pass
371
372 if not took_action:
373 return 'Nothing to freeze.'
374
375
376def get_branch_tree():
377 """Get the dictionary of {branch: parent}, compatible with topo_iter.
378
379 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
380 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000381 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000382 skipped = set()
383 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000384
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000385 for branch in branches():
386 parent = upstream(branch)
387 if not parent:
388 skipped.add(branch)
389 continue
390 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000391
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000392 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000393
394
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000395def get_or_create_merge_base(branch, parent=None):
396 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000397
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000398 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000399 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000400 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000401 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000402 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000403 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000404 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000405 actual_merge_base = run('merge-base', parent, branch)
406
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000407 if base_upstream != parent:
408 base = None
409 base_upstream = None
410
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000411 def is_ancestor(a, b):
412 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
413
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000414 if base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000415 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000416 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
417 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000418 elif is_ancestor(base, actual_merge_base):
419 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
420 base = None
421 else:
422 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000423
424 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000425 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000426 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000427
428 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000429
430
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000431def hash_multi(*reflike):
432 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000433
434
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000435def hash_one(reflike, short=False):
436 args = ['rev-parse', reflike]
437 if short:
438 args.insert(1, '--short')
439 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000440
441
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000442def in_rebase():
443 git_dir = run('rev-parse', '--git-dir')
444 return (
445 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
446 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000447
448
449def intern_f(f, kind='blob'):
450 """Interns a file object into the git object store.
451
452 Args:
453 f (file-like object) - The file-like object to intern
454 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
455
456 Returns the git hash of the interned object (hex encoded).
457 """
458 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
459 f.close()
460 return ret
461
462
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000463def is_dormant(branch):
464 # TODO(iannucci): Do an oldness check?
465 return branch_config(branch, 'dormant', 'false') != 'false'
466
467
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000468def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000469 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000470 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000471
472
473def mktree(treedict):
474 """Makes a git tree object and returns its hash.
475
476 See |tree()| for the values of mode, type, and ref.
477
478 Args:
479 treedict - { name: (mode, type, ref) }
480 """
481 with tempfile.TemporaryFile() as f:
482 for name, (mode, typ, ref) in treedict.iteritems():
483 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
484 f.seek(0)
485 return run('mktree', '-z', stdin=f)
486
487
488def parse_commitrefs(*commitrefs):
489 """Returns binary encoded commit hashes for one or more commitrefs.
490
491 A commitref is anything which can resolve to a commit. Popular examples:
492 * 'HEAD'
493 * 'origin/master'
494 * 'cool_branch~2'
495 """
496 try:
497 return map(binascii.unhexlify, hash_multi(*commitrefs))
498 except subprocess2.CalledProcessError:
499 raise BadCommitRefException(commitrefs)
500
501
sbc@chromium.org384039b2014-10-13 21:01:00 +0000502RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000503
504
505def rebase(parent, start, branch, abort=False):
506 """Rebases |start|..|branch| onto the branch |parent|.
507
508 Args:
509 parent - The new parent ref for the rebased commits.
510 start - The commit to start from
511 branch - The branch to rebase
512 abort - If True, will call git-rebase --abort in the event that the rebase
513 doesn't complete successfully.
514
515 Returns a namedtuple with fields:
516 success - a boolean indicating that the rebase command completed
517 successfully.
518 message - if the rebase failed, this contains the stdout of the failed
519 rebase.
520 """
521 try:
522 args = ['--onto', parent, start, branch]
523 if TEST_MODE:
524 args.insert(0, '--committer-date-is-author-date')
525 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000526 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000527 except subprocess2.CalledProcessError as cpe:
528 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000529 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000530 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000531
532
533def remove_merge_base(branch):
534 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000535 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000536
537
538def root():
539 return config('depot-tools.upstream', 'origin/master')
540
541
542def run(*cmd, **kwargs):
543 """The same as run_with_stderr, except it only returns stdout."""
544 return run_with_stderr(*cmd, **kwargs)[0]
545
546
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000547def run_with_retcode(*cmd, **kwargs):
548 """Run a command but only return the status code."""
549 try:
550 run(*cmd, **kwargs)
551 return 0
552 except subprocess2.CalledProcessError as cpe:
553 return cpe.returncode
554
555
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000556def run_stream(*cmd, **kwargs):
557 """Runs a git command. Returns stdout as a PIPE (file-like object).
558
559 stderr is dropped to avoid races if the process outputs to both stdout and
560 stderr.
561 """
562 kwargs.setdefault('stderr', subprocess2.VOID)
563 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000564 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000565 proc = subprocess2.Popen(cmd, **kwargs)
566 return proc.stdout
567
568
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000569@contextlib.contextmanager
570def run_stream_with_retcode(*cmd, **kwargs):
571 """Runs a git command as context manager yielding stdout as a PIPE.
572
573 stderr is dropped to avoid races if the process outputs to both stdout and
574 stderr.
575
576 Raises subprocess2.CalledProcessError on nonzero return code.
577 """
578 kwargs.setdefault('stderr', subprocess2.VOID)
579 kwargs.setdefault('stdout', subprocess2.PIPE)
580 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
581 try:
582 proc = subprocess2.Popen(cmd, **kwargs)
583 yield proc.stdout
584 finally:
585 retcode = proc.wait()
586 if retcode != 0:
587 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
588 None, None)
589
590
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000591def run_with_stderr(*cmd, **kwargs):
592 """Runs a git command.
593
594 Returns (stdout, stderr) as a pair of strings.
595
596 kwargs
597 autostrip (bool) - Strip the output. Defaults to True.
598 indata (str) - Specifies stdin data for the process.
599 """
600 kwargs.setdefault('stdin', subprocess2.PIPE)
601 kwargs.setdefault('stdout', subprocess2.PIPE)
602 kwargs.setdefault('stderr', subprocess2.PIPE)
603 autostrip = kwargs.pop('autostrip', True)
604 indata = kwargs.pop('indata', None)
605
iannucci@chromium.org21980022014-04-11 04:51:49 +0000606 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000607 proc = subprocess2.Popen(cmd, **kwargs)
608 ret, err = proc.communicate(indata)
609 retcode = proc.wait()
610 if retcode != 0:
611 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
612
613 if autostrip:
614 ret = (ret or '').strip()
615 err = (err or '').strip()
616
617 return ret, err
618
619
620def set_branch_config(branch, option, value, scope='local'):
621 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
622
623
624def set_config(option, value, scope='local'):
625 run('config', '--' + scope, option, value)
626
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000627
sbc@chromium.org71437c02015-04-09 19:29:40 +0000628def get_dirty_files():
629 # Make sure index is up-to-date before running diff-index.
630 run_with_retcode('update-index', '--refresh', '-q')
631 return run('diff-index', '--name-status', 'HEAD')
632
633
634def is_dirty_git_tree(cmd):
635 dirty = get_dirty_files()
636 if dirty:
637 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
638 print 'Uncommitted files: (git diff-index --name-status HEAD)'
639 print dirty[:4096]
640 if len(dirty) > 4096: # pragma: no cover
641 print '... (run "git diff-index --name-status HEAD" to see full output).'
642 return True
643 return False
644
645
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000646def squash_current_branch(header=None, merge_base=None):
647 header = header or 'git squash commit.'
648 merge_base = merge_base or get_or_create_merge_base(current_branch())
649 log_msg = header + '\n'
650 if log_msg:
651 log_msg += '\n'
652 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
653 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000654
655 if not get_dirty_files():
656 # Sometimes the squash can result in the same tree, meaning that there is
657 # nothing to commit at this point.
658 print 'Nothing to commit; squashed branch is empty'
659 return False
maruel@chromium.org25b9ab22015-06-18 18:49:03 +0000660 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000661 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000662
663
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000664def tags(*args):
665 return run('tag', *args).splitlines()
666
667
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000668def thaw():
669 took_action = False
670 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
671 msg = run('show', '--format=%f%b', '-s', 'HEAD')
672 match = FREEZE_MATCHER.match(msg)
673 if not match:
674 if not took_action:
675 return 'Nothing to thaw.'
676 break
677
678 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
679 took_action = True
680
681
682def topo_iter(branch_tree, top_down=True):
683 """Generates (branch, parent) in topographical order for a branch tree.
684
685 Given a tree:
686
687 A1
688 B1 B2
689 C1 C2 C3
690 D1
691
692 branch_tree would look like: {
693 'D1': 'C3',
694 'C3': 'B2',
695 'B2': 'A1',
696 'C1': 'B1',
697 'C2': 'B1',
698 'B1': 'A1',
699 }
700
701 It is OK to have multiple 'root' nodes in your graph.
702
703 if top_down is True, items are yielded from A->D. Otherwise they're yielded
704 from D->A. Within a layer the branches will be yielded in sorted order.
705 """
706 branch_tree = branch_tree.copy()
707
708 # TODO(iannucci): There is probably a more efficient way to do these.
709 if top_down:
710 while branch_tree:
711 this_pass = [(b, p) for b, p in branch_tree.iteritems()
712 if p not in branch_tree]
713 assert this_pass, "Branch tree has cycles: %r" % branch_tree
714 for branch, parent in sorted(this_pass):
715 yield branch, parent
716 del branch_tree[branch]
717 else:
718 parent_to_branches = collections.defaultdict(set)
719 for branch, parent in branch_tree.iteritems():
720 parent_to_branches[parent].add(branch)
721
722 while branch_tree:
723 this_pass = [(b, p) for b, p in branch_tree.iteritems()
724 if not parent_to_branches[b]]
725 assert this_pass, "Branch tree has cycles: %r" % branch_tree
726 for branch, parent in sorted(this_pass):
727 yield branch, parent
728 parent_to_branches[parent].discard(branch)
729 del branch_tree[branch]
730
731
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000732def tree(treeref, recurse=False):
733 """Returns a dict representation of a git tree object.
734
735 Args:
736 treeref (str) - a git ref which resolves to a tree (commits count as trees).
737 recurse (bool) - include all of the tree's decendants too. File names will
738 take the form of 'some/path/to/file'.
739
740 Return format:
741 { 'file_name': (mode, type, ref) }
742
743 mode is an integer where:
744 * 0040000 - Directory
745 * 0100644 - Regular non-executable file
746 * 0100664 - Regular non-executable group-writeable file
747 * 0100755 - Regular executable file
748 * 0120000 - Symbolic link
749 * 0160000 - Gitlink
750
751 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
752
753 ref is the hex encoded hash of the entry.
754 """
755 ret = {}
756 opts = ['ls-tree', '--full-tree']
757 if recurse:
758 opts.append('-r')
759 opts.append(treeref)
760 try:
761 for line in run(*opts).splitlines():
762 mode, typ, ref, name = line.split(None, 3)
763 ret[name] = (mode, typ, ref)
764 except subprocess2.CalledProcessError:
765 return None
766 return ret
767
768
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000769def upstream(branch):
770 try:
771 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
772 branch+'@{upstream}')
773 except subprocess2.CalledProcessError:
774 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000775
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000776
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000777def get_git_version():
778 """Returns a tuple that contains the numeric components of the current git
779 version."""
780 version_string = run('--version')
781 version_match = re.search(r'(\d+.)+(\d+)', version_string)
782 version = version_match.group() if version_match else ''
783
784 return tuple(int(x) for x in version.split('.'))
785
786
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000787def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000788 format_string = (
789 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
790
791 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000792 if (include_tracking_status and
793 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000794 format_string += '%(upstream:track)'
795
796 info_map = {}
797 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000798 BranchesInfo = collections.namedtuple(
799 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000800 for line in data.splitlines():
801 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
802
803 ahead_match = re.search(r'ahead (\d+)', tracking_status)
804 ahead = int(ahead_match.group(1)) if ahead_match else None
805
806 behind_match = re.search(r'behind (\d+)', tracking_status)
807 behind = int(behind_match.group(1)) if behind_match else None
808
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000809 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000810 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
811
812 # Set None for upstreams which are not branches (e.g empty upstream, remotes
813 # and deleted upstream branches).
814 missing_upstreams = {}
815 for info in info_map.values():
816 if info.upstream not in info_map and info.upstream not in missing_upstreams:
817 missing_upstreams[info.upstream] = None
818
819 return dict(info_map.items() + missing_upstreams.items())