blob: e837519c6652904e3ae04d3b860e753f8ac7ee71 [file] [log] [blame]
[email protected]c050a5b2014-03-26 06:18:501# Copyright 2014 The Chromium Authors. All rights reserved.
[email protected]aa74cf62013-11-19 20:00:492# 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
[email protected]c050a5b2014-03-26 06:18:5019import collections
[email protected]aa74cf62013-11-19 20:00:4920import contextlib
21import functools
22import logging
[email protected]97345eb2014-03-13 07:55:1523import os
[email protected]c050a5b2014-03-26 06:18:5024import re
[email protected]596cd5c2016-04-04 21:34:3925import setup_color
[email protected]900a33f2015-09-29 06:57:0926import shutil
[email protected]aa74cf62013-11-19 20:00:4927import signal
28import sys
29import tempfile
[email protected]3f23cdf2014-04-15 20:02:4430import textwrap
[email protected]aa74cf62013-11-19 20:00:4931import threading
32
33import subprocess2
34
agable02b3c982016-06-22 14:51:2235from StringIO import StringIO
[email protected]aa74cf62013-11-19 20:00:4936
agable02b3c982016-06-22 14:51:2237
38ROOT = os.path.abspath(os.path.dirname(__file__))
[email protected]0d9e59c2016-01-09 08:08:4139IS_WIN = sys.platform == 'win32'
[email protected]c050a5b2014-03-26 06:18:5040TEST_MODE = False
41
Dan Jacques209a6812017-07-12 18:40:2042
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
[email protected]c050a5b2014-03-26 06:18:5055FREEZE = 'FREEZE'
56FREEZE_SECTIONS = {
57 'indexed': 'soft',
58 'unindexed': 'mixed'
59}
60FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
[email protected]aa74cf62013-11-19 20:00:4961
62
Dan Jacques2f8b0c12017-04-05 19:57:2163# 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#
[email protected]de219ec2014-07-28 17:39:0871# 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
[email protected]6e95d402014-08-29 22:10:5580 r'!.*\[remote rejected\].*\(error in hook\)',
[email protected]de219ec2014-07-28 17:39:0881
82 # crbug.com/289932
[email protected]6e95d402014-08-29 22:10:5583 r'!.*\[remote rejected\].*\(failed to lock\)',
[email protected]de219ec2014-07-28 17:39:0884
85 # crbug.com/307156
[email protected]6e95d402014-08-29 22:10:5586 r'!.*\[remote rejected\].*\(error in Gerrit backend\)',
[email protected]de219ec2014-07-28 17:39:0887
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
[email protected]de219ec2014-07-28 17:39:08109 # crbug.com/388876
110 r'Connection timed out',
[email protected]45cddd62014-11-06 19:36:42111
112 # crbug.com/430343
113 # TODO(dnj): Resync with Chromite.
114 r'The requested URL returned error: 5\d+',
Arikonb3a21482016-07-22 17:12:24115
116 r'Connection reset by peer',
117
118 r'Unable to look up',
119
120 r'Couldn\'t resolve host',
[email protected]de219ec2014-07-28 17:39:08121)
122
123GIT_TRANSIENT_ERRORS_RE = re.compile('|'.join(GIT_TRANSIENT_ERRORS),
124 re.IGNORECASE)
125
[email protected]58d05b02015-06-24 08:54:41126# 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)
[email protected]de219ec2014-07-28 17:39:08130
[email protected]aa74cf62013-11-19 20:00:49131class 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."""
[email protected]97345eb2014-03-13 07:55:15240 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
[email protected]aa74cf62013-11-19 20:00:49241 """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.
[email protected]97345eb2014-03-13 07:55:15256 fout (file-like) - The stream to print status messages to.
[email protected]aa74cf62013-11-19 20:00:49257 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()
[email protected]97345eb2014-03-13 07:55:15269 self._stream = fout
[email protected]aa74cf62013-11-19 20:00:49270 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
[email protected]c050a5b2014-03-26 06:18:50300def 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 14:47:00312def die(message, *args):
313 print >> sys.stderr, textwrap.dedent(message % args)
314 sys.exit(1)
315
[email protected]c050a5b2014-03-26 06:18:50316
Mark Mentovaif548d082017-03-08 18:32:00317def blame(filename, revision=None, porcelain=False, abbrev=None, *_args):
[email protected]81937562016-02-03 08:00:53318 command = ['blame']
319 if porcelain:
320 command.append('-p')
321 if revision is not None:
322 command.append(revision)
Mark Mentovaif548d082017-03-08 18:32:00323 if abbrev is not None:
324 command.append('--abbrev=%d' % abbrev)
[email protected]81937562016-02-03 08:00:53325 command.extend(['--', filename])
326 return run(*command)
327
328
[email protected]c050a5b2014-03-26 06:18:50329def branch_config(branch, option, default=None):
agable7aa2ddd2016-06-21 14:47:00330 return get_config('branch.%s.%s' % (branch, option), default=default)
[email protected]0d9e59c2016-01-09 08:08:41331
332
[email protected]c050a5b2014-03-26 06:18:50333def 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 14:47:00337 lines = get_config_regexp(reg.pattern)
[email protected]c050a5b2014-03-26 06:18:50338 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 19:10:20343def branches(use_limit=True, *args):
[email protected]58888e12015-06-09 15:26:37344 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached')
[email protected]3f23cdf2014-04-15 20:02:44345
346 key = 'depot-tools.branch-limit'
agable7aa2ddd2016-06-21 14:47:00347 limit = get_config_int(key, 20)
[email protected]3f23cdf2014-04-15 20:02:44348
349 raw_branches = run('branch', *args).splitlines()
350
351 num = len(raw_branches)
[email protected]3f23cdf2014-04-15 20:02:44352
Francois Dorayd42c6812017-05-30 19:10:20353 if use_limit and num > limit:
agable7aa2ddd2016-06-21 14:47:00354 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:
[email protected]3f23cdf2014-04-15 20:02:44358 git config %s <new_limit>
agable7aa2ddd2016-06-21 14:47:00359
360 You may also try cleaning up your old branches by running:
361 git cl archive
362 """, num, limit, key)
[email protected]3f23cdf2014-04-15 20:02:44363
364 for line in raw_branches:
[email protected]8bc9b5c2014-03-12 01:36:18365 if line.startswith(NO_BRANCH):
366 continue
367 yield line.split()[-1]
368
369
agable7aa2ddd2016-06-21 14:47:00370def get_config(option, default=None):
[email protected]c050a5b2014-03-26 06:18:50371 try:
372 return run('config', '--get', option) or default
373 except subprocess2.CalledProcessError:
374 return default
375
376
agable7aa2ddd2016-06-21 14:47:00377def 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):
[email protected]8bc9b5c2014-03-12 01:36:18386 try:
387 return run('config', '--get-all', option).split()
388 except subprocess2.CalledProcessError:
389 return []
390
391
agable7aa2ddd2016-06-21 14:47:00392def 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
[email protected]8bc9b5c2014-03-12 01:36:18401def current_branch():
[email protected]aa74cf62013-11-19 20:00:49402 try:
[email protected]c050a5b2014-03-26 06:18:50403 return run('rev-parse', '--abbrev-ref', 'HEAD')
[email protected]aa74cf62013-11-19 20:00:49404 except subprocess2.CalledProcessError:
[email protected]c050a5b2014-03-26 06:18:50405 return None
[email protected]aa74cf62013-11-19 20:00:49406
407
[email protected]c050a5b2014-03-26 06:18:50408def del_branch_config(branch, option, scope='local'):
409 del_config('branch.%s.%s' % (branch, option), scope=scope)
[email protected]aa74cf62013-11-19 20:00:49410
[email protected]aa74cf62013-11-19 20:00:49411
[email protected]c050a5b2014-03-26 06:18:50412def del_config(option, scope='local'):
413 try:
414 run('config', '--' + scope, '--unset', option)
415 except subprocess2.CalledProcessError:
416 pass
417
418
[email protected]01d2cde2016-02-05 03:25:41419def diff(oldrev, newrev, *args):
420 return run('diff', oldrev, newrev, *args)
421
422
[email protected]c050a5b2014-03-26 06:18:50423def freeze():
424 took_action = False
agable02b3c982016-06-22 14:51:22425 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 23:46:50430 root_path = repo_root()
431
agable02b3c982016-06-22 14:51:22432 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 23:46:50437 untracked_bytes += os.stat(os.path.join(root_path, f)).st_size
Bruce Dawson4bff3fd2018-01-04 22:44:23438 if limit_mb > 0 and 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.
agable02b3c982016-06-22 14:51:22442
Bruce Dawson4bff3fd2018-01-04 22:44:23443 Run `git status` to see what it is.
agable02b3c982016-06-22 14:51:22444
Bruce Dawson4bff3fd2018-01-04 22:44:23445 In addition to making many git commands slower, this will prevent
446 depot_tools from freezing your in-progress changes.
agable02b3c982016-06-22 14:51:22447
Bruce Dawson4bff3fd2018-01-04 22:44:23448 You should add untracked data that you want to ignore to your repo's
449 .git/info/exclude
450 file. See `git help ignore` for the format of this file.
agable02b3c982016-06-22 14:51:22451
Bruce Dawson4bff3fd2018-01-04 22:44:23452 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)
[email protected]c050a5b2014-03-26 06:18:50457
458 try:
[email protected]3b4f2282015-09-17 15:46:00459 run('commit', '--no-verify', '-m', FREEZE + '.indexed')
[email protected]c050a5b2014-03-26 06:18:50460 took_action = True
461 except subprocess2.CalledProcessError:
462 pass
463
agable96e179b2016-06-24 17:32:51464 add_errors = False
[email protected]c050a5b2014-03-26 06:18:50465 try:
agable96e179b2016-06-24 17:32:51466 run('add', '-A', '--ignore-errors')
467 except subprocess2.CalledProcessError:
468 add_errors = True
469
470 try:
[email protected]3b4f2282015-09-17 15:46:00471 run('commit', '--no-verify', '-m', FREEZE + '.unindexed')
[email protected]c050a5b2014-03-26 06:18:50472 took_action = True
473 except subprocess2.CalledProcessError:
474 pass
475
agable96e179b2016-06-24 17:32:51476 ret = []
477 if add_errors:
478 ret.append('Failed to index some unindexed files.')
[email protected]c050a5b2014-03-26 06:18:50479 if not took_action:
agable96e179b2016-06-24 17:32:51480 ret.append('Nothing to freeze.')
481 return ' '.join(ret) or None
[email protected]c050a5b2014-03-26 06:18:50482
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.
[email protected]aa74cf62013-11-19 20:00:49489 """
[email protected]c050a5b2014-03-26 06:18:50490 skipped = set()
491 branch_tree = {}
[email protected]97345eb2014-03-13 07:55:15492
[email protected]c050a5b2014-03-26 06:18:50493 for branch in branches():
494 parent = upstream(branch)
495 if not parent:
496 skipped.add(branch)
497 continue
498 branch_tree[branch] = parent
[email protected]97345eb2014-03-13 07:55:15499
[email protected]c050a5b2014-03-26 06:18:50500 return skipped, branch_tree
[email protected]aa74cf62013-11-19 20:00:49501
502
[email protected]c050a5b2014-03-26 06:18:50503def get_or_create_merge_base(branch, parent=None):
504 """Finds the configured merge base for branch.
[email protected]97345eb2014-03-13 07:55:15505
[email protected]c050a5b2014-03-26 06:18:50506 If parent is supplied, it's used instead of calling upstream(branch).
[email protected]97345eb2014-03-13 07:55:15507 """
[email protected]c050a5b2014-03-26 06:18:50508 base = branch_config(branch, 'base')
[email protected]10fbe872014-05-16 22:31:13509 base_upstream = branch_config(branch, 'base-upstream')
[email protected]edeaa812014-03-26 21:27:47510 parent = parent or upstream(branch)
[email protected]79706062015-01-14 21:18:12511 if parent is None or branch is None:
[email protected]10fbe872014-05-16 22:31:13512 return None
[email protected]edeaa812014-03-26 21:27:47513 actual_merge_base = run('merge-base', parent, branch)
514
[email protected]10fbe872014-05-16 22:31:13515 if base_upstream != parent:
516 base = None
517 base_upstream = None
518
[email protected]edeaa812014-03-26 21:27:47519 def is_ancestor(a, b):
520 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
521
[email protected]c3fe99d2016-04-19 08:39:55522 if base and base != actual_merge_base:
[email protected]edeaa812014-03-26 21:27:47523 if not is_ancestor(base, branch):
[email protected]c050a5b2014-03-26 06:18:50524 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
525 base = None
[email protected]edeaa812014-03-26 21:27:47526 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)
[email protected]c050a5b2014-03-26 06:18:50531
532 if not base:
[email protected]edeaa812014-03-26 21:27:47533 base = actual_merge_base
[email protected]10fbe872014-05-16 22:31:13534 manual_merge_base(branch, base, parent)
[email protected]c050a5b2014-03-26 06:18:50535
536 return base
[email protected]97345eb2014-03-13 07:55:15537
538
[email protected]c050a5b2014-03-26 06:18:50539def hash_multi(*reflike):
540 return run('rev-parse', *reflike).splitlines()
[email protected]97345eb2014-03-13 07:55:15541
542
[email protected]9d2c8802014-09-03 02:04:46543def hash_one(reflike, short=False):
544 args = ['rev-parse', reflike]
545 if short:
546 args.insert(1, '--short')
547 return run(*args)
[email protected]8bc9b5c2014-03-12 01:36:18548
549
[email protected]c050a5b2014-03-26 06:18:50550def 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')))
[email protected]aa74cf62013-11-19 20:00:49555
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
[email protected]c050a5b2014-03-26 06:18:50571def is_dormant(branch):
572 # TODO(iannucci): Do an oldness check?
573 return branch_config(branch, 'dormant', 'false') != 'false'
574
575
agable02b3c982016-06-22 14:51:22576def 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
[email protected]10fbe872014-05-16 22:31:13583def manual_merge_base(branch, base, parent):
[email protected]c050a5b2014-03-26 06:18:50584 set_branch_config(branch, 'base', base)
[email protected]10fbe872014-05-16 22:31:13585 set_branch_config(branch, 'base-upstream', parent)
[email protected]c050a5b2014-03-26 06:18:50586
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
[email protected]384039b2014-10-13 21:01:00617RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
[email protected]c050a5b2014-03-26 06:18:50618
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)
[email protected]384039b2014-10-13 21:01:00641 return RebaseRet(True, '', '')
[email protected]c050a5b2014-03-26 06:18:50642 except subprocess2.CalledProcessError as cpe:
643 if abort:
[email protected]dabb78b2015-06-11 23:17:28644 run_with_retcode('rebase', '--abort') # ignore failure
[email protected]384039b2014-10-13 21:01:00645 return RebaseRet(False, cpe.stdout, cpe.stderr)
[email protected]c050a5b2014-03-26 06:18:50646
647
648def remove_merge_base(branch):
649 del_branch_config(branch, 'base')
[email protected]10fbe872014-05-16 22:31:13650 del_branch_config(branch, 'base-upstream')
[email protected]c050a5b2014-03-26 06:18:50651
652
[email protected]81937562016-02-03 08:00:53653def repo_root():
654 """Returns the absolute path to the repository root."""
655 return run('rev-parse', '--show-toplevel')
656
657
[email protected]c050a5b2014-03-26 06:18:50658def root():
agable7aa2ddd2016-06-21 14:47:00659 return get_config('depot-tools.upstream', 'origin/master')
[email protected]c050a5b2014-03-26 06:18:50660
661
[email protected]81937562016-02-03 08:00:53662@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 """
[email protected]596cd5c2016-04-04 21:34:39669 if not setup_color.IS_TTY:
[email protected]81937562016-02-03 08:00:53670 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
[email protected]c050a5b2014-03-26 06:18:50686def 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
[email protected]d629fb42014-10-01 09:40:10691def 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
[email protected]c050a5b2014-03-26 06:18:50699def 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)
[email protected]0d9e59c2016-01-09 08:08:41707 kwargs.setdefault('shell', False)
[email protected]21980022014-04-11 04:51:49708 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
[email protected]c050a5b2014-03-26 06:18:50709 proc = subprocess2.Popen(cmd, **kwargs)
710 return proc.stdout
711
712
[email protected]6c143102015-06-11 19:21:02713@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)
[email protected]0d9e59c2016-01-09 08:08:41724 kwargs.setdefault('shell', False)
[email protected]6c143102015-06-11 19:21:02725 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
[email protected]c050a5b2014-03-26 06:18:50736def 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)
[email protected]0d9e59c2016-01-09 08:08:41748 kwargs.setdefault('shell', False)
[email protected]c050a5b2014-03-26 06:18:50749 autostrip = kwargs.pop('autostrip', True)
750 indata = kwargs.pop('indata', None)
751
[email protected]21980022014-04-11 04:51:49752 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
[email protected]c050a5b2014-03-26 06:18:50753 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
[email protected]d629fb42014-10-01 09:40:10773
[email protected]71437c02015-04-09 19:29:40774def 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-16 00:32:31781 w = lambda s: sys.stderr.write(s+"\n")
782
[email protected]71437c02015-04-09 19:29:40783 dirty = get_dirty_files()
784 if dirty:
iannuccie38699b2016-08-16 00:32:31785 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])
[email protected]71437c02015-04-09 19:29:40789 if len(dirty) > 4096: # pragma: no cover
iannuccie38699b2016-08-16 00:32:31790 w('... (run "git diff-index --name-status HEAD" to see full output).')
[email protected]71437c02015-04-09 19:29:40791 return True
792 return False
793
794
agable02b3c982016-06-22 14:51:22795def 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
[email protected]c050a5b2014-03-26 06:18:50834def squash_current_branch(header=None, merge_base=None):
Alan Cutter00017822016-12-20 06:39:59835 header = header or 'git squash commit for %s.' % current_branch()
[email protected]c050a5b2014-03-26 06:18:50836 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)
[email protected]71437c02015-04-09 19:29:40842
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
[email protected]25b9ab22015-06-18 18:49:03848 run('commit', '--no-verify', '-a', '-F', '-', indata=log_msg)
[email protected]71437c02015-04-09 19:29:40849 return True
[email protected]c050a5b2014-03-26 06:18:50850
851
[email protected]8bc9b5c2014-03-12 01:36:18852def tags(*args):
853 return run('tag', *args).splitlines()
854
855
[email protected]c050a5b2014-03-26 06:18:50856def 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
[email protected]aa74cf62013-11-19 20:00:49920def 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 16:18:40925 recurse (bool) - include all of the tree's descendants too. File names will
[email protected]aa74cf62013-11-19 20:00:49926 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 22:46:20957def get_remote_url(remote='origin'):
958 try:
959 return run('config', 'remote.%s.url' % remote)
960 except subprocess2.CalledProcessError:
961 return None
962
963
[email protected]8bc9b5c2014-03-12 01:36:18964def upstream(branch):
965 try:
966 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
967 branch+'@{upstream}')
968 except subprocess2.CalledProcessError:
969 return None
[email protected]9d2c8802014-09-03 02:04:46970
[email protected]d629fb42014-10-01 09:40:10971
[email protected]9d2c8802014-09-03 02:04:46972def 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
[email protected]745ffa62014-09-08 01:03:19982def get_branches_info(include_tracking_status):
[email protected]9d2c8802014-09-03 02:04:46983 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.
[email protected]745ffa62014-09-08 01:03:19987 if (include_tracking_status and
988 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
[email protected]9d2c8802014-09-03 02:04:46989 format_string += '%(upstream:track)'
990
991 info_map = {}
992 data = run('for-each-ref', format_string, 'refs/heads')
[email protected]745ffa62014-09-08 01:03:19993 BranchesInfo = collections.namedtuple(
994 'BranchesInfo', 'hash upstream ahead behind')
[email protected]9d2c8802014-09-03 02:04:46995 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
[email protected]745ffa62014-09-08 01:03:191004 info_map[branch] = BranchesInfo(
[email protected]9d2c8802014-09-03 02:04:461005 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())
[email protected]900a33f2015-09-29 06:57:091015
1016
1017def make_workdir_common(repository, new_workdir, files_to_symlink,
[email protected]d4218d42015-10-07 23:49:201018 files_to_copy, symlink=None):
1019 if not symlink:
1020 symlink = os.symlink
[email protected]900a33f2015-09-29 06:57:091021 os.makedirs(new_workdir)
1022 for entry in files_to_symlink:
[email protected]d4218d42015-10-07 23:49:201023 clone_file(repository, new_workdir, entry, symlink)
[email protected]900a33f2015-09-29 06:57:091024 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',
[email protected]900a33f2015-09-29 06:57:091039 ]
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)
Henrique Ferreirofd4ad242018-01-10 11:19:181050 src = os.path.join(repository, link)
1051 if os.path.islink(src):
Henrique Ferreiroaea45d22018-02-19 08:48:361052 src = os.path.realpath(src)
Henrique Ferreirofd4ad242018-01-10 11:19:181053 operation(src, os.path.join(new_workdir, link))