blob: c602103308cbfffbb297c4f7a493daf718540c92 [file] [log] [blame]
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +00001# Copyright 2014 The Chromium Authors. All rights reserved.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5# Monkeypatch IMapIterator so that Ctrl-C can kill everything properly.
6# Derived from https://gist.github.com/aljungberg/626518
7import multiprocessing.pool
8from multiprocessing.pool import IMapIterator
9def wrapper(func):
10 def wrap(self, timeout=None):
11 return func(self, timeout=timeout or 1e100)
12 return wrap
13IMapIterator.next = wrapper(IMapIterator.next)
14IMapIterator.__next__ = IMapIterator.next
15# TODO(iannucci): Monkeypatch all other 'wait' methods too.
16
17
18import binascii
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000019import collections
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000020import contextlib
21import functools
22import logging
iannucci@chromium.org97345eb2014-03-13 07:55:15 +000023import os
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000024import re
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000025import setup_color
sammc@chromium.org900a33f2015-09-29 06:57:09 +000026import shutil
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000027import signal
28import sys
29import tempfile
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +000030import textwrap
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000031import threading
32
33import subprocess2
34
agable02b3c982016-06-22 07:51:22 -070035from StringIO import StringIO
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000036
agable02b3c982016-06-22 07:51:22 -070037
38ROOT = os.path.abspath(os.path.dirname(__file__))
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +000039IS_WIN = sys.platform == 'win32'
40GIT_EXE = ROOT+'\\git.bat' if IS_WIN else 'git'
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000041TEST_MODE = False
42
43FREEZE = 'FREEZE'
44FREEZE_SECTIONS = {
45 'indexed': 'soft',
46 'unindexed': 'mixed'
47}
48FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000049
50
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000051# Retry a git operation if git returns a error response with any of these
52# messages. It's all observed 'bad' GoB responses so far.
53#
54# This list is inspired/derived from the one in ChromiumOS's Chromite:
55# <CHROMITE>/lib/git.py::GIT_TRANSIENT_ERRORS
56#
57# It was last imported from '7add3ac29564d98ac35ce426bc295e743e7c0c02'.
58GIT_TRANSIENT_ERRORS = (
59 # crbug.com/285832
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000060 r'!.*\[remote rejected\].*\(error in hook\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000061
62 # crbug.com/289932
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000063 r'!.*\[remote rejected\].*\(failed to lock\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000064
65 # crbug.com/307156
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000066 r'!.*\[remote rejected\].*\(error in Gerrit backend\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000067
68 # crbug.com/285832
69 r'remote error: Internal Server Error',
70
71 # crbug.com/294449
72 r'fatal: Couldn\'t find remote ref ',
73
74 # crbug.com/220543
75 r'git fetch_pack: expected ACK/NAK, got',
76
77 # crbug.com/189455
78 r'protocol error: bad pack header',
79
80 # crbug.com/202807
81 r'The remote end hung up unexpectedly',
82
83 # crbug.com/298189
84 r'TLS packet with unexpected length was received',
85
86 # crbug.com/187444
87 r'RPC failed; result=\d+, HTTP code = \d+',
88
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000089 # crbug.com/388876
90 r'Connection timed out',
dnj@chromium.org45cddd62014-11-06 19:36:42 +000091
92 # crbug.com/430343
93 # TODO(dnj): Resync with Chromite.
94 r'The requested URL returned error: 5\d+',
Arikonb3a21482016-07-22 10:12:24 -070095
96 r'Connection reset by peer',
97
98 r'Unable to look up',
99
100 r'Couldn\'t resolve host',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000101)
102
103GIT_TRANSIENT_ERRORS_RE = re.compile('|'.join(GIT_TRANSIENT_ERRORS),
104 re.IGNORECASE)
105
raphael.kubo.da.costa@intel.com58d05b02015-06-24 08:54:41 +0000106# git's for-each-ref command first supported the upstream:track token in its
107# format string in version 1.9.0, but some usages were broken until 2.3.0.
108# See git commit b6160d95 for more information.
109MIN_UPSTREAM_TRACK_GIT_VERSION = (2, 3)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000110
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000111class BadCommitRefException(Exception):
112 def __init__(self, refs):
113 msg = ('one of %s does not seem to be a valid commitref.' %
114 str(refs))
115 super(BadCommitRefException, self).__init__(msg)
116
117
118def memoize_one(**kwargs):
119 """Memoizes a single-argument pure function.
120
121 Values of None are not cached.
122
123 Kwargs:
124 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
125 cache manipulation functions. This is a kwarg so that users of memoize_one
126 are forced to explicitly and verbosely pick True or False.
127
128 Adds three methods to the decorated function:
129 * get(key, default=None) - Gets the value for this key from the cache.
130 * set(key, value) - Sets the value for this key from the cache.
131 * clear() - Drops the entire contents of the cache. Useful for unittests.
132 * update(other) - Updates the contents of the cache from another dict.
133 """
134 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
135 threadsafe = kwargs['threadsafe']
136
137 if threadsafe:
138 def withlock(lock, f):
139 def inner(*args, **kwargs):
140 with lock:
141 return f(*args, **kwargs)
142 return inner
143 else:
144 def withlock(_lock, f):
145 return f
146
147 def decorator(f):
148 # Instantiate the lock in decorator, in case users of memoize_one do:
149 #
150 # memoizer = memoize_one(threadsafe=True)
151 #
152 # @memoizer
153 # def fn1(val): ...
154 #
155 # @memoizer
156 # def fn2(val): ...
157
158 lock = threading.Lock() if threadsafe else None
159 cache = {}
160 _get = withlock(lock, cache.get)
161 _set = withlock(lock, cache.__setitem__)
162
163 @functools.wraps(f)
164 def inner(arg):
165 ret = _get(arg)
166 if ret is None:
167 ret = f(arg)
168 if ret is not None:
169 _set(arg, ret)
170 return ret
171 inner.get = _get
172 inner.set = _set
173 inner.clear = withlock(lock, cache.clear)
174 inner.update = withlock(lock, cache.update)
175 return inner
176 return decorator
177
178
179def _ScopedPool_initer(orig, orig_args): # pragma: no cover
180 """Initializer method for ScopedPool's subprocesses.
181
182 This helps ScopedPool handle Ctrl-C's correctly.
183 """
184 signal.signal(signal.SIGINT, signal.SIG_IGN)
185 if orig:
186 orig(*orig_args)
187
188
189@contextlib.contextmanager
190def ScopedPool(*args, **kwargs):
191 """Context Manager which returns a multiprocessing.pool instance which
192 correctly deals with thrown exceptions.
193
194 *args - Arguments to multiprocessing.pool
195
196 Kwargs:
197 kind ('threads', 'procs') - The type of underlying coprocess to use.
198 **etc - Arguments to multiprocessing.pool
199 """
200 if kwargs.pop('kind', None) == 'threads':
201 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
202 else:
203 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
204 kwargs['initializer'] = _ScopedPool_initer
205 kwargs['initargs'] = orig, orig_args
206 pool = multiprocessing.pool.Pool(*args, **kwargs)
207
208 try:
209 yield pool
210 pool.close()
211 except:
212 pool.terminate()
213 raise
214 finally:
215 pool.join()
216
217
218class ProgressPrinter(object):
219 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000220 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000221 """Create a ProgressPrinter.
222
223 Use it as a context manager which produces a simple 'increment' method:
224
225 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
226 for i in xrange(1000):
227 # do stuff
228 if i % 10 == 0:
229 inc(10)
230
231 Args:
232 fmt - String format with a single '%(count)d' where the counter value
233 should go.
234 enabled (bool) - If this is None, will default to True if
235 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000236 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000237 period (float) - The time in seconds for the printer thread to wait
238 between printing.
239 """
240 self.fmt = fmt
241 if enabled is None: # pragma: no cover
242 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
243 else:
244 self.enabled = enabled
245
246 self._count = 0
247 self._dead = False
248 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000249 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000250 self._thread = threading.Thread(target=self._run)
251 self._period = period
252
253 def _emit(self, s):
254 if self.enabled:
255 self._stream.write('\r' + s)
256 self._stream.flush()
257
258 def _run(self):
259 with self._dead_cond:
260 while not self._dead:
261 self._emit(self.fmt % {'count': self._count})
262 self._dead_cond.wait(self._period)
263 self._emit((self.fmt + '\n') % {'count': self._count})
264
265 def inc(self, amount=1):
266 self._count += amount
267
268 def __enter__(self):
269 self._thread.start()
270 return self.inc
271
272 def __exit__(self, _exc_type, _exc_value, _traceback):
273 self._dead = True
274 with self._dead_cond:
275 self._dead_cond.notifyAll()
276 self._thread.join()
277 del self._thread
278
279
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000280def once(function):
281 """@Decorates |function| so that it only performs its action once, no matter
282 how many times the decorated |function| is called."""
283 def _inner_gen():
284 yield function()
285 while True:
286 yield
287 return _inner_gen().next
288
289
290## Git functions
291
agable7aa2ddd2016-06-21 07:47:00 -0700292def die(message, *args):
293 print >> sys.stderr, textwrap.dedent(message % args)
294 sys.exit(1)
295
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000296
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000297def blame(filename, revision=None, porcelain=False, *_args):
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000298 command = ['blame']
299 if porcelain:
300 command.append('-p')
301 if revision is not None:
302 command.append(revision)
303 command.extend(['--', filename])
304 return run(*command)
305
306
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000307def branch_config(branch, option, default=None):
agable7aa2ddd2016-06-21 07:47:00 -0700308 return get_config('branch.%s.%s' % (branch, option), default=default)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000309
310
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000311def branch_config_map(option):
312 """Return {branch: <|option| value>} for all branches."""
313 try:
314 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
agable7aa2ddd2016-06-21 07:47:00 -0700315 lines = get_config_regexp(reg.pattern)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000316 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
317 except subprocess2.CalledProcessError:
318 return {}
319
320
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000321def branches(*args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000322 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000323
324 key = 'depot-tools.branch-limit'
agable7aa2ddd2016-06-21 07:47:00 -0700325 limit = get_config_int(key, 20)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000326
327 raw_branches = run('branch', *args).splitlines()
328
329 num = len(raw_branches)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000330
agable7aa2ddd2016-06-21 07:47:00 -0700331 if num > limit:
332 die("""\
333 Your git repo has too many branches (%d/%d) for this tool to work well.
334
335 You may adjust this limit by running:
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000336 git config %s <new_limit>
agable7aa2ddd2016-06-21 07:47:00 -0700337
338 You may also try cleaning up your old branches by running:
339 git cl archive
340 """, num, limit, key)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000341
342 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000343 if line.startswith(NO_BRANCH):
344 continue
345 yield line.split()[-1]
346
347
agable7aa2ddd2016-06-21 07:47:00 -0700348def get_config(option, default=None):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000349 try:
350 return run('config', '--get', option) or default
351 except subprocess2.CalledProcessError:
352 return default
353
354
agable7aa2ddd2016-06-21 07:47:00 -0700355def get_config_int(option, default=0):
356 assert isinstance(default, int)
357 try:
358 return int(get_config(option, default))
359 except ValueError:
360 return default
361
362
363def get_config_list(option):
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000364 try:
365 return run('config', '--get-all', option).split()
366 except subprocess2.CalledProcessError:
367 return []
368
369
agable7aa2ddd2016-06-21 07:47:00 -0700370def get_config_regexp(pattern):
371 if IS_WIN: # pragma: no cover
372 # this madness is because we call git.bat which calls git.exe which calls
373 # bash.exe (or something to that effect). Each layer divides the number of
374 # ^'s by 2.
375 pattern = pattern.replace('^', '^' * 8)
376 return run('config', '--get-regexp', pattern).splitlines()
377
378
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000379def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000380 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000381 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000382 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000383 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000384
385
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000386def del_branch_config(branch, option, scope='local'):
387 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000388
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000389
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000390def del_config(option, scope='local'):
391 try:
392 run('config', '--' + scope, '--unset', option)
393 except subprocess2.CalledProcessError:
394 pass
395
396
mgiuca@chromium.org01d2cde2016-02-05 03:25:41 +0000397def diff(oldrev, newrev, *args):
398 return run('diff', oldrev, newrev, *args)
399
400
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000401def freeze():
402 took_action = False
agable02b3c982016-06-22 07:51:22 -0700403 key = 'depot-tools.freeze-size-limit'
404 MB = 2**20
405 limit_mb = get_config_int(key, 100)
406 untracked_bytes = 0
407
408 for f, s in status():
409 if is_unmerged(s):
410 die("Cannot freeze unmerged changes!")
411 if limit_mb > 0:
412 if s.lstat == '?':
413 untracked_bytes += os.stat(f).st_size
414 if untracked_bytes > limit_mb * MB:
415 die("""\
416 You appear to have too much untracked+unignored data in your git
417 checkout: %.1f / %d MB.
418
419 Run `git status` to see what it is.
420
421 In addition to making many git commands slower, this will prevent
422 depot_tools from freezing your in-progress changes.
423
424 You should add untracked data that you want to ignore to your repo's
425 .git/info/excludes
426 file. See `git help ignore` for the format of this file.
427
428 If this data is indended as part of your commit, you may adjust the
429 freeze limit by running:
430 git config %s <new_limit>
431 Where <new_limit> is an integer threshold in megabytes.""",
432 untracked_bytes / (MB * 1.0), limit_mb, key)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000433
434 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000435 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000436 took_action = True
437 except subprocess2.CalledProcessError:
438 pass
439
agable96e179b2016-06-24 10:32:51 -0700440 add_errors = False
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000441 try:
agable96e179b2016-06-24 10:32:51 -0700442 run('add', '-A', '--ignore-errors')
443 except subprocess2.CalledProcessError:
444 add_errors = True
445
446 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000447 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000448 took_action = True
449 except subprocess2.CalledProcessError:
450 pass
451
agable96e179b2016-06-24 10:32:51 -0700452 ret = []
453 if add_errors:
454 ret.append('Failed to index some unindexed files.')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000455 if not took_action:
agable96e179b2016-06-24 10:32:51 -0700456 ret.append('Nothing to freeze.')
457 return ' '.join(ret) or None
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000458
459
460def get_branch_tree():
461 """Get the dictionary of {branch: parent}, compatible with topo_iter.
462
463 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
464 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000465 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000466 skipped = set()
467 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000468
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000469 for branch in branches():
470 parent = upstream(branch)
471 if not parent:
472 skipped.add(branch)
473 continue
474 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000475
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000476 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000477
478
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000479def get_or_create_merge_base(branch, parent=None):
480 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000481
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000482 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000483 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000484 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000485 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000486 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000487 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000488 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000489 actual_merge_base = run('merge-base', parent, branch)
490
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000491 if base_upstream != parent:
492 base = None
493 base_upstream = None
494
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000495 def is_ancestor(a, b):
496 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
497
clemensh@chromium.orgc3fe99d2016-04-19 08:39:55 +0000498 if base and base != actual_merge_base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000499 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000500 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
501 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000502 elif is_ancestor(base, actual_merge_base):
503 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
504 base = None
505 else:
506 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000507
508 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000509 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000510 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000511
512 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000513
514
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000515def hash_multi(*reflike):
516 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000517
518
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000519def hash_one(reflike, short=False):
520 args = ['rev-parse', reflike]
521 if short:
522 args.insert(1, '--short')
523 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000524
525
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000526def in_rebase():
527 git_dir = run('rev-parse', '--git-dir')
528 return (
529 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
530 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000531
532
533def intern_f(f, kind='blob'):
534 """Interns a file object into the git object store.
535
536 Args:
537 f (file-like object) - The file-like object to intern
538 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
539
540 Returns the git hash of the interned object (hex encoded).
541 """
542 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
543 f.close()
544 return ret
545
546
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000547def is_dormant(branch):
548 # TODO(iannucci): Do an oldness check?
549 return branch_config(branch, 'dormant', 'false') != 'false'
550
551
agable02b3c982016-06-22 07:51:22 -0700552def is_unmerged(stat_value):
553 return (
554 'U' in (stat_value.lstat, stat_value.rstat) or
555 ((stat_value.lstat == stat_value.rstat) and stat_value.lstat in 'AD')
556 )
557
558
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000559def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000560 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000561 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000562
563
564def mktree(treedict):
565 """Makes a git tree object and returns its hash.
566
567 See |tree()| for the values of mode, type, and ref.
568
569 Args:
570 treedict - { name: (mode, type, ref) }
571 """
572 with tempfile.TemporaryFile() as f:
573 for name, (mode, typ, ref) in treedict.iteritems():
574 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
575 f.seek(0)
576 return run('mktree', '-z', stdin=f)
577
578
579def parse_commitrefs(*commitrefs):
580 """Returns binary encoded commit hashes for one or more commitrefs.
581
582 A commitref is anything which can resolve to a commit. Popular examples:
583 * 'HEAD'
584 * 'origin/master'
585 * 'cool_branch~2'
586 """
587 try:
588 return map(binascii.unhexlify, hash_multi(*commitrefs))
589 except subprocess2.CalledProcessError:
590 raise BadCommitRefException(commitrefs)
591
592
sbc@chromium.org384039b2014-10-13 21:01:00 +0000593RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000594
595
596def rebase(parent, start, branch, abort=False):
597 """Rebases |start|..|branch| onto the branch |parent|.
598
599 Args:
600 parent - The new parent ref for the rebased commits.
601 start - The commit to start from
602 branch - The branch to rebase
603 abort - If True, will call git-rebase --abort in the event that the rebase
604 doesn't complete successfully.
605
606 Returns a namedtuple with fields:
607 success - a boolean indicating that the rebase command completed
608 successfully.
609 message - if the rebase failed, this contains the stdout of the failed
610 rebase.
611 """
612 try:
613 args = ['--onto', parent, start, branch]
614 if TEST_MODE:
615 args.insert(0, '--committer-date-is-author-date')
616 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000617 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000618 except subprocess2.CalledProcessError as cpe:
619 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000620 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000621 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000622
623
624def remove_merge_base(branch):
625 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000626 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000627
628
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000629def repo_root():
630 """Returns the absolute path to the repository root."""
631 return run('rev-parse', '--show-toplevel')
632
633
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000634def root():
agable7aa2ddd2016-06-21 07:47:00 -0700635 return get_config('depot-tools.upstream', 'origin/master')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000636
637
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000638@contextlib.contextmanager
639def less(): # pragma: no cover
640 """Runs 'less' as context manager yielding its stdin as a PIPE.
641
642 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
643 running less and just yields sys.stdout.
644 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000645 if not setup_color.IS_TTY:
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000646 yield sys.stdout
647 return
648
649 # Run with the same options that git uses (see setup_pager in git repo).
650 # -F: Automatically quit if the output is less than one screen.
651 # -R: Don't escape ANSI color codes.
652 # -X: Don't clear the screen before starting.
653 cmd = ('less', '-FRX')
654 try:
655 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
656 yield proc.stdin
657 finally:
658 proc.stdin.close()
659 proc.wait()
660
661
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000662def run(*cmd, **kwargs):
663 """The same as run_with_stderr, except it only returns stdout."""
664 return run_with_stderr(*cmd, **kwargs)[0]
665
666
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000667def run_with_retcode(*cmd, **kwargs):
668 """Run a command but only return the status code."""
669 try:
670 run(*cmd, **kwargs)
671 return 0
672 except subprocess2.CalledProcessError as cpe:
673 return cpe.returncode
674
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000675def run_stream(*cmd, **kwargs):
676 """Runs a git command. Returns stdout as a PIPE (file-like object).
677
678 stderr is dropped to avoid races if the process outputs to both stdout and
679 stderr.
680 """
681 kwargs.setdefault('stderr', subprocess2.VOID)
682 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000683 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000684 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000685 proc = subprocess2.Popen(cmd, **kwargs)
686 return proc.stdout
687
688
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000689@contextlib.contextmanager
690def run_stream_with_retcode(*cmd, **kwargs):
691 """Runs a git command as context manager yielding stdout as a PIPE.
692
693 stderr is dropped to avoid races if the process outputs to both stdout and
694 stderr.
695
696 Raises subprocess2.CalledProcessError on nonzero return code.
697 """
698 kwargs.setdefault('stderr', subprocess2.VOID)
699 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000700 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000701 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
702 try:
703 proc = subprocess2.Popen(cmd, **kwargs)
704 yield proc.stdout
705 finally:
706 retcode = proc.wait()
707 if retcode != 0:
708 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
709 None, None)
710
711
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000712def run_with_stderr(*cmd, **kwargs):
713 """Runs a git command.
714
715 Returns (stdout, stderr) as a pair of strings.
716
717 kwargs
718 autostrip (bool) - Strip the output. Defaults to True.
719 indata (str) - Specifies stdin data for the process.
720 """
721 kwargs.setdefault('stdin', subprocess2.PIPE)
722 kwargs.setdefault('stdout', subprocess2.PIPE)
723 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000724 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000725 autostrip = kwargs.pop('autostrip', True)
726 indata = kwargs.pop('indata', None)
727
iannucci@chromium.org21980022014-04-11 04:51:49 +0000728 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000729 proc = subprocess2.Popen(cmd, **kwargs)
730 ret, err = proc.communicate(indata)
731 retcode = proc.wait()
732 if retcode != 0:
733 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
734
735 if autostrip:
736 ret = (ret or '').strip()
737 err = (err or '').strip()
738
739 return ret, err
740
741
742def set_branch_config(branch, option, value, scope='local'):
743 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
744
745
746def set_config(option, value, scope='local'):
747 run('config', '--' + scope, option, value)
748
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000749
sbc@chromium.org71437c02015-04-09 19:29:40 +0000750def get_dirty_files():
751 # Make sure index is up-to-date before running diff-index.
752 run_with_retcode('update-index', '--refresh', '-q')
753 return run('diff-index', '--name-status', 'HEAD')
754
755
756def is_dirty_git_tree(cmd):
757 dirty = get_dirty_files()
758 if dirty:
techtonikbf2a3412016-07-20 23:36:42 -0700759 print 'Cannot %s with a dirty tree. '\
760 'Commit, freeze or stash your changes first.' % cmd
sbc@chromium.org71437c02015-04-09 19:29:40 +0000761 print 'Uncommitted files: (git diff-index --name-status HEAD)'
762 print dirty[:4096]
763 if len(dirty) > 4096: # pragma: no cover
764 print '... (run "git diff-index --name-status HEAD" to see full output).'
765 return True
766 return False
767
768
agable02b3c982016-06-22 07:51:22 -0700769def status():
770 """Returns a parsed version of git-status.
771
772 Returns a generator of (current_name, (lstat, rstat, src)) pairs where:
773 * current_name is the name of the file
774 * lstat is the left status code letter from git-status
775 * rstat is the left status code letter from git-status
776 * src is the current name of the file, or the original name of the file
777 if lstat == 'R'
778 """
779 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src')
780
781 def tokenizer(stream):
782 acc = StringIO()
783 c = None
784 while c != '':
785 c = stream.read(1)
786 if c in (None, '', '\0'):
787 if acc.len:
788 yield acc.getvalue()
789 acc = StringIO()
790 else:
791 acc.write(c)
792
793 def parser(tokens):
794 while True:
795 # Raises StopIteration if it runs out of tokens.
796 status_dest = next(tokens)
797 stat, dest = status_dest[:2], status_dest[3:]
798 lstat, rstat = stat
799 if lstat == 'R':
800 src = next(tokens)
801 else:
802 src = dest
803 yield (dest, stat_entry(lstat, rstat, src))
804
805 return parser(tokenizer(run_stream('status', '-z', bufsize=-1)))
806
807
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000808def squash_current_branch(header=None, merge_base=None):
809 header = header or 'git squash commit.'
810 merge_base = merge_base or get_or_create_merge_base(current_branch())
811 log_msg = header + '\n'
812 if log_msg:
813 log_msg += '\n'
814 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
815 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000816
817 if not get_dirty_files():
818 # Sometimes the squash can result in the same tree, meaning that there is
819 # nothing to commit at this point.
820 print 'Nothing to commit; squashed branch is empty'
821 return False
maruel@chromium.org25b9ab22015-06-18 18:49:03 +0000822 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000823 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000824
825
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000826def tags(*args):
827 return run('tag', *args).splitlines()
828
829
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000830def thaw():
831 took_action = False
832 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
833 msg = run('show', '--format=%f%b', '-s', 'HEAD')
834 match = FREEZE_MATCHER.match(msg)
835 if not match:
836 if not took_action:
837 return 'Nothing to thaw.'
838 break
839
840 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
841 took_action = True
842
843
844def topo_iter(branch_tree, top_down=True):
845 """Generates (branch, parent) in topographical order for a branch tree.
846
847 Given a tree:
848
849 A1
850 B1 B2
851 C1 C2 C3
852 D1
853
854 branch_tree would look like: {
855 'D1': 'C3',
856 'C3': 'B2',
857 'B2': 'A1',
858 'C1': 'B1',
859 'C2': 'B1',
860 'B1': 'A1',
861 }
862
863 It is OK to have multiple 'root' nodes in your graph.
864
865 if top_down is True, items are yielded from A->D. Otherwise they're yielded
866 from D->A. Within a layer the branches will be yielded in sorted order.
867 """
868 branch_tree = branch_tree.copy()
869
870 # TODO(iannucci): There is probably a more efficient way to do these.
871 if top_down:
872 while branch_tree:
873 this_pass = [(b, p) for b, p in branch_tree.iteritems()
874 if p not in branch_tree]
875 assert this_pass, "Branch tree has cycles: %r" % branch_tree
876 for branch, parent in sorted(this_pass):
877 yield branch, parent
878 del branch_tree[branch]
879 else:
880 parent_to_branches = collections.defaultdict(set)
881 for branch, parent in branch_tree.iteritems():
882 parent_to_branches[parent].add(branch)
883
884 while branch_tree:
885 this_pass = [(b, p) for b, p in branch_tree.iteritems()
886 if not parent_to_branches[b]]
887 assert this_pass, "Branch tree has cycles: %r" % branch_tree
888 for branch, parent in sorted(this_pass):
889 yield branch, parent
890 parent_to_branches[parent].discard(branch)
891 del branch_tree[branch]
892
893
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000894def tree(treeref, recurse=False):
895 """Returns a dict representation of a git tree object.
896
897 Args:
898 treeref (str) - a git ref which resolves to a tree (commits count as trees).
899 recurse (bool) - include all of the tree's decendants too. File names will
900 take the form of 'some/path/to/file'.
901
902 Return format:
903 { 'file_name': (mode, type, ref) }
904
905 mode is an integer where:
906 * 0040000 - Directory
907 * 0100644 - Regular non-executable file
908 * 0100664 - Regular non-executable group-writeable file
909 * 0100755 - Regular executable file
910 * 0120000 - Symbolic link
911 * 0160000 - Gitlink
912
913 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
914
915 ref is the hex encoded hash of the entry.
916 """
917 ret = {}
918 opts = ['ls-tree', '--full-tree']
919 if recurse:
920 opts.append('-r')
921 opts.append(treeref)
922 try:
923 for line in run(*opts).splitlines():
924 mode, typ, ref, name = line.split(None, 3)
925 ret[name] = (mode, typ, ref)
926 except subprocess2.CalledProcessError:
927 return None
928 return ret
929
930
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000931def upstream(branch):
932 try:
933 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
934 branch+'@{upstream}')
935 except subprocess2.CalledProcessError:
936 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000937
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000938
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000939def get_git_version():
940 """Returns a tuple that contains the numeric components of the current git
941 version."""
942 version_string = run('--version')
943 version_match = re.search(r'(\d+.)+(\d+)', version_string)
944 version = version_match.group() if version_match else ''
945
946 return tuple(int(x) for x in version.split('.'))
947
948
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000949def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000950 format_string = (
951 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
952
953 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000954 if (include_tracking_status and
955 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000956 format_string += '%(upstream:track)'
957
958 info_map = {}
959 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000960 BranchesInfo = collections.namedtuple(
961 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000962 for line in data.splitlines():
963 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
964
965 ahead_match = re.search(r'ahead (\d+)', tracking_status)
966 ahead = int(ahead_match.group(1)) if ahead_match else None
967
968 behind_match = re.search(r'behind (\d+)', tracking_status)
969 behind = int(behind_match.group(1)) if behind_match else None
970
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000971 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000972 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
973
974 # Set None for upstreams which are not branches (e.g empty upstream, remotes
975 # and deleted upstream branches).
976 missing_upstreams = {}
977 for info in info_map.values():
978 if info.upstream not in info_map and info.upstream not in missing_upstreams:
979 missing_upstreams[info.upstream] = None
980
981 return dict(info_map.items() + missing_upstreams.items())
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000982
983
984def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000985 files_to_copy, symlink=None):
986 if not symlink:
987 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000988 os.makedirs(new_workdir)
989 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000990 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000991 for entry in files_to_copy:
992 clone_file(repository, new_workdir, entry, shutil.copy)
993
994
995def make_workdir(repository, new_workdir):
996 GIT_DIRECTORY_WHITELIST = [
997 'config',
998 'info',
999 'hooks',
1000 'logs/refs',
1001 'objects',
1002 'packed-refs',
1003 'refs',
1004 'remotes',
1005 'rr-cache',
1006 'svn'
1007 ]
1008 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
1009 ['HEAD'])
1010
1011
1012def clone_file(repository, new_workdir, link, operation):
1013 if not os.path.exists(os.path.join(repository, link)):
1014 return
1015 link_dir = os.path.dirname(os.path.join(new_workdir, link))
1016 if not os.path.exists(link_dir):
1017 os.makedirs(link_dir)
1018 operation(os.path.join(repository, link), os.path.join(new_workdir, link))