blob: ab936cb17600fc918d59bd1f8e4167422af842a8 [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
Mark Mentovaif548d082017-03-08 13:32:00 -0500297def blame(filename, revision=None, porcelain=False, abbrev=None, *_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)
Mark Mentovaif548d082017-03-08 13:32:00 -0500303 if abbrev is not None:
304 command.append('--abbrev=%d' % abbrev)
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000305 command.extend(['--', filename])
306 return run(*command)
307
308
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000309def branch_config(branch, option, default=None):
agable7aa2ddd2016-06-21 07:47:00 -0700310 return get_config('branch.%s.%s' % (branch, option), default=default)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000311
312
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000313def branch_config_map(option):
314 """Return {branch: <|option| value>} for all branches."""
315 try:
316 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
agable7aa2ddd2016-06-21 07:47:00 -0700317 lines = get_config_regexp(reg.pattern)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000318 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
319 except subprocess2.CalledProcessError:
320 return {}
321
322
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000323def branches(*args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000324 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000325
326 key = 'depot-tools.branch-limit'
agable7aa2ddd2016-06-21 07:47:00 -0700327 limit = get_config_int(key, 20)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000328
329 raw_branches = run('branch', *args).splitlines()
330
331 num = len(raw_branches)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000332
agable7aa2ddd2016-06-21 07:47:00 -0700333 if num > limit:
334 die("""\
335 Your git repo has too many branches (%d/%d) for this tool to work well.
336
337 You may adjust this limit by running:
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000338 git config %s <new_limit>
agable7aa2ddd2016-06-21 07:47:00 -0700339
340 You may also try cleaning up your old branches by running:
341 git cl archive
342 """, num, limit, key)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000343
344 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000345 if line.startswith(NO_BRANCH):
346 continue
347 yield line.split()[-1]
348
349
agable7aa2ddd2016-06-21 07:47:00 -0700350def get_config(option, default=None):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000351 try:
352 return run('config', '--get', option) or default
353 except subprocess2.CalledProcessError:
354 return default
355
356
agable7aa2ddd2016-06-21 07:47:00 -0700357def get_config_int(option, default=0):
358 assert isinstance(default, int)
359 try:
360 return int(get_config(option, default))
361 except ValueError:
362 return default
363
364
365def get_config_list(option):
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000366 try:
367 return run('config', '--get-all', option).split()
368 except subprocess2.CalledProcessError:
369 return []
370
371
agable7aa2ddd2016-06-21 07:47:00 -0700372def get_config_regexp(pattern):
373 if IS_WIN: # pragma: no cover
374 # this madness is because we call git.bat which calls git.exe which calls
375 # bash.exe (or something to that effect). Each layer divides the number of
376 # ^'s by 2.
377 pattern = pattern.replace('^', '^' * 8)
378 return run('config', '--get-regexp', pattern).splitlines()
379
380
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000381def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000382 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000383 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000384 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000385 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000386
387
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000388def del_branch_config(branch, option, scope='local'):
389 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000390
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000391
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000392def del_config(option, scope='local'):
393 try:
394 run('config', '--' + scope, '--unset', option)
395 except subprocess2.CalledProcessError:
396 pass
397
398
mgiuca@chromium.org01d2cde2016-02-05 03:25:41 +0000399def diff(oldrev, newrev, *args):
400 return run('diff', oldrev, newrev, *args)
401
402
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000403def freeze():
404 took_action = False
agable02b3c982016-06-22 07:51:22 -0700405 key = 'depot-tools.freeze-size-limit'
406 MB = 2**20
407 limit_mb = get_config_int(key, 100)
408 untracked_bytes = 0
409
iannuccieaca0332016-08-03 16:46:50 -0700410 root_path = repo_root()
411
agable02b3c982016-06-22 07:51:22 -0700412 for f, s in status():
413 if is_unmerged(s):
414 die("Cannot freeze unmerged changes!")
415 if limit_mb > 0:
416 if s.lstat == '?':
iannuccieaca0332016-08-03 16:46:50 -0700417 untracked_bytes += os.stat(os.path.join(root_path, f)).st_size
agable02b3c982016-06-22 07:51:22 -0700418 if untracked_bytes > limit_mb * MB:
419 die("""\
420 You appear to have too much untracked+unignored data in your git
421 checkout: %.1f / %d MB.
422
423 Run `git status` to see what it is.
424
425 In addition to making many git commands slower, this will prevent
426 depot_tools from freezing your in-progress changes.
427
428 You should add untracked data that you want to ignore to your repo's
Marc-Antoine Ruel328b00f2017-02-04 17:44:21 -0500429 .git/info/exclude
agable02b3c982016-06-22 07:51:22 -0700430 file. See `git help ignore` for the format of this file.
431
432 If this data is indended as part of your commit, you may adjust the
433 freeze limit by running:
434 git config %s <new_limit>
435 Where <new_limit> is an integer threshold in megabytes.""",
436 untracked_bytes / (MB * 1.0), limit_mb, key)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000437
438 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000439 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000440 took_action = True
441 except subprocess2.CalledProcessError:
442 pass
443
agable96e179b2016-06-24 10:32:51 -0700444 add_errors = False
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000445 try:
agable96e179b2016-06-24 10:32:51 -0700446 run('add', '-A', '--ignore-errors')
447 except subprocess2.CalledProcessError:
448 add_errors = True
449
450 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000451 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000452 took_action = True
453 except subprocess2.CalledProcessError:
454 pass
455
agable96e179b2016-06-24 10:32:51 -0700456 ret = []
457 if add_errors:
458 ret.append('Failed to index some unindexed files.')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000459 if not took_action:
agable96e179b2016-06-24 10:32:51 -0700460 ret.append('Nothing to freeze.')
461 return ' '.join(ret) or None
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000462
463
464def get_branch_tree():
465 """Get the dictionary of {branch: parent}, compatible with topo_iter.
466
467 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
468 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000469 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000470 skipped = set()
471 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000472
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000473 for branch in branches():
474 parent = upstream(branch)
475 if not parent:
476 skipped.add(branch)
477 continue
478 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000479
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000480 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000481
482
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000483def get_or_create_merge_base(branch, parent=None):
484 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000485
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000486 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000487 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000488 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000489 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000490 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000491 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000492 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000493 actual_merge_base = run('merge-base', parent, branch)
494
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000495 if base_upstream != parent:
496 base = None
497 base_upstream = None
498
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000499 def is_ancestor(a, b):
500 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
501
clemensh@chromium.orgc3fe99d2016-04-19 08:39:55 +0000502 if base and base != actual_merge_base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000503 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000504 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
505 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000506 elif is_ancestor(base, actual_merge_base):
507 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
508 base = None
509 else:
510 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000511
512 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000513 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000514 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000515
516 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000517
518
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000519def hash_multi(*reflike):
520 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000521
522
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000523def hash_one(reflike, short=False):
524 args = ['rev-parse', reflike]
525 if short:
526 args.insert(1, '--short')
527 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000528
529
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000530def in_rebase():
531 git_dir = run('rev-parse', '--git-dir')
532 return (
533 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
534 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000535
536
537def intern_f(f, kind='blob'):
538 """Interns a file object into the git object store.
539
540 Args:
541 f (file-like object) - The file-like object to intern
542 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
543
544 Returns the git hash of the interned object (hex encoded).
545 """
546 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
547 f.close()
548 return ret
549
550
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000551def is_dormant(branch):
552 # TODO(iannucci): Do an oldness check?
553 return branch_config(branch, 'dormant', 'false') != 'false'
554
555
agable02b3c982016-06-22 07:51:22 -0700556def is_unmerged(stat_value):
557 return (
558 'U' in (stat_value.lstat, stat_value.rstat) or
559 ((stat_value.lstat == stat_value.rstat) and stat_value.lstat in 'AD')
560 )
561
562
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000563def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000564 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000565 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000566
567
568def mktree(treedict):
569 """Makes a git tree object and returns its hash.
570
571 See |tree()| for the values of mode, type, and ref.
572
573 Args:
574 treedict - { name: (mode, type, ref) }
575 """
576 with tempfile.TemporaryFile() as f:
577 for name, (mode, typ, ref) in treedict.iteritems():
578 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
579 f.seek(0)
580 return run('mktree', '-z', stdin=f)
581
582
583def parse_commitrefs(*commitrefs):
584 """Returns binary encoded commit hashes for one or more commitrefs.
585
586 A commitref is anything which can resolve to a commit. Popular examples:
587 * 'HEAD'
588 * 'origin/master'
589 * 'cool_branch~2'
590 """
591 try:
592 return map(binascii.unhexlify, hash_multi(*commitrefs))
593 except subprocess2.CalledProcessError:
594 raise BadCommitRefException(commitrefs)
595
596
sbc@chromium.org384039b2014-10-13 21:01:00 +0000597RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000598
599
600def rebase(parent, start, branch, abort=False):
601 """Rebases |start|..|branch| onto the branch |parent|.
602
603 Args:
604 parent - The new parent ref for the rebased commits.
605 start - The commit to start from
606 branch - The branch to rebase
607 abort - If True, will call git-rebase --abort in the event that the rebase
608 doesn't complete successfully.
609
610 Returns a namedtuple with fields:
611 success - a boolean indicating that the rebase command completed
612 successfully.
613 message - if the rebase failed, this contains the stdout of the failed
614 rebase.
615 """
616 try:
617 args = ['--onto', parent, start, branch]
618 if TEST_MODE:
619 args.insert(0, '--committer-date-is-author-date')
620 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000621 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000622 except subprocess2.CalledProcessError as cpe:
623 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000624 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000625 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000626
627
628def remove_merge_base(branch):
629 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000630 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000631
632
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000633def repo_root():
634 """Returns the absolute path to the repository root."""
635 return run('rev-parse', '--show-toplevel')
636
637
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000638def root():
agable7aa2ddd2016-06-21 07:47:00 -0700639 return get_config('depot-tools.upstream', 'origin/master')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000640
641
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000642@contextlib.contextmanager
643def less(): # pragma: no cover
644 """Runs 'less' as context manager yielding its stdin as a PIPE.
645
646 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
647 running less and just yields sys.stdout.
648 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000649 if not setup_color.IS_TTY:
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000650 yield sys.stdout
651 return
652
653 # Run with the same options that git uses (see setup_pager in git repo).
654 # -F: Automatically quit if the output is less than one screen.
655 # -R: Don't escape ANSI color codes.
656 # -X: Don't clear the screen before starting.
657 cmd = ('less', '-FRX')
658 try:
659 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
660 yield proc.stdin
661 finally:
662 proc.stdin.close()
663 proc.wait()
664
665
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000666def run(*cmd, **kwargs):
667 """The same as run_with_stderr, except it only returns stdout."""
668 return run_with_stderr(*cmd, **kwargs)[0]
669
670
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000671def run_with_retcode(*cmd, **kwargs):
672 """Run a command but only return the status code."""
673 try:
674 run(*cmd, **kwargs)
675 return 0
676 except subprocess2.CalledProcessError as cpe:
677 return cpe.returncode
678
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000679def run_stream(*cmd, **kwargs):
680 """Runs a git command. Returns stdout as a PIPE (file-like object).
681
682 stderr is dropped to avoid races if the process outputs to both stdout and
683 stderr.
684 """
685 kwargs.setdefault('stderr', subprocess2.VOID)
686 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000687 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000688 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000689 proc = subprocess2.Popen(cmd, **kwargs)
690 return proc.stdout
691
692
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000693@contextlib.contextmanager
694def run_stream_with_retcode(*cmd, **kwargs):
695 """Runs a git command as context manager yielding stdout as a PIPE.
696
697 stderr is dropped to avoid races if the process outputs to both stdout and
698 stderr.
699
700 Raises subprocess2.CalledProcessError on nonzero return code.
701 """
702 kwargs.setdefault('stderr', subprocess2.VOID)
703 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000704 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000705 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
706 try:
707 proc = subprocess2.Popen(cmd, **kwargs)
708 yield proc.stdout
709 finally:
710 retcode = proc.wait()
711 if retcode != 0:
712 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
713 None, None)
714
715
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000716def run_with_stderr(*cmd, **kwargs):
717 """Runs a git command.
718
719 Returns (stdout, stderr) as a pair of strings.
720
721 kwargs
722 autostrip (bool) - Strip the output. Defaults to True.
723 indata (str) - Specifies stdin data for the process.
724 """
725 kwargs.setdefault('stdin', subprocess2.PIPE)
726 kwargs.setdefault('stdout', subprocess2.PIPE)
727 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000728 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000729 autostrip = kwargs.pop('autostrip', True)
730 indata = kwargs.pop('indata', None)
731
iannucci@chromium.org21980022014-04-11 04:51:49 +0000732 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000733 proc = subprocess2.Popen(cmd, **kwargs)
734 ret, err = proc.communicate(indata)
735 retcode = proc.wait()
736 if retcode != 0:
737 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
738
739 if autostrip:
740 ret = (ret or '').strip()
741 err = (err or '').strip()
742
743 return ret, err
744
745
746def set_branch_config(branch, option, value, scope='local'):
747 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
748
749
750def set_config(option, value, scope='local'):
751 run('config', '--' + scope, option, value)
752
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000753
sbc@chromium.org71437c02015-04-09 19:29:40 +0000754def get_dirty_files():
755 # Make sure index is up-to-date before running diff-index.
756 run_with_retcode('update-index', '--refresh', '-q')
757 return run('diff-index', '--name-status', 'HEAD')
758
759
760def is_dirty_git_tree(cmd):
iannuccie38699b2016-08-15 17:32:31 -0700761 w = lambda s: sys.stderr.write(s+"\n")
762
sbc@chromium.org71437c02015-04-09 19:29:40 +0000763 dirty = get_dirty_files()
764 if dirty:
iannuccie38699b2016-08-15 17:32:31 -0700765 w('Cannot %s with a dirty tree. Commit, freeze or stash your changes first.'
766 % cmd)
767 w('Uncommitted files: (git diff-index --name-status HEAD)')
768 w(dirty[:4096])
sbc@chromium.org71437c02015-04-09 19:29:40 +0000769 if len(dirty) > 4096: # pragma: no cover
iannuccie38699b2016-08-15 17:32:31 -0700770 w('... (run "git diff-index --name-status HEAD" to see full output).')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000771 return True
772 return False
773
774
agable02b3c982016-06-22 07:51:22 -0700775def status():
776 """Returns a parsed version of git-status.
777
778 Returns a generator of (current_name, (lstat, rstat, src)) pairs where:
779 * current_name is the name of the file
780 * lstat is the left status code letter from git-status
781 * rstat is the left status code letter from git-status
782 * src is the current name of the file, or the original name of the file
783 if lstat == 'R'
784 """
785 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src')
786
787 def tokenizer(stream):
788 acc = StringIO()
789 c = None
790 while c != '':
791 c = stream.read(1)
792 if c in (None, '', '\0'):
793 if acc.len:
794 yield acc.getvalue()
795 acc = StringIO()
796 else:
797 acc.write(c)
798
799 def parser(tokens):
800 while True:
801 # Raises StopIteration if it runs out of tokens.
802 status_dest = next(tokens)
803 stat, dest = status_dest[:2], status_dest[3:]
804 lstat, rstat = stat
805 if lstat == 'R':
806 src = next(tokens)
807 else:
808 src = dest
809 yield (dest, stat_entry(lstat, rstat, src))
810
811 return parser(tokenizer(run_stream('status', '-z', bufsize=-1)))
812
813
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000814def squash_current_branch(header=None, merge_base=None):
Alan Cutter00017822016-12-20 17:39:59 +1100815 header = header or 'git squash commit for %s.' % current_branch()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000816 merge_base = merge_base or get_or_create_merge_base(current_branch())
817 log_msg = header + '\n'
818 if log_msg:
819 log_msg += '\n'
820 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
821 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000822
823 if not get_dirty_files():
824 # Sometimes the squash can result in the same tree, meaning that there is
825 # nothing to commit at this point.
826 print 'Nothing to commit; squashed branch is empty'
827 return False
maruel@chromium.org25b9ab22015-06-18 18:49:03 +0000828 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000829 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000830
831
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000832def tags(*args):
833 return run('tag', *args).splitlines()
834
835
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000836def thaw():
837 took_action = False
838 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
839 msg = run('show', '--format=%f%b', '-s', 'HEAD')
840 match = FREEZE_MATCHER.match(msg)
841 if not match:
842 if not took_action:
843 return 'Nothing to thaw.'
844 break
845
846 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
847 took_action = True
848
849
850def topo_iter(branch_tree, top_down=True):
851 """Generates (branch, parent) in topographical order for a branch tree.
852
853 Given a tree:
854
855 A1
856 B1 B2
857 C1 C2 C3
858 D1
859
860 branch_tree would look like: {
861 'D1': 'C3',
862 'C3': 'B2',
863 'B2': 'A1',
864 'C1': 'B1',
865 'C2': 'B1',
866 'B1': 'A1',
867 }
868
869 It is OK to have multiple 'root' nodes in your graph.
870
871 if top_down is True, items are yielded from A->D. Otherwise they're yielded
872 from D->A. Within a layer the branches will be yielded in sorted order.
873 """
874 branch_tree = branch_tree.copy()
875
876 # TODO(iannucci): There is probably a more efficient way to do these.
877 if top_down:
878 while branch_tree:
879 this_pass = [(b, p) for b, p in branch_tree.iteritems()
880 if p not in branch_tree]
881 assert this_pass, "Branch tree has cycles: %r" % branch_tree
882 for branch, parent in sorted(this_pass):
883 yield branch, parent
884 del branch_tree[branch]
885 else:
886 parent_to_branches = collections.defaultdict(set)
887 for branch, parent in branch_tree.iteritems():
888 parent_to_branches[parent].add(branch)
889
890 while branch_tree:
891 this_pass = [(b, p) for b, p in branch_tree.iteritems()
892 if not parent_to_branches[b]]
893 assert this_pass, "Branch tree has cycles: %r" % branch_tree
894 for branch, parent in sorted(this_pass):
895 yield branch, parent
896 parent_to_branches[parent].discard(branch)
897 del branch_tree[branch]
898
899
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000900def tree(treeref, recurse=False):
901 """Returns a dict representation of a git tree object.
902
903 Args:
904 treeref (str) - a git ref which resolves to a tree (commits count as trees).
qyearsley12fa6ff2016-08-24 09:18:40 -0700905 recurse (bool) - include all of the tree's descendants too. File names will
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000906 take the form of 'some/path/to/file'.
907
908 Return format:
909 { 'file_name': (mode, type, ref) }
910
911 mode is an integer where:
912 * 0040000 - Directory
913 * 0100644 - Regular non-executable file
914 * 0100664 - Regular non-executable group-writeable file
915 * 0100755 - Regular executable file
916 * 0120000 - Symbolic link
917 * 0160000 - Gitlink
918
919 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
920
921 ref is the hex encoded hash of the entry.
922 """
923 ret = {}
924 opts = ['ls-tree', '--full-tree']
925 if recurse:
926 opts.append('-r')
927 opts.append(treeref)
928 try:
929 for line in run(*opts).splitlines():
930 mode, typ, ref, name = line.split(None, 3)
931 ret[name] = (mode, typ, ref)
932 except subprocess2.CalledProcessError:
933 return None
934 return ret
935
936
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000937def upstream(branch):
938 try:
939 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
940 branch+'@{upstream}')
941 except subprocess2.CalledProcessError:
942 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000943
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000944
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000945def get_git_version():
946 """Returns a tuple that contains the numeric components of the current git
947 version."""
948 version_string = run('--version')
949 version_match = re.search(r'(\d+.)+(\d+)', version_string)
950 version = version_match.group() if version_match else ''
951
952 return tuple(int(x) for x in version.split('.'))
953
954
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000955def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000956 format_string = (
957 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
958
959 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000960 if (include_tracking_status and
961 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000962 format_string += '%(upstream:track)'
963
964 info_map = {}
965 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000966 BranchesInfo = collections.namedtuple(
967 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000968 for line in data.splitlines():
969 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
970
971 ahead_match = re.search(r'ahead (\d+)', tracking_status)
972 ahead = int(ahead_match.group(1)) if ahead_match else None
973
974 behind_match = re.search(r'behind (\d+)', tracking_status)
975 behind = int(behind_match.group(1)) if behind_match else None
976
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000977 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000978 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
979
980 # Set None for upstreams which are not branches (e.g empty upstream, remotes
981 # and deleted upstream branches).
982 missing_upstreams = {}
983 for info in info_map.values():
984 if info.upstream not in info_map and info.upstream not in missing_upstreams:
985 missing_upstreams[info.upstream] = None
986
987 return dict(info_map.items() + missing_upstreams.items())
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000988
989
990def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000991 files_to_copy, symlink=None):
992 if not symlink:
993 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000994 os.makedirs(new_workdir)
995 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +0000996 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +0000997 for entry in files_to_copy:
998 clone_file(repository, new_workdir, entry, shutil.copy)
999
1000
1001def make_workdir(repository, new_workdir):
1002 GIT_DIRECTORY_WHITELIST = [
1003 'config',
1004 'info',
1005 'hooks',
1006 'logs/refs',
1007 'objects',
1008 'packed-refs',
1009 'refs',
1010 'remotes',
1011 'rr-cache',
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001012 ]
1013 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
1014 ['HEAD'])
1015
1016
1017def clone_file(repository, new_workdir, link, operation):
1018 if not os.path.exists(os.path.join(repository, link)):
1019 return
1020 link_dir = os.path.dirname(os.path.join(new_workdir, link))
1021 if not os.path.exists(link_dir):
1022 os.makedirs(link_dir)
1023 operation(os.path.join(repository, link), os.path.join(new_workdir, link))