blob: 949ba4695e0437f1ef6b9b078a9fca70f27377f5 [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
mgiuca@chromium.org01d2cde2016-02-05 03:25:41 +0000377def diff(oldrev, newrev, *args):
378 return run('diff', oldrev, newrev, *args)
379
380
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000381def freeze():
382 took_action = False
383
384 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000385 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000386 took_action = True
387 except subprocess2.CalledProcessError:
388 pass
389
390 try:
391 run('add', '-A')
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000392 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000393 took_action = True
394 except subprocess2.CalledProcessError:
395 pass
396
397 if not took_action:
398 return 'Nothing to freeze.'
399
400
401def get_branch_tree():
402 """Get the dictionary of {branch: parent}, compatible with topo_iter.
403
404 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
405 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000406 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000407 skipped = set()
408 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000409
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000410 for branch in branches():
411 parent = upstream(branch)
412 if not parent:
413 skipped.add(branch)
414 continue
415 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000416
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000417 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000418
419
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000420def get_or_create_merge_base(branch, parent=None):
421 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000422
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000423 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000424 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000425 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000426 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000427 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000428 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000429 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000430 actual_merge_base = run('merge-base', parent, branch)
431
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000432 if base_upstream != parent:
433 base = None
434 base_upstream = None
435
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000436 def is_ancestor(a, b):
437 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
438
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000439 if base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000440 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000441 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
442 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000443 elif is_ancestor(base, actual_merge_base):
444 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
445 base = None
446 else:
447 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000448
449 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000450 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000451 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000452
453 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000454
455
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000456def hash_multi(*reflike):
457 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000458
459
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000460def hash_one(reflike, short=False):
461 args = ['rev-parse', reflike]
462 if short:
463 args.insert(1, '--short')
464 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000465
466
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000467def in_rebase():
468 git_dir = run('rev-parse', '--git-dir')
469 return (
470 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
471 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000472
473
474def intern_f(f, kind='blob'):
475 """Interns a file object into the git object store.
476
477 Args:
478 f (file-like object) - The file-like object to intern
479 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
480
481 Returns the git hash of the interned object (hex encoded).
482 """
483 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
484 f.close()
485 return ret
486
487
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000488def is_dormant(branch):
489 # TODO(iannucci): Do an oldness check?
490 return branch_config(branch, 'dormant', 'false') != 'false'
491
492
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000493def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000494 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000495 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000496
497
498def mktree(treedict):
499 """Makes a git tree object and returns its hash.
500
501 See |tree()| for the values of mode, type, and ref.
502
503 Args:
504 treedict - { name: (mode, type, ref) }
505 """
506 with tempfile.TemporaryFile() as f:
507 for name, (mode, typ, ref) in treedict.iteritems():
508 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
509 f.seek(0)
510 return run('mktree', '-z', stdin=f)
511
512
513def parse_commitrefs(*commitrefs):
514 """Returns binary encoded commit hashes for one or more commitrefs.
515
516 A commitref is anything which can resolve to a commit. Popular examples:
517 * 'HEAD'
518 * 'origin/master'
519 * 'cool_branch~2'
520 """
521 try:
522 return map(binascii.unhexlify, hash_multi(*commitrefs))
523 except subprocess2.CalledProcessError:
524 raise BadCommitRefException(commitrefs)
525
526
sbc@chromium.org384039b2014-10-13 21:01:00 +0000527RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000528
529
530def rebase(parent, start, branch, abort=False):
531 """Rebases |start|..|branch| onto the branch |parent|.
532
533 Args:
534 parent - The new parent ref for the rebased commits.
535 start - The commit to start from
536 branch - The branch to rebase
537 abort - If True, will call git-rebase --abort in the event that the rebase
538 doesn't complete successfully.
539
540 Returns a namedtuple with fields:
541 success - a boolean indicating that the rebase command completed
542 successfully.
543 message - if the rebase failed, this contains the stdout of the failed
544 rebase.
545 """
546 try:
547 args = ['--onto', parent, start, branch]
548 if TEST_MODE:
549 args.insert(0, '--committer-date-is-author-date')
550 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000551 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000552 except subprocess2.CalledProcessError as cpe:
553 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000554 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000555 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000556
557
558def remove_merge_base(branch):
559 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000560 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000561
562
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000563def repo_root():
564 """Returns the absolute path to the repository root."""
565 return run('rev-parse', '--show-toplevel')
566
567
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000568def root():
569 return config('depot-tools.upstream', 'origin/master')
570
571
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000572@contextlib.contextmanager
573def less(): # pragma: no cover
574 """Runs 'less' as context manager yielding its stdin as a PIPE.
575
576 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
577 running less and just yields sys.stdout.
578 """
579 if not sys.stdout.isatty():
580 yield sys.stdout
581 return
582
583 # Run with the same options that git uses (see setup_pager in git repo).
584 # -F: Automatically quit if the output is less than one screen.
585 # -R: Don't escape ANSI color codes.
586 # -X: Don't clear the screen before starting.
587 cmd = ('less', '-FRX')
588 try:
589 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
590 yield proc.stdin
591 finally:
592 proc.stdin.close()
593 proc.wait()
594
595
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000596def run(*cmd, **kwargs):
597 """The same as run_with_stderr, except it only returns stdout."""
598 return run_with_stderr(*cmd, **kwargs)[0]
599
600
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000601def run_with_retcode(*cmd, **kwargs):
602 """Run a command but only return the status code."""
603 try:
604 run(*cmd, **kwargs)
605 return 0
606 except subprocess2.CalledProcessError as cpe:
607 return cpe.returncode
608
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000609def run_stream(*cmd, **kwargs):
610 """Runs a git command. Returns stdout as a PIPE (file-like object).
611
612 stderr is dropped to avoid races if the process outputs to both stdout and
613 stderr.
614 """
615 kwargs.setdefault('stderr', subprocess2.VOID)
616 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000617 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000618 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000619 proc = subprocess2.Popen(cmd, **kwargs)
620 return proc.stdout
621
622
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000623@contextlib.contextmanager
624def run_stream_with_retcode(*cmd, **kwargs):
625 """Runs a git command as context manager yielding stdout as a PIPE.
626
627 stderr is dropped to avoid races if the process outputs to both stdout and
628 stderr.
629
630 Raises subprocess2.CalledProcessError on nonzero return code.
631 """
632 kwargs.setdefault('stderr', subprocess2.VOID)
633 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000634 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000635 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
636 try:
637 proc = subprocess2.Popen(cmd, **kwargs)
638 yield proc.stdout
639 finally:
640 retcode = proc.wait()
641 if retcode != 0:
642 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
643 None, None)
644
645
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000646def run_with_stderr(*cmd, **kwargs):
647 """Runs a git command.
648
649 Returns (stdout, stderr) as a pair of strings.
650
651 kwargs
652 autostrip (bool) - Strip the output. Defaults to True.
653 indata (str) - Specifies stdin data for the process.
654 """
655 kwargs.setdefault('stdin', subprocess2.PIPE)
656 kwargs.setdefault('stdout', subprocess2.PIPE)
657 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000658 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000659 autostrip = kwargs.pop('autostrip', True)
660 indata = kwargs.pop('indata', None)
661
iannucci@chromium.org21980022014-04-11 04:51:49 +0000662 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000663 proc = subprocess2.Popen(cmd, **kwargs)
664 ret, err = proc.communicate(indata)
665 retcode = proc.wait()
666 if retcode != 0:
667 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
668
669 if autostrip:
670 ret = (ret or '').strip()
671 err = (err or '').strip()
672
673 return ret, err
674
675
676def set_branch_config(branch, option, value, scope='local'):
677 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
678
679
680def set_config(option, value, scope='local'):
681 run('config', '--' + scope, option, value)
682
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000683
sbc@chromium.org71437c02015-04-09 19:29:40 +0000684def get_dirty_files():
685 # Make sure index is up-to-date before running diff-index.
686 run_with_retcode('update-index', '--refresh', '-q')
687 return run('diff-index', '--name-status', 'HEAD')
688
689
690def is_dirty_git_tree(cmd):
691 dirty = get_dirty_files()
692 if dirty:
693 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
694 print 'Uncommitted files: (git diff-index --name-status HEAD)'
695 print dirty[:4096]
696 if len(dirty) > 4096: # pragma: no cover
697 print '... (run "git diff-index --name-status HEAD" to see full output).'
698 return True
699 return False
700
701
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000702def squash_current_branch(header=None, merge_base=None):
703 header = header or 'git squash commit.'
704 merge_base = merge_base or get_or_create_merge_base(current_branch())
705 log_msg = header + '\n'
706 if log_msg:
707 log_msg += '\n'
708 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
709 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000710
711 if not get_dirty_files():
712 # Sometimes the squash can result in the same tree, meaning that there is
713 # nothing to commit at this point.
714 print 'Nothing to commit; squashed branch is empty'
715 return False
maruel@chromium.org25b9ab22015-06-18 18:49:03 +0000716 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000717 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000718
719
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000720def tags(*args):
721 return run('tag', *args).splitlines()
722
723
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000724def thaw():
725 took_action = False
726 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
727 msg = run('show', '--format=%f%b', '-s', 'HEAD')
728 match = FREEZE_MATCHER.match(msg)
729 if not match:
730 if not took_action:
731 return 'Nothing to thaw.'
732 break
733
734 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
735 took_action = True
736
737
738def topo_iter(branch_tree, top_down=True):
739 """Generates (branch, parent) in topographical order for a branch tree.
740
741 Given a tree:
742
743 A1
744 B1 B2
745 C1 C2 C3
746 D1
747
748 branch_tree would look like: {
749 'D1': 'C3',
750 'C3': 'B2',
751 'B2': 'A1',
752 'C1': 'B1',
753 'C2': 'B1',
754 'B1': 'A1',
755 }
756
757 It is OK to have multiple 'root' nodes in your graph.
758
759 if top_down is True, items are yielded from A->D. Otherwise they're yielded
760 from D->A. Within a layer the branches will be yielded in sorted order.
761 """
762 branch_tree = branch_tree.copy()
763
764 # TODO(iannucci): There is probably a more efficient way to do these.
765 if top_down:
766 while branch_tree:
767 this_pass = [(b, p) for b, p in branch_tree.iteritems()
768 if p not in branch_tree]
769 assert this_pass, "Branch tree has cycles: %r" % branch_tree
770 for branch, parent in sorted(this_pass):
771 yield branch, parent
772 del branch_tree[branch]
773 else:
774 parent_to_branches = collections.defaultdict(set)
775 for branch, parent in branch_tree.iteritems():
776 parent_to_branches[parent].add(branch)
777
778 while branch_tree:
779 this_pass = [(b, p) for b, p in branch_tree.iteritems()
780 if not parent_to_branches[b]]
781 assert this_pass, "Branch tree has cycles: %r" % branch_tree
782 for branch, parent in sorted(this_pass):
783 yield branch, parent
784 parent_to_branches[parent].discard(branch)
785 del branch_tree[branch]
786
787
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000788def tree(treeref, recurse=False):
789 """Returns a dict representation of a git tree object.
790
791 Args:
792 treeref (str) - a git ref which resolves to a tree (commits count as trees).
793 recurse (bool) - include all of the tree's decendants too. File names will
794 take the form of 'some/path/to/file'.
795
796 Return format:
797 { 'file_name': (mode, type, ref) }
798
799 mode is an integer where:
800 * 0040000 - Directory
801 * 0100644 - Regular non-executable file
802 * 0100664 - Regular non-executable group-writeable file
803 * 0100755 - Regular executable file
804 * 0120000 - Symbolic link
805 * 0160000 - Gitlink
806
807 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
808
809 ref is the hex encoded hash of the entry.
810 """
811 ret = {}
812 opts = ['ls-tree', '--full-tree']
813 if recurse:
814 opts.append('-r')
815 opts.append(treeref)
816 try:
817 for line in run(*opts).splitlines():
818 mode, typ, ref, name = line.split(None, 3)
819 ret[name] = (mode, typ, ref)
820 except subprocess2.CalledProcessError:
821 return None
822 return ret
823
824
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000825def upstream(branch):
826 try:
827 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
828 branch+'@{upstream}')
829 except subprocess2.CalledProcessError:
830 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000831
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000832
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000833def get_git_version():
834 """Returns a tuple that contains the numeric components of the current git
835 version."""
836 version_string = run('--version')
837 version_match = re.search(r'(\d+.)+(\d+)', version_string)
838 version = version_match.group() if version_match else ''
839
840 return tuple(int(x) for x in version.split('.'))
841
842
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000843def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000844 format_string = (
845 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
846
847 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000848 if (include_tracking_status and
849 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000850 format_string += '%(upstream:track)'
851
852 info_map = {}
853 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000854 BranchesInfo = collections.namedtuple(
855 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000856 for line in data.splitlines():
857 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
858
859 ahead_match = re.search(r'ahead (\d+)', tracking_status)
860 ahead = int(ahead_match.group(1)) if ahead_match else None
861
862 behind_match = re.search(r'behind (\d+)', tracking_status)
863 behind = int(behind_match.group(1)) if behind_match else None
864
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000865 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000866 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
867
868 # Set None for upstreams which are not branches (e.g empty upstream, remotes
869 # and deleted upstream branches).
870 missing_upstreams = {}
871 for info in info_map.values():
872 if info.upstream not in info_map and info.upstream not in missing_upstreams:
873 missing_upstreams[info.upstream] = None
874
875 return dict(info_map.items() + missing_upstreams.items())
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000876
877
878def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000879 files_to_copy, symlink=None):
880 if not symlink:
881 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000882 os.makedirs(new_workdir)
883 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000884 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000885 for entry in files_to_copy:
886 clone_file(repository, new_workdir, entry, shutil.copy)
887
888
889def make_workdir(repository, new_workdir):
890 GIT_DIRECTORY_WHITELIST = [
891 'config',
892 'info',
893 'hooks',
894 'logs/refs',
895 'objects',
896 'packed-refs',
897 'refs',
898 'remotes',
899 'rr-cache',
900 'svn'
901 ]
902 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
903 ['HEAD'])
904
905
906def clone_file(repository, new_workdir, link, operation):
907 if not os.path.exists(os.path.join(repository, link)):
908 return
909 link_dir = os.path.dirname(os.path.join(new_workdir, link))
910 if not os.path.exists(link_dir):
911 os.makedirs(link_dir)
912 operation(os.path.join(repository, link), os.path.join(new_workdir, link))