Chromium Code Reviews| 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 | 
| 22 import os | 23 import os | 
| 24 import re | |
| 23 import signal | 25 import signal | 
| 24 import sys | 26 import sys | 
| 25 import tempfile | 27 import tempfile | 
| 26 import threading | 28 import threading | 
| 27 | 29 | 
| 28 import subprocess2 | 30 import subprocess2 | 
| 29 | 31 | 
| 30 | 32 | 
| 31 GIT_EXE = 'git.bat' if sys.platform.startswith('win') else 'git' | 33 GIT_EXE = 'git.bat' if sys.platform.startswith('win') else 'git' | 
| 34 TEST_MODE = False | |
| 32 | 35 | 
| 33 | 36 | 
| 34 class BadCommitRefException(Exception): | 37 class BadCommitRefException(Exception): | 
| 35 def __init__(self, refs): | 38 def __init__(self, refs): | 
| 36 msg = ('one of %s does not seem to be a valid commitref.' % | 39 msg = ('one of %s does not seem to be a valid commitref.' % | 
| 37 str(refs)) | 40 str(refs)) | 
| 38 super(BadCommitRefException, self).__init__(msg) | 41 super(BadCommitRefException, self).__init__(msg) | 
| 39 | 42 | 
| 40 | 43 | 
| 41 def memoize_one(**kwargs): | 44 def memoize_one(**kwargs): | 
| (...skipping 151 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 193 return self.inc | 196 return self.inc | 
| 194 | 197 | 
| 195 def __exit__(self, _exc_type, _exc_value, _traceback): | 198 def __exit__(self, _exc_type, _exc_value, _traceback): | 
| 196 self._dead = True | 199 self._dead = True | 
| 197 with self._dead_cond: | 200 with self._dead_cond: | 
| 198 self._dead_cond.notifyAll() | 201 self._dead_cond.notifyAll() | 
| 199 self._thread.join() | 202 self._thread.join() | 
| 200 del self._thread | 203 del self._thread | 
| 201 | 204 | 
| 202 | 205 | 
| 206 def once(function): | |
| 207 """@Decorates |function| so that it only performs its action once, no matter | |
| 
 
ghost stip (do not use)
2014/03/25 23:44:22
nit: """One line.
"""
 
 | |
| 208 how many times the decorated |function| is called.""" | |
| 209 def _inner_gen(): | |
| 210 yield function() | |
| 211 while True: | |
| 212 yield | |
| 213 return _inner_gen().next | |
| 
 
ghost stip (do not use)
2014/03/25 23:44:22
can you add a comment how this works?
 
 | |
| 214 | |
| 215 | |
| 216 ## Git functions | |
| 217 | |
| 203 def branches(*args): | 218 def branches(*args): | 
| 204 NO_BRANCH = ('* (no branch)', '* (detached from ') | 219 NO_BRANCH = ('* (no branch', '* (detached from ') | 
| 
 
ghost stip (do not use)
2014/03/25 23:44:22
why remove the )?
 
 | |
| 205 for line in run('branch', *args).splitlines(): | 220 for line in run('branch', *args).splitlines(): | 
| 206 if line.startswith(NO_BRANCH): | 221 if line.startswith(NO_BRANCH): | 
| 207 continue | 222 continue | 
| 208 yield line.split()[-1] | 223 yield line.split()[-1] | 
| 209 | 224 | 
| 210 | 225 | 
| 226 def branch_config(branch, option, default=None): | |
| 227 return config('branch.%s.%s' % (branch, option), default=default) | |
| 228 | |
| 229 | |
| 230 def config(option, default=None): | |
| 231 try: | |
| 232 return run('config', '--get', option) or default | |
| 233 except subprocess2.CalledProcessError: | |
| 234 return default | |
| 235 | |
| 236 | |
| 211 def config_list(option): | 237 def config_list(option): | 
| 212 try: | 238 try: | 
| 213 return run('config', '--get-all', option).split() | 239 return run('config', '--get-all', option).split() | 
| 214 except subprocess2.CalledProcessError: | 240 except subprocess2.CalledProcessError: | 
| 215 return [] | 241 return [] | 
| 216 | 242 | 
| 217 | 243 | 
| 244 def branch_config_map(option): | |
| 245 """Return {branch: <|option| value>} for all branches.""" | |
| 246 try: | |
| 247 reg = re.compile(r'^branch\.(.*)\.%s$' % option) | |
| 248 lines = run('config', '--get-regexp', reg.pattern).splitlines() | |
| 249 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)} | |
| 250 except subprocess2.CalledProcessError: | |
| 251 return {} | |
| 252 | |
| 253 | |
| 218 def current_branch(): | 254 def current_branch(): | 
| 219 return run('rev-parse', '--abbrev-ref', 'HEAD') | 255 try: | 
| 256 return run('rev-parse', '--abbrev-ref', 'HEAD') | |
| 257 except subprocess2.CalledProcessError: | |
| 258 return None | |
| 259 | |
| 260 | |
| 261 def del_branch_config(branch, option, scope='local'): | |
| 262 del_config('branch.%s.%s' % (branch, option), scope=scope) | |
| 263 | |
| 264 | |
| 265 def del_config(option, scope='local'): | |
| 266 try: | |
| 267 run('config', '--' + scope, '--unset', option) | |
| 268 except subprocess2.CalledProcessError: | |
| 269 pass | |
| 270 | |
| 271 | |
| 272 def get_branch_tree(): | |
| 273 """Get the dictionary of {branch: parent}, compatible with topo_iter. | |
| 274 | |
| 275 Returns a tuple of (skipped, <branch_tree dict>) where skipped is a set of | |
| 276 branches without upstream branches defined. | |
| 277 """ | |
| 278 skipped = set() | |
| 279 branch_tree = {} | |
| 280 | |
| 281 for branch in branches(): | |
| 282 parent = upstream(branch) | |
| 283 if not parent: | |
| 284 skipped.add(branch) | |
| 285 continue | |
| 286 branch_tree[branch] = parent | |
| 287 | |
| 288 return skipped, branch_tree | |
| 220 | 289 | 
| 221 | 290 | 
| 222 def parse_commitrefs(*commitrefs): | 291 def parse_commitrefs(*commitrefs): | 
| 223 """Returns binary encoded commit hashes for one or more commitrefs. | 292 """Returns binary encoded commit hashes for one or more commitrefs. | 
| 224 | 293 | 
| 225 A commitref is anything which can resolve to a commit. Popular examples: | 294 A commitref is anything which can resolve to a commit. Popular examples: | 
| 226 * 'HEAD' | 295 * 'HEAD' | 
| 227 * 'origin/master' | 296 * 'origin/master' | 
| 228 * 'cool_branch~2' | 297 * 'cool_branch~2' | 
| 229 """ | 298 """ | 
| 230 try: | 299 try: | 
| 231 return map(binascii.unhexlify, hash_multi(*commitrefs)) | 300 return map(binascii.unhexlify, hash_multi(*commitrefs)) | 
| 232 except subprocess2.CalledProcessError: | 301 except subprocess2.CalledProcessError: | 
| 233 raise BadCommitRefException(commitrefs) | 302 raise BadCommitRefException(commitrefs) | 
| 234 | 303 | 
| 235 | 304 | 
| 305 def root(): | |
| 306 return config('depot-tools.upstream', 'origin/master') | |
| 307 | |
| 308 | |
| 236 def run(*cmd, **kwargs): | 309 def run(*cmd, **kwargs): | 
| 237 """Runs a git command. Returns stdout as a string. | 310 """The same as run_with_stderr, except it only returns stdout.""" | 
| 
 
ghost stip (do not use)
2014/03/25 23:44:22
'Calls run_with_stderr but only returns stdout.'
 
 | |
| 311 return run_with_stderr(*cmd, **kwargs)[0] | |
| 238 | 312 | 
| 239 If logging is DEBUG, we'll print the command before we run it. | 313 | 
| 314 def run_with_stderr(*cmd, **kwargs): | |
| 315 """Runs a git command. | |
| 316 | |
| 317 Returns (stdout, stderr) as a pair of strings. | |
| 240 | 318 | 
| 241 kwargs | 319 kwargs | 
| 242 autostrip (bool) - Strip the output. Defaults to True. | 320 autostrip (bool) - Strip the output. Defaults to True. | 
| 321 indata (str) - Specifies stdin data for the process. | |
| 243 """ | 322 """ | 
| 323 kwargs.setdefault('stdin', subprocess2.PIPE) | |
| 324 kwargs.setdefault('stdout', subprocess2.PIPE) | |
| 325 kwargs.setdefault('stderr', subprocess2.PIPE) | |
| 244 autostrip = kwargs.pop('autostrip', True) | 326 autostrip = kwargs.pop('autostrip', True) | 
| 327 indata = kwargs.pop('indata', None) | |
| 245 | 328 | 
| 246 retstream, proc = stream_proc(*cmd, **kwargs) | 329 cmd = (GIT_EXE,) + cmd | 
| 247 ret = retstream.read() | 330 proc = subprocess2.Popen(cmd, **kwargs) | 
| 331 ret, err = proc.communicate(indata) | |
| 248 retcode = proc.wait() | 332 retcode = proc.wait() | 
| 249 if retcode != 0: | 333 if retcode != 0: | 
| 250 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, None) | 334 raise subprocess2.CalledProcessError(retcode, cmd, os.getcwd(), ret, err) | 
| 251 | 335 | 
| 252 if autostrip: | 336 if autostrip: | 
| 253 ret = (ret or '').strip() | 337 ret = (ret or '').strip() | 
| 254 return ret | 338 err = (err or '').strip() | 
| 339 | |
| 340 return ret, err | |
| 255 | 341 | 
| 256 | 342 | 
| 257 def stream_proc(*cmd, **kwargs): | 343 def run_stream(*cmd, **kwargs): | 
| 258 """Runs a git command. Returns stdout as a file. | 344 """Runs a git command. Returns stdout as a PIPE (file-like object). | 
| 259 | 345 | 
| 260 If logging is DEBUG, we'll print the command before we run it. | 346 stderr is dropped to avoid races if the process outputs to both stdout and | 
| 347 stderr. | |
| 261 """ | 348 """ | 
| 349 kwargs.setdefault('stderr', subprocess2.VOID) | |
| 350 kwargs.setdefault('stdout', subprocess2.PIPE) | |
| 262 cmd = (GIT_EXE,) + cmd | 351 cmd = (GIT_EXE,) + cmd | 
| 263 logging.debug('Running %s', ' '.join(repr(tok) for tok in cmd)) | 352 proc = subprocess2.Popen(cmd, **kwargs) | 
| 264 proc = subprocess2.Popen(cmd, stderr=subprocess2.VOID, | 353 return proc.stdout | 
| 265 stdout=subprocess2.PIPE, **kwargs) | |
| 266 return proc.stdout, proc | |
| 267 | 354 | 
| 268 | 355 | 
| 269 def stream(*cmd, **kwargs): | 356 def set_branch_config(branch, option, value, scope='local'): | 
| 270 return stream_proc(*cmd, **kwargs)[0] | 357 set_config('branch.%s.%s' % (branch, option), value, scope=scope) | 
| 358 | |
| 359 | |
| 360 def set_config(option, value, scope='local'): | |
| 361 run('config', '--' + scope, option, value) | |
| 362 | |
| 363 | |
| 364 def get_or_create_merge_base(branch, parent=None): | |
| 365 """Finds the configured merge base for branch. | |
| 366 | |
| 367 If parent is supplied, it's used instead of calling upstream(branch). | |
| 368 """ | |
| 369 base = branch_config(branch, 'base') | |
| 370 if base: | |
| 371 try: | |
| 372 run('merge-base', '--is-ancestor', base, branch) | |
| 373 logging.debug('Found pre-set merge-base for %s: %s', branch, base) | |
| 374 except subprocess2.CalledProcessError: | |
| 375 logging.debug('Found WRONG pre-set merge-base for %s: %s', branch, base) | |
| 376 base = None | |
| 377 | |
| 378 if not base: | |
| 379 base = run('merge-base', parent or upstream(branch), branch) | |
| 380 manual_merge_base(branch, base) | |
| 381 | |
| 382 return base | |
| 271 | 383 | 
| 272 | 384 | 
| 273 def hash_one(reflike): | 385 def hash_one(reflike): | 
| 274 return run('rev-parse', reflike) | 386 return run('rev-parse', reflike) | 
| 275 | 387 | 
| 276 | 388 | 
| 277 def hash_multi(*reflike): | 389 def hash_multi(*reflike): | 
| 278 return run('rev-parse', *reflike).splitlines() | 390 return run('rev-parse', *reflike).splitlines() | 
| 279 | 391 | 
| 280 | 392 | 
| 393 def in_rebase(): | |
| 394 git_dir = run('rev-parse', '--git-dir') | |
| 395 return ( | |
| 396 os.path.exists(os.path.join(git_dir, 'rebase-merge')) or | |
| 397 os.path.exists(os.path.join(git_dir, 'rebase-apply'))) | |
| 398 | |
| 399 | |
| 281 def intern_f(f, kind='blob'): | 400 def intern_f(f, kind='blob'): | 
| 282 """Interns a file object into the git object store. | 401 """Interns a file object into the git object store. | 
| 283 | 402 | 
| 284 Args: | 403 Args: | 
| 285 f (file-like object) - The file-like object to intern | 404 f (file-like object) - The file-like object to intern | 
| 286 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'. | 405 kind (git object type) - One of 'blob', 'commit', 'tree', 'tag'. | 
| 287 | 406 | 
| 288 Returns the git hash of the interned object (hex encoded). | 407 Returns the git hash of the interned object (hex encoded). | 
| 289 """ | 408 """ | 
| 290 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f) | 409 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f) | 
| 291 f.close() | 410 f.close() | 
| 292 return ret | 411 return ret | 
| 293 | 412 | 
| 294 | 413 | 
| 414 def is_dormant(branch): | |
| 415 # TODO(iannucci): Do an oldness check? | |
| 416 return branch_config(branch, 'dormant', 'false') != 'false' | |
| 417 | |
| 418 | |
| 419 def manual_merge_base(branch, base): | |
| 420 set_branch_config(branch, 'base', base) | |
| 421 | |
| 422 | |
| 423 def mktree(treedict): | |
| 424 """Makes a git tree object and returns its hash. | |
| 425 | |
| 426 See |tree()| for the values of mode, type, and ref. | |
| 427 | |
| 428 Args: | |
| 429 treedict - { name: (mode, type, ref) } | |
| 430 """ | |
| 431 with tempfile.TemporaryFile() as f: | |
| 432 for name, (mode, typ, ref) in treedict.iteritems(): | |
| 433 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name)) | |
| 434 f.seek(0) | |
| 435 return run('mktree', '-z', stdin=f) | |
| 436 | |
| 437 | |
| 438 def remove_merge_base(branch): | |
| 439 del_branch_config(branch, 'base') | |
| 440 | |
| 441 | |
| 442 RebaseRet = collections.namedtuple('RebaseRet', 'success message') | |
| 
 
ghost stip (do not use)
2014/03/25 23:44:22
wouldn't this be like DefaultRebaseRet? RebaseRetT
 
 | |
| 443 | |
| 444 | |
| 445 def rebase(parent, start, branch, abort=False): | |
| 446 """Rebases |start|..|branch| onto the branch |parent|. | |
| 447 | |
| 448 Args: | |
| 449 parent - The new parent ref for the rebased commits. | |
| 450 start - The commit to start from | |
| 451 branch - The branch to rebase | |
| 452 abort - If True, will call git-rebase --abort in the event that the rebase | |
| 453 doesn't complete successfully. | |
| 454 | |
| 455 Returns a namedtuple with fields: | |
| 456 success - a boolean indicating that the rebase command completed | |
| 457 successfully. | |
| 458 message - if the rebase failed, this contains the stdout of the failed | |
| 459 rebase. | |
| 460 """ | |
| 461 try: | |
| 462 args = ['--onto', parent, start, branch] | |
| 463 if TEST_MODE: | |
| 464 args.insert(0, '--committer-date-is-author-date') | |
| 465 run('rebase', *args) | |
| 466 return RebaseRet(True, '') | |
| 467 except subprocess2.CalledProcessError as cpe: | |
| 468 if abort: | |
| 469 run('rebase', '--abort') | |
| 470 return RebaseRet(False, cpe.output) | |
| 471 | |
| 472 | |
| 473 def squash_current_branch(header=None, merge_base=None): | |
| 474 header = header or 'git squash commit.' | |
| 475 merge_base = merge_base or get_or_create_merge_base(current_branch()) | |
| 476 log_msg = header + '\n' | |
| 477 if log_msg: | |
| 478 log_msg += '\n' | |
| 479 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base) | |
| 480 run('reset', '--soft', merge_base) | |
| 481 run('commit', '-a', '-F', '-', indata=log_msg) | |
| 482 | |
| 483 | |
| 295 def tags(*args): | 484 def tags(*args): | 
| 296 return run('tag', *args).splitlines() | 485 return run('tag', *args).splitlines() | 
| 297 | 486 | 
| 298 | 487 | 
| 488 def topo_iter(branch_tree, top_down=True): | |
| 489 """Generates (branch, parent) in topographical order for a branch tree. | |
| 490 | |
| 491 Given a tree: | |
| 492 | |
| 493 A1 | |
| 494 B1 B2 | |
| 495 C1 C2 C3 | |
| 496 D1 | |
| 497 | |
| 498 branch_tree would look like: { | |
| 499 'D1': 'C3', | |
| 500 'C3': 'B2', | |
| 501 'B2': 'A1', | |
| 502 'C1': 'B1', | |
| 503 'C2': 'B1', | |
| 504 'B1': 'A1', | |
| 505 } | |
| 506 | |
| 507 It is OK to have multiple 'root' nodes in your graph. | |
| 508 | |
| 509 if top_down is True, items are yielded from A->D. Otherwise they're yielded | |
| 510 from D->A. Within a layer the branches will be yielded in sorted order. | |
| 511 """ | |
| 512 branch_tree = branch_tree.copy() | |
| 513 | |
| 514 # TODO(iannucci): There is probably a more efficient way to do these. | |
| 515 if top_down: | |
| 516 while branch_tree: | |
| 517 this_pass = [(b, p) for b, p in branch_tree.iteritems() | |
| 518 if p not in branch_tree] | |
| 519 assert this_pass, "Branch tree has cycles: %r" % branch_tree | |
| 520 for branch, parent in sorted(this_pass): | |
| 521 yield branch, parent | |
| 522 del branch_tree[branch] | |
| 523 else: | |
| 524 parent_to_branches = collections.defaultdict(set) | |
| 525 for branch, parent in branch_tree.iteritems(): | |
| 526 parent_to_branches[parent].add(branch) | |
| 527 | |
| 528 while branch_tree: | |
| 529 this_pass = [(b, p) for b, p in branch_tree.iteritems() | |
| 530 if not parent_to_branches[b]] | |
| 531 assert this_pass, "Branch tree has cycles: %r" % branch_tree | |
| 532 for branch, parent in sorted(this_pass): | |
| 533 yield branch, parent | |
| 534 parent_to_branches[parent].discard(branch) | |
| 535 del branch_tree[branch] | |
| 536 | |
| 537 | |
| 299 def tree(treeref, recurse=False): | 538 def tree(treeref, recurse=False): | 
| 300 """Returns a dict representation of a git tree object. | 539 """Returns a dict representation of a git tree object. | 
| 301 | 540 | 
| 302 Args: | 541 Args: | 
| 303 treeref (str) - a git ref which resolves to a tree (commits count as trees). | 542 treeref (str) - a git ref which resolves to a tree (commits count as trees). | 
| 304 recurse (bool) - include all of the tree's decendants too. File names will | 543 recurse (bool) - include all of the tree's decendants too. File names will | 
| 305 take the form of 'some/path/to/file'. | 544 take the form of 'some/path/to/file'. | 
| 306 | 545 | 
| 307 Return format: | 546 Return format: | 
| 308 { 'file_name': (mode, type, ref) } | 547 { 'file_name': (mode, type, ref) } | 
| (...skipping 23 matching lines...) Expand all Loading... | |
| 332 return None | 571 return None | 
| 333 return ret | 572 return ret | 
| 334 | 573 | 
| 335 | 574 | 
| 336 def upstream(branch): | 575 def upstream(branch): | 
| 337 try: | 576 try: | 
| 338 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name', | 577 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name', | 
| 339 branch+'@{upstream}') | 578 branch+'@{upstream}') | 
| 340 except subprocess2.CalledProcessError: | 579 except subprocess2.CalledProcessError: | 
| 341 return None | 580 return None | 
| 342 | |
| 343 | |
| 344 def mktree(treedict): | |
| 345 """Makes a git tree object and returns its hash. | |
| 346 | |
| 347 See |tree()| for the values of mode, type, and ref. | |
| 348 | |
| 349 Args: | |
| 350 treedict - { name: (mode, type, ref) } | |
| 351 """ | |
| 352 with tempfile.TemporaryFile() as f: | |
| 353 for name, (mode, typ, ref) in treedict.iteritems(): | |
| 354 f.write('%s %s %s\t%s\0' % (mode, typ, ref, name)) | |
| 355 f.seek(0) | |
| 356 return run('mktree', '-z', stdin=f) | |
| OLD | NEW |