Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(31)

Side by Side Diff: appengine/third_party_local/depot_tools/git_common.py

Issue 1052993003: Roll multiple files from depot_tools into luci. (Closed) Base URL: git@github.com:luci/luci-py.git@master
Patch Set: Created 5 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 # Copyright 2013 The Chromium Authors. All rights reserved. 1 # Copyright 2014 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 # Monkeypatch IMapIterator so that Ctrl-C can kill everything properly. 5 # Monkeypatch IMapIterator so that Ctrl-C can kill everything properly.
6 # Derived from https://gist.github.com/aljungberg/626518 6 # Derived from https://gist.github.com/aljungberg/626518
7 import multiprocessing.pool 7 import multiprocessing.pool
8 from multiprocessing.pool import IMapIterator 8 from multiprocessing.pool import IMapIterator
9 def wrapper(func): 9 def wrapper(func):
10 def wrap(self, timeout=None): 10 def wrap(self, timeout=None):
11 return func(self, timeout=timeout or 1e100) 11 return func(self, timeout=timeout or 1e100)
12 return wrap 12 return wrap
13 IMapIterator.next = wrapper(IMapIterator.next) 13 IMapIterator.next = wrapper(IMapIterator.next)
14 IMapIterator.__next__ = IMapIterator.next 14 IMapIterator.__next__ = IMapIterator.next
15 # TODO(iannucci): Monkeypatch all other 'wait' methods too. 15 # TODO(iannucci): Monkeypatch all other 'wait' methods too.
16 16
17 17
18 import binascii 18 import binascii
19 import collections
19 import contextlib 20 import contextlib
20 import functools 21 import functools
21 import logging 22 import logging
23 import os
24 import re
22 import signal 25 import signal
23 import sys 26 import sys
24 import tempfile 27 import tempfile
28 import textwrap
25 import threading 29 import threading
26 30
27 import subprocess2 31 import subprocess2
28 32
33 ROOT = os.path.abspath(os.path.dirname(__file__))
29 34
30 GIT_EXE = 'git.bat' if sys.platform.startswith('win') else 'git' 35 GIT_EXE = ROOT+'\\git.bat' if sys.platform.startswith('win') else 'git'
36 TEST_MODE = False
31 37
38 FREEZE = 'FREEZE'
39 FREEZE_SECTIONS = {
40 'indexed': 'soft',
41 'unindexed': 'mixed'
42 }
43 FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS)))
44
45
46 # Retry a git operation if git returns a error response with any of these
47 # messages. It's all observed 'bad' GoB responses so far.
48 #
49 # This list is inspired/derived from the one in ChromiumOS's Chromite:
50 # <CHROMITE>/lib/git.py::GIT_TRANSIENT_ERRORS
51 #
52 # It was last imported from '7add3ac29564d98ac35ce426bc295e743e7c0c02'.
53 GIT_TRANSIENT_ERRORS = (
54 # crbug.com/285832
55 r'!.*\[remote rejected\].*\(error in hook\)',
56
57 # crbug.com/289932
58 r'!.*\[remote rejected\].*\(failed to lock\)',
59
60 # crbug.com/307156
61 r'!.*\[remote rejected\].*\(error in Gerrit backend\)',
62
63 # crbug.com/285832
64 r'remote error: Internal Server Error',
65
66 # crbug.com/294449
67 r'fatal: Couldn\'t find remote ref ',
68
69 # crbug.com/220543
70 r'git fetch_pack: expected ACK/NAK, got',
71
72 # crbug.com/189455
73 r'protocol error: bad pack header',
74
75 # crbug.com/202807
76 r'The remote end hung up unexpectedly',
77
78 # crbug.com/298189
79 r'TLS packet with unexpected length was received',
80
81 # crbug.com/187444
82 r'RPC failed; result=\d+, HTTP code = \d+',
83
84 # crbug.com/388876
85 r'Connection timed out',
86
87 # crbug.com/430343
88 # TODO(dnj): Resync with Chromite.
89 r'The requested URL returned error: 5\d+',
90 )
91
92 GIT_TRANSIENT_ERRORS_RE = re.compile('|'.join(GIT_TRANSIENT_ERRORS),
93 re.IGNORECASE)
94
95 # First version where the for-each-ref command's format string supported the
96 # upstream:track token.
97 MIN_UPSTREAM_TRACK_GIT_VERSION = (1, 9)
32 98
33 class BadCommitRefException(Exception): 99 class BadCommitRefException(Exception):
34 def __init__(self, refs): 100 def __init__(self, refs):
35 msg = ('one of %s does not seem to be a valid commitref.' % 101 msg = ('one of %s does not seem to be a valid commitref.' %
36 str(refs)) 102 str(refs))
37 super(BadCommitRefException, self).__init__(msg) 103 super(BadCommitRefException, self).__init__(msg)
38 104
39 105
40 def memoize_one(**kwargs): 106 def memoize_one(**kwargs):
41 """Memoizes a single-argument pure function. 107 """Memoizes a single-argument pure function.
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after
132 pool.close() 198 pool.close()
133 except: 199 except:
134 pool.terminate() 200 pool.terminate()
135 raise 201 raise
136 finally: 202 finally:
137 pool.join() 203 pool.join()
138 204
139 205
140 class ProgressPrinter(object): 206 class ProgressPrinter(object):
141 """Threaded single-stat status message printer.""" 207 """Threaded single-stat status message printer."""
142 def __init__(self, fmt, enabled=None, stream=sys.stderr, period=0.5): 208 def __init__(self, fmt, enabled=None, fout=sys.stderr, period=0.5):
143 """Create a ProgressPrinter. 209 """Create a ProgressPrinter.
144 210
145 Use it as a context manager which produces a simple 'increment' method: 211 Use it as a context manager which produces a simple 'increment' method:
146 212
147 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc: 213 with ProgressPrinter('(%%(count)d/%d)' % 1000) as inc:
148 for i in xrange(1000): 214 for i in xrange(1000):
149 # do stuff 215 # do stuff
150 if i % 10 == 0: 216 if i % 10 == 0:
151 inc(10) 217 inc(10)
152 218
153 Args: 219 Args:
154 fmt - String format with a single '%(count)d' where the counter value 220 fmt - String format with a single '%(count)d' where the counter value
155 should go. 221 should go.
156 enabled (bool) - If this is None, will default to True if 222 enabled (bool) - If this is None, will default to True if
157 logging.getLogger() is set to INFO or more verbose. 223 logging.getLogger() is set to INFO or more verbose.
158 stream (file-like) - The stream to print status messages to. 224 fout (file-like) - The stream to print status messages to.
159 period (float) - The time in seconds for the printer thread to wait 225 period (float) - The time in seconds for the printer thread to wait
160 between printing. 226 between printing.
161 """ 227 """
162 self.fmt = fmt 228 self.fmt = fmt
163 if enabled is None: # pragma: no cover 229 if enabled is None: # pragma: no cover
164 self.enabled = logging.getLogger().isEnabledFor(logging.INFO) 230 self.enabled = logging.getLogger().isEnabledFor(logging.INFO)
165 else: 231 else:
166 self.enabled = enabled 232 self.enabled = enabled
167 233
168 self._count = 0 234 self._count = 0
169 self._dead = False 235 self._dead = False
170 self._dead_cond = threading.Condition() 236 self._dead_cond = threading.Condition()
171 self._stream = stream 237 self._stream = fout
172 self._thread = threading.Thread(target=self._run) 238 self._thread = threading.Thread(target=self._run)
173 self._period = period 239 self._period = period
174 240
175 def _emit(self, s): 241 def _emit(self, s):
176 if self.enabled: 242 if self.enabled:
177 self._stream.write('\r' + s) 243 self._stream.write('\r' + s)
178 self._stream.flush() 244 self._stream.flush()
179 245
180 def _run(self): 246 def _run(self):
181 with self._dead_cond: 247 with self._dead_cond:
(...skipping 10 matching lines...) Expand all
192 return self.inc 258 return self.inc
193 259
194 def __exit__(self, _exc_type, _exc_value, _traceback): 260 def __exit__(self, _exc_type, _exc_value, _traceback):
195 self._dead = True 261 self._dead = True
196 with self._dead_cond: 262 with self._dead_cond:
197 self._dead_cond.notifyAll() 263 self._dead_cond.notifyAll()
198 self._thread.join() 264 self._thread.join()
199 del self._thread 265 del self._thread
200 266
201 267
268 def once(function):
269 """@Decorates |function| so that it only performs its action once, no matter
270 how many times the decorated |function| is called."""
271 def _inner_gen():
272 yield function()
273 while True:
274 yield
275 return _inner_gen().next
276
277
278 ## Git functions
279
280
281 def branch_config(branch, option, default=None):
282 return config('branch.%s.%s' % (branch, option), default=default)
283
284
285 def branch_config_map(option):
286 """Return {branch: <|option| value>} for all branches."""
287 try:
288 reg = re.compile(r'^branch\.(.*)\.%s$' % option)
289 lines = run('config', '--get-regexp', reg.pattern).splitlines()
290 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)}
291 except subprocess2.CalledProcessError:
292 return {}
293
294
295 def branches(*args):
296 NO_BRANCH = ('* (no branch', '* (detached from ')
297
298 key = 'depot-tools.branch-limit'
299 limit = 20
300 try:
301 limit = int(config(key, limit))
302 except ValueError:
303 pass
304
305 raw_branches = run('branch', *args).splitlines()
306
307 num = len(raw_branches)
308 if num > limit:
309 print >> sys.stderr, textwrap.dedent("""\
310 Your git repo has too many branches (%d/%d) for this tool to work well.
311
312 You may adjust this limit by running:
313 git config %s <new_limit>
314 """ % (num, limit, key))
315 sys.exit(1)
316
317 for line in raw_branches:
318 if line.startswith(NO_BRANCH):
319 continue
320 yield line.split()[-1]
321
322
323 def config(option, default=None):
324 try:
325 return run('config', '--get', option) or default
326 except subprocess2.CalledProcessError:
327 return default
328
329
330 def config_list(option):
331 try:
332 return run('config', '--get-all', option).split()
333 except subprocess2.CalledProcessError:
334 return []
335
336
337 def current_branch():
338 try:
339 return run('rev-parse', '--abbrev-ref', 'HEAD')
340 except subprocess2.CalledProcessError:
341 return None
342
343
344 def del_branch_config(branch, option, scope='local'):
345 del_config('branch.%s.%s' % (branch, option), scope=scope)
346
347
348 def del_config(option, scope='local'):
349 try:
350 run('config', '--' + scope, '--unset', option)
351 except subprocess2.CalledProcessError:
352 pass
353
354
355 def freeze():
356 took_action = False
357
358 try:
359 run('commit', '-m', FREEZE + '.indexed')
360 took_action = True
361 except subprocess2.CalledProcessError:
362 pass
363
364 try:
365 run('add', '-A')
366 run('commit', '-m', FREEZE + '.unindexed')
367 took_action = True
368 except subprocess2.CalledProcessError:
369 pass
370
371 if not took_action:
372 return 'Nothing to freeze.'
373
374
375 def get_branch_tree():
376 """Get the dictionary of {branch: parent}, compatible with topo_iter.
377
378 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of
379 branches without upstream branches defined.
380 """
381 skipped = set()
382 branch_tree = {}
383
384 for branch in branches():
385 parent = upstream(branch)
386 if not parent:
387 skipped.add(branch)
388 continue
389 branch_tree[branch] = parent
390
391 return skipped, branch_tree
392
393
394 def get_or_create_merge_base(branch, parent=None):
395 """Finds the configured merge base for branch.
396
397 If parent is supplied, it's used instead of calling upstream(branch).
398 """
399 base = branch_config(branch, 'base')
400 base_upstream = branch_config(branch, 'base-upstream')
401 parent = parent or upstream(branch)
402 if parent is None or branch is None:
403 return None
404 actual_merge_base = run('merge-base', parent, branch)
405
406 if base_upstream != parent:
407 base = None
408 base_upstream = None
409
410 def is_ancestor(a, b):
411 return run_with_retcode('merge-base', '--is-ancestor', a, b) == 0
412
413 if base:
414 if not is_ancestor(base, branch):
415 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base)
416 base = None
417 elif is_ancestor(base, actual_merge_base):
418 logging.debug('Found OLD pre-set merge-base for %s: %s', branch, base)
419 base = None
420 else:
421 logging.debug('Found pre-set merge-base for %s: %s', branch, base)
422
423 if not base:
424 base = actual_merge_base
425 manual_merge_base(branch, base, parent)
426
427 return base
428
429
430 def hash_multi(*reflike):
431 return run('rev-parse', *reflike).splitlines()
432
433
434 def hash_one(reflike, short=False):
435 args = ['rev-parse', reflike]
436 if short:
437 args.insert(1, '--short')
438 return run(*args)
439
440
441 def in_rebase():
442 git_dir = run('rev-parse', '--git-dir')
443 return (
444 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or
445 os.path.exists(os.path.join(git_dir, 'rebase-apply')))
446
447
448 def intern_f(f, kind='blob'):
449 """Interns a file object into the git object store.
450
451 Args:
452 f (file-like object) - The file-like object to intern
453 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
454
455 Returns the git hash of the interned object (hex encoded).
456 """
457 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f)
458 f.close()
459 return ret
460
461
462 def is_dormant(branch):
463 # TODO(iannucci): Do an oldness check?
464 return branch_config(branch, 'dormant', 'false') != 'false'
465
466
467 def manual_merge_base(branch, base, parent):
468 set_branch_config(branch, 'base', base)
469 set_branch_config(branch, 'base-upstream', parent)
470
471
472 def mktree(treedict):
473 """Makes a git tree object and returns its hash.
474
475 See |tree()| for the values of mode, type, and ref.
476
477 Args:
478 treedict - { name: (mode, type, ref) }
479 """
480 with tempfile.TemporaryFile() as f:
481 for name, (mode, typ, ref) in treedict.iteritems():
482 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name))
483 f.seek(0)
484 return run('mktree', '-z', stdin=f)
485
486
202 def parse_commitrefs(*commitrefs): 487 def parse_commitrefs(*commitrefs):
203 """Returns binary encoded commit hashes for one or more commitrefs. 488 """Returns binary encoded commit hashes for one or more commitrefs.
204 489
205 A commitref is anything which can resolve to a commit. Popular examples: 490 A commitref is anything which can resolve to a commit. Popular examples:
206 * 'HEAD' 491 * 'HEAD'
207 * 'origin/master' 492 * 'origin/master'
208 * 'cool_branch~2' 493 * 'cool_branch~2'
209 """ 494 """
210 try: 495 try:
211 return map(binascii.unhexlify, hashes(*commitrefs)) 496 return map(binascii.unhexlify, hash_multi(*commitrefs))
212 except subprocess2.CalledProcessError: 497 except subprocess2.CalledProcessError:
213 raise BadCommitRefException(commitrefs) 498 raise BadCommitRefException(commitrefs)
214 499
215 500
501 RebaseRet = collections.namedtuple('RebaseRet', 'success stdout stderr')
502
503
504 def rebase(parent, start, branch, abort=False):
505 """Rebases |start|..|branch| onto the branch |parent|.
506
507 Args:
508 parent - The new parent ref for the rebased commits.
509 start - The commit to start from
510 branch - The branch to rebase
511 abort - If True, will call git-rebase --abort in the event that the rebase
512 doesn't complete successfully.
513
514 Returns a namedtuple with fields:
515 success - a boolean indicating that the rebase command completed
516 successfully.
517 message - if the rebase failed, this contains the stdout of the failed
518 rebase.
519 """
520 try:
521 args = ['--onto', parent, start, branch]
522 if TEST_MODE:
523 args.insert(0, '--committer-date-is-author-date')
524 run('rebase', *args)
525 return RebaseRet(True, '', '')
526 except subprocess2.CalledProcessError as cpe:
527 if abort:
528 run('rebase', '--abort')
529 return RebaseRet(False, cpe.stdout, cpe.stderr)
530
531
532 def remove_merge_base(branch):
533 del_branch_config(branch, 'base')
534 del_branch_config(branch, 'base-upstream')
535
536
537 def root():
538 return config('depot-tools.upstream', 'origin/master')
539
540
216 def run(*cmd, **kwargs): 541 def run(*cmd, **kwargs):
217 """Runs a git command. Returns stdout as a string. 542 """The same as run_with_stderr, except it only returns stdout."""
543 return run_with_stderr(*cmd, **kwargs)[0]
218 544
219 If logging is DEBUG, we'll print the command before we run it. 545
546 def run_with_retcode(*cmd, **kwargs):
547 """Run a command but only return the status code."""
548 try:
549 run(*cmd, **kwargs)
550 return 0
551 except subprocess2.CalledProcessError as cpe:
552 return cpe.returncode
553
554
555 def run_stream(*cmd, **kwargs):
556 """Runs a git command. Returns stdout as a PIPE (file-like object).
557
558 stderr is dropped to avoid races if the process outputs to both stdout and
559 stderr.
560 """
561 kwargs.setdefault('stderr', subprocess2.VOID)
562 kwargs.setdefault('stdout', subprocess2.PIPE)
563 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
564 proc = subprocess2.Popen(cmd, **kwargs)
565 return proc.stdout
566
567
568 def run_with_stderr(*cmd, **kwargs):
569 """Runs a git command.
570
571 Returns (stdout, stderr) as a pair of strings.
220 572
221 kwargs 573 kwargs
222 autostrip (bool) - Strip the output. Defaults to True. 574 autostrip (bool) - Strip the output. Defaults to True.
223 Output string is always strip()'d. 575 indata (str) - Specifies stdin data for the process.
224 """ 576 """
577 kwargs.setdefault('stdin', subprocess2.PIPE)
578 kwargs.setdefault('stdout', subprocess2.PIPE)
579 kwargs.setdefault('stderr', subprocess2.PIPE)
225 autostrip = kwargs.pop('autostrip', True) 580 autostrip = kwargs.pop('autostrip', True)
226 cmd = (GIT_EXE,) + cmd 581 indata = kwargs.pop('indata', None)
227 logging.debug('Running %s', ' '.join(repr(tok) for tok in cmd)) 582
228 ret = subprocess2.check_output(cmd, stderr=subprocess2.PIPE, **kwargs) 583 cmd = (GIT_EXE, '-c', 'color.ui=never') + cmd
584 proc = subprocess2.Popen(cmd, **kwargs)
585 ret, err = proc.communicate(indata)
586 retcode = proc.wait()
587 if retcode != 0:
588 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err)
589
229 if autostrip: 590 if autostrip:
230 ret = (ret or '').strip() 591 ret = (ret or '').strip()
231 return ret 592 err = (err or '').strip()
593
594 return ret, err
232 595
233 596
234 def hashes(*reflike): 597 def set_branch_config(branch, option, value, scope='local'):
235 return run('rev-parse', *reflike).splitlines() 598 set_config('branch.%s.%s' % (branch, option), value, scope=scope)
236 599
237 600
238 def intern_f(f, kind='blob'): 601 def set_config(option, value, scope='local'):
239 """Interns a file object into the git object store. 602 run('config', '--' + scope, option, value)
240 603
241 Args:
242 f (file-like object) - The file-like object to intern
243 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'.
244 604
245 Returns the git hash of the interned object (hex encoded). 605 def squash_current_branch(header=None, merge_base=None):
606 header = header or 'git squash commit.'
607 merge_base = merge_base or get_or_create_merge_base(current_branch())
608 log_msg = header + '\n'
609 if log_msg:
610 log_msg += '\n'
611 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base)
612 run('reset', '--soft', merge_base)
613 run('commit', '-a', '-F', '-', indata=log_msg)
614
615
616 def tags(*args):
617 return run('tag', *args).splitlines()
618
619
620 def thaw():
621 took_action = False
622 for sha in (s.strip() for s in run_stream('rev-list', 'HEAD').xreadlines()):
623 msg = run('show', '--format=%f%b', '-s', 'HEAD')
624 match = FREEZE_MATCHER.match(msg)
625 if not match:
626 if not took_action:
627 return 'Nothing to thaw.'
628 break
629
630 run('reset', '--' + FREEZE_SECTIONS[match.group(1)], sha)
631 took_action = True
632
633
634 def topo_iter(branch_tree, top_down=True):
635 """Generates (branch, parent) in topographical order for a branch tree.
636
637 Given a tree:
638
639 A1
640 B1 B2
641 C1 C2 C3
642 D1
643
644 branch_tree would look like: {
645 'D1': 'C3',
646 'C3': 'B2',
647 'B2': 'A1',
648 'C1': 'B1',
649 'C2': 'B1',
650 'B1': 'A1',
651 }
652
653 It is OK to have multiple 'root' nodes in your graph.
654
655 if top_down is True, items are yielded from A->D. Otherwise they're yielded
656 from D->A. Within a layer the branches will be yielded in sorted order.
246 """ 657 """
247 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f) 658 branch_tree = branch_tree.copy()
248 f.close() 659
249 return ret 660 # TODO(iannucci): There is probably a more efficient way to do these.
661 if top_down:
662 while branch_tree:
663 this_pass = [(b, p) for b, p in branch_tree.iteritems()
664 if p not in branch_tree]
665 assert this_pass, "Branch tree has cycles: %r" % branch_tree
666 for branch, parent in sorted(this_pass):
667 yield branch, parent
668 del branch_tree[branch]
669 else:
670 parent_to_branches = collections.defaultdict(set)
671 for branch, parent in branch_tree.iteritems():
672 parent_to_branches[parent].add(branch)
673
674 while branch_tree:
675 this_pass = [(b, p) for b, p in branch_tree.iteritems()
676 if not parent_to_branches[b]]
677 assert this_pass, "Branch tree has cycles: %r" % branch_tree
678 for branch, parent in sorted(this_pass):
679 yield branch, parent
680 parent_to_branches[parent].discard(branch)
681 del branch_tree[branch]
250 682
251 683
252 def tree(treeref, recurse=False): 684 def tree(treeref, recurse=False):
253 """Returns a dict representation of a git tree object. 685 """Returns a dict representation of a git tree object.
254 686
255 Args: 687 Args:
256 treeref (str) - a git ref which resolves to a tree (commits count as trees). 688 treeref (str) - a git ref which resolves to a tree (commits count as trees).
257 recurse (bool) - include all of the tree's decendants too. File names will 689 recurse (bool) - include all of the tree's decendants too. File names will
258 take the form of 'some/path/to/file'. 690 take the form of 'some/path/to/file'.
259 691
(...skipping 19 matching lines...) Expand all
279 opts.append(treeref) 711 opts.append(treeref)
280 try: 712 try:
281 for line in run(*opts).splitlines(): 713 for line in run(*opts).splitlines():
282 mode, typ, ref, name = line.split(None, 3) 714 mode, typ, ref, name = line.split(None, 3)
283 ret[name] = (mode, typ, ref) 715 ret[name] = (mode, typ, ref)
284 except subprocess2.CalledProcessError: 716 except subprocess2.CalledProcessError:
285 return None 717 return None
286 return ret 718 return ret
287 719
288 720
289 def mktree(treedict): 721 def upstream(branch):
290 """Makes a git tree object and returns its hash. 722 try:
723 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name',
724 branch+'@{upstream}')
725 except subprocess2.CalledProcessError:
726 return None
291 727
292 See |tree()| for the values of mode, type, and ref.
293 728
294 Args: 729 def get_git_version():
295 treedict - { name: (mode, type, ref) } 730 """Returns a tuple that contains the numeric components of the current git
296 """ 731 version."""
297 with tempfile.TemporaryFile() as f: 732 version_string = run('--version')
298 for name, (mode, typ, ref) in treedict.iteritems(): 733 version_match = re.search(r'(\d+.)+(\d+)', version_string)
299 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name)) 734 version = version_match.group() if version_match else ''
300 f.seek(0) 735
301 return run('mktree', '-z', stdin=f) 736 return tuple(int(x) for x in version.split('.'))
737
738
739 def get_branches_info(include_tracking_status):
740 format_string = (
741 '--format=%(refname:short):%(objectname:short):%(upstream:short):')
742
743 # This is not covered by the depot_tools CQ which only has git version 1.8.
744 if (include_tracking_status and
745 get_git_version() >= MIN_UPSTREAM_TRACK_GIT_VERSION): # pragma: no cover
746 format_string += '%(upstream:track)'
747
748 info_map = {}
749 data = run('for-each-ref', format_string, 'refs/heads')
750 BranchesInfo = collections.namedtuple(
751 'BranchesInfo', 'hash upstream ahead behind')
752 for line in data.splitlines():
753 (branch, branch_hash, upstream_branch, tracking_status) = line.split(':')
754
755 ahead_match = re.search(r'ahead (\d+)', tracking_status)
756 ahead = int(ahead_match.group(1)) if ahead_match else None
757
758 behind_match = re.search(r'behind (\d+)', tracking_status)
759 behind = int(behind_match.group(1)) if behind_match else None
760
761 info_map[branch] = BranchesInfo(
762 hash=branch_hash, upstream=upstream_branch, ahead=ahead, behind=behind)
763
764 # Set None for upstreams which are not branches (e.g empty upstream, remotes
765 # and deleted upstream branches).
766 missing_upstreams = {}
767 for info in info_map.values():
768 if info.upstream not in info_map and info.upstream not in missing_upstreams:
769 missing_upstreams[info.upstream] = None
770
771 return dict(info_map.items() + missing_upstreams.items())
OLDNEW
« no previous file with comments | « appengine/third_party_local/depot_tools/auto_stub.py ('k') | appengine/third_party_local/depot_tools/git_number.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698