blob: 506365257ecd63556258f4656350cd3240142f2d [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
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +000036IS_WIN = sys.platform == 'win32'
37GIT_EXE = ROOT+'\\git.bat' if IS_WIN else 'git'
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000038TEST_MODE = False
39
40FREEZE = 'FREEZE'
41FREEZE_SECTIONS = {
42 'indexed': 'soft',
43 'unindexed': 'mixed'
44}
45FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000046
47
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000048# Retry a git operation if git returns a error response with any of these
49# messages. It's all observed 'bad' GoB responses so far.
50#
51# This list is inspired/derived from the one in ChromiumOS's Chromite:
52# <CHROMITE>/lib/git.py::GIT_TRANSIENT_ERRORS
53#
54# It was last imported from '7add3ac29564d98ac35ce426bc295e743e7c0c02'.
55GIT_TRANSIENT_ERRORS = (
56 # crbug.com/285832
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000057 r'!.*\[remote rejected\].*\(error in hook\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000058
59 # crbug.com/289932
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000060 r'!.*\[remote rejected\].*\(failed to lock\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000061
62 # crbug.com/307156
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000063 r'!.*\[remote rejected\].*\(error in Gerrit backend\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000064
65 # crbug.com/285832
66 r'remote error: Internal Server Error',
67
68 # crbug.com/294449
69 r'fatal: Couldn\'t find remote ref ',
70
71 # crbug.com/220543
72 r'git fetch_pack: expected ACK/NAK, got',
73
74 # crbug.com/189455
75 r'protocol error: bad pack header',
76
77 # crbug.com/202807
78 r'The remote end hung up unexpectedly',
79
80 # crbug.com/298189
81 r'TLS packet with unexpected length was received',
82
83 # crbug.com/187444
84 r'RPC failed; result=\d+, HTTP code = \d+',
85
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000086 # crbug.com/388876
87 r'Connection timed out',
dnj@chromium.org45cddd62014-11-06 19:36:42 +000088
89 # crbug.com/430343
90 # TODO(dnj): Resync with Chromite.
91 r'The requested URL returned error: 5\d+',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000092)
93
94GIT_TRANSIENT_ERRORS_RE = re.compile('|'.join(GIT_TRANSIENT_ERRORS),
95 re.IGNORECASE)
96
raphael.kubo.da.costa@intel.com58d05b02015-06-24 08:54:41 +000097# git's for-each-ref command first supported the upstream:track token in its
98# format string in version 1.9.0, but some usages were broken until 2.3.0.
99# See git commit b6160d95 for more information.
100MIN_UPSTREAM_TRACK_GIT_VERSION = (2, 3)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000101
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000102class BadCommitRefException(Exception):
103 def __init__(self, refs):
104 msg = ('one of %s does not seem to be a valid commitref.' %
105 str(refs))
106 super(BadCommitRefException, self).__init__(msg)
107
108
109def memoize_one(**kwargs):
110 """Memoizes a single-argument pure function.
111
112 Values of None are not cached.
113
114 Kwargs:
115 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
116 cache manipulation functions. This is a kwarg so that users of memoize_one
117 are forced to explicitly and verbosely pick True or False.
118
119 Adds three methods to the decorated function:
120 * get(key, default=None) - Gets the value for this key from the cache.
121 * set(key, value) - Sets the value for this key from the cache.
122 * clear() - Drops the entire contents of the cache. Useful for unittests.
123 * update(other) - Updates the contents of the cache from another dict.
124 """
125 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
126 threadsafe = kwargs['threadsafe']
127
128 if threadsafe:
129 def withlock(lock, f):
130 def inner(*args, **kwargs):
131 with lock:
132 return f(*args, **kwargs)
133 return inner
134 else:
135 def withlock(_lock, f):
136 return f
137
138 def decorator(f):
139 # Instantiate the lock in decorator, in case users of memoize_one do:
140 #
141 # memoizer = memoize_one(threadsafe=True)
142 #
143 # @memoizer
144 # def fn1(val): ...
145 #
146 # @memoizer
147 # def fn2(val): ...
148
149 lock = threading.Lock() if threadsafe else None
150 cache = {}
151 _get = withlock(lock, cache.get)
152 _set = withlock(lock, cache.__setitem__)
153
154 @functools.wraps(f)
155 def inner(arg):
156 ret = _get(arg)
157 if ret is None:
158 ret = f(arg)
159 if ret is not None:
160 _set(arg, ret)
161 return ret
162 inner.get = _get
163 inner.set = _set
164 inner.clear = withlock(lock, cache.clear)
165 inner.update = withlock(lock, cache.update)
166 return inner
167 return decorator
168
169
170def _ScopedPool_initer(orig, orig_args): # pragma: no cover
171 """Initializer method for ScopedPool's subprocesses.
172
173 This helps ScopedPool handle Ctrl-C's correctly.
174 """
175 signal.signal(signal.SIGINT, signal.SIG_IGN)
176 if orig:
177 orig(*orig_args)
178
179
180@contextlib.contextmanager
181def ScopedPool(*args, **kwargs):
182 """Context Manager which returns a multiprocessing.pool instance which
183 correctly deals with thrown exceptions.
184
185 *args - Arguments to multiprocessing.pool
186
187 Kwargs:
188 kind ('threads', 'procs') - The type of underlying coprocess to use.
189 **etc - Arguments to multiprocessing.pool
190 """
191 if kwargs.pop('kind', None) == 'threads':
192 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
193 else:
194 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
195 kwargs['initializer'] = _ScopedPool_initer
196 kwargs['initargs'] = orig, orig_args
197 pool = multiprocessing.pool.Pool(*args, **kwargs)
198
199 try:
200 yield pool
201 pool.close()
202 except:
203 pool.terminate()
204 raise
205 finally:
206 pool.join()
207
208
209class ProgressPrinter(object):
210 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000211 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000212 """Create a ProgressPrinter.
213
214 Use it as a context manager which produces a simple 'increment' method:
215
216 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
217 for i in xrange(1000):
218 # do stuff
219 if i % 10 == 0:
220 inc(10)
221
222 Args:
223 fmt - String format with a single '%(count)d' where the counter value
224 should go.
225 enabled (bool) - If this is None, will default to True if
226 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000227 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000228 period (float) - The time in seconds for the printer thread to wait
229 between printing.
230 """
231 self.fmt = fmt
232 if enabled is None: # pragma: no cover
233 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
234 else:
235 self.enabled = enabled
236
237 self._count = 0
238 self._dead = False
239 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000240 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000241 self._thread = threading.Thread(target=self._run)
242 self._period = period
243
244 def _emit(self, s):
245 if self.enabled:
246 self._stream.write('\r' + s)
247 self._stream.flush()
248
249 def _run(self):
250 with self._dead_cond:
251 while not self._dead:
252 self._emit(self.fmt % {'count': self._count})
253 self._dead_cond.wait(self._period)
254 self._emit((self.fmt + '\n') % {'count': self._count})
255
256 def inc(self, amount=1):
257 self._count += amount
258
259 def __enter__(self):
260 self._thread.start()
261 return self.inc
262
263 def __exit__(self, _exc_type, _exc_value, _traceback):
264 self._dead = True
265 with self._dead_cond:
266 self._dead_cond.notifyAll()
267 self._thread.join()
268 del self._thread
269
270
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000271def once(function):
272 """@Decorates |function| so that it only performs its action once, no matter
273 how many times the decorated |function| is called."""
274 def _inner_gen():
275 yield function()
276 while True:
277 yield
278 return _inner_gen().next
279
280
281## Git functions
282
283
284def branch_config(branch, option, default=None):
285 return config('branch.%s.%s' % (branch, option), default=default)
286
287
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000288def config_regexp(pattern):
289 if IS_WIN: # pragma: no cover
290 # this madness is because we call git.bat which calls git.exe which calls
291 # bash.exe (or something to that effect). Each layer divides the number of
292 # ^'s by 2.
293 pattern = pattern.replace('^', '^' * 8)
294 return run('config', '--get-regexp', pattern).splitlines()
295
296
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000297def branch_config_map(option):
298 """Return {branch: <|option| value>} for all branches."""
299 try:
300 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000301 lines = config_regexp(reg.pattern)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000302 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
303 except subprocess2.CalledProcessError:
304 return {}
305
306
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000307def branches(*args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000308 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000309
310 key = 'depot-tools.branch-limit'
311 limit = 20
312 try:
313 limit = int(config(key, limit))
314 except ValueError:
315 pass
316
317 raw_branches = run('branch', *args).splitlines()
318
319 num = len(raw_branches)
320 if num > limit:
321 print >> sys.stderr, textwrap.dedent("""\
322 Your git repo has too many branches (%d/%d) for this tool to work well.
323
324 You may adjust this limit by running:
325 git config %s <new_limit>
326 """ % (num, limit, key))
327 sys.exit(1)
328
329 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000330 if line.startswith(NO_BRANCH):
331 continue
332 yield line.split()[-1]
333
334
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000335def config(option, default=None):
336 try:
337 return run('config', '--get', option) or default
338 except subprocess2.CalledProcessError:
339 return default
340
341
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000342def config_list(option):
343 try:
344 return run('config', '--get-all', option).split()
345 except subprocess2.CalledProcessError:
346 return []
347
348
349def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000350 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000351 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000352 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000353 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000354
355
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000356def del_branch_config(branch, option, scope='local'):
357 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000358
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000359
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000360def del_config(option, scope='local'):
361 try:
362 run('config', '--' + scope, '--unset', option)
363 except subprocess2.CalledProcessError:
364 pass
365
366
367def freeze():
368 took_action = False
369
370 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000371 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000372 took_action = True
373 except subprocess2.CalledProcessError:
374 pass
375
376 try:
377 run('add', '-A')
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000378 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000379 took_action = True
380 except subprocess2.CalledProcessError:
381 pass
382
383 if not took_action:
384 return 'Nothing to freeze.'
385
386
387def get_branch_tree():
388 """Get the dictionary of {branch: parent}, compatible with topo_iter.
389
390 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
391 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000392 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000393 skipped = set()
394 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000395
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000396 for branch in branches():
397 parent = upstream(branch)
398 if not parent:
399 skipped.add(branch)
400 continue
401 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000402
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000403 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000404
405
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000406def get_or_create_merge_base(branch, parent=None):
407 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000408
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000409 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000410 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000411 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000412 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000413 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000414 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000415 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000416 actual_merge_base = run('merge-base', parent, branch)
417
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000418 if base_upstream != parent:
419 base = None
420 base_upstream = None
421
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000422 def is_ancestor(a, b):
423 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
424
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000425 if base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000426 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000427 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
428 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000429 elif is_ancestor(base, actual_merge_base):
430 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
431 base = None
432 else:
433 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000434
435 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000436 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000437 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000438
439 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000440
441
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000442def hash_multi(*reflike):
443 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000444
445
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000446def hash_one(reflike, short=False):
447 args = ['rev-parse', reflike]
448 if short:
449 args.insert(1, '--short')
450 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000451
452
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000453def in_rebase():
454 git_dir = run('rev-parse', '--git-dir')
455 return (
456 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
457 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000458
459
460def intern_f(f, kind='blob'):
461 """Interns a file object into the git object store.
462
463 Args:
464 f (file-like object) - The file-like object to intern
465 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
466
467 Returns the git hash of the interned object (hex encoded).
468 """
469 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
470 f.close()
471 return ret
472
473
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000474def is_dormant(branch):
475 # TODO(iannucci): Do an oldness check?
476 return branch_config(branch, 'dormant', 'false') != 'false'
477
478
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000479def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000480 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000481 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000482
483
484def mktree(treedict):
485 """Makes a git tree object and returns its hash.
486
487 See |tree()| for the values of mode, type, and ref.
488
489 Args:
490 treedict - { name: (mode, type, ref) }
491 """
492 with tempfile.TemporaryFile() as f:
493 for name, (mode, typ, ref) in treedict.iteritems():
494 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
495 f.seek(0)
496 return run('mktree', '-z', stdin=f)
497
498
499def parse_commitrefs(*commitrefs):
500 """Returns binary encoded commit hashes for one or more commitrefs.
501
502 A commitref is anything which can resolve to a commit. Popular examples:
503 * 'HEAD'
504 * 'origin/master'
505 * 'cool_branch~2'
506 """
507 try:
508 return map(binascii.unhexlify, hash_multi(*commitrefs))
509 except subprocess2.CalledProcessError:
510 raise BadCommitRefException(commitrefs)
511
512
sbc@chromium.org384039b2014-10-13 21:01:00 +0000513RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000514
515
516def rebase(parent, start, branch, abort=False):
517 """Rebases |start|..|branch| onto the branch |parent|.
518
519 Args:
520 parent - The new parent ref for the rebased commits.
521 start - The commit to start from
522 branch - The branch to rebase
523 abort - If True, will call git-rebase --abort in the event that the rebase
524 doesn't complete successfully.
525
526 Returns a namedtuple with fields:
527 success - a boolean indicating that the rebase command completed
528 successfully.
529 message - if the rebase failed, this contains the stdout of the failed
530 rebase.
531 """
532 try:
533 args = ['--onto', parent, start, branch]
534 if TEST_MODE:
535 args.insert(0, '--committer-date-is-author-date')
536 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000537 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000538 except subprocess2.CalledProcessError as cpe:
539 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000540 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000541 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000542
543
544def remove_merge_base(branch):
545 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000546 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000547
548
549def root():
550 return config('depot-tools.upstream', 'origin/master')
551
552
553def run(*cmd, **kwargs):
554 """The same as run_with_stderr, except it only returns stdout."""
555 return run_with_stderr(*cmd, **kwargs)[0]
556
557
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000558def run_with_retcode(*cmd, **kwargs):
559 """Run a command but only return the status code."""
560 try:
561 run(*cmd, **kwargs)
562 return 0
563 except subprocess2.CalledProcessError as cpe:
564 return cpe.returncode
565
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000566def run_stream(*cmd, **kwargs):
567 """Runs a git command. Returns stdout as a PIPE (file-like object).
568
569 stderr is dropped to avoid races if the process outputs to both stdout and
570 stderr.
571 """
572 kwargs.setdefault('stderr', subprocess2.VOID)
573 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000574 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000575 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000576 proc = subprocess2.Popen(cmd, **kwargs)
577 return proc.stdout
578
579
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000580@contextlib.contextmanager
581def run_stream_with_retcode(*cmd, **kwargs):
582 """Runs a git command as context manager yielding stdout as a PIPE.
583
584 stderr is dropped to avoid races if the process outputs to both stdout and
585 stderr.
586
587 Raises subprocess2.CalledProcessError on nonzero return code.
588 """
589 kwargs.setdefault('stderr', subprocess2.VOID)
590 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000591 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000592 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
593 try:
594 proc = subprocess2.Popen(cmd, **kwargs)
595 yield proc.stdout
596 finally:
597 retcode = proc.wait()
598 if retcode != 0:
599 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
600 None, None)
601
602
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000603def run_with_stderr(*cmd, **kwargs):
604 """Runs a git command.
605
606 Returns (stdout, stderr) as a pair of strings.
607
608 kwargs
609 autostrip (bool) - Strip the output. Defaults to True.
610 indata (str) - Specifies stdin data for the process.
611 """
612 kwargs.setdefault('stdin', subprocess2.PIPE)
613 kwargs.setdefault('stdout', subprocess2.PIPE)
614 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000615 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000616 autostrip = kwargs.pop('autostrip', True)
617 indata = kwargs.pop('indata', None)
618
iannucci@chromium.org21980022014-04-11 04:51:49 +0000619 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000620 proc = subprocess2.Popen(cmd, **kwargs)
621 ret, err = proc.communicate(indata)
622 retcode = proc.wait()
623 if retcode != 0:
624 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
625
626 if autostrip:
627 ret = (ret or '').strip()
628 err = (err or '').strip()
629
630 return ret, err
631
632
633def set_branch_config(branch, option, value, scope='local'):
634 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
635
636
637def set_config(option, value, scope='local'):
638 run('config', '--' + scope, option, value)
639
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000640
sbc@chromium.org71437c02015-04-09 19:29:40 +0000641def get_dirty_files():
642 # Make sure index is up-to-date before running diff-index.
643 run_with_retcode('update-index', '--refresh', '-q')
644 return run('diff-index', '--name-status', 'HEAD')
645
646
647def is_dirty_git_tree(cmd):
648 dirty = get_dirty_files()
649 if dirty:
650 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
651 print 'Uncommitted files: (git diff-index --name-status HEAD)'
652 print dirty[:4096]
653 if len(dirty) > 4096: # pragma: no cover
654 print '... (run "git diff-index --name-status HEAD" to see full output).'
655 return True
656 return False
657
658
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000659def squash_current_branch(header=None, merge_base=None):
660 header = header or 'git squash commit.'
661 merge_base = merge_base or get_or_create_merge_base(current_branch())
662 log_msg = header + '\n'
663 if log_msg:
664 log_msg += '\n'
665 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
666 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000667
668 if not get_dirty_files():
669 # Sometimes the squash can result in the same tree, meaning that there is
670 # nothing to commit at this point.
671 print 'Nothing to commit; squashed branch is empty'
672 return False
maruel@chromium.org25b9ab22015-06-18 18:49:03 +0000673 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000674 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000675
676
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000677def tags(*args):
678 return run('tag', *args).splitlines()
679
680
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000681def thaw():
682 took_action = False
683 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
684 msg = run('show', '--format=%f%b', '-s', 'HEAD')
685 match = FREEZE_MATCHER.match(msg)
686 if not match:
687 if not took_action:
688 return 'Nothing to thaw.'
689 break
690
691 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
692 took_action = True
693
694
695def topo_iter(branch_tree, top_down=True):
696 """Generates (branch, parent) in topographical order for a branch tree.
697
698 Given a tree:
699
700 A1
701 B1 B2
702 C1 C2 C3
703 D1
704
705 branch_tree would look like: {
706 'D1': 'C3',
707 'C3': 'B2',
708 'B2': 'A1',
709 'C1': 'B1',
710 'C2': 'B1',
711 'B1': 'A1',
712 }
713
714 It is OK to have multiple 'root' nodes in your graph.
715
716 if top_down is True, items are yielded from A->D. Otherwise they're yielded
717 from D->A. Within a layer the branches will be yielded in sorted order.
718 """
719 branch_tree = branch_tree.copy()
720
721 # TODO(iannucci): There is probably a more efficient way to do these.
722 if top_down:
723 while branch_tree:
724 this_pass = [(b, p) for b, p in branch_tree.iteritems()
725 if p not in branch_tree]
726 assert this_pass, "Branch tree has cycles: %r" % branch_tree
727 for branch, parent in sorted(this_pass):
728 yield branch, parent
729 del branch_tree[branch]
730 else:
731 parent_to_branches = collections.defaultdict(set)
732 for branch, parent in branch_tree.iteritems():
733 parent_to_branches[parent].add(branch)
734
735 while branch_tree:
736 this_pass = [(b, p) for b, p in branch_tree.iteritems()
737 if not parent_to_branches[b]]
738 assert this_pass, "Branch tree has cycles: %r" % branch_tree
739 for branch, parent in sorted(this_pass):
740 yield branch, parent
741 parent_to_branches[parent].discard(branch)
742 del branch_tree[branch]
743
744
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000745def tree(treeref, recurse=False):
746 """Returns a dict representation of a git tree object.
747
748 Args:
749 treeref (str) - a git ref which resolves to a tree (commits count as trees).
750 recurse (bool) - include all of the tree's decendants too. File names will
751 take the form of 'some/path/to/file'.
752
753 Return format:
754 { 'file_name': (mode, type, ref) }
755
756 mode is an integer where:
757 * 0040000 - Directory
758 * 0100644 - Regular non-executable file
759 * 0100664 - Regular non-executable group-writeable file
760 * 0100755 - Regular executable file
761 * 0120000 - Symbolic link
762 * 0160000 - Gitlink
763
764 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
765
766 ref is the hex encoded hash of the entry.
767 """
768 ret = {}
769 opts = ['ls-tree', '--full-tree']
770 if recurse:
771 opts.append('-r')
772 opts.append(treeref)
773 try:
774 for line in run(*opts).splitlines():
775 mode, typ, ref, name = line.split(None, 3)
776 ret[name] = (mode, typ, ref)
777 except subprocess2.CalledProcessError:
778 return None
779 return ret
780
781
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000782def upstream(branch):
783 try:
784 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
785 branch+'@{upstream}')
786 except subprocess2.CalledProcessError:
787 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000788
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000789
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000790def get_git_version():
791 """Returns a tuple that contains the numeric components of the current git
792 version."""
793 version_string = run('--version')
794 version_match = re.search(r'(\d+.)+(\d+)', version_string)
795 version = version_match.group() if version_match else ''
796
797 return tuple(int(x) for x in version.split('.'))
798
799
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000800def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000801 format_string = (
802 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
803
804 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000805 if (include_tracking_status and
806 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000807 format_string += '%(upstream:track)'
808
809 info_map = {}
810 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000811 BranchesInfo = collections.namedtuple(
812 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000813 for line in data.splitlines():
814 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
815
816 ahead_match = re.search(r'ahead (\d+)', tracking_status)
817 ahead = int(ahead_match.group(1)) if ahead_match else None
818
819 behind_match = re.search(r'behind (\d+)', tracking_status)
820 behind = int(behind_match.group(1)) if behind_match else None
821
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000822 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000823 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
824
825 # Set None for upstreams which are not branches (e.g empty upstream, remotes
826 # and deleted upstream branches).
827 missing_upstreams = {}
828 for info in info_map.values():
829 if info.upstream not in info_map and info.upstream not in missing_upstreams:
830 missing_upstreams[info.upstream] = None
831
832 return dict(info_map.items() + missing_upstreams.items())
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000833
834
835def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000836 files_to_copy, symlink=None):
837 if not symlink:
838 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000839 os.makedirs(new_workdir)
840 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000841 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000842 for entry in files_to_copy:
843 clone_file(repository, new_workdir, entry, shutil.copy)
844
845
846def make_workdir(repository, new_workdir):
847 GIT_DIRECTORY_WHITELIST = [
848 'config',
849 'info',
850 'hooks',
851 'logs/refs',
852 'objects',
853 'packed-refs',
854 'refs',
855 'remotes',
856 'rr-cache',
857 'svn'
858 ]
859 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
860 ['HEAD'])
861
862
863def clone_file(repository, new_workdir, link, operation):
864 if not os.path.exists(os.path.join(repository, link)):
865 return
866 link_dir = os.path.dirname(os.path.join(new_workdir, link))
867 if not os.path.exists(link_dir):
868 os.makedirs(link_dir)
869 operation(os.path.join(repository, link), os.path.join(new_workdir, link))