blob: d571f820b7de02bd4bd3814716912d89dffa7f3d [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
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000284def blame(filename, revision=None, porcelain=False, *args):
285 command = ['blame']
286 if porcelain:
287 command.append('-p')
288 if revision is not None:
289 command.append(revision)
290 command.extend(['--', filename])
291 return run(*command)
292
293
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000294def branch_config(branch, option, default=None):
295 return config('branch.%s.%s' % (branch, option), default=default)
296
297
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000298def config_regexp(pattern):
299 if IS_WIN: # pragma: no cover
300 # this madness is because we call git.bat which calls git.exe which calls
301 # bash.exe (or something to that effect). Each layer divides the number of
302 # ^'s by 2.
303 pattern = pattern.replace('^', '^' * 8)
304 return run('config', '--get-regexp', pattern).splitlines()
305
306
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000307def branch_config_map(option):
308 """Return {branch: <|option| value>} for all branches."""
309 try:
310 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000311 lines = config_regexp(reg.pattern)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000312 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
313 except subprocess2.CalledProcessError:
314 return {}
315
316
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000317def branches(*args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000318 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000319
320 key = 'depot-tools.branch-limit'
321 limit = 20
322 try:
323 limit = int(config(key, limit))
324 except ValueError:
325 pass
326
327 raw_branches = run('branch', *args).splitlines()
328
329 num = len(raw_branches)
330 if num > limit:
331 print >> sys.stderr, textwrap.dedent("""\
332 Your git repo has too many branches (%d/%d) for this tool to work well.
333
334 You may adjust this limit by running:
335 git config %s <new_limit>
336 """ % (num, limit, key))
337 sys.exit(1)
338
339 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000340 if line.startswith(NO_BRANCH):
341 continue
342 yield line.split()[-1]
343
344
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000345def config(option, default=None):
346 try:
347 return run('config', '--get', option) or default
348 except subprocess2.CalledProcessError:
349 return default
350
351
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000352def config_list(option):
353 try:
354 return run('config', '--get-all', option).split()
355 except subprocess2.CalledProcessError:
356 return []
357
358
359def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000360 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000361 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000362 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000363 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000364
365
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000366def del_branch_config(branch, option, scope='local'):
367 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000368
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000369
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000370def del_config(option, scope='local'):
371 try:
372 run('config', '--' + scope, '--unset', option)
373 except subprocess2.CalledProcessError:
374 pass
375
376
377def freeze():
378 took_action = False
379
380 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000381 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000382 took_action = True
383 except subprocess2.CalledProcessError:
384 pass
385
386 try:
387 run('add', '-A')
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000388 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000389 took_action = True
390 except subprocess2.CalledProcessError:
391 pass
392
393 if not took_action:
394 return 'Nothing to freeze.'
395
396
397def get_branch_tree():
398 """Get the dictionary of {branch: parent}, compatible with topo_iter.
399
400 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
401 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000402 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000403 skipped = set()
404 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000405
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000406 for branch in branches():
407 parent = upstream(branch)
408 if not parent:
409 skipped.add(branch)
410 continue
411 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000412
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000413 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000414
415
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000416def get_or_create_merge_base(branch, parent=None):
417 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000418
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000419 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000420 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000421 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000422 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000423 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000424 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000425 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000426 actual_merge_base = run('merge-base', parent, branch)
427
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000428 if base_upstream != parent:
429 base = None
430 base_upstream = None
431
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000432 def is_ancestor(a, b):
433 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
434
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000435 if base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000436 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000437 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
438 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000439 elif is_ancestor(base, actual_merge_base):
440 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
441 base = None
442 else:
443 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000444
445 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000446 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000447 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000448
449 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000450
451
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000452def hash_multi(*reflike):
453 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000454
455
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000456def hash_one(reflike, short=False):
457 args = ['rev-parse', reflike]
458 if short:
459 args.insert(1, '--short')
460 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000461
462
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000463def in_rebase():
464 git_dir = run('rev-parse', '--git-dir')
465 return (
466 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
467 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000468
469
470def intern_f(f, kind='blob'):
471 """Interns a file object into the git object store.
472
473 Args:
474 f (file-like object) - The file-like object to intern
475 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
476
477 Returns the git hash of the interned object (hex encoded).
478 """
479 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
480 f.close()
481 return ret
482
483
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000484def is_dormant(branch):
485 # TODO(iannucci): Do an oldness check?
486 return branch_config(branch, 'dormant', 'false') != 'false'
487
488
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000489def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000490 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000491 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000492
493
494def mktree(treedict):
495 """Makes a git tree object and returns its hash.
496
497 See |tree()| for the values of mode, type, and ref.
498
499 Args:
500 treedict - { name: (mode, type, ref) }
501 """
502 with tempfile.TemporaryFile() as f:
503 for name, (mode, typ, ref) in treedict.iteritems():
504 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
505 f.seek(0)
506 return run('mktree', '-z', stdin=f)
507
508
509def parse_commitrefs(*commitrefs):
510 """Returns binary encoded commit hashes for one or more commitrefs.
511
512 A commitref is anything which can resolve to a commit. Popular examples:
513 * 'HEAD'
514 * 'origin/master'
515 * 'cool_branch~2'
516 """
517 try:
518 return map(binascii.unhexlify, hash_multi(*commitrefs))
519 except subprocess2.CalledProcessError:
520 raise BadCommitRefException(commitrefs)
521
522
sbc@chromium.org384039b2014-10-13 21:01:00 +0000523RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000524
525
526def rebase(parent, start, branch, abort=False):
527 """Rebases |start|..|branch| onto the branch |parent|.
528
529 Args:
530 parent - The new parent ref for the rebased commits.
531 start - The commit to start from
532 branch - The branch to rebase
533 abort - If True, will call git-rebase --abort in the event that the rebase
534 doesn't complete successfully.
535
536 Returns a namedtuple with fields:
537 success - a boolean indicating that the rebase command completed
538 successfully.
539 message - if the rebase failed, this contains the stdout of the failed
540 rebase.
541 """
542 try:
543 args = ['--onto', parent, start, branch]
544 if TEST_MODE:
545 args.insert(0, '--committer-date-is-author-date')
546 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000547 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000548 except subprocess2.CalledProcessError as cpe:
549 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000550 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000551 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000552
553
554def remove_merge_base(branch):
555 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000556 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000557
558
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000559def repo_root():
560 """Returns the absolute path to the repository root."""
561 return run('rev-parse', '--show-toplevel')
562
563
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000564def root():
565 return config('depot-tools.upstream', 'origin/master')
566
567
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000568@contextlib.contextmanager
569def less(): # pragma: no cover
570 """Runs 'less' as context manager yielding its stdin as a PIPE.
571
572 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
573 running less and just yields sys.stdout.
574 """
575 if not sys.stdout.isatty():
576 yield sys.stdout
577 return
578
579 # Run with the same options that git uses (see setup_pager in git repo).
580 # -F: Automatically quit if the output is less than one screen.
581 # -R: Don't escape ANSI color codes.
582 # -X: Don't clear the screen before starting.
583 cmd = ('less', '-FRX')
584 try:
585 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
586 yield proc.stdin
587 finally:
588 proc.stdin.close()
589 proc.wait()
590
591
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000592def run(*cmd, **kwargs):
593 """The same as run_with_stderr, except it only returns stdout."""
594 return run_with_stderr(*cmd, **kwargs)[0]
595
596
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000597def run_with_retcode(*cmd, **kwargs):
598 """Run a command but only return the status code."""
599 try:
600 run(*cmd, **kwargs)
601 return 0
602 except subprocess2.CalledProcessError as cpe:
603 return cpe.returncode
604
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000605def run_stream(*cmd, **kwargs):
606 """Runs a git command. Returns stdout as a PIPE (file-like object).
607
608 stderr is dropped to avoid races if the process outputs to both stdout and
609 stderr.
610 """
611 kwargs.setdefault('stderr', subprocess2.VOID)
612 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000613 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000614 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000615 proc = subprocess2.Popen(cmd, **kwargs)
616 return proc.stdout
617
618
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000619@contextlib.contextmanager
620def run_stream_with_retcode(*cmd, **kwargs):
621 """Runs a git command as context manager yielding stdout as a PIPE.
622
623 stderr is dropped to avoid races if the process outputs to both stdout and
624 stderr.
625
626 Raises subprocess2.CalledProcessError on nonzero return code.
627 """
628 kwargs.setdefault('stderr', subprocess2.VOID)
629 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000630 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000631 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
632 try:
633 proc = subprocess2.Popen(cmd, **kwargs)
634 yield proc.stdout
635 finally:
636 retcode = proc.wait()
637 if retcode != 0:
638 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
639 None, None)
640
641
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000642def run_with_stderr(*cmd, **kwargs):
643 """Runs a git command.
644
645 Returns (stdout, stderr) as a pair of strings.
646
647 kwargs
648 autostrip (bool) - Strip the output. Defaults to True.
649 indata (str) - Specifies stdin data for the process.
650 """
651 kwargs.setdefault('stdin', subprocess2.PIPE)
652 kwargs.setdefault('stdout', subprocess2.PIPE)
653 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000654 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000655 autostrip = kwargs.pop('autostrip', True)
656 indata = kwargs.pop('indata', None)
657
iannucci@chromium.org21980022014-04-11 04:51:49 +0000658 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000659 proc = subprocess2.Popen(cmd, **kwargs)
660 ret, err = proc.communicate(indata)
661 retcode = proc.wait()
662 if retcode != 0:
663 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
664
665 if autostrip:
666 ret = (ret or '').strip()
667 err = (err or '').strip()
668
669 return ret, err
670
671
672def set_branch_config(branch, option, value, scope='local'):
673 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
674
675
676def set_config(option, value, scope='local'):
677 run('config', '--' + scope, option, value)
678
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000679
sbc@chromium.org71437c02015-04-09 19:29:40 +0000680def get_dirty_files():
681 # Make sure index is up-to-date before running diff-index.
682 run_with_retcode('update-index', '--refresh', '-q')
683 return run('diff-index', '--name-status', 'HEAD')
684
685
686def is_dirty_git_tree(cmd):
687 dirty = get_dirty_files()
688 if dirty:
689 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
690 print 'Uncommitted files: (git diff-index --name-status HEAD)'
691 print dirty[:4096]
692 if len(dirty) > 4096: # pragma: no cover
693 print '... (run "git diff-index --name-status HEAD" to see full output).'
694 return True
695 return False
696
697
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000698def squash_current_branch(header=None, merge_base=None):
699 header = header or 'git squash commit.'
700 merge_base = merge_base or get_or_create_merge_base(current_branch())
701 log_msg = header + '\n'
702 if log_msg:
703 log_msg += '\n'
704 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
705 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000706
707 if not get_dirty_files():
708 # Sometimes the squash can result in the same tree, meaning that there is
709 # nothing to commit at this point.
710 print 'Nothing to commit; squashed branch is empty'
711 return False
maruel@chromium.org25b9ab22015-06-18 18:49:03 +0000712 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000713 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000714
715
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000716def tags(*args):
717 return run('tag', *args).splitlines()
718
719
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000720def thaw():
721 took_action = False
722 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
723 msg = run('show', '--format=%f%b', '-s', 'HEAD')
724 match = FREEZE_MATCHER.match(msg)
725 if not match:
726 if not took_action:
727 return 'Nothing to thaw.'
728 break
729
730 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
731 took_action = True
732
733
734def topo_iter(branch_tree, top_down=True):
735 """Generates (branch, parent) in topographical order for a branch tree.
736
737 Given a tree:
738
739 A1
740 B1 B2
741 C1 C2 C3
742 D1
743
744 branch_tree would look like: {
745 'D1': 'C3',
746 'C3': 'B2',
747 'B2': 'A1',
748 'C1': 'B1',
749 'C2': 'B1',
750 'B1': 'A1',
751 }
752
753 It is OK to have multiple 'root' nodes in your graph.
754
755 if top_down is True, items are yielded from A->D. Otherwise they're yielded
756 from D->A. Within a layer the branches will be yielded in sorted order.
757 """
758 branch_tree = branch_tree.copy()
759
760 # TODO(iannucci): There is probably a more efficient way to do these.
761 if top_down:
762 while branch_tree:
763 this_pass = [(b, p) for b, p in branch_tree.iteritems()
764 if p not in branch_tree]
765 assert this_pass, "Branch tree has cycles: %r" % branch_tree
766 for branch, parent in sorted(this_pass):
767 yield branch, parent
768 del branch_tree[branch]
769 else:
770 parent_to_branches = collections.defaultdict(set)
771 for branch, parent in branch_tree.iteritems():
772 parent_to_branches[parent].add(branch)
773
774 while branch_tree:
775 this_pass = [(b, p) for b, p in branch_tree.iteritems()
776 if not parent_to_branches[b]]
777 assert this_pass, "Branch tree has cycles: %r" % branch_tree
778 for branch, parent in sorted(this_pass):
779 yield branch, parent
780 parent_to_branches[parent].discard(branch)
781 del branch_tree[branch]
782
783
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000784def tree(treeref, recurse=False):
785 """Returns a dict representation of a git tree object.
786
787 Args:
788 treeref (str) - a git ref which resolves to a tree (commits count as trees).
789 recurse (bool) - include all of the tree's decendants too. File names will
790 take the form of 'some/path/to/file'.
791
792 Return format:
793 { 'file_name': (mode, type, ref) }
794
795 mode is an integer where:
796 * 0040000 - Directory
797 * 0100644 - Regular non-executable file
798 * 0100664 - Regular non-executable group-writeable file
799 * 0100755 - Regular executable file
800 * 0120000 - Symbolic link
801 * 0160000 - Gitlink
802
803 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
804
805 ref is the hex encoded hash of the entry.
806 """
807 ret = {}
808 opts = ['ls-tree', '--full-tree']
809 if recurse:
810 opts.append('-r')
811 opts.append(treeref)
812 try:
813 for line in run(*opts).splitlines():
814 mode, typ, ref, name = line.split(None, 3)
815 ret[name] = (mode, typ, ref)
816 except subprocess2.CalledProcessError:
817 return None
818 return ret
819
820
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000821def upstream(branch):
822 try:
823 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
824 branch+'@{upstream}')
825 except subprocess2.CalledProcessError:
826 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000827
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000828
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000829def get_git_version():
830 """Returns a tuple that contains the numeric components of the current git
831 version."""
832 version_string = run('--version')
833 version_match = re.search(r'(\d+.)+(\d+)', version_string)
834 version = version_match.group() if version_match else ''
835
836 return tuple(int(x) for x in version.split('.'))
837
838
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000839def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000840 format_string = (
841 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
842
843 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000844 if (include_tracking_status and
845 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000846 format_string += '%(upstream:track)'
847
848 info_map = {}
849 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000850 BranchesInfo = collections.namedtuple(
851 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000852 for line in data.splitlines():
853 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
854
855 ahead_match = re.search(r'ahead (\d+)', tracking_status)
856 ahead = int(ahead_match.group(1)) if ahead_match else None
857
858 behind_match = re.search(r'behind (\d+)', tracking_status)
859 behind = int(behind_match.group(1)) if behind_match else None
860
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000861 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000862 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
863
864 # Set None for upstreams which are not branches (e.g empty upstream, remotes
865 # and deleted upstream branches).
866 missing_upstreams = {}
867 for info in info_map.values():
868 if info.upstream not in info_map and info.upstream not in missing_upstreams:
869 missing_upstreams[info.upstream] = None
870
871 return dict(info_map.items() + missing_upstreams.items())
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000872
873
874def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000875 files_to_copy, symlink=None):
876 if not symlink:
877 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000878 os.makedirs(new_workdir)
879 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000880 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000881 for entry in files_to_copy:
882 clone_file(repository, new_workdir, entry, shutil.copy)
883
884
885def make_workdir(repository, new_workdir):
886 GIT_DIRECTORY_WHITELIST = [
887 'config',
888 'info',
889 'hooks',
890 'logs/refs',
891 'objects',
892 'packed-refs',
893 'refs',
894 'remotes',
895 'rr-cache',
896 'svn'
897 ]
898 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
899 ['HEAD'])
900
901
902def clone_file(repository, new_workdir, link, operation):
903 if not os.path.exists(os.path.join(repository, link)):
904 return
905 link_dir = os.path.dirname(os.path.join(new_workdir, link))
906 if not os.path.exists(link_dir):
907 os.makedirs(link_dir)
908 operation(os.path.join(repository, link), os.path.join(new_workdir, link))