blob: 5a01c835a96b67929c55cbca573a1804fd648d11 [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
sammc@chromium.org900a33f2015-09-29 06:57:09 +000025import shutil
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000026import signal
27import sys
28import tempfile
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +000029import textwrap
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000030import threading
31
32import subprocess2
33
techtonik@gmail.coma5a945a2014-08-15 20:01:53 +000034ROOT = os.path.abspath(os.path.dirname(__file__))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000035
techtonik@gmail.coma5a945a2014-08-15 20:01:53 +000036GIT_EXE = ROOT+'\\git.bat' if sys.platform.startswith('win') else 'git'
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000037TEST_MODE = False
38
39FREEZE = 'FREEZE'
40FREEZE_SECTIONS = {
41 'indexed': 'soft',
42 'unindexed': 'mixed'
43}
44FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000045
46
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000047# Retry a git operation if git returns a error response with any of these
48# messages. It's all observed 'bad' GoB responses so far.
49#
50# This list is inspired/derived from the one in ChromiumOS's Chromite:
51# <CHROMITE>/lib/git.py::GIT_TRANSIENT_ERRORS
52#
53# It was last imported from '7add3ac29564d98ac35ce426bc295e743e7c0c02'.
54GIT_TRANSIENT_ERRORS = (
55 # crbug.com/285832
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000056 r'!.*\[remote rejected\].*\(error in hook\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000057
58 # crbug.com/289932
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000059 r'!.*\[remote rejected\].*\(failed to lock\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000060
61 # crbug.com/307156
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000062 r'!.*\[remote rejected\].*\(error in Gerrit backend\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000063
64 # crbug.com/285832
65 r'remote error: Internal Server Error',
66
67 # crbug.com/294449
68 r'fatal: Couldn\'t find remote ref ',
69
70 # crbug.com/220543
71 r'git fetch_pack: expected ACK/NAK, got',
72
73 # crbug.com/189455
74 r'protocol error: bad pack header',
75
76 # crbug.com/202807
77 r'The remote end hung up unexpectedly',
78
79 # crbug.com/298189
80 r'TLS packet with unexpected length was received',
81
82 # crbug.com/187444
83 r'RPC failed; result=\d+, HTTP code = \d+',
84
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000085 # crbug.com/388876
86 r'Connection timed out',
dnj@chromium.org45cddd62014-11-06 19:36:42 +000087
88 # crbug.com/430343
89 # TODO(dnj): Resync with Chromite.
90 r'The requested URL returned error: 5\d+',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000091)
92
93GIT_TRANSIENT_ERRORS_RE = re.compile('|'.join(GIT_TRANSIENT_ERRORS),
94 re.IGNORECASE)
95
raphael.kubo.da.costa@intel.com58d05b02015-06-24 08:54:41 +000096# git's for-each-ref command first supported the upstream:track token in its
97# format string in version 1.9.0, but some usages were broken until 2.3.0.
98# See git commit b6160d95 for more information.
99MIN_UPSTREAM_TRACK_GIT_VERSION = (2, 3)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000100
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000101class BadCommitRefException(Exception):
102 def __init__(self, refs):
103 msg = ('one of %s does not seem to be a valid commitref.' %
104 str(refs))
105 super(BadCommitRefException, self).__init__(msg)
106
107
108def memoize_one(**kwargs):
109 """Memoizes a single-argument pure function.
110
111 Values of None are not cached.
112
113 Kwargs:
114 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
115 cache manipulation functions. This is a kwarg so that users of memoize_one
116 are forced to explicitly and verbosely pick True or False.
117
118 Adds three methods to the decorated function:
119 * get(key, default=None) - Gets the value for this key from the cache.
120 * set(key, value) - Sets the value for this key from the cache.
121 * clear() - Drops the entire contents of the cache. Useful for unittests.
122 * update(other) - Updates the contents of the cache from another dict.
123 """
124 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
125 threadsafe = kwargs['threadsafe']
126
127 if threadsafe:
128 def withlock(lock, f):
129 def inner(*args, **kwargs):
130 with lock:
131 return f(*args, **kwargs)
132 return inner
133 else:
134 def withlock(_lock, f):
135 return f
136
137 def decorator(f):
138 # Instantiate the lock in decorator, in case users of memoize_one do:
139 #
140 # memoizer = memoize_one(threadsafe=True)
141 #
142 # @memoizer
143 # def fn1(val): ...
144 #
145 # @memoizer
146 # def fn2(val): ...
147
148 lock = threading.Lock() if threadsafe else None
149 cache = {}
150 _get = withlock(lock, cache.get)
151 _set = withlock(lock, cache.__setitem__)
152
153 @functools.wraps(f)
154 def inner(arg):
155 ret = _get(arg)
156 if ret is None:
157 ret = f(arg)
158 if ret is not None:
159 _set(arg, ret)
160 return ret
161 inner.get = _get
162 inner.set = _set
163 inner.clear = withlock(lock, cache.clear)
164 inner.update = withlock(lock, cache.update)
165 return inner
166 return decorator
167
168
169def _ScopedPool_initer(orig, orig_args): # pragma: no cover
170 """Initializer method for ScopedPool's subprocesses.
171
172 This helps ScopedPool handle Ctrl-C's correctly.
173 """
174 signal.signal(signal.SIGINT, signal.SIG_IGN)
175 if orig:
176 orig(*orig_args)
177
178
179@contextlib.contextmanager
180def ScopedPool(*args, **kwargs):
181 """Context Manager which returns a multiprocessing.pool instance which
182 correctly deals with thrown exceptions.
183
184 *args - Arguments to multiprocessing.pool
185
186 Kwargs:
187 kind ('threads', 'procs') - The type of underlying coprocess to use.
188 **etc - Arguments to multiprocessing.pool
189 """
190 if kwargs.pop('kind', None) == 'threads':
191 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
192 else:
193 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
194 kwargs['initializer'] = _ScopedPool_initer
195 kwargs['initargs'] = orig, orig_args
196 pool = multiprocessing.pool.Pool(*args, **kwargs)
197
198 try:
199 yield pool
200 pool.close()
201 except:
202 pool.terminate()
203 raise
204 finally:
205 pool.join()
206
207
208class ProgressPrinter(object):
209 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000210 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000211 """Create a ProgressPrinter.
212
213 Use it as a context manager which produces a simple 'increment' method:
214
215 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
216 for i in xrange(1000):
217 # do stuff
218 if i % 10 == 0:
219 inc(10)
220
221 Args:
222 fmt - String format with a single '%(count)d' where the counter value
223 should go.
224 enabled (bool) - If this is None, will default to True if
225 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000226 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000227 period (float) - The time in seconds for the printer thread to wait
228 between printing.
229 """
230 self.fmt = fmt
231 if enabled is None: # pragma: no cover
232 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
233 else:
234 self.enabled = enabled
235
236 self._count = 0
237 self._dead = False
238 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000239 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000240 self._thread = threading.Thread(target=self._run)
241 self._period = period
242
243 def _emit(self, s):
244 if self.enabled:
245 self._stream.write('\r' + s)
246 self._stream.flush()
247
248 def _run(self):
249 with self._dead_cond:
250 while not self._dead:
251 self._emit(self.fmt % {'count': self._count})
252 self._dead_cond.wait(self._period)
253 self._emit((self.fmt + '\n') % {'count': self._count})
254
255 def inc(self, amount=1):
256 self._count += amount
257
258 def __enter__(self):
259 self._thread.start()
260 return self.inc
261
262 def __exit__(self, _exc_type, _exc_value, _traceback):
263 self._dead = True
264 with self._dead_cond:
265 self._dead_cond.notifyAll()
266 self._thread.join()
267 del self._thread
268
269
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000270def once(function):
271 """@Decorates |function| so that it only performs its action once, no matter
272 how many times the decorated |function| is called."""
273 def _inner_gen():
274 yield function()
275 while True:
276 yield
277 return _inner_gen().next
278
279
280## Git functions
281
282
283def branch_config(branch, option, default=None):
284 return config('branch.%s.%s' % (branch, option), default=default)
285
286
287def branch_config_map(option):
288 """Return {branch: <|option| value>} for all branches."""
289 try:
290 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
291 lines = run('config', '--get-regexp', reg.pattern).splitlines()
292 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
293 except subprocess2.CalledProcessError:
294 return {}
295
296
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000297def branches(*args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000298 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000299
300 key = 'depot-tools.branch-limit'
301 limit = 20
302 try:
303 limit = int(config(key, limit))
304 except ValueError:
305 pass
306
307 raw_branches = run('branch', *args).splitlines()
308
309 num = len(raw_branches)
310 if num > limit:
311 print >> sys.stderr, textwrap.dedent("""\
312 Your git repo has too many branches (%d/%d) for this tool to work well.
313
314 You may adjust this limit by running:
315 git config %s <new_limit>
316 """ % (num, limit, key))
317 sys.exit(1)
318
319 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000320 if line.startswith(NO_BRANCH):
321 continue
322 yield line.split()[-1]
323
324
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000325def config(option, default=None):
326 try:
327 return run('config', '--get', option) or default
328 except subprocess2.CalledProcessError:
329 return default
330
331
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000332def config_list(option):
333 try:
334 return run('config', '--get-all', option).split()
335 except subprocess2.CalledProcessError:
336 return []
337
338
339def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000340 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000341 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000342 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000343 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000344
345
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000346def del_branch_config(branch, option, scope='local'):
347 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000348
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000349
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000350def del_config(option, scope='local'):
351 try:
352 run('config', '--' + scope, '--unset', option)
353 except subprocess2.CalledProcessError:
354 pass
355
356
357def freeze():
358 took_action = False
359
360 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000361 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000362 took_action = True
363 except subprocess2.CalledProcessError:
364 pass
365
366 try:
367 run('add', '-A')
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000368 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000369 took_action = True
370 except subprocess2.CalledProcessError:
371 pass
372
373 if not took_action:
374 return 'Nothing to freeze.'
375
376
377def get_branch_tree():
378 """Get the dictionary of {branch: parent}, compatible with topo_iter.
379
380 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
381 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000382 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000383 skipped = set()
384 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000385
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000386 for branch in branches():
387 parent = upstream(branch)
388 if not parent:
389 skipped.add(branch)
390 continue
391 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000392
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000393 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000394
395
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000396def get_or_create_merge_base(branch, parent=None):
397 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000398
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000399 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000400 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000401 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000402 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000403 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000404 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000405 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000406 actual_merge_base = run('merge-base', parent, branch)
407
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000408 if base_upstream != parent:
409 base = None
410 base_upstream = None
411
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000412 def is_ancestor(a, b):
413 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
414
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000415 if base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000416 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000417 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
418 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000419 elif is_ancestor(base, actual_merge_base):
420 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
421 base = None
422 else:
423 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000424
425 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000426 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000427 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000428
429 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000430
431
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000432def hash_multi(*reflike):
433 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000434
435
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000436def hash_one(reflike, short=False):
437 args = ['rev-parse', reflike]
438 if short:
439 args.insert(1, '--short')
440 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000441
442
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000443def in_rebase():
444 git_dir = run('rev-parse', '--git-dir')
445 return (
446 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
447 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000448
449
450def intern_f(f, kind='blob'):
451 """Interns a file object into the git object store.
452
453 Args:
454 f (file-like object) - The file-like object to intern
455 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
456
457 Returns the git hash of the interned object (hex encoded).
458 """
459 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
460 f.close()
461 return ret
462
463
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000464def is_dormant(branch):
465 # TODO(iannucci): Do an oldness check?
466 return branch_config(branch, 'dormant', 'false') != 'false'
467
468
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000469def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000470 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000471 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000472
473
474def mktree(treedict):
475 """Makes a git tree object and returns its hash.
476
477 See |tree()| for the values of mode, type, and ref.
478
479 Args:
480 treedict - { name: (mode, type, ref) }
481 """
482 with tempfile.TemporaryFile() as f:
483 for name, (mode, typ, ref) in treedict.iteritems():
484 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
485 f.seek(0)
486 return run('mktree', '-z', stdin=f)
487
488
489def parse_commitrefs(*commitrefs):
490 """Returns binary encoded commit hashes for one or more commitrefs.
491
492 A commitref is anything which can resolve to a commit. Popular examples:
493 * 'HEAD'
494 * 'origin/master'
495 * 'cool_branch~2'
496 """
497 try:
498 return map(binascii.unhexlify, hash_multi(*commitrefs))
499 except subprocess2.CalledProcessError:
500 raise BadCommitRefException(commitrefs)
501
502
sbc@chromium.org384039b2014-10-13 21:01:00 +0000503RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000504
505
506def rebase(parent, start, branch, abort=False):
507 """Rebases |start|..|branch| onto the branch |parent|.
508
509 Args:
510 parent - The new parent ref for the rebased commits.
511 start - The commit to start from
512 branch - The branch to rebase
513 abort - If True, will call git-rebase --abort in the event that the rebase
514 doesn't complete successfully.
515
516 Returns a namedtuple with fields:
517 success - a boolean indicating that the rebase command completed
518 successfully.
519 message - if the rebase failed, this contains the stdout of the failed
520 rebase.
521 """
522 try:
523 args = ['--onto', parent, start, branch]
524 if TEST_MODE:
525 args.insert(0, '--committer-date-is-author-date')
526 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000527 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000528 except subprocess2.CalledProcessError as cpe:
529 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000530 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000531 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000532
533
534def remove_merge_base(branch):
535 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000536 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000537
538
539def root():
540 return config('depot-tools.upstream', 'origin/master')
541
542
543def run(*cmd, **kwargs):
544 """The same as run_with_stderr, except it only returns stdout."""
545 return run_with_stderr(*cmd, **kwargs)[0]
546
547
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000548def run_with_retcode(*cmd, **kwargs):
549 """Run a command but only return the status code."""
550 try:
551 run(*cmd, **kwargs)
552 return 0
553 except subprocess2.CalledProcessError as cpe:
554 return cpe.returncode
555
556
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000557def run_stream(*cmd, **kwargs):
558 """Runs a git command. Returns stdout as a PIPE (file-like object).
559
560 stderr is dropped to avoid races if the process outputs to both stdout and
561 stderr.
562 """
563 kwargs.setdefault('stderr', subprocess2.VOID)
564 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000565 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000566 proc = subprocess2.Popen(cmd, **kwargs)
567 return proc.stdout
568
569
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000570@contextlib.contextmanager
571def run_stream_with_retcode(*cmd, **kwargs):
572 """Runs a git command as context manager yielding stdout as a PIPE.
573
574 stderr is dropped to avoid races if the process outputs to both stdout and
575 stderr.
576
577 Raises subprocess2.CalledProcessError on nonzero return code.
578 """
579 kwargs.setdefault('stderr', subprocess2.VOID)
580 kwargs.setdefault('stdout', subprocess2.PIPE)
581 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
582 try:
583 proc = subprocess2.Popen(cmd, **kwargs)
584 yield proc.stdout
585 finally:
586 retcode = proc.wait()
587 if retcode != 0:
588 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
589 None, None)
590
591
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000592def run_with_stderr(*cmd, **kwargs):
593 """Runs a git command.
594
595 Returns (stdout, stderr) as a pair of strings.
596
597 kwargs
598 autostrip (bool) - Strip the output. Defaults to True.
599 indata (str) - Specifies stdin data for the process.
600 """
601 kwargs.setdefault('stdin', subprocess2.PIPE)
602 kwargs.setdefault('stdout', subprocess2.PIPE)
603 kwargs.setdefault('stderr', subprocess2.PIPE)
604 autostrip = kwargs.pop('autostrip', True)
605 indata = kwargs.pop('indata', None)
606
iannucci@chromium.org21980022014-04-11 04:51:49 +0000607 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000608 proc = subprocess2.Popen(cmd, **kwargs)
609 ret, err = proc.communicate(indata)
610 retcode = proc.wait()
611 if retcode != 0:
612 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
613
614 if autostrip:
615 ret = (ret or '').strip()
616 err = (err or '').strip()
617
618 return ret, err
619
620
621def set_branch_config(branch, option, value, scope='local'):
622 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
623
624
625def set_config(option, value, scope='local'):
626 run('config', '--' + scope, option, value)
627
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000628
sbc@chromium.org71437c02015-04-09 19:29:40 +0000629def get_dirty_files():
630 # Make sure index is up-to-date before running diff-index.
631 run_with_retcode('update-index', '--refresh', '-q')
632 return run('diff-index', '--name-status', 'HEAD')
633
634
635def is_dirty_git_tree(cmd):
636 dirty = get_dirty_files()
637 if dirty:
638 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
639 print 'Uncommitted files: (git diff-index --name-status HEAD)'
640 print dirty[:4096]
641 if len(dirty) > 4096: # pragma: no cover
642 print '... (run "git diff-index --name-status HEAD" to see full output).'
643 return True
644 return False
645
646
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000647def squash_current_branch(header=None, merge_base=None):
648 header = header or 'git squash commit.'
649 merge_base = merge_base or get_or_create_merge_base(current_branch())
650 log_msg = header + '\n'
651 if log_msg:
652 log_msg += '\n'
653 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
654 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000655
656 if not get_dirty_files():
657 # Sometimes the squash can result in the same tree, meaning that there is
658 # nothing to commit at this point.
659 print 'Nothing to commit; squashed branch is empty'
660 return False
maruel@chromium.org25b9ab22015-06-18 18:49:03 +0000661 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000662 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000663
664
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000665def tags(*args):
666 return run('tag', *args).splitlines()
667
668
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000669def thaw():
670 took_action = False
671 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
672 msg = run('show', '--format=%f%b', '-s', 'HEAD')
673 match = FREEZE_MATCHER.match(msg)
674 if not match:
675 if not took_action:
676 return 'Nothing to thaw.'
677 break
678
679 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
680 took_action = True
681
682
683def topo_iter(branch_tree, top_down=True):
684 """Generates (branch, parent) in topographical order for a branch tree.
685
686 Given a tree:
687
688 A1
689 B1 B2
690 C1 C2 C3
691 D1
692
693 branch_tree would look like: {
694 'D1': 'C3',
695 'C3': 'B2',
696 'B2': 'A1',
697 'C1': 'B1',
698 'C2': 'B1',
699 'B1': 'A1',
700 }
701
702 It is OK to have multiple 'root' nodes in your graph.
703
704 if top_down is True, items are yielded from A->D. Otherwise they're yielded
705 from D->A. Within a layer the branches will be yielded in sorted order.
706 """
707 branch_tree = branch_tree.copy()
708
709 # TODO(iannucci): There is probably a more efficient way to do these.
710 if top_down:
711 while branch_tree:
712 this_pass = [(b, p) for b, p in branch_tree.iteritems()
713 if p not in branch_tree]
714 assert this_pass, "Branch tree has cycles: %r" % branch_tree
715 for branch, parent in sorted(this_pass):
716 yield branch, parent
717 del branch_tree[branch]
718 else:
719 parent_to_branches = collections.defaultdict(set)
720 for branch, parent in branch_tree.iteritems():
721 parent_to_branches[parent].add(branch)
722
723 while branch_tree:
724 this_pass = [(b, p) for b, p in branch_tree.iteritems()
725 if not parent_to_branches[b]]
726 assert this_pass, "Branch tree has cycles: %r" % branch_tree
727 for branch, parent in sorted(this_pass):
728 yield branch, parent
729 parent_to_branches[parent].discard(branch)
730 del branch_tree[branch]
731
732
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000733def tree(treeref, recurse=False):
734 """Returns a dict representation of a git tree object.
735
736 Args:
737 treeref (str) - a git ref which resolves to a tree (commits count as trees).
738 recurse (bool) - include all of the tree's decendants too. File names will
739 take the form of 'some/path/to/file'.
740
741 Return format:
742 { 'file_name': (mode, type, ref) }
743
744 mode is an integer where:
745 * 0040000 - Directory
746 * 0100644 - Regular non-executable file
747 * 0100664 - Regular non-executable group-writeable file
748 * 0100755 - Regular executable file
749 * 0120000 - Symbolic link
750 * 0160000 - Gitlink
751
752 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
753
754 ref is the hex encoded hash of the entry.
755 """
756 ret = {}
757 opts = ['ls-tree', '--full-tree']
758 if recurse:
759 opts.append('-r')
760 opts.append(treeref)
761 try:
762 for line in run(*opts).splitlines():
763 mode, typ, ref, name = line.split(None, 3)
764 ret[name] = (mode, typ, ref)
765 except subprocess2.CalledProcessError:
766 return None
767 return ret
768
769
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000770def upstream(branch):
771 try:
772 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
773 branch+'@{upstream}')
774 except subprocess2.CalledProcessError:
775 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000776
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000777
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000778def get_git_version():
779 """Returns a tuple that contains the numeric components of the current git
780 version."""
781 version_string = run('--version')
782 version_match = re.search(r'(\d+.)+(\d+)', version_string)
783 version = version_match.group() if version_match else ''
784
785 return tuple(int(x) for x in version.split('.'))
786
787
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000788def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000789 format_string = (
790 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
791
792 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000793 if (include_tracking_status and
794 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000795 format_string += '%(upstream:track)'
796
797 info_map = {}
798 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000799 BranchesInfo = collections.namedtuple(
800 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000801 for line in data.splitlines():
802 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
803
804 ahead_match = re.search(r'ahead (\d+)', tracking_status)
805 ahead = int(ahead_match.group(1)) if ahead_match else None
806
807 behind_match = re.search(r'behind (\d+)', tracking_status)
808 behind = int(behind_match.group(1)) if behind_match else None
809
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000810 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000811 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
812
813 # Set None for upstreams which are not branches (e.g empty upstream, remotes
814 # and deleted upstream branches).
815 missing_upstreams = {}
816 for info in info_map.values():
817 if info.upstream not in info_map and info.upstream not in missing_upstreams:
818 missing_upstreams[info.upstream] = None
819
820 return dict(info_map.items() + missing_upstreams.items())
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000821
822
823def make_workdir_common(repository, new_workdir, files_to_symlink,
824 files_to_copy):
825 os.makedirs(new_workdir)
826 for entry in files_to_symlink:
827 clone_file(repository, new_workdir, entry, os.symlink)
828 for entry in files_to_copy:
829 clone_file(repository, new_workdir, entry, shutil.copy)
830
831
832def make_workdir(repository, new_workdir):
833 GIT_DIRECTORY_WHITELIST = [
834 'config',
835 'info',
836 'hooks',
837 'logs/refs',
838 'objects',
839 'packed-refs',
840 'refs',
841 'remotes',
842 'rr-cache',
843 'svn'
844 ]
845 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
846 ['HEAD'])
847
848
849def clone_file(repository, new_workdir, link, operation):
850 if not os.path.exists(os.path.join(repository, link)):
851 return
852 link_dir = os.path.dirname(os.path.join(new_workdir, link))
853 if not os.path.exists(link_dir):
854 os.makedirs(link_dir)
855 operation(os.path.join(repository, link), os.path.join(new_workdir, link))