blob: 59a904b93bcb959f60f49e03fcc50493fa2826da [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'
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000040TEST_MODE = False
41
Dan Jacques209a6812017-07-12 11:40:20 -070042
43def win_find_git():
44 for elem in os.environ.get('PATH', '').split(os.pathsep):
45 for candidate in ('git.exe', 'git.bat'):
46 path = os.path.join(elem, candidate)
47 if os.path.isfile(path):
48 return path
49 raise ValueError('Could not find Git on PATH.')
50
51
52GIT_EXE = 'git' if not IS_WIN else win_find_git()
53
54
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000055FREEZE = 'FREEZE'
56FREEZE_SECTIONS = {
57 'indexed': 'soft',
58 'unindexed': 'mixed'
59}
60FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000061
62
Dan Jacques2f8b0c12017-04-05 12:57:21 -070063# NOTE: This list is DEPRECATED in favor of the Infra Git wrapper:
64# https://chromium.googlesource.com/infra/infra/+/master/go/src/infra/tools/git
65#
66# New entries should be added to the Git wrapper, NOT to this list. "git_retry"
67# is, similarly, being deprecated in favor of the Git wrapper.
68#
69# ---
70#
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000071# Retry a git operation if git returns a error response with any of these
72# messages. It's all observed 'bad' GoB responses so far.
73#
74# This list is inspired/derived from the one in ChromiumOS's Chromite:
75# <CHROMITE>/lib/git.py::GIT_TRANSIENT_ERRORS
76#
77# It was last imported from '7add3ac29564d98ac35ce426bc295e743e7c0c02'.
78GIT_TRANSIENT_ERRORS = (
79 # crbug.com/285832
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000080 r'!.*\[remote rejected\].*\(error in hook\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000081
82 # crbug.com/289932
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000083 r'!.*\[remote rejected\].*\(failed to lock\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000084
85 # crbug.com/307156
iannucci@chromium.org6e95d402014-08-29 22:10:55 +000086 r'!.*\[remote rejected\].*\(error in Gerrit backend\)',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000087
88 # crbug.com/285832
89 r'remote error: Internal Server Error',
90
91 # crbug.com/294449
92 r'fatal: Couldn\'t find remote ref ',
93
94 # crbug.com/220543
95 r'git fetch_pack: expected ACK/NAK, got',
96
97 # crbug.com/189455
98 r'protocol error: bad pack header',
99
100 # crbug.com/202807
101 r'The remote end hung up unexpectedly',
102
103 # crbug.com/298189
104 r'TLS packet with unexpected length was received',
105
106 # crbug.com/187444
107 r'RPC failed; result=\d+, HTTP code = \d+',
108
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000109 # crbug.com/388876
110 r'Connection timed out',
dnj@chromium.org45cddd62014-11-06 19:36:42 +0000111
112 # crbug.com/430343
113 # TODO(dnj): Resync with Chromite.
114 r'The requested URL returned error: 5\d+',
Arikonb3a21482016-07-22 10:12:24 -0700115
116 r'Connection reset by peer',
117
118 r'Unable to look up',
119
120 r'Couldn\'t resolve host',
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000121)
122
123GIT_TRANSIENT_ERRORS_RE = re.compile('|'.join(GIT_TRANSIENT_ERRORS),
124 re.IGNORECASE)
125
raphael.kubo.da.costa@intel.com58d05b02015-06-24 08:54:41 +0000126# git's for-each-ref command first supported the upstream:track token in its
127# format string in version 1.9.0, but some usages were broken until 2.3.0.
128# See git commit b6160d95 for more information.
129MIN_UPSTREAM_TRACK_GIT_VERSION = (2, 3)
dnj@chromium.orgde219ec2014-07-28 17:39:08 +0000130
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000131class BadCommitRefException(Exception):
132 def __init__(self, refs):
133 msg = ('one of %s does not seem to be a valid commitref.' %
134 str(refs))
135 super(BadCommitRefException, self).__init__(msg)
136
137
138def memoize_one(**kwargs):
139 """Memoizes a single-argument pure function.
140
141 Values of None are not cached.
142
143 Kwargs:
144 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
145 cache manipulation functions. This is a kwarg so that users of memoize_one
146 are forced to explicitly and verbosely pick True or False.
147
148 Adds three methods to the decorated function:
149 * get(key, default=None) - Gets the value for this key from the cache.
150 * set(key, value) - Sets the value for this key from the cache.
151 * clear() - Drops the entire contents of the cache. Useful for unittests.
152 * update(other) - Updates the contents of the cache from another dict.
153 """
154 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
155 threadsafe = kwargs['threadsafe']
156
157 if threadsafe:
158 def withlock(lock, f):
159 def inner(*args, **kwargs):
160 with lock:
161 return f(*args, **kwargs)
162 return inner
163 else:
164 def withlock(_lock, f):
165 return f
166
167 def decorator(f):
168 # Instantiate the lock in decorator, in case users of memoize_one do:
169 #
170 # memoizer = memoize_one(threadsafe=True)
171 #
172 # @memoizer
173 # def fn1(val): ...
174 #
175 # @memoizer
176 # def fn2(val): ...
177
178 lock = threading.Lock() if threadsafe else None
179 cache = {}
180 _get = withlock(lock, cache.get)
181 _set = withlock(lock, cache.__setitem__)
182
183 @functools.wraps(f)
184 def inner(arg):
185 ret = _get(arg)
186 if ret is None:
187 ret = f(arg)
188 if ret is not None:
189 _set(arg, ret)
190 return ret
191 inner.get = _get
192 inner.set = _set
193 inner.clear = withlock(lock, cache.clear)
194 inner.update = withlock(lock, cache.update)
195 return inner
196 return decorator
197
198
199def _ScopedPool_initer(orig, orig_args): # pragma: no cover
200 """Initializer method for ScopedPool's subprocesses.
201
202 This helps ScopedPool handle Ctrl-C's correctly.
203 """
204 signal.signal(signal.SIGINT, signal.SIG_IGN)
205 if orig:
206 orig(*orig_args)
207
208
209@contextlib.contextmanager
210def ScopedPool(*args, **kwargs):
211 """Context Manager which returns a multiprocessing.pool instance which
212 correctly deals with thrown exceptions.
213
214 *args - Arguments to multiprocessing.pool
215
216 Kwargs:
217 kind ('threads', 'procs') - The type of underlying coprocess to use.
218 **etc - Arguments to multiprocessing.pool
219 """
220 if kwargs.pop('kind', None) == 'threads':
221 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
222 else:
223 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
224 kwargs['initializer'] = _ScopedPool_initer
225 kwargs['initargs'] = orig, orig_args
226 pool = multiprocessing.pool.Pool(*args, **kwargs)
227
228 try:
229 yield pool
230 pool.close()
231 except:
232 pool.terminate()
233 raise
234 finally:
235 pool.join()
236
237
238class ProgressPrinter(object):
239 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000240 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000241 """Create a ProgressPrinter.
242
243 Use it as a context manager which produces a simple 'increment' method:
244
245 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
246 for i in xrange(1000):
247 # do stuff
248 if i % 10 == 0:
249 inc(10)
250
251 Args:
252 fmt - String format with a single '%(count)d' where the counter value
253 should go.
254 enabled (bool) - If this is None, will default to True if
255 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000256 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000257 period (float) - The time in seconds for the printer thread to wait
258 between printing.
259 """
260 self.fmt = fmt
261 if enabled is None: # pragma: no cover
262 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
263 else:
264 self.enabled = enabled
265
266 self._count = 0
267 self._dead = False
268 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000269 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000270 self._thread = threading.Thread(target=self._run)
271 self._period = period
272
273 def _emit(self, s):
274 if self.enabled:
275 self._stream.write('\r' + s)
276 self._stream.flush()
277
278 def _run(self):
279 with self._dead_cond:
280 while not self._dead:
281 self._emit(self.fmt % {'count': self._count})
282 self._dead_cond.wait(self._period)
283 self._emit((self.fmt + '\n') % {'count': self._count})
284
285 def inc(self, amount=1):
286 self._count += amount
287
288 def __enter__(self):
289 self._thread.start()
290 return self.inc
291
292 def __exit__(self, _exc_type, _exc_value, _traceback):
293 self._dead = True
294 with self._dead_cond:
295 self._dead_cond.notifyAll()
296 self._thread.join()
297 del self._thread
298
299
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000300def once(function):
301 """@Decorates |function| so that it only performs its action once, no matter
302 how many times the decorated |function| is called."""
303 def _inner_gen():
304 yield function()
305 while True:
306 yield
307 return _inner_gen().next
308
309
310## Git functions
311
agable7aa2ddd2016-06-21 07:47:00 -0700312def die(message, *args):
313 print >> sys.stderr, textwrap.dedent(message % args)
314 sys.exit(1)
315
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000316
Mark Mentovaif548d082017-03-08 13:32:00 -0500317def blame(filename, revision=None, porcelain=False, abbrev=None, *_args):
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000318 command = ['blame']
319 if porcelain:
320 command.append('-p')
321 if revision is not None:
322 command.append(revision)
Mark Mentovaif548d082017-03-08 13:32:00 -0500323 if abbrev is not None:
324 command.append('--abbrev=%d' % abbrev)
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000325 command.extend(['--', filename])
326 return run(*command)
327
328
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000329def branch_config(branch, option, default=None):
agable7aa2ddd2016-06-21 07:47:00 -0700330 return get_config('branch.%s.%s' % (branch, option), default=default)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000331
332
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000333def branch_config_map(option):
334 """Return {branch: <|option| value>} for all branches."""
335 try:
336 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
agable7aa2ddd2016-06-21 07:47:00 -0700337 lines = get_config_regexp(reg.pattern)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000338 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
339 except subprocess2.CalledProcessError:
340 return {}
341
342
Francois Dorayd42c6812017-05-30 15:10:20 -0400343def branches(use_limit=True, *args):
akuegel@chromium.org58888e12015-06-09 15:26:37 +0000344 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000345
346 key = 'depot-tools.branch-limit'
agable7aa2ddd2016-06-21 07:47:00 -0700347 limit = get_config_int(key, 20)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000348
349 raw_branches = run('branch', *args).splitlines()
350
351 num = len(raw_branches)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000352
Francois Dorayd42c6812017-05-30 15:10:20 -0400353 if use_limit and num > limit:
agable7aa2ddd2016-06-21 07:47:00 -0700354 die("""\
355 Your git repo has too many branches (%d/%d) for this tool to work well.
356
357 You may adjust this limit by running:
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000358 git config %s <new_limit>
agable7aa2ddd2016-06-21 07:47:00 -0700359
360 You may also try cleaning up your old branches by running:
361 git cl archive
362 """, num, limit, key)
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000363
364 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000365 if line.startswith(NO_BRANCH):
366 continue
367 yield line.split()[-1]
368
369
agable7aa2ddd2016-06-21 07:47:00 -0700370def get_config(option, default=None):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000371 try:
372 return run('config', '--get', option) or default
373 except subprocess2.CalledProcessError:
374 return default
375
376
agable7aa2ddd2016-06-21 07:47:00 -0700377def get_config_int(option, default=0):
378 assert isinstance(default, int)
379 try:
380 return int(get_config(option, default))
381 except ValueError:
382 return default
383
384
385def get_config_list(option):
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000386 try:
387 return run('config', '--get-all', option).split()
388 except subprocess2.CalledProcessError:
389 return []
390
391
agable7aa2ddd2016-06-21 07:47:00 -0700392def get_config_regexp(pattern):
393 if IS_WIN: # pragma: no cover
394 # this madness is because we call git.bat which calls git.exe which calls
395 # bash.exe (or something to that effect). Each layer divides the number of
396 # ^'s by 2.
397 pattern = pattern.replace('^', '^' * 8)
398 return run('config', '--get-regexp', pattern).splitlines()
399
400
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000401def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000402 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000403 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000404 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000405 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000406
407
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000408def del_branch_config(branch, option, scope='local'):
409 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000410
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000411
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000412def del_config(option, scope='local'):
413 try:
414 run('config', '--' + scope, '--unset', option)
415 except subprocess2.CalledProcessError:
416 pass
417
418
mgiuca@chromium.org01d2cde2016-02-05 03:25:41 +0000419def diff(oldrev, newrev, *args):
420 return run('diff', oldrev, newrev, *args)
421
422
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000423def freeze():
424 took_action = False
agable02b3c982016-06-22 07:51:22 -0700425 key = 'depot-tools.freeze-size-limit'
426 MB = 2**20
427 limit_mb = get_config_int(key, 100)
428 untracked_bytes = 0
429
iannuccieaca0332016-08-03 16:46:50 -0700430 root_path = repo_root()
431
agable02b3c982016-06-22 07:51:22 -0700432 for f, s in status():
433 if is_unmerged(s):
434 die("Cannot freeze unmerged changes!")
435 if limit_mb > 0:
436 if s.lstat == '?':
iannuccieaca0332016-08-03 16:46:50 -0700437 untracked_bytes += os.stat(os.path.join(root_path, f)).st_size
agable02b3c982016-06-22 07:51:22 -0700438 if untracked_bytes > limit_mb * MB:
439 die("""\
440 You appear to have too much untracked+unignored data in your git
441 checkout: %.1f / %d MB.
442
443 Run `git status` to see what it is.
444
445 In addition to making many git commands slower, this will prevent
446 depot_tools from freezing your in-progress changes.
447
448 You should add untracked data that you want to ignore to your repo's
Marc-Antoine Ruel328b00f2017-02-04 17:44:21 -0500449 .git/info/exclude
agable02b3c982016-06-22 07:51:22 -0700450 file. See `git help ignore` for the format of this file.
451
452 If this data is indended as part of your commit, you may adjust the
453 freeze limit by running:
454 git config %s <new_limit>
455 Where <new_limit> is an integer threshold in megabytes.""",
456 untracked_bytes / (MB * 1.0), limit_mb, key)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000457
458 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000459 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000460 took_action = True
461 except subprocess2.CalledProcessError:
462 pass
463
agable96e179b2016-06-24 10:32:51 -0700464 add_errors = False
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000465 try:
agable96e179b2016-06-24 10:32:51 -0700466 run('add', '-A', '--ignore-errors')
467 except subprocess2.CalledProcessError:
468 add_errors = True
469
470 try:
iannucci@chromium.org3b4f2282015-09-17 15:46:00 +0000471 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000472 took_action = True
473 except subprocess2.CalledProcessError:
474 pass
475
agable96e179b2016-06-24 10:32:51 -0700476 ret = []
477 if add_errors:
478 ret.append('Failed to index some unindexed files.')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000479 if not took_action:
agable96e179b2016-06-24 10:32:51 -0700480 ret.append('Nothing to freeze.')
481 return ' '.join(ret) or None
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000482
483
484def get_branch_tree():
485 """Get the dictionary of {branch: parent}, compatible with topo_iter.
486
487 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
488 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000489 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000490 skipped = set()
491 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000492
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000493 for branch in branches():
494 parent = upstream(branch)
495 if not parent:
496 skipped.add(branch)
497 continue
498 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000499
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000500 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000501
502
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000503def get_or_create_merge_base(branch, parent=None):
504 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000505
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000506 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000507 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000508 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000509 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000510 parent = parent or upstream(branch)
sbc@chromium.org79706062015-01-14 21:18:12 +0000511 if parent is None or branch is None:
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000512 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000513 actual_merge_base = run('merge-base', parent, branch)
514
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000515 if base_upstream != parent:
516 base = None
517 base_upstream = None
518
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000519 def is_ancestor(a, b):
520 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
521
clemensh@chromium.orgc3fe99d2016-04-19 08:39:55 +0000522 if base and base != actual_merge_base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000523 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000524 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
525 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000526 elif is_ancestor(base, actual_merge_base):
527 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
528 base = None
529 else:
530 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000531
532 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000533 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000534 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000535
536 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000537
538
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000539def hash_multi(*reflike):
540 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000541
542
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000543def hash_one(reflike, short=False):
544 args = ['rev-parse', reflike]
545 if short:
546 args.insert(1, '--short')
547 return run(*args)
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000548
549
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000550def in_rebase():
551 git_dir = run('rev-parse', '--git-dir')
552 return (
553 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
554 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000555
556
557def intern_f(f, kind='blob'):
558 """Interns a file object into the git object store.
559
560 Args:
561 f (file-like object) - The file-like object to intern
562 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
563
564 Returns the git hash of the interned object (hex encoded).
565 """
566 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
567 f.close()
568 return ret
569
570
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000571def is_dormant(branch):
572 # TODO(iannucci): Do an oldness check?
573 return branch_config(branch, 'dormant', 'false') != 'false'
574
575
agable02b3c982016-06-22 07:51:22 -0700576def is_unmerged(stat_value):
577 return (
578 'U' in (stat_value.lstat, stat_value.rstat) or
579 ((stat_value.lstat == stat_value.rstat) and stat_value.lstat in 'AD')
580 )
581
582
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000583def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000584 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000585 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000586
587
588def mktree(treedict):
589 """Makes a git tree object and returns its hash.
590
591 See |tree()| for the values of mode, type, and ref.
592
593 Args:
594 treedict - { name: (mode, type, ref) }
595 """
596 with tempfile.TemporaryFile() as f:
597 for name, (mode, typ, ref) in treedict.iteritems():
598 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
599 f.seek(0)
600 return run('mktree', '-z', stdin=f)
601
602
603def parse_commitrefs(*commitrefs):
604 """Returns binary encoded commit hashes for one or more commitrefs.
605
606 A commitref is anything which can resolve to a commit. Popular examples:
607 * 'HEAD'
608 * 'origin/master'
609 * 'cool_branch~2'
610 """
611 try:
612 return map(binascii.unhexlify, hash_multi(*commitrefs))
613 except subprocess2.CalledProcessError:
614 raise BadCommitRefException(commitrefs)
615
616
sbc@chromium.org384039b2014-10-13 21:01:00 +0000617RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000618
619
620def rebase(parent, start, branch, abort=False):
621 """Rebases |start|..|branch| onto the branch |parent|.
622
623 Args:
624 parent - The new parent ref for the rebased commits.
625 start - The commit to start from
626 branch - The branch to rebase
627 abort - If True, will call git-rebase --abort in the event that the rebase
628 doesn't complete successfully.
629
630 Returns a namedtuple with fields:
631 success - a boolean indicating that the rebase command completed
632 successfully.
633 message - if the rebase failed, this contains the stdout of the failed
634 rebase.
635 """
636 try:
637 args = ['--onto', parent, start, branch]
638 if TEST_MODE:
639 args.insert(0, '--committer-date-is-author-date')
640 run('rebase', *args)
sbc@chromium.org384039b2014-10-13 21:01:00 +0000641 return RebaseRet(True, '', '')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000642 except subprocess2.CalledProcessError as cpe:
643 if abort:
iannucci@chromium.orgdabb78b2015-06-11 23:17:28 +0000644 run_with_retcode('rebase', '--abort') # ignore failure
sbc@chromium.org384039b2014-10-13 21:01:00 +0000645 return RebaseRet(False, cpe.stdout, cpe.stderr)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000646
647
648def remove_merge_base(branch):
649 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000650 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000651
652
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000653def repo_root():
654 """Returns the absolute path to the repository root."""
655 return run('rev-parse', '--show-toplevel')
656
657
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000658def root():
agable7aa2ddd2016-06-21 07:47:00 -0700659 return get_config('depot-tools.upstream', 'origin/master')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000660
661
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000662@contextlib.contextmanager
663def less(): # pragma: no cover
664 """Runs 'less' as context manager yielding its stdin as a PIPE.
665
666 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids
667 running less and just yields sys.stdout.
668 """
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +0000669 if not setup_color.IS_TTY:
mgiuca@chromium.org81937562016-02-03 08:00:53 +0000670 yield sys.stdout
671 return
672
673 # Run with the same options that git uses (see setup_pager in git repo).
674 # -F: Automatically quit if the output is less than one screen.
675 # -R: Don't escape ANSI color codes.
676 # -X: Don't clear the screen before starting.
677 cmd = ('less', '-FRX')
678 try:
679 proc = subprocess2.Popen(cmd, stdin=subprocess2.PIPE)
680 yield proc.stdin
681 finally:
682 proc.stdin.close()
683 proc.wait()
684
685
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000686def run(*cmd, **kwargs):
687 """The same as run_with_stderr, except it only returns stdout."""
688 return run_with_stderr(*cmd, **kwargs)[0]
689
690
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000691def run_with_retcode(*cmd, **kwargs):
692 """Run a command but only return the status code."""
693 try:
694 run(*cmd, **kwargs)
695 return 0
696 except subprocess2.CalledProcessError as cpe:
697 return cpe.returncode
698
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000699def run_stream(*cmd, **kwargs):
700 """Runs a git command. Returns stdout as a PIPE (file-like object).
701
702 stderr is dropped to avoid races if the process outputs to both stdout and
703 stderr.
704 """
705 kwargs.setdefault('stderr', subprocess2.VOID)
706 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000707 kwargs.setdefault('shell', False)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000708 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000709 proc = subprocess2.Popen(cmd, **kwargs)
710 return proc.stdout
711
712
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000713@contextlib.contextmanager
714def run_stream_with_retcode(*cmd, **kwargs):
715 """Runs a git command as context manager yielding stdout as a PIPE.
716
717 stderr is dropped to avoid races if the process outputs to both stdout and
718 stderr.
719
720 Raises subprocess2.CalledProcessError on nonzero return code.
721 """
722 kwargs.setdefault('stderr', subprocess2.VOID)
723 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000724 kwargs.setdefault('shell', False)
tandrii@chromium.org6c143102015-06-11 19:21:02 +0000725 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
726 try:
727 proc = subprocess2.Popen(cmd, **kwargs)
728 yield proc.stdout
729 finally:
730 retcode = proc.wait()
731 if retcode != 0:
732 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(),
733 None, None)
734
735
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000736def run_with_stderr(*cmd, **kwargs):
737 """Runs a git command.
738
739 Returns (stdout, stderr) as a pair of strings.
740
741 kwargs
742 autostrip (bool) - Strip the output. Defaults to True.
743 indata (str) - Specifies stdin data for the process.
744 """
745 kwargs.setdefault('stdin', subprocess2.PIPE)
746 kwargs.setdefault('stdout', subprocess2.PIPE)
747 kwargs.setdefault('stderr', subprocess2.PIPE)
iannucci@chromium.org0d9e59c2016-01-09 08:08:41 +0000748 kwargs.setdefault('shell', False)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000749 autostrip = kwargs.pop('autostrip', True)
750 indata = kwargs.pop('indata', None)
751
iannucci@chromium.org21980022014-04-11 04:51:49 +0000752 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000753 proc = subprocess2.Popen(cmd, **kwargs)
754 ret, err = proc.communicate(indata)
755 retcode = proc.wait()
756 if retcode != 0:
757 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
758
759 if autostrip:
760 ret = (ret or '').strip()
761 err = (err or '').strip()
762
763 return ret, err
764
765
766def set_branch_config(branch, option, value, scope='local'):
767 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
768
769
770def set_config(option, value, scope='local'):
771 run('config', '--' + scope, option, value)
772
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000773
sbc@chromium.org71437c02015-04-09 19:29:40 +0000774def get_dirty_files():
775 # Make sure index is up-to-date before running diff-index.
776 run_with_retcode('update-index', '--refresh', '-q')
777 return run('diff-index', '--name-status', 'HEAD')
778
779
780def is_dirty_git_tree(cmd):
iannuccie38699b2016-08-15 17:32:31 -0700781 w = lambda s: sys.stderr.write(s+"\n")
782
sbc@chromium.org71437c02015-04-09 19:29:40 +0000783 dirty = get_dirty_files()
784 if dirty:
iannuccie38699b2016-08-15 17:32:31 -0700785 w('Cannot %s with a dirty tree. Commit, freeze or stash your changes first.'
786 % cmd)
787 w('Uncommitted files: (git diff-index --name-status HEAD)')
788 w(dirty[:4096])
sbc@chromium.org71437c02015-04-09 19:29:40 +0000789 if len(dirty) > 4096: # pragma: no cover
iannuccie38699b2016-08-15 17:32:31 -0700790 w('... (run "git diff-index --name-status HEAD" to see full output).')
sbc@chromium.org71437c02015-04-09 19:29:40 +0000791 return True
792 return False
793
794
agable02b3c982016-06-22 07:51:22 -0700795def status():
796 """Returns a parsed version of git-status.
797
798 Returns a generator of (current_name, (lstat, rstat, src)) pairs where:
799 * current_name is the name of the file
800 * lstat is the left status code letter from git-status
801 * rstat is the left status code letter from git-status
802 * src is the current name of the file, or the original name of the file
803 if lstat == 'R'
804 """
805 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src')
806
807 def tokenizer(stream):
808 acc = StringIO()
809 c = None
810 while c != '':
811 c = stream.read(1)
812 if c in (None, '', '\0'):
813 if acc.len:
814 yield acc.getvalue()
815 acc = StringIO()
816 else:
817 acc.write(c)
818
819 def parser(tokens):
820 while True:
821 # Raises StopIteration if it runs out of tokens.
822 status_dest = next(tokens)
823 stat, dest = status_dest[:2], status_dest[3:]
824 lstat, rstat = stat
825 if lstat == 'R':
826 src = next(tokens)
827 else:
828 src = dest
829 yield (dest, stat_entry(lstat, rstat, src))
830
831 return parser(tokenizer(run_stream('status', '-z', bufsize=-1)))
832
833
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000834def squash_current_branch(header=None, merge_base=None):
Alan Cutter00017822016-12-20 17:39:59 +1100835 header = header or 'git squash commit for %s.' % current_branch()
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000836 merge_base = merge_base or get_or_create_merge_base(current_branch())
837 log_msg = header + '\n'
838 if log_msg:
839 log_msg += '\n'
840 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
841 run('reset', '--soft', merge_base)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000842
843 if not get_dirty_files():
844 # Sometimes the squash can result in the same tree, meaning that there is
845 # nothing to commit at this point.
846 print 'Nothing to commit; squashed branch is empty'
847 return False
maruel@chromium.org25b9ab22015-06-18 18:49:03 +0000848 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg)
sbc@chromium.org71437c02015-04-09 19:29:40 +0000849 return True
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000850
851
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000852def tags(*args):
853 return run('tag', *args).splitlines()
854
855
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000856def thaw():
857 took_action = False
858 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
859 msg = run('show', '--format=%f%b', '-s', 'HEAD')
860 match = FREEZE_MATCHER.match(msg)
861 if not match:
862 if not took_action:
863 return 'Nothing to thaw.'
864 break
865
866 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
867 took_action = True
868
869
870def topo_iter(branch_tree, top_down=True):
871 """Generates (branch, parent) in topographical order for a branch tree.
872
873 Given a tree:
874
875 A1
876 B1 B2
877 C1 C2 C3
878 D1
879
880 branch_tree would look like: {
881 'D1': 'C3',
882 'C3': 'B2',
883 'B2': 'A1',
884 'C1': 'B1',
885 'C2': 'B1',
886 'B1': 'A1',
887 }
888
889 It is OK to have multiple 'root' nodes in your graph.
890
891 if top_down is True, items are yielded from A->D. Otherwise they're yielded
892 from D->A. Within a layer the branches will be yielded in sorted order.
893 """
894 branch_tree = branch_tree.copy()
895
896 # TODO(iannucci): There is probably a more efficient way to do these.
897 if top_down:
898 while branch_tree:
899 this_pass = [(b, p) for b, p in branch_tree.iteritems()
900 if p not in branch_tree]
901 assert this_pass, "Branch tree has cycles: %r" % branch_tree
902 for branch, parent in sorted(this_pass):
903 yield branch, parent
904 del branch_tree[branch]
905 else:
906 parent_to_branches = collections.defaultdict(set)
907 for branch, parent in branch_tree.iteritems():
908 parent_to_branches[parent].add(branch)
909
910 while branch_tree:
911 this_pass = [(b, p) for b, p in branch_tree.iteritems()
912 if not parent_to_branches[b]]
913 assert this_pass, "Branch tree has cycles: %r" % branch_tree
914 for branch, parent in sorted(this_pass):
915 yield branch, parent
916 parent_to_branches[parent].discard(branch)
917 del branch_tree[branch]
918
919
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000920def tree(treeref, recurse=False):
921 """Returns a dict representation of a git tree object.
922
923 Args:
924 treeref (str) - a git ref which resolves to a tree (commits count as trees).
qyearsley12fa6ff2016-08-24 09:18:40 -0700925 recurse (bool) - include all of the tree's descendants too. File names will
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000926 take the form of 'some/path/to/file'.
927
928 Return format:
929 { 'file_name': (mode, type, ref) }
930
931 mode is an integer where:
932 * 0040000 - Directory
933 * 0100644 - Regular non-executable file
934 * 0100664 - Regular non-executable group-writeable file
935 * 0100755 - Regular executable file
936 * 0120000 - Symbolic link
937 * 0160000 - Gitlink
938
939 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
940
941 ref is the hex encoded hash of the entry.
942 """
943 ret = {}
944 opts = ['ls-tree', '--full-tree']
945 if recurse:
946 opts.append('-r')
947 opts.append(treeref)
948 try:
949 for line in run(*opts).splitlines():
950 mode, typ, ref, name = line.split(None, 3)
951 ret[name] = (mode, typ, ref)
952 except subprocess2.CalledProcessError:
953 return None
954 return ret
955
956
Mun Yong Jang781e71e2017-10-25 15:46:20 -0700957def get_remote_url(remote='origin'):
958 try:
959 return run('config', 'remote.%s.url' % remote)
960 except subprocess2.CalledProcessError:
961 return None
962
963
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000964def upstream(branch):
965 try:
966 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
967 branch+'@{upstream}')
968 except subprocess2.CalledProcessError:
969 return None
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000970
agable@chromium.orgd629fb42014-10-01 09:40:10 +0000971
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000972def get_git_version():
973 """Returns a tuple that contains the numeric components of the current git
974 version."""
975 version_string = run('--version')
976 version_match = re.search(r'(\d+.)+(\d+)', version_string)
977 version = version_match.group() if version_match else ''
978
979 return tuple(int(x) for x in version.split('.'))
980
981
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000982def get_branches_info(include_tracking_status):
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000983 format_string = (
984 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
985
986 # This is not covered by the depot_tools CQ which only has git version 1.8.
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000987 if (include_tracking_status and
988 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000989 format_string += '%(upstream:track)'
990
991 info_map = {}
992 data = run('for-each-ref', format_string, 'refs/heads')
calamity@chromium.org745ffa62014-09-08 01:03:19 +0000993 BranchesInfo = collections.namedtuple(
994 'BranchesInfo', 'hash upstream ahead behind')
calamity@chromium.org9d2c8802014-09-03 02:04:46 +0000995 for line in data.splitlines():
996 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
997
998 ahead_match = re.search(r'ahead (\d+)', tracking_status)
999 ahead = int(ahead_match.group(1)) if ahead_match else None
1000
1001 behind_match = re.search(r'behind (\d+)', tracking_status)
1002 behind = int(behind_match.group(1)) if behind_match else None
1003
calamity@chromium.org745ffa62014-09-08 01:03:19 +00001004 info_map[branch] = BranchesInfo(
calamity@chromium.org9d2c8802014-09-03 02:04:46 +00001005 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
1006
1007 # Set None for upstreams which are not branches (e.g empty upstream, remotes
1008 # and deleted upstream branches).
1009 missing_upstreams = {}
1010 for info in info_map.values():
1011 if info.upstream not in info_map and info.upstream not in missing_upstreams:
1012 missing_upstreams[info.upstream] = None
1013
1014 return dict(info_map.items() + missing_upstreams.items())
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001015
1016
1017def make_workdir_common(repository, new_workdir, files_to_symlink,
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001018 files_to_copy, symlink=None):
1019 if not symlink:
1020 symlink = os.symlink
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001021 os.makedirs(new_workdir)
1022 for entry in files_to_symlink:
scottmg@chromium.orgd4218d42015-10-07 23:49:20 +00001023 clone_file(repository, new_workdir, entry, symlink)
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001024 for entry in files_to_copy:
1025 clone_file(repository, new_workdir, entry, shutil.copy)
1026
1027
1028def make_workdir(repository, new_workdir):
1029 GIT_DIRECTORY_WHITELIST = [
1030 'config',
1031 'info',
1032 'hooks',
1033 'logs/refs',
1034 'objects',
1035 'packed-refs',
1036 'refs',
1037 'remotes',
1038 'rr-cache',
sammc@chromium.org900a33f2015-09-29 06:57:09 +00001039 ]
1040 make_workdir_common(repository, new_workdir, GIT_DIRECTORY_WHITELIST,
1041 ['HEAD'])
1042
1043
1044def clone_file(repository, new_workdir, link, operation):
1045 if not os.path.exists(os.path.join(repository, link)):
1046 return
1047 link_dir = os.path.dirname(os.path.join(new_workdir, link))
1048 if not os.path.exists(link_dir):
1049 os.makedirs(link_dir)
1050 operation(os.path.join(repository, link), os.path.join(new_workdir, link))