blob: e164ba9865792187f816f852e956a211cdef62bd [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.orgaa74cf62013-11-19 20:00:49 +000025import signal
26import sys
27import tempfile
28import threading
29
30import subprocess2
31
32
33GIT_EXE = 'git.bat' if sys.platform.startswith('win') else 'git'
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000034TEST_MODE = False
35
36FREEZE = 'FREEZE'
37FREEZE_SECTIONS = {
38 'indexed': 'soft',
39 'unindexed': 'mixed'
40}
41FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000042
43
44class BadCommitRefException(Exception):
45 def __init__(self, refs):
46 msg = ('one of %s does not seem to be a valid commitref.' %
47 str(refs))
48 super(BadCommitRefException, self).__init__(msg)
49
50
51def memoize_one(**kwargs):
52 """Memoizes a single-argument pure function.
53
54 Values of None are not cached.
55
56 Kwargs:
57 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
58 cache manipulation functions. This is a kwarg so that users of memoize_one
59 are forced to explicitly and verbosely pick True or False.
60
61 Adds three methods to the decorated function:
62 * get(key, default=None) - Gets the value for this key from the cache.
63 * set(key, value) - Sets the value for this key from the cache.
64 * clear() - Drops the entire contents of the cache. Useful for unittests.
65 * update(other) - Updates the contents of the cache from another dict.
66 """
67 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
68 threadsafe = kwargs['threadsafe']
69
70 if threadsafe:
71 def withlock(lock, f):
72 def inner(*args, **kwargs):
73 with lock:
74 return f(*args, **kwargs)
75 return inner
76 else:
77 def withlock(_lock, f):
78 return f
79
80 def decorator(f):
81 # Instantiate the lock in decorator, in case users of memoize_one do:
82 #
83 # memoizer = memoize_one(threadsafe=True)
84 #
85 # @memoizer
86 # def fn1(val): ...
87 #
88 # @memoizer
89 # def fn2(val): ...
90
91 lock = threading.Lock() if threadsafe else None
92 cache = {}
93 _get = withlock(lock, cache.get)
94 _set = withlock(lock, cache.__setitem__)
95
96 @functools.wraps(f)
97 def inner(arg):
98 ret = _get(arg)
99 if ret is None:
100 ret = f(arg)
101 if ret is not None:
102 _set(arg, ret)
103 return ret
104 inner.get = _get
105 inner.set = _set
106 inner.clear = withlock(lock, cache.clear)
107 inner.update = withlock(lock, cache.update)
108 return inner
109 return decorator
110
111
112def _ScopedPool_initer(orig, orig_args): # pragma: no cover
113 """Initializer method for ScopedPool's subprocesses.
114
115 This helps ScopedPool handle Ctrl-C's correctly.
116 """
117 signal.signal(signal.SIGINT, signal.SIG_IGN)
118 if orig:
119 orig(*orig_args)
120
121
122@contextlib.contextmanager
123def ScopedPool(*args, **kwargs):
124 """Context Manager which returns a multiprocessing.pool instance which
125 correctly deals with thrown exceptions.
126
127 *args - Arguments to multiprocessing.pool
128
129 Kwargs:
130 kind ('threads', 'procs') - The type of underlying coprocess to use.
131 **etc - Arguments to multiprocessing.pool
132 """
133 if kwargs.pop('kind', None) == 'threads':
134 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
135 else:
136 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
137 kwargs['initializer'] = _ScopedPool_initer
138 kwargs['initargs'] = orig, orig_args
139 pool = multiprocessing.pool.Pool(*args, **kwargs)
140
141 try:
142 yield pool
143 pool.close()
144 except:
145 pool.terminate()
146 raise
147 finally:
148 pool.join()
149
150
151class ProgressPrinter(object):
152 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000153 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000154 """Create a ProgressPrinter.
155
156 Use it as a context manager which produces a simple 'increment' method:
157
158 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
159 for i in xrange(1000):
160 # do stuff
161 if i % 10 == 0:
162 inc(10)
163
164 Args:
165 fmt - String format with a single '%(count)d' where the counter value
166 should go.
167 enabled (bool) - If this is None, will default to True if
168 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000169 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000170 period (float) - The time in seconds for the printer thread to wait
171 between printing.
172 """
173 self.fmt = fmt
174 if enabled is None: # pragma: no cover
175 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
176 else:
177 self.enabled = enabled
178
179 self._count = 0
180 self._dead = False
181 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000182 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000183 self._thread = threading.Thread(target=self._run)
184 self._period = period
185
186 def _emit(self, s):
187 if self.enabled:
188 self._stream.write('\r' + s)
189 self._stream.flush()
190
191 def _run(self):
192 with self._dead_cond:
193 while not self._dead:
194 self._emit(self.fmt % {'count': self._count})
195 self._dead_cond.wait(self._period)
196 self._emit((self.fmt + '\n') % {'count': self._count})
197
198 def inc(self, amount=1):
199 self._count += amount
200
201 def __enter__(self):
202 self._thread.start()
203 return self.inc
204
205 def __exit__(self, _exc_type, _exc_value, _traceback):
206 self._dead = True
207 with self._dead_cond:
208 self._dead_cond.notifyAll()
209 self._thread.join()
210 del self._thread
211
212
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000213def once(function):
214 """@Decorates |function| so that it only performs its action once, no matter
215 how many times the decorated |function| is called."""
216 def _inner_gen():
217 yield function()
218 while True:
219 yield
220 return _inner_gen().next
221
222
223## Git functions
224
225
226def branch_config(branch, option, default=None):
227 return config('branch.%s.%s' % (branch, option), default=default)
228
229
230def branch_config_map(option):
231 """Return {branch: <|option| value>} for all branches."""
232 try:
233 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
234 lines = run('config', '--get-regexp', reg.pattern).splitlines()
235 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
236 except subprocess2.CalledProcessError:
237 return {}
238
239
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000240def branches(*args):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000241 NO_BRANCH = ('* (no branch', '* (detached from ')
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000242 for line in run('branch', *args).splitlines():
243 if line.startswith(NO_BRANCH):
244 continue
245 yield line.split()[-1]
246
247
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000248def run_with_retcode(*cmd, **kwargs):
249 """Run a command but only return the status code."""
250 try:
251 run(*cmd, **kwargs)
252 return 0
253 except subprocess2.CalledProcessError as cpe:
254 return cpe.returncode
255
256
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000257def config(option, default=None):
258 try:
259 return run('config', '--get', option) or default
260 except subprocess2.CalledProcessError:
261 return default
262
263
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000264def config_list(option):
265 try:
266 return run('config', '--get-all', option).split()
267 except subprocess2.CalledProcessError:
268 return []
269
270
271def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000272 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000273 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000274 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000275 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000276
277
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000278def del_branch_config(branch, option, scope='local'):
279 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000280
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000281
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000282def del_config(option, scope='local'):
283 try:
284 run('config', '--' + scope, '--unset', option)
285 except subprocess2.CalledProcessError:
286 pass
287
288
289def freeze():
290 took_action = False
291
292 try:
293 run('commit', '-m', FREEZE + '.indexed')
294 took_action = True
295 except subprocess2.CalledProcessError:
296 pass
297
298 try:
299 run('add', '-A')
300 run('commit', '-m', FREEZE + '.unindexed')
301 took_action = True
302 except subprocess2.CalledProcessError:
303 pass
304
305 if not took_action:
306 return 'Nothing to freeze.'
307
308
309def get_branch_tree():
310 """Get the dictionary of {branch: parent}, compatible with topo_iter.
311
312 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
313 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000314 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000315 skipped = set()
316 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000317
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000318 for branch in branches():
319 parent = upstream(branch)
320 if not parent:
321 skipped.add(branch)
322 continue
323 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000324
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000325 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000326
327
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000328def get_or_create_merge_base(branch, parent=None):
329 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000330
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000331 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000332 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000333 base = branch_config(branch, 'base')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000334 parent = parent or upstream(branch)
335 actual_merge_base = run('merge-base', parent, branch)
336
337 def is_ancestor(a, b):
338 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
339
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000340 if base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000341 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000342 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
343 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000344 elif is_ancestor(base, actual_merge_base):
345 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
346 base = None
347 else:
348 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000349
350 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000351 base = actual_merge_base
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000352 manual_merge_base(branch, base)
353
354 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000355
356
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000357def hash_multi(*reflike):
358 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000359
360
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000361def hash_one(reflike):
362 return run('rev-parse', reflike)
363
364
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000365def in_rebase():
366 git_dir = run('rev-parse', '--git-dir')
367 return (
368 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
369 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000370
371
372def intern_f(f, kind='blob'):
373 """Interns a file object into the git object store.
374
375 Args:
376 f (file-like object) - The file-like object to intern
377 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
378
379 Returns the git hash of the interned object (hex encoded).
380 """
381 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
382 f.close()
383 return ret
384
385
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000386def is_dormant(branch):
387 # TODO(iannucci): Do an oldness check?
388 return branch_config(branch, 'dormant', 'false') != 'false'
389
390
391def manual_merge_base(branch, base):
392 set_branch_config(branch, 'base', base)
393
394
395def mktree(treedict):
396 """Makes a git tree object and returns its hash.
397
398 See |tree()| for the values of mode, type, and ref.
399
400 Args:
401 treedict - { name: (mode, type, ref) }
402 """
403 with tempfile.TemporaryFile() as f:
404 for name, (mode, typ, ref) in treedict.iteritems():
405 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
406 f.seek(0)
407 return run('mktree', '-z', stdin=f)
408
409
410def parse_commitrefs(*commitrefs):
411 """Returns binary encoded commit hashes for one or more commitrefs.
412
413 A commitref is anything which can resolve to a commit. Popular examples:
414 * 'HEAD'
415 * 'origin/master'
416 * 'cool_branch~2'
417 """
418 try:
419 return map(binascii.unhexlify, hash_multi(*commitrefs))
420 except subprocess2.CalledProcessError:
421 raise BadCommitRefException(commitrefs)
422
423
424RebaseRet = collections.namedtuple('RebaseRet', 'success message')
425
426
427def rebase(parent, start, branch, abort=False):
428 """Rebases |start|..|branch| onto the branch |parent|.
429
430 Args:
431 parent - The new parent ref for the rebased commits.
432 start - The commit to start from
433 branch - The branch to rebase
434 abort - If True, will call git-rebase --abort in the event that the rebase
435 doesn't complete successfully.
436
437 Returns a namedtuple with fields:
438 success - a boolean indicating that the rebase command completed
439 successfully.
440 message - if the rebase failed, this contains the stdout of the failed
441 rebase.
442 """
443 try:
444 args = ['--onto', parent, start, branch]
445 if TEST_MODE:
446 args.insert(0, '--committer-date-is-author-date')
447 run('rebase', *args)
448 return RebaseRet(True, '')
449 except subprocess2.CalledProcessError as cpe:
450 if abort:
451 run('rebase', '--abort')
iannucci@chromium.org56a624a2014-03-26 21:23:09 +0000452 return RebaseRet(False, cpe.stdout)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000453
454
455def remove_merge_base(branch):
456 del_branch_config(branch, 'base')
457
458
459def root():
460 return config('depot-tools.upstream', 'origin/master')
461
462
463def run(*cmd, **kwargs):
464 """The same as run_with_stderr, except it only returns stdout."""
465 return run_with_stderr(*cmd, **kwargs)[0]
466
467
468def run_stream(*cmd, **kwargs):
469 """Runs a git command. Returns stdout as a PIPE (file-like object).
470
471 stderr is dropped to avoid races if the process outputs to both stdout and
472 stderr.
473 """
474 kwargs.setdefault('stderr', subprocess2.VOID)
475 kwargs.setdefault('stdout', subprocess2.PIPE)
476 cmd = (GIT_EXE,) + cmd
477 proc = subprocess2.Popen(cmd, **kwargs)
478 return proc.stdout
479
480
481def run_with_stderr(*cmd, **kwargs):
482 """Runs a git command.
483
484 Returns (stdout, stderr) as a pair of strings.
485
486 kwargs
487 autostrip (bool) - Strip the output. Defaults to True.
488 indata (str) - Specifies stdin data for the process.
489 """
490 kwargs.setdefault('stdin', subprocess2.PIPE)
491 kwargs.setdefault('stdout', subprocess2.PIPE)
492 kwargs.setdefault('stderr', subprocess2.PIPE)
493 autostrip = kwargs.pop('autostrip', True)
494 indata = kwargs.pop('indata', None)
495
496 cmd = (GIT_EXE,) + cmd
497 proc = subprocess2.Popen(cmd, **kwargs)
498 ret, err = proc.communicate(indata)
499 retcode = proc.wait()
500 if retcode != 0:
501 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
502
503 if autostrip:
504 ret = (ret or '').strip()
505 err = (err or '').strip()
506
507 return ret, err
508
509
510def set_branch_config(branch, option, value, scope='local'):
511 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
512
513
514def set_config(option, value, scope='local'):
515 run('config', '--' + scope, option, value)
516
517def squash_current_branch(header=None, merge_base=None):
518 header = header or 'git squash commit.'
519 merge_base = merge_base or get_or_create_merge_base(current_branch())
520 log_msg = header + '\n'
521 if log_msg:
522 log_msg += '\n'
523 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
524 run('reset', '--soft', merge_base)
525 run('commit', '-a', '-F', '-', indata=log_msg)
526
527
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000528def tags(*args):
529 return run('tag', *args).splitlines()
530
531
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000532def thaw():
533 took_action = False
534 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
535 msg = run('show', '--format=%f%b', '-s', 'HEAD')
536 match = FREEZE_MATCHER.match(msg)
537 if not match:
538 if not took_action:
539 return 'Nothing to thaw.'
540 break
541
542 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
543 took_action = True
544
545
546def topo_iter(branch_tree, top_down=True):
547 """Generates (branch, parent) in topographical order for a branch tree.
548
549 Given a tree:
550
551 A1
552 B1 B2
553 C1 C2 C3
554 D1
555
556 branch_tree would look like: {
557 'D1': 'C3',
558 'C3': 'B2',
559 'B2': 'A1',
560 'C1': 'B1',
561 'C2': 'B1',
562 'B1': 'A1',
563 }
564
565 It is OK to have multiple 'root' nodes in your graph.
566
567 if top_down is True, items are yielded from A->D. Otherwise they're yielded
568 from D->A. Within a layer the branches will be yielded in sorted order.
569 """
570 branch_tree = branch_tree.copy()
571
572 # TODO(iannucci): There is probably a more efficient way to do these.
573 if top_down:
574 while branch_tree:
575 this_pass = [(b, p) for b, p in branch_tree.iteritems()
576 if p not in branch_tree]
577 assert this_pass, "Branch tree has cycles: %r" % branch_tree
578 for branch, parent in sorted(this_pass):
579 yield branch, parent
580 del branch_tree[branch]
581 else:
582 parent_to_branches = collections.defaultdict(set)
583 for branch, parent in branch_tree.iteritems():
584 parent_to_branches[parent].add(branch)
585
586 while branch_tree:
587 this_pass = [(b, p) for b, p in branch_tree.iteritems()
588 if not parent_to_branches[b]]
589 assert this_pass, "Branch tree has cycles: %r" % branch_tree
590 for branch, parent in sorted(this_pass):
591 yield branch, parent
592 parent_to_branches[parent].discard(branch)
593 del branch_tree[branch]
594
595
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000596def tree(treeref, recurse=False):
597 """Returns a dict representation of a git tree object.
598
599 Args:
600 treeref (str) - a git ref which resolves to a tree (commits count as trees).
601 recurse (bool) - include all of the tree's decendants too. File names will
602 take the form of 'some/path/to/file'.
603
604 Return format:
605 { 'file_name': (mode, type, ref) }
606
607 mode is an integer where:
608 * 0040000 - Directory
609 * 0100644 - Regular non-executable file
610 * 0100664 - Regular non-executable group-writeable file
611 * 0100755 - Regular executable file
612 * 0120000 - Symbolic link
613 * 0160000 - Gitlink
614
615 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
616
617 ref is the hex encoded hash of the entry.
618 """
619 ret = {}
620 opts = ['ls-tree', '--full-tree']
621 if recurse:
622 opts.append('-r')
623 opts.append(treeref)
624 try:
625 for line in run(*opts).splitlines():
626 mode, typ, ref, name = line.split(None, 3)
627 ret[name] = (mode, typ, ref)
628 except subprocess2.CalledProcessError:
629 return None
630 return ret
631
632
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000633def upstream(branch):
634 try:
635 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
636 branch+'@{upstream}')
637 except subprocess2.CalledProcessError:
638 return None