blob: 0c537134ffea38674a5db7b61f70b4a3612e224b [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
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +000028import textwrap
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000029import threading
30
31import subprocess2
32
33
34GIT_EXE = 'git.bat' if sys.platform.startswith('win') else 'git'
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +000035TEST_MODE = False
36
37FREEZE = 'FREEZE'
38FREEZE_SECTIONS = {
39 'indexed': 'soft',
40 'unindexed': 'mixed'
41}
42FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000043
44
dnj@chromium.orgde219ec2014-07-28 17:39:08 +000045# Retry a git operation if git returns a error response with any of these
46# messages. It's all observed 'bad' GoB responses so far.
47#
48# This list is inspired/derived from the one in ChromiumOS's Chromite:
49# <CHROMITE>/lib/git.py::GIT_TRANSIENT_ERRORS
50#
51# It was last imported from '7add3ac29564d98ac35ce426bc295e743e7c0c02'.
52GIT_TRANSIENT_ERRORS = (
53 # crbug.com/285832
54 r'! \[remote rejected\].*\(error in hook\)',
55
56 # crbug.com/289932
57 r'! \[remote rejected\].*\(failed to lock\)',
58
59 # crbug.com/307156
60 r'! \[remote rejected\].*\(error in Gerrit backend\)',
61
62 # crbug.com/285832
63 r'remote error: Internal Server Error',
64
65 # crbug.com/294449
66 r'fatal: Couldn\'t find remote ref ',
67
68 # crbug.com/220543
69 r'git fetch_pack: expected ACK/NAK, got',
70
71 # crbug.com/189455
72 r'protocol error: bad pack header',
73
74 # crbug.com/202807
75 r'The remote end hung up unexpectedly',
76
77 # crbug.com/298189
78 r'TLS packet with unexpected length was received',
79
80 # crbug.com/187444
81 r'RPC failed; result=\d+, HTTP code = \d+',
82
83 # crbug.com/315421
84 r'The requested URL returned error: 500 while accessing',
85
86 # crbug.com/388876
87 r'Connection timed out',
88)
89
90GIT_TRANSIENT_ERRORS_RE = re.compile('|'.join(GIT_TRANSIENT_ERRORS),
91 re.IGNORECASE)
92
93
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +000094class BadCommitRefException(Exception):
95 def __init__(self, refs):
96 msg = ('one of %s does not seem to be a valid commitref.' %
97 str(refs))
98 super(BadCommitRefException, self).__init__(msg)
99
100
101def memoize_one(**kwargs):
102 """Memoizes a single-argument pure function.
103
104 Values of None are not cached.
105
106 Kwargs:
107 threadsafe (bool) - REQUIRED. Specifies whether to use locking around
108 cache manipulation functions. This is a kwarg so that users of memoize_one
109 are forced to explicitly and verbosely pick True or False.
110
111 Adds three methods to the decorated function:
112 * get(key, default=None) - Gets the value for this key from the cache.
113 * set(key, value) - Sets the value for this key from the cache.
114 * clear() - Drops the entire contents of the cache. Useful for unittests.
115 * update(other) - Updates the contents of the cache from another dict.
116 """
117 assert 'threadsafe' in kwargs, 'Must specify threadsafe={True,False}'
118 threadsafe = kwargs['threadsafe']
119
120 if threadsafe:
121 def withlock(lock, f):
122 def inner(*args, **kwargs):
123 with lock:
124 return f(*args, **kwargs)
125 return inner
126 else:
127 def withlock(_lock, f):
128 return f
129
130 def decorator(f):
131 # Instantiate the lock in decorator, in case users of memoize_one do:
132 #
133 # memoizer = memoize_one(threadsafe=True)
134 #
135 # @memoizer
136 # def fn1(val): ...
137 #
138 # @memoizer
139 # def fn2(val): ...
140
141 lock = threading.Lock() if threadsafe else None
142 cache = {}
143 _get = withlock(lock, cache.get)
144 _set = withlock(lock, cache.__setitem__)
145
146 @functools.wraps(f)
147 def inner(arg):
148 ret = _get(arg)
149 if ret is None:
150 ret = f(arg)
151 if ret is not None:
152 _set(arg, ret)
153 return ret
154 inner.get = _get
155 inner.set = _set
156 inner.clear = withlock(lock, cache.clear)
157 inner.update = withlock(lock, cache.update)
158 return inner
159 return decorator
160
161
162def _ScopedPool_initer(orig, orig_args): # pragma: no cover
163 """Initializer method for ScopedPool's subprocesses.
164
165 This helps ScopedPool handle Ctrl-C's correctly.
166 """
167 signal.signal(signal.SIGINT, signal.SIG_IGN)
168 if orig:
169 orig(*orig_args)
170
171
172@contextlib.contextmanager
173def ScopedPool(*args, **kwargs):
174 """Context Manager which returns a multiprocessing.pool instance which
175 correctly deals with thrown exceptions.
176
177 *args - Arguments to multiprocessing.pool
178
179 Kwargs:
180 kind ('threads', 'procs') - The type of underlying coprocess to use.
181 **etc - Arguments to multiprocessing.pool
182 """
183 if kwargs.pop('kind', None) == 'threads':
184 pool = multiprocessing.pool.ThreadPool(*args, **kwargs)
185 else:
186 orig, orig_args = kwargs.get('initializer'), kwargs.get('initargs', ())
187 kwargs['initializer'] = _ScopedPool_initer
188 kwargs['initargs'] = orig, orig_args
189 pool = multiprocessing.pool.Pool(*args, **kwargs)
190
191 try:
192 yield pool
193 pool.close()
194 except:
195 pool.terminate()
196 raise
197 finally:
198 pool.join()
199
200
201class ProgressPrinter(object):
202 """Threaded single-stat status message printer."""
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000203 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000204 """Create a ProgressPrinter.
205
206 Use it as a context manager which produces a simple 'increment' method:
207
208 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
209 for i in xrange(1000):
210 # do stuff
211 if i % 10 == 0:
212 inc(10)
213
214 Args:
215 fmt - String format with a single '%(count)d' where the counter value
216 should go.
217 enabled (bool) - If this is None, will default to True if
218 logging.getLogger() is set to INFO or more verbose.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000219 fout (file-like) - The stream to print status messages to.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000220 period (float) - The time in seconds for the printer thread to wait
221 between printing.
222 """
223 self.fmt = fmt
224 if enabled is None: # pragma: no cover
225 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
226 else:
227 self.enabled = enabled
228
229 self._count = 0
230 self._dead = False
231 self._dead_cond = threading.Condition()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000232 self._stream = fout
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000233 self._thread = threading.Thread(target=self._run)
234 self._period = period
235
236 def _emit(self, s):
237 if self.enabled:
238 self._stream.write('\r' + s)
239 self._stream.flush()
240
241 def _run(self):
242 with self._dead_cond:
243 while not self._dead:
244 self._emit(self.fmt % {'count': self._count})
245 self._dead_cond.wait(self._period)
246 self._emit((self.fmt + '\n') % {'count': self._count})
247
248 def inc(self, amount=1):
249 self._count += amount
250
251 def __enter__(self):
252 self._thread.start()
253 return self.inc
254
255 def __exit__(self, _exc_type, _exc_value, _traceback):
256 self._dead = True
257 with self._dead_cond:
258 self._dead_cond.notifyAll()
259 self._thread.join()
260 del self._thread
261
262
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000263def once(function):
264 """@Decorates |function| so that it only performs its action once, no matter
265 how many times the decorated |function| is called."""
266 def _inner_gen():
267 yield function()
268 while True:
269 yield
270 return _inner_gen().next
271
272
273## Git functions
274
275
276def branch_config(branch, option, default=None):
277 return config('branch.%s.%s' % (branch, option), default=default)
278
279
280def branch_config_map(option):
281 """Return {branch: <|option| value>} for all branches."""
282 try:
283 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
284 lines = run('config', '--get-regexp', reg.pattern).splitlines()
285 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
286 except subprocess2.CalledProcessError:
287 return {}
288
289
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000290def branches(*args):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000291 NO_BRANCH = ('* (no branch', '* (detached from ')
iannucci@chromium.org3f23cdf2014-04-15 20:02:44 +0000292
293 key = 'depot-tools.branch-limit'
294 limit = 20
295 try:
296 limit = int(config(key, limit))
297 except ValueError:
298 pass
299
300 raw_branches = run('branch', *args).splitlines()
301
302 num = len(raw_branches)
303 if num > limit:
304 print >> sys.stderr, textwrap.dedent("""\
305 Your git repo has too many branches (%d/%d) for this tool to work well.
306
307 You may adjust this limit by running:
308 git config %s <new_limit>
309 """ % (num, limit, key))
310 sys.exit(1)
311
312 for line in raw_branches:
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000313 if line.startswith(NO_BRANCH):
314 continue
315 yield line.split()[-1]
316
317
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000318def run_with_retcode(*cmd, **kwargs):
319 """Run a command but only return the status code."""
320 try:
321 run(*cmd, **kwargs)
322 return 0
323 except subprocess2.CalledProcessError as cpe:
324 return cpe.returncode
325
326
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000327def config(option, default=None):
328 try:
329 return run('config', '--get', option) or default
330 except subprocess2.CalledProcessError:
331 return default
332
333
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000334def config_list(option):
335 try:
336 return run('config', '--get-all', option).split()
337 except subprocess2.CalledProcessError:
338 return []
339
340
341def current_branch():
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000342 try:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000343 return run('rev-parse', '--abbrev-ref', 'HEAD')
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000344 except subprocess2.CalledProcessError:
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000345 return None
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000346
347
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000348def del_branch_config(branch, option, scope='local'):
349 del_config('branch.%s.%s' % (branch, option), scope=scope)
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000350
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000351
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000352def del_config(option, scope='local'):
353 try:
354 run('config', '--' + scope, '--unset', option)
355 except subprocess2.CalledProcessError:
356 pass
357
358
359def freeze():
360 took_action = False
361
362 try:
363 run('commit', '-m', FREEZE + '.indexed')
364 took_action = True
365 except subprocess2.CalledProcessError:
366 pass
367
368 try:
369 run('add', '-A')
370 run('commit', '-m', FREEZE + '.unindexed')
371 took_action = True
372 except subprocess2.CalledProcessError:
373 pass
374
375 if not took_action:
376 return 'Nothing to freeze.'
377
378
379def get_branch_tree():
380 """Get the dictionary of {branch: parent}, compatible with topo_iter.
381
382 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
383 branches without upstream branches defined.
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000384 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000385 skipped = set()
386 branch_tree = {}
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000387
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000388 for branch in branches():
389 parent = upstream(branch)
390 if not parent:
391 skipped.add(branch)
392 continue
393 branch_tree[branch] = parent
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000394
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000395 return skipped, branch_tree
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000396
397
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000398def get_or_create_merge_base(branch, parent=None):
399 """Finds the configured merge base for branch.
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000400
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000401 If parent is supplied, it's used instead of calling upstream(branch).
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000402 """
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000403 base = branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000404 base_upstream = branch_config(branch, 'base-upstream')
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000405 parent = parent or upstream(branch)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000406 if not parent:
407 return None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000408 actual_merge_base = run('merge-base', parent, branch)
409
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000410 if base_upstream != parent:
411 base = None
412 base_upstream = None
413
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000414 def is_ancestor(a, b):
415 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
416
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000417 if base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000418 if not is_ancestor(base, branch):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000419 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
420 base = None
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000421 elif is_ancestor(base, actual_merge_base):
422 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
423 base = None
424 else:
425 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000426
427 if not base:
iannucci@chromium.orgedeaa812014-03-26 21:27:47 +0000428 base = actual_merge_base
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000429 manual_merge_base(branch, base, parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000430
431 return base
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000432
433
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000434def hash_multi(*reflike):
435 return run('rev-parse', *reflike).splitlines()
iannucci@chromium.org97345eb2014-03-13 07:55:15 +0000436
437
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000438def hash_one(reflike):
439 return run('rev-parse', reflike)
440
441
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000442def in_rebase():
443 git_dir = run('rev-parse', '--git-dir')
444 return (
445 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
446 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000447
448
449def intern_f(f, kind='blob'):
450 """Interns a file object into the git object store.
451
452 Args:
453 f (file-like object) - The file-like object to intern
454 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
455
456 Returns the git hash of the interned object (hex encoded).
457 """
458 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
459 f.close()
460 return ret
461
462
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000463def is_dormant(branch):
464 # TODO(iannucci): Do an oldness check?
465 return branch_config(branch, 'dormant', 'false') != 'false'
466
467
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000468def manual_merge_base(branch, base, parent):
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000469 set_branch_config(branch, 'base', base)
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000470 set_branch_config(branch, 'base-upstream', parent)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000471
472
473def mktree(treedict):
474 """Makes a git tree object and returns its hash.
475
476 See |tree()| for the values of mode, type, and ref.
477
478 Args:
479 treedict - { name: (mode, type, ref) }
480 """
481 with tempfile.TemporaryFile() as f:
482 for name, (mode, typ, ref) in treedict.iteritems():
483 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
484 f.seek(0)
485 return run('mktree', '-z', stdin=f)
486
487
488def parse_commitrefs(*commitrefs):
489 """Returns binary encoded commit hashes for one or more commitrefs.
490
491 A commitref is anything which can resolve to a commit. Popular examples:
492 * 'HEAD'
493 * 'origin/master'
494 * 'cool_branch~2'
495 """
496 try:
497 return map(binascii.unhexlify, hash_multi(*commitrefs))
498 except subprocess2.CalledProcessError:
499 raise BadCommitRefException(commitrefs)
500
501
502RebaseRet = collections.namedtuple('RebaseRet', 'success message')
503
504
505def rebase(parent, start, branch, abort=False):
506 """Rebases |start|..|branch| onto the branch |parent|.
507
508 Args:
509 parent - The new parent ref for the rebased commits.
510 start - The commit to start from
511 branch - The branch to rebase
512 abort - If True, will call git-rebase --abort in the event that the rebase
513 doesn't complete successfully.
514
515 Returns a namedtuple with fields:
516 success - a boolean indicating that the rebase command completed
517 successfully.
518 message - if the rebase failed, this contains the stdout of the failed
519 rebase.
520 """
521 try:
522 args = ['--onto', parent, start, branch]
523 if TEST_MODE:
524 args.insert(0, '--committer-date-is-author-date')
525 run('rebase', *args)
526 return RebaseRet(True, '')
527 except subprocess2.CalledProcessError as cpe:
528 if abort:
529 run('rebase', '--abort')
iannucci@chromium.org56a624a2014-03-26 21:23:09 +0000530 return RebaseRet(False, cpe.stdout)
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000531
532
533def remove_merge_base(branch):
534 del_branch_config(branch, 'base')
iannucci@chromium.org10fbe872014-05-16 22:31:13 +0000535 del_branch_config(branch, 'base-upstream')
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000536
537
538def root():
539 return config('depot-tools.upstream', 'origin/master')
540
541
542def run(*cmd, **kwargs):
543 """The same as run_with_stderr, except it only returns stdout."""
544 return run_with_stderr(*cmd, **kwargs)[0]
545
546
547def run_stream(*cmd, **kwargs):
548 """Runs a git command. Returns stdout as a PIPE (file-like object).
549
550 stderr is dropped to avoid races if the process outputs to both stdout and
551 stderr.
552 """
553 kwargs.setdefault('stderr', subprocess2.VOID)
554 kwargs.setdefault('stdout', subprocess2.PIPE)
iannucci@chromium.org21980022014-04-11 04:51:49 +0000555 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000556 proc = subprocess2.Popen(cmd, **kwargs)
557 return proc.stdout
558
559
560def run_with_stderr(*cmd, **kwargs):
561 """Runs a git command.
562
563 Returns (stdout, stderr) as a pair of strings.
564
565 kwargs
566 autostrip (bool) - Strip the output. Defaults to True.
567 indata (str) - Specifies stdin data for the process.
568 """
569 kwargs.setdefault('stdin', subprocess2.PIPE)
570 kwargs.setdefault('stdout', subprocess2.PIPE)
571 kwargs.setdefault('stderr', subprocess2.PIPE)
572 autostrip = kwargs.pop('autostrip', True)
573 indata = kwargs.pop('indata', None)
574
iannucci@chromium.org21980022014-04-11 04:51:49 +0000575 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000576 proc = subprocess2.Popen(cmd, **kwargs)
577 ret, err = proc.communicate(indata)
578 retcode = proc.wait()
579 if retcode != 0:
580 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
581
582 if autostrip:
583 ret = (ret or '').strip()
584 err = (err or '').strip()
585
586 return ret, err
587
588
589def set_branch_config(branch, option, value, scope='local'):
590 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
591
592
593def set_config(option, value, scope='local'):
594 run('config', '--' + scope, option, value)
595
596def squash_current_branch(header=None, merge_base=None):
597 header = header or 'git squash commit.'
598 merge_base = merge_base or get_or_create_merge_base(current_branch())
599 log_msg = header + '\n'
600 if log_msg:
601 log_msg += '\n'
602 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
603 run('reset', '--soft', merge_base)
604 run('commit', '-a', '-F', '-', indata=log_msg)
605
606
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000607def tags(*args):
608 return run('tag', *args).splitlines()
609
610
iannucci@chromium.orgc050a5b2014-03-26 06:18:50 +0000611def thaw():
612 took_action = False
613 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
614 msg = run('show', '--format=%f%b', '-s', 'HEAD')
615 match = FREEZE_MATCHER.match(msg)
616 if not match:
617 if not took_action:
618 return 'Nothing to thaw.'
619 break
620
621 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
622 took_action = True
623
624
625def topo_iter(branch_tree, top_down=True):
626 """Generates (branch, parent) in topographical order for a branch tree.
627
628 Given a tree:
629
630 A1
631 B1 B2
632 C1 C2 C3
633 D1
634
635 branch_tree would look like: {
636 'D1': 'C3',
637 'C3': 'B2',
638 'B2': 'A1',
639 'C1': 'B1',
640 'C2': 'B1',
641 'B1': 'A1',
642 }
643
644 It is OK to have multiple 'root' nodes in your graph.
645
646 if top_down is True, items are yielded from A->D. Otherwise they're yielded
647 from D->A. Within a layer the branches will be yielded in sorted order.
648 """
649 branch_tree = branch_tree.copy()
650
651 # TODO(iannucci): There is probably a more efficient way to do these.
652 if top_down:
653 while branch_tree:
654 this_pass = [(b, p) for b, p in branch_tree.iteritems()
655 if p not in branch_tree]
656 assert this_pass, "Branch tree has cycles: %r" % branch_tree
657 for branch, parent in sorted(this_pass):
658 yield branch, parent
659 del branch_tree[branch]
660 else:
661 parent_to_branches = collections.defaultdict(set)
662 for branch, parent in branch_tree.iteritems():
663 parent_to_branches[parent].add(branch)
664
665 while branch_tree:
666 this_pass = [(b, p) for b, p in branch_tree.iteritems()
667 if not parent_to_branches[b]]
668 assert this_pass, "Branch tree has cycles: %r" % branch_tree
669 for branch, parent in sorted(this_pass):
670 yield branch, parent
671 parent_to_branches[parent].discard(branch)
672 del branch_tree[branch]
673
674
iannucci@chromium.orgaa74cf62013-11-19 20:00:49 +0000675def tree(treeref, recurse=False):
676 """Returns a dict representation of a git tree object.
677
678 Args:
679 treeref (str) - a git ref which resolves to a tree (commits count as trees).
680 recurse (bool) - include all of the tree's decendants too. File names will
681 take the form of 'some/path/to/file'.
682
683 Return format:
684 { 'file_name': (mode, type, ref) }
685
686 mode is an integer where:
687 * 0040000 - Directory
688 * 0100644 - Regular non-executable file
689 * 0100664 - Regular non-executable group-writeable file
690 * 0100755 - Regular executable file
691 * 0120000 - Symbolic link
692 * 0160000 - Gitlink
693
694 type is a string where it's one of 'blob', 'commit', 'tree', 'tag'.
695
696 ref is the hex encoded hash of the entry.
697 """
698 ret = {}
699 opts = ['ls-tree', '--full-tree']
700 if recurse:
701 opts.append('-r')
702 opts.append(treeref)
703 try:
704 for line in run(*opts).splitlines():
705 mode, typ, ref, name = line.split(None, 3)
706 ret[name] = (mode, typ, ref)
707 except subprocess2.CalledProcessError:
708 return None
709 return ret
710
711
iannucci@chromium.org8bc9b5c2014-03-12 01:36:18 +0000712def upstream(branch):
713 try:
714 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
715 branch+'@{upstream}')
716 except subprocess2.CalledProcessError:
717 return None