blob: 0fd1e7f6cba8dbae78b8b28e34f21903296465a3 [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
iannuccieaca0332016-08-03 16:46:50 -0700408 root_path = repo_root()
409
agable02b3c982016-06-22 07:51:22 -0700410 for f, s in status():
411 if is_unmerged(s):
412 die("Cannot freeze unmerged changes!")
413 if limit_mb > 0:
414 if s.lstat == '?':
iannuccieaca0332016-08-03 16:46:50 -0700415 untracked_bytes += os.stat(os.path.join(root_path, f)).st_size
agable02b3c982016-06-22 07:51:22 -0700416 if untracked_bytes > limit_mb * MB:
417 die("""\
418 You appear to have too much untracked+unignored data in your git
419 checkout: %.1f / %d MB.
420
421 Run `git status` to see what it is.
422
423 In addition to making many git commands slower, this will prevent
424 depot_tools from freezing your in-progress changes.
425
426 You should add untracked data that you want to ignore to your repo's
427 .git/info/excludes
428 file. See `git help ignore` for the format of this file.
429
430 If this data is indended as part of your commit, you may adjust the
431 freeze limit by running:
432 git config %s <new_limit>
433 Where <new_limit> is an integer threshold in megabytes.""",
434 untracked_bytes / (MB * 1.0), limit_mb, key)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000435
436 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000437 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000438 took_action = True
439 except subprocess2.CalledProcessError:
440 pass
441
agable96e179b2016-06-24 10:32:51 -0700442 add_errors = False
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000443 try:
agable96e179b2016-06-24 10:32:51 -0700444 run('add', '-A', '--ignore-errors')
445 except subprocess2.CalledProcessError:
446 add_errors = True
447
448 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000449 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000450 took_action = True
451 except subprocess2.CalledProcessError:
452 pass
453
agable96e179b2016-06-24 10:32:51 -0700454 ret = []
455 if add_errors:
456 ret.append('Failed to index some unindexed files.')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000457 if not took_action:
agable96e179b2016-06-24 10:32:51 -0700458 ret.append('Nothing to freeze.')
459 return ' '.join(ret) or None
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000460
461
462def get_branch_tree():
463 """Get the dictionary of {branch: parent}, compatible with topo_iter.
464
465 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
466 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000467 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000468 skipped = set()
469 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000470
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000471 for branch in branches():
472 parent = upstream(branch)
473 if not parent:
474 skipped.add(branch)
475 continue
476 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000477
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000478 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000479
480
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000481def get_or_create_merge_base(branch, parent=None):
482 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000483
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000484 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000485 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000486 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000487 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000488 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000489 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000490 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000491 actual_merge_base = run('merge-base', parent, branch)
492
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000493 if base_upstream != parent:
494 base = None
495 base_upstream = None
496
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000497 def is_ancestor(a, b):
498 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
499
clemensh@chromium.orgc3fe99d2016-04-19 08:39:55 +0000500 if base and base != actual_merge_base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000501 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000502 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
503 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000504 elif is_ancestor(base, actual_merge_base):
505 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
506 base = None
507 else:
508 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000509
510 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000511 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000512 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000513
514 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000515
516
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000517def hash_multi(*reflike):
518 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000519
520
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000521def hash_one(reflike, short=False):
522 args = ['rev-parse', reflike]
523 if short:
524 args.insert(1, '--short')
525 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000526
527
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000528def in_rebase():
529 git_dir = run('rev-parse', '--git-dir')
530 return (
531 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
532 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000533
534
535def intern_f(f, kind='blob'):
536 """Interns a file object into the git object store.
537
538 Args:
539 f (file-like object) - The file-like object to intern
540 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
541
542 Returns the git hash of the interned object (hex encoded).
543 """
544 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
545 f.close()
546 return ret
547
548
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000549def is_dormant(branch):
550 # TODO(iannucci): Do an oldness check?
551 return branch_config(branch, 'dormant', 'false') != 'false'
552
553
agable02b3c982016-06-22 07:51:22 -0700554def is_unmerged(stat_value):
555 return (
556 'U' in (stat_value.lstat, stat_value.rstat) or
557 ((stat_value.lstat == stat_value.rstat) and stat_value.lstat in 'AD')
558 )
559
560
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000561def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000562 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000563 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000564
565
566def mktree(treedict):
567 """Makes a git tree object and returns its hash.
568
569 See |tree()| for the values of mode, type, and ref.
570
571 Args:
572 treedict - { name: (mode, type, ref) }
573 """
574 with tempfile.TemporaryFile() as f:
575 for name, (mode, typ, ref) in treedict.iteritems():
576 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
577 f.seek(0)
578 return run('mktree', '-z', stdin=f)
579
580
581def parse_commitrefs(*commitrefs):
582 """Returns binary encoded commit hashes for one or more commitrefs.
583
584 A commitref is anything which can resolve to a commit. Popular examples:
585 * 'HEAD'
586 * 'origin/master'
587 * 'cool_branch~2'
588 """
589 try:
590 return map(binascii.unhexlify, hash_multi(*commitrefs))
591 except subprocess2.CalledProcessError:
592 raise BadCommitRefException(commitrefs)
593
594
sbc@chromium.org384039b2014-10-13 21:01:00 +0000595RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000596
597
598def rebase(parent, start, branch, abort=False):
599 """Rebases |start|..|branch| onto the branch |parent|.
600
601 Args:
602 parent - The new parent ref for the rebased commits.
603 start - The commit to start from
604 branch - The branch to rebase
605 abort - If True, will call git-rebase --abort in the event that the rebase
606 doesn't complete successfully.
607
608 Returns a namedtuple with fields:
609 success - a boolean indicating that the rebase command completed
610 successfully.
611 message - if the rebase failed, this contains the stdout of the failed
612 rebase.
613 """
614 try:
615 args = ['--onto', parent, start, branch]
616 if TEST_MODE:
617 args.insert(0, '--committer-date-is-author-date')
618 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000619 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000620 except subprocess2.CalledProcessError as cpe:
621 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000622 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000623 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000624
625
626def remove_merge_base(branch):
627 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000628 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000629
630
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000631def repo_root():
632 """Returns the absolute path to the repository root."""
633 return run('rev-parse', '--show-toplevel')
634
635
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000636def root():
agable7aa2ddd2016-06-21 07:47:00 -0700637 return get_config('depot-tools.upstream', 'origin/master')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000638
639
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000640@contextlib.contextmanager
641def less(): # pragma: no cover
642 """Runs 'less' as context manager yielding its stdin as a PIPE.
643
644 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
645 running less and just yields sys.stdout.
646 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000647 if not setup_color.IS_TTY:
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000648 yield sys.stdout
649 return
650
651 # Run with the same options that git uses (see setup_pager in git repo).
652 # -F: Automatically quit if the output is less than one screen.
653 # -R: Don't escape ANSI color codes.
654 # -X: Don't clear the screen before starting.
655 cmd = ('less', '-FRX')
656 try:
657 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
658 yield proc.stdin
659 finally:
660 proc.stdin.close()
661 proc.wait()
662
663
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000664def run(*cmd, **kwargs):
665 """The same as run_with_stderr, except it only returns stdout."""
666 return run_with_stderr(*cmd, **kwargs)[0]
667
668
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000669def run_with_retcode(*cmd, **kwargs):
670 """Run a command but only return the status code."""
671 try:
672 run(*cmd, **kwargs)
673 return 0
674 except subprocess2.CalledProcessError as cpe:
675 return cpe.returncode
676
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000677def run_stream(*cmd, **kwargs):
678 """Runs a git command. Returns stdout as a PIPE (file-like object).
679
680 stderr is dropped to avoid races if the process outputs to both stdout and
681 stderr.
682 """
683 kwargs.setdefault('stderr', subprocess2.VOID)
684 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000685 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000686 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000687 proc = subprocess2.Popen(cmd, **kwargs)
688 return proc.stdout
689
690
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000691@contextlib.contextmanager
692def run_stream_with_retcode(*cmd, **kwargs):
693 """Runs a git command as context manager yielding stdout as a PIPE.
694
695 stderr is dropped to avoid races if the process outputs to both stdout and
696 stderr.
697
698 Raises subprocess2.CalledProcessError on nonzero return code.
699 """
700 kwargs.setdefault('stderr', subprocess2.VOID)
701 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000702 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000703 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
704 try:
705 proc = subprocess2.Popen(cmd, **kwargs)
706 yield proc.stdout
707 finally:
708 retcode = proc.wait()
709 if retcode != 0:
710 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
711 None, None)
712
713
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000714def run_with_stderr(*cmd, **kwargs):
715 """Runs a git command.
716
717 Returns (stdout, stderr) as a pair of strings.
718
719 kwargs
720 autostrip (bool) - Strip the output. Defaults to True.
721 indata (str) - Specifies stdin data for the process.
722 """
723 kwargs.setdefault('stdin', subprocess2.PIPE)
724 kwargs.setdefault('stdout', subprocess2.PIPE)
725 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000726 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000727 autostrip = kwargs.pop('autostrip', True)
728 indata = kwargs.pop('indata', None)
729
iannucci@chromium.org21980022014-04-11 04:51:49 +0000730 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000731 proc = subprocess2.Popen(cmd, **kwargs)
732 ret, err = proc.communicate(indata)
733 retcode = proc.wait()
734 if retcode != 0:
735 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
736
737 if autostrip:
738 ret = (ret or '').strip()
739 err = (err or '').strip()
740
741 return ret, err
742
743
744def set_branch_config(branch, option, value, scope='local'):
745 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
746
747
748def set_config(option, value, scope='local'):
749 run('config', '--' + scope, option, value)
750
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000751
sbc@chromium.org71437c02015-04-09 19:29:40 +0000752def get_dirty_files():
753 # Make sure index is up-to-date before running diff-index.
754 run_with_retcode('update-index', '--refresh', '-q')
755 return run('diff-index', '--name-status', 'HEAD')
756
757
758def is_dirty_git_tree(cmd):
iannuccie38699b2016-08-15 17:32:31 -0700759 w = lambda s: sys.stderr.write(s+"\n")
760
sbc@chromium.org71437c02015-04-09 19:29:40 +0000761 dirty = get_dirty_files()
762 if dirty:
iannuccie38699b2016-08-15 17:32:31 -0700763 w('Cannot %s with a dirty tree. Commit, freeze or stash your changes first.'
764 % cmd)
765 w('Uncommitted files: (git diff-index --name-status HEAD)')
766 w(dirty[:4096])
sbc@chromium.org71437c02015-04-09 19:29:40 +0000767 if len(dirty) > 4096: # pragma: no cover
iannuccie38699b2016-08-15 17:32:31 -0700768 w('... (run "git diff-index --name-status HEAD" to see full output).')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000769 return True
770 return False
771
772
agable02b3c982016-06-22 07:51:22 -0700773def status():
774 """Returns a parsed version of git-status.
775
776 Returns a generator of (current_name, (lstat, rstat, src)) pairs where:
777 * current_name is the name of the file
778 * lstat is the left status code letter from git-status
779 * rstat is the left status code letter from git-status
780 * src is the current name of the file, or the original name of the file
781 if lstat == 'R'
782 """
783 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src')
784
785 def tokenizer(stream):
786 acc = StringIO()
787 c = None
788 while c != '':
789 c = stream.read(1)
790 if c in (None, '', '\0'):
791 if acc.len:
792 yield acc.getvalue()
793 acc = StringIO()
794 else:
795 acc.write(c)
796
797 def parser(tokens):
798 while True:
799 # Raises StopIteration if it runs out of tokens.
800 status_dest = next(tokens)
801 stat, dest = status_dest[:2], status_dest[3:]
802 lstat, rstat = stat
803 if lstat == 'R':
804 src = next(tokens)
805 else:
806 src = dest
807 yield (dest, stat_entry(lstat, rstat, src))
808
809 return parser(tokenizer(run_stream('status', '-z', bufsize=-1)))
810
811
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000812def squash_current_branch(header=None, merge_base=None):
813 header = header or 'git squash commit.'
814 merge_base = merge_base or get_or_create_merge_base(current_branch())
815 log_msg = header + '\n'
816 if log_msg:
817 log_msg += '\n'
818 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
819 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000820
821 if not get_dirty_files():
822 # Sometimes the squash can result in the same tree, meaning that there is
823 # nothing to commit at this point.
824 print 'Nothing to commit; squashed branch is empty'
825 return False
maruel@chromium.org25b9ab22015-06-18 18:49:03 +0000826 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000827 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000828
829
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000830def tags(*args):
831 return run('tag', *args).splitlines()
832
833
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000834def thaw():
835 took_action = False
836 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
837 msg = run('show', '--format=%f%b', '-s', 'HEAD')
838 match = FREEZE_MATCHER.match(msg)
839 if not match:
840 if not took_action:
841 return 'Nothing to thaw.'
842 break
843
844 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
845 took_action = True
846
847
848def topo_iter(branch_tree, top_down=True):
849 """Generates (branch, parent) in topographical order for a branch tree.
850
851 Given a tree:
852
853 A1
854 B1 B2
855 C1 C2 C3
856 D1
857
858 branch_tree would look like: {
859 'D1': 'C3',
860 'C3': 'B2',
861 'B2': 'A1',
862 'C1': 'B1',
863 'C2': 'B1',
864 'B1': 'A1',
865 }
866
867 It is OK to have multiple 'root' nodes in your graph.
868
869 if top_down is True, items are yielded from A->D. Otherwise they're yielded
870 from D->A. Within a layer the branches will be yielded in sorted order.
871 """
872 branch_tree = branch_tree.copy()
873
874 # TODO(iannucci): There is probably a more efficient way to do these.
875 if top_down:
876 while branch_tree:
877 this_pass = [(b, p) for b, p in branch_tree.iteritems()
878 if p not in branch_tree]
879 assert this_pass, "Branch tree has cycles: %r" % branch_tree
880 for branch, parent in sorted(this_pass):
881 yield branch, parent
882 del branch_tree[branch]
883 else:
884 parent_to_branches = collections.defaultdict(set)
885 for branch, parent in branch_tree.iteritems():
886 parent_to_branches[parent].add(branch)
887
888 while branch_tree:
889 this_pass = [(b, p) for b, p in branch_tree.iteritems()
890 if not parent_to_branches[b]]
891 assert this_pass, "Branch tree has cycles: %r" % branch_tree
892 for branch, parent in sorted(this_pass):
893 yield branch, parent
894 parent_to_branches[parent].discard(branch)
895 del branch_tree[branch]
896
897
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000898def tree(treeref, recurse=False):
899 """Returns a dict representation of a git tree object.
900
901 Args:
902 treeref (str) - a git ref which resolves to a tree (commits count as trees).
903 recurse (bool) - include all of the tree's decendants too. File names will
904 take the form of 'some/path/to/file'.
905
906 Return format:
907 { 'file_name': (mode, type, ref) }
908
909 mode is an integer where:
910 * 0040000 - Directory
911 * 0100644 - Regular non-executable file
912 * 0100664 - Regular non-executable group-writeable file
913 * 0100755 - Regular executable file
914 * 0120000 - Symbolic link
915 * 0160000 - Gitlink
916
917 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
918
919 ref is the hex encoded hash of the entry.
920 """
921 ret = {}
922 opts = ['ls-tree', '--full-tree']
923 if recurse:
924 opts.append('-r')
925 opts.append(treeref)
926 try:
927 for line in run(*opts).splitlines():
928 mode, typ, ref, name = line.split(None, 3)
929 ret[name] = (mode, typ, ref)
930 except subprocess2.CalledProcessError:
931 return None
932 return ret
933
934
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000935def upstream(branch):
936 try:
937 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
938 branch+'@{upstream}')
939 except subprocess2.CalledProcessError:
940 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000941
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000942
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000943def get_git_version():
944 """Returns a tuple that contains the numeric components of the current git
945 version."""
946 version_string = run('--version')
947 version_match = re.search(r'(\d+.)+(\d+)', version_string)
948 version = version_match.group() if version_match else ''
949
950 return tuple(int(x) for x in version.split('.'))
951
952
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000953def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000954 format_string = (
955 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
956
957 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000958 if (include_tracking_status and
959 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000960 format_string += '%(upstream:track)'
961
962 info_map = {}
963 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000964 BranchesInfo = collections.namedtuple(
965 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000966 for line in data.splitlines():
967 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
968
969 ahead_match = re.search(r'ahead (\d+)', tracking_status)
970 ahead = int(ahead_match.group(1)) if ahead_match else None
971
972 behind_match = re.search(r'behind (\d+)', tracking_status)
973 behind = int(behind_match.group(1)) if behind_match else None
974
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000975 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000976 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
977
978 # Set None for upstreams which are not branches (e.g empty upstream, remotes
979 # and deleted upstream branches).
980 missing_upstreams = {}
981 for info in info_map.values():
982 if info.upstream not in info_map and info.upstream not in missing_upstreams:
983 missing_upstreams[info.upstream] = None
984
985 return dict(info_map.items() + missing_upstreams.items())
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000986
987
988def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000989 files_to_copy, symlink=None):
990 if not symlink:
991 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000992 os.makedirs(new_workdir)
993 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000994 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000995 for entry in files_to_copy:
996 clone_file(repository, new_workdir, entry, shutil.copy)
997
998
999def make_workdir(repository, new_workdir):
1000 GIT_DIRECTORY_WHITELIST = [
1001 'config',
1002 'info',
1003 'hooks',
1004 'logs/refs',
1005 'objects',
1006 'packed-refs',
1007 'refs',
1008 'remotes',
1009 'rr-cache',
1010 'svn'
1011 ]
1012 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
1013 ['HEAD'])
1014
1015
1016def clone_file(repository, new_workdir, link, operation):
1017 if not os.path.exists(os.path.join(repository, link)):
1018 return
1019 link_dir = os.path.dirname(os.path.join(new_workdir, link))
1020 if not os.path.exists(link_dir):
1021 os.makedirs(link_dir)
1022 operation(os.path.join(repository, link), os.path.join(new_workdir, link))