OLD | NEW |
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 Loading... |
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 Loading... |
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 Loading... |
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()) |
OLD | NEW |