Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2014 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): |
| (...skipping 12 matching lines...) Expand all Loading... | |
| 23 import os | 23 import os |
| 24 import re | 24 import re |
| 25 import signal | 25 import signal |
| 26 import sys | 26 import sys |
| 27 import tempfile | 27 import tempfile |
| 28 import textwrap | 28 import textwrap |
| 29 import threading | 29 import threading |
| 30 | 30 |
| 31 import subprocess2 | 31 import subprocess2 |
| 32 | 32 |
| 33 from cStringIO import StringIO | |
| 34 | |
| 33 | 35 |
| 34 GIT_EXE = 'git.bat' if sys.platform.startswith('win') else 'git' | 36 GIT_EXE = 'git.bat' if sys.platform.startswith('win') else 'git' |
| 35 TEST_MODE = False | 37 TEST_MODE = False |
| 36 | 38 |
| 37 FREEZE = 'FREEZE' | 39 FREEZE = 'FREEZE' |
| 38 FREEZE_SECTIONS = { | 40 FREEZE_SECTIONS = { |
| 39 'indexed': 'soft', | 41 'indexed': 'soft', |
| 40 'unindexed': 'mixed' | 42 'unindexed': 'mixed' |
| 41 } | 43 } |
| 42 FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS))) | 44 FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS))) |
| (...skipping 173 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 216 how many times the decorated |function| is called.""" | 218 how many times the decorated |function| is called.""" |
| 217 def _inner_gen(): | 219 def _inner_gen(): |
| 218 yield function() | 220 yield function() |
| 219 while True: | 221 while True: |
| 220 yield | 222 yield |
| 221 return _inner_gen().next | 223 return _inner_gen().next |
| 222 | 224 |
| 223 | 225 |
| 224 ## Git functions | 226 ## Git functions |
| 225 | 227 |
| 228 def die_if(cond, message, *args): | |
|
agable
2014/06/11 21:30:16
I'd prefer just die(message, *args), and leave the
iannucci
2014/06/28 18:08:55
Done.
| |
| 229 if cond: | |
| 230 print >> sys.stderr, textwrap.dedent(message % args) | |
| 231 sys.exit(1) | |
| 232 | |
| 226 | 233 |
| 227 def branch_config(branch, option, default=None): | 234 def branch_config(branch, option, default=None): |
| 228 return config('branch.%s.%s' % (branch, option), default=default) | 235 return config('branch.%s.%s' % (branch, option), default=default) |
| 229 | 236 |
| 230 | 237 |
| 231 def branch_config_map(option): | 238 def branch_config_map(option): |
| 232 """Return {branch: <|option| value>} for all branches.""" | 239 """Return {branch: <|option| value>} for all branches.""" |
| 233 try: | 240 try: |
| 234 reg = re.compile(r'^branch\.(.*)\.%s$' % option) | 241 reg = re.compile(r'^branch\.(.*)\.%s$' % option) |
| 235 lines = run('config', '--get-regexp', reg.pattern).splitlines() | 242 lines = run('config', '--get-regexp', reg.pattern).splitlines() |
| 236 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)} | 243 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)} |
| 237 except subprocess2.CalledProcessError: | 244 except subprocess2.CalledProcessError: |
| 238 return {} | 245 return {} |
| 239 | 246 |
| 240 | 247 |
| 241 def branches(*args): | 248 def branches(*args): |
| 242 NO_BRANCH = ('* (no branch', '* (detached from ') | 249 NO_BRANCH = ('* (no branch', '* (detached from ') |
| 243 | 250 |
| 244 key = 'depot-tools.branch-limit' | 251 key = 'depot-tools.branch-limit' |
| 245 limit = 20 | 252 limit = config_int(key, 20) |
|
agable
2014/06/11 21:30:16
See comment below. This is super weird -- it looks
iannucci
2014/06/28 18:08:55
Done.
| |
| 246 try: | |
| 247 limit = int(config(key, limit)) | |
| 248 except ValueError: | |
| 249 pass | |
| 250 | 253 |
| 251 raw_branches = run('branch', *args).splitlines() | 254 raw_branches = run('branch', *args).splitlines() |
| 252 | 255 |
| 253 num = len(raw_branches) | 256 num = len(raw_branches) |
| 254 if num > limit: | 257 |
| 255 print >> sys.stderr, textwrap.dedent("""\ | 258 die_if(num > limit, """\ |
| 256 Your git repo has too many branches (%d/%d) for this tool to work well. | 259 Your git repo has too many branches (%d/%d) for this tool to work well. |
| 257 | 260 |
| 258 You may adjust this limit by running: | 261 You may adjust this limit by running: |
| 259 git config %s <new_limit> | 262 git config %s <new_limit> |
| 260 """ % (num, limit, key)) | 263 """, num, limit, key) |
| 261 sys.exit(1) | |
| 262 | 264 |
| 263 for line in raw_branches: | 265 for line in raw_branches: |
| 264 if line.startswith(NO_BRANCH): | 266 if line.startswith(NO_BRANCH): |
| 265 continue | 267 continue |
| 266 yield line.split()[-1] | 268 yield line.split()[-1] |
| 267 | 269 |
| 268 | 270 |
| 269 def run_with_retcode(*cmd, **kwargs): | 271 def run_with_retcode(*cmd, **kwargs): |
| 270 """Run a command but only return the status code.""" | 272 """Run a command but only return the status code.""" |
| 271 try: | 273 try: |
| 272 run(*cmd, **kwargs) | 274 run(*cmd, **kwargs) |
| 273 return 0 | 275 return 0 |
| 274 except subprocess2.CalledProcessError as cpe: | 276 except subprocess2.CalledProcessError as cpe: |
| 275 return cpe.returncode | 277 return cpe.returncode |
| 276 | 278 |
| 277 | 279 |
| 278 def config(option, default=None): | 280 def config(option, default=None): |
|
agable
2014/06/11 21:30:17
I find this API weird. "git config key value" *set
iannucci
2014/06/28 18:08:55
Done.
| |
| 279 try: | 281 try: |
| 280 return run('config', '--get', option) or default | 282 return run('config', '--get', option) or default |
| 281 except subprocess2.CalledProcessError: | 283 except subprocess2.CalledProcessError: |
| 282 return default | 284 return default |
| 283 | 285 |
| 284 | 286 |
| 287 def config_int(option, default=0): | |
| 288 assert isinstance(default, int) | |
| 289 try: | |
| 290 return int(config(option, default)) | |
| 291 except ValueError: | |
| 292 return default | |
| 293 | |
| 294 | |
| 285 def config_list(option): | 295 def config_list(option): |
| 286 try: | 296 try: |
| 287 return run('config', '--get-all', option).split() | 297 return run('config', '--get-all', option).split() |
| 288 except subprocess2.CalledProcessError: | 298 except subprocess2.CalledProcessError: |
| 289 return [] | 299 return [] |
| 290 | 300 |
| 291 | 301 |
| 292 def current_branch(): | 302 def current_branch(): |
| 293 try: | 303 try: |
| 294 return run('rev-parse', '--abbrev-ref', 'HEAD') | 304 return run('rev-parse', '--abbrev-ref', 'HEAD') |
| 295 except subprocess2.CalledProcessError: | 305 except subprocess2.CalledProcessError: |
| 296 return None | 306 return None |
| 297 | 307 |
| 298 | 308 |
| 299 def del_branch_config(branch, option, scope='local'): | 309 def del_branch_config(branch, option, scope='local'): |
| 300 del_config('branch.%s.%s' % (branch, option), scope=scope) | 310 del_config('branch.%s.%s' % (branch, option), scope=scope) |
| 301 | 311 |
| 302 | 312 |
| 303 def del_config(option, scope='local'): | 313 def del_config(option, scope='local'): |
| 304 try: | 314 try: |
| 305 run('config', '--' + scope, '--unset', option) | 315 run('config', '--' + scope, '--unset', option) |
| 306 except subprocess2.CalledProcessError: | 316 except subprocess2.CalledProcessError: |
| 307 pass | 317 pass |
| 308 | 318 |
| 309 | 319 |
| 310 def freeze(): | 320 def freeze(): |
| 311 took_action = False | 321 took_action = False |
| 312 | 322 |
| 323 stat = status() | |
| 324 die_if(any('U' in (s.lstat, s.rstat) for s in stat.itervalues()), | |
| 325 "Cannot freeze unmerged changes!") | |
| 326 | |
| 327 key = 'depot-tools.freeze-size-limit' | |
| 328 MB = 2**20 | |
| 329 limit_mb = config_int(key, 100) | |
| 330 if limit_mb > 0: | |
| 331 untracked_size = sum( | |
| 332 os.stat(f).st_size | |
| 333 for f, s in stat.iteritems() if s.lstat == '?' | |
| 334 ) / MB | |
| 335 die_if(untracked_size > limit_mb, """\ | |
| 336 You have too much untracked+unignored data in your git checkout: %d/%dMB. | |
| 337 Run `git status` to see what it is. | |
| 338 | |
| 339 In addition to making many git commands slower, this will prevent | |
| 340 depot_tools from freezing your in-progress changes. | |
| 341 | |
| 342 You should add untracked data that you want to ignore to your repo's | |
| 343 .git/info/excludes | |
| 344 file. See `git help ignore` for the format of this file. | |
| 345 | |
| 346 If this data is indended as part of your commit, you may adjust the freeze | |
| 347 limit by running: | |
| 348 git config %s <new_limit> | |
| 349 Where <new_limit> is an integer threshold in megabytes.""", | |
| 350 untracked_size, limit_mb, key) | |
| 351 | |
| 313 try: | 352 try: |
| 314 run('commit', '-m', FREEZE + '.indexed') | 353 run('commit', '-m', FREEZE + '.indexed') |
| 315 took_action = True | 354 took_action = True |
| 316 except subprocess2.CalledProcessError: | 355 except subprocess2.CalledProcessError: |
| 317 pass | 356 pass |
| 318 | 357 |
| 319 try: | 358 try: |
| 320 run('add', '-A') | 359 run('add', '-A') |
| 321 run('commit', '-m', FREEZE + '.unindexed') | 360 run('commit', '-m', FREEZE + '.unindexed') |
| 322 took_action = True | 361 took_action = True |
| (...skipping 214 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 537 return ret, err | 576 return ret, err |
| 538 | 577 |
| 539 | 578 |
| 540 def set_branch_config(branch, option, value, scope='local'): | 579 def set_branch_config(branch, option, value, scope='local'): |
| 541 set_config('branch.%s.%s' % (branch, option), value, scope=scope) | 580 set_config('branch.%s.%s' % (branch, option), value, scope=scope) |
| 542 | 581 |
| 543 | 582 |
| 544 def set_config(option, value, scope='local'): | 583 def set_config(option, value, scope='local'): |
| 545 run('config', '--' + scope, option, value) | 584 run('config', '--' + scope, option, value) |
| 546 | 585 |
| 586 | |
| 587 def status(): | |
| 588 """Returns a parsed version of git-status. | |
| 589 | |
| 590 Returns a dictionary of {current_name: (lstat, rstat, src)} where: | |
| 591 * lstat is the left status code letter from git-status | |
| 592 * rstat is the left status code letter from git-status | |
| 593 * src is the 'original' name of the file if lstat == 'R', or | |
|
agable
2014/06/11 21:30:16
src is the current name of the file, or the 'origi
iannucci
2014/06/28 18:08:55
Done.
| |
| 594 the current_name. | |
| 595 """ | |
| 596 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src') | |
| 597 | |
| 598 def tokenizer(stream): | |
| 599 acc = StringIO() | |
| 600 c = None | |
| 601 while c != '': | |
| 602 c = stream.read(1) | |
| 603 if c in (None, '', '\0'): | |
| 604 s = acc.getvalue() | |
| 605 acc = StringIO() | |
| 606 if s: | |
| 607 yield s | |
| 608 else: | |
| 609 acc.write(c) | |
| 610 | |
| 611 def parser(tokens): | |
| 612 END = object() | |
| 613 tok = lambda: next(tokens, END) | |
| 614 while True: | |
| 615 status_dest = tok() | |
| 616 if status_dest is END: | |
| 617 return | |
| 618 stat, dest = status_dest[:2], status_dest[3:] | |
| 619 lstat, rstat = stat | |
| 620 if lstat == 'R': | |
| 621 src = tok() | |
| 622 assert src is not END | |
| 623 else: | |
| 624 src = dest | |
| 625 yield (dest, stat_entry(lstat, rstat, src)) | |
| 626 | |
| 627 return dict(parser(tokenizer(run_stream('status', '-z', bufsize=-1)))) | |
|
agable
2014/06/11 21:30:16
Why not use --porcelain and then iterate over read
iannucci
2014/06/28 18:08:55
Then I have to tokenzie quotes. --porcelain is way
| |
| 628 | |
| 629 | |
| 547 def squash_current_branch(header=None, merge_base=None): | 630 def squash_current_branch(header=None, merge_base=None): |
| 548 header = header or 'git squash commit.' | 631 header = header or 'git squash commit.' |
| 549 merge_base = merge_base or get_or_create_merge_base(current_branch()) | 632 merge_base = merge_base or get_or_create_merge_base(current_branch()) |
| 550 log_msg = header + '\n' | 633 log_msg = header + '\n' |
| 551 if log_msg: | 634 if log_msg: |
| 552 log_msg += '\n' | 635 log_msg += '\n' |
| 553 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base) | 636 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base) |
| 554 run('reset', '--soft', merge_base) | 637 run('reset', '--soft', merge_base) |
| 555 run('commit', '-a', '-F', '-', indata=log_msg) | 638 run('commit', '-a', '-F', '-', indata=log_msg) |
| 556 | 639 |
| (...skipping 102 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 659 return None | 742 return None |
| 660 return ret | 743 return ret |
| 661 | 744 |
| 662 | 745 |
| 663 def upstream(branch): | 746 def upstream(branch): |
| 664 try: | 747 try: |
| 665 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name', | 748 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name', |
| 666 branch+'@{upstream}') | 749 branch+'@{upstream}') |
| 667 except subprocess2.CalledProcessError: | 750 except subprocess2.CalledProcessError: |
| 668 return None | 751 return None |
| OLD | NEW |