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