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 14 matching lines...) Expand all Loading... | |
25 import setup_color | 25 import setup_color |
26 import shutil | 26 import shutil |
27 import signal | 27 import signal |
28 import sys | 28 import sys |
29 import tempfile | 29 import tempfile |
30 import textwrap | 30 import textwrap |
31 import threading | 31 import threading |
32 | 32 |
33 import subprocess2 | 33 import subprocess2 |
34 | 34 |
35 from cStringIO import StringIO | |
36 | |
37 | |
35 ROOT = os.path.abspath(os.path.dirname(__file__)) | 38 ROOT = os.path.abspath(os.path.dirname(__file__)) |
36 | |
37 IS_WIN = sys.platform == 'win32' | 39 IS_WIN = sys.platform == 'win32' |
38 GIT_EXE = ROOT+'\\git.bat' if IS_WIN else 'git' | 40 GIT_EXE = ROOT+'\\git.bat' if IS_WIN else 'git' |
39 TEST_MODE = False | 41 TEST_MODE = False |
40 | 42 |
41 FREEZE = 'FREEZE' | 43 FREEZE = 'FREEZE' |
42 FREEZE_SECTIONS = { | 44 FREEZE_SECTIONS = { |
43 'indexed': 'soft', | 45 'indexed': 'soft', |
44 'unindexed': 'mixed' | 46 'unindexed': 'mixed' |
45 } | 47 } |
46 FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS))) | 48 FREEZE_MATCHER = re.compile(r'%s.(%s)' % (FREEZE, '|'.join(FREEZE_SECTIONS))) |
(...skipping 227 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
274 how many times the decorated |function| is called.""" | 276 how many times the decorated |function| is called.""" |
275 def _inner_gen(): | 277 def _inner_gen(): |
276 yield function() | 278 yield function() |
277 while True: | 279 while True: |
278 yield | 280 yield |
279 return _inner_gen().next | 281 return _inner_gen().next |
280 | 282 |
281 | 283 |
282 ## Git functions | 284 ## Git functions |
283 | 285 |
286 def die(message, *args): | |
287 print >> sys.stderr, textwrap.dedent(message % args) | |
288 sys.exit(1) | |
289 | |
284 | 290 |
285 def blame(filename, revision=None, porcelain=False, *_args): | 291 def blame(filename, revision=None, porcelain=False, *_args): |
286 command = ['blame'] | 292 command = ['blame'] |
287 if porcelain: | 293 if porcelain: |
288 command.append('-p') | 294 command.append('-p') |
289 if revision is not None: | 295 if revision is not None: |
290 command.append(revision) | 296 command.append(revision) |
291 command.extend(['--', filename]) | 297 command.extend(['--', filename]) |
292 return run(*command) | 298 return run(*command) |
293 | 299 |
294 | 300 |
295 def branch_config(branch, option, default=None): | 301 def branch_config(branch, option, default=None): |
296 return config('branch.%s.%s' % (branch, option), default=default) | 302 return get_config('branch.%s.%s' % (branch, option), default=default) |
297 | 303 |
298 | 304 |
299 def config_regexp(pattern): | 305 def config_regexp(pattern): |
300 if IS_WIN: # pragma: no cover | 306 if IS_WIN: # pragma: no cover |
301 # this madness is because we call git.bat which calls git.exe which calls | 307 # this madness is because we call git.bat which calls git.exe which calls |
302 # bash.exe (or something to that effect). Each layer divides the number of | 308 # bash.exe (or something to that effect). Each layer divides the number of |
303 # ^'s by 2. | 309 # ^'s by 2. |
304 pattern = pattern.replace('^', '^' * 8) | 310 pattern = pattern.replace('^', '^' * 8) |
305 return run('config', '--get-regexp', pattern).splitlines() | 311 return run('config', '--get-regexp', pattern).splitlines() |
306 | 312 |
307 | 313 |
308 def branch_config_map(option): | 314 def branch_config_map(option): |
309 """Return {branch: <|option| value>} for all branches.""" | 315 """Return {branch: <|option| value>} for all branches.""" |
310 try: | 316 try: |
311 reg = re.compile(r'^branch\.(.*)\.%s$' % option) | 317 reg = re.compile(r'^branch\.(.*)\.%s$' % option) |
312 lines = config_regexp(reg.pattern) | 318 lines = config_regexp(reg.pattern) |
313 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)} | 319 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)} |
314 except subprocess2.CalledProcessError: | 320 except subprocess2.CalledProcessError: |
315 return {} | 321 return {} |
316 | 322 |
317 | 323 |
318 def branches(*args): | 324 def branches(*args): |
319 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached') | 325 NO_BRANCH = ('* (no branch', '* (detached', '* (HEAD detached') |
320 | 326 |
321 key = 'depot-tools.branch-limit' | 327 key = 'depot-tools.branch-limit' |
322 limit = 20 | 328 limit = get_config_int(key, 20) |
323 try: | |
324 limit = int(config(key, limit)) | |
325 except ValueError: | |
326 pass | |
327 | 329 |
328 raw_branches = run('branch', *args).splitlines() | 330 raw_branches = run('branch', *args).splitlines() |
329 | 331 |
330 num = len(raw_branches) | 332 num = len(raw_branches) |
333 | |
331 if num > limit: | 334 if num > limit: |
332 print >> sys.stderr, textwrap.dedent("""\ | 335 die("""\ |
333 Your git repo has too many branches (%d/%d) for this tool to work well. | 336 Your git repo has too many branches (%d/%d) for this tool to work well. |
334 | 337 |
335 You may adjust this limit by running: | 338 You may adjust this limit by running: |
336 git config %s <new_limit> | 339 git config %s <new_limit> |
337 """ % (num, limit, key)) | 340 """, num, limit, key) |
338 sys.exit(1) | |
339 | 341 |
340 for line in raw_branches: | 342 for line in raw_branches: |
341 if line.startswith(NO_BRANCH): | 343 if line.startswith(NO_BRANCH): |
342 continue | 344 continue |
343 yield line.split()[-1] | 345 yield line.split()[-1] |
344 | 346 |
345 | 347 |
346 def config(option, default=None): | 348 def get_config(option, default=None): |
iannucci
2016/06/16 00:24:20
would you mind terribly doing the config and die r
agable
2016/06/16 12:47:32
Done: https://codereview.chromium.org/2075603002
| |
347 try: | 349 try: |
348 return run('config', '--get', option) or default | 350 return run('config', '--get', option) or default |
349 except subprocess2.CalledProcessError: | 351 except subprocess2.CalledProcessError: |
350 return default | 352 return default |
351 | 353 |
352 | 354 |
353 def config_list(option): | 355 def get_config_int(option, default=0): |
356 assert isinstance(default, int) | |
357 try: | |
358 return int(get_config(option, default)) | |
359 except ValueError: | |
360 return default | |
361 | |
362 | |
363 def get_config_list(option): | |
354 try: | 364 try: |
355 return run('config', '--get-all', option).split() | 365 return run('config', '--get-all', option).split() |
356 except subprocess2.CalledProcessError: | 366 except subprocess2.CalledProcessError: |
357 return [] | 367 return [] |
358 | 368 |
359 | 369 |
360 def current_branch(): | 370 def current_branch(): |
361 try: | 371 try: |
362 return run('rev-parse', '--abbrev-ref', 'HEAD') | 372 return run('rev-parse', '--abbrev-ref', 'HEAD') |
363 except subprocess2.CalledProcessError: | 373 except subprocess2.CalledProcessError: |
(...skipping 11 matching lines...) Expand all Loading... | |
375 pass | 385 pass |
376 | 386 |
377 | 387 |
378 def diff(oldrev, newrev, *args): | 388 def diff(oldrev, newrev, *args): |
379 return run('diff', oldrev, newrev, *args) | 389 return run('diff', oldrev, newrev, *args) |
380 | 390 |
381 | 391 |
382 def freeze(): | 392 def freeze(): |
383 took_action = False | 393 took_action = False |
384 | 394 |
395 stat = status() | |
396 if any(is_unmerged(s) for s in stat.itervalues()): | |
397 die("Cannot freeze unmerged changes!") | |
398 | |
399 key = 'depot-tools.freeze-size-limit' | |
400 MB = 2**20 | |
401 limit_mb = get_config_int(key, 100) | |
402 if limit_mb > 0: | |
403 untracked_size = sum( | |
iannucci
2016/06/16 00:24:20
I would consider doing this as a proper loop: if t
agable
2016/06/16 15:26:55
Done.
| |
404 os.stat(f).st_size | |
405 for f, s in stat.iteritems() if s.lstat == '?' | |
406 ) / MB | |
407 if untracked_size > limit_mb: | |
408 die("""\ | |
409 You appear to have too much untracked+unignored data in your git | |
410 checkout: %d/%dMB. | |
411 | |
412 Run `git status` to see what it is. | |
413 | |
414 In addition to making many git commands slower, this will prevent | |
415 depot_tools from freezing your in-progress changes. | |
416 | |
417 You should add untracked data that you want to ignore to your repo's | |
418 .git/info/excludes | |
419 file. See `git help ignore` for the format of this file. | |
420 | |
421 If this data is indended as part of your commit, you may adjust the | |
422 freeze limit by running: | |
423 git config %s <new_limit> | |
424 Where <new_limit> is an integer threshold in megabytes.""", | |
425 untracked_size, limit_mb, key) | |
426 | |
385 try: | 427 try: |
386 run('commit', '--no-verify', '-m', FREEZE + '.indexed') | 428 run('commit', '--no-verify', '-m', FREEZE + '.indexed') |
387 took_action = True | 429 took_action = True |
388 except subprocess2.CalledProcessError: | 430 except subprocess2.CalledProcessError: |
389 pass | 431 pass |
390 | 432 |
391 try: | 433 try: |
392 run('add', '-A') | 434 run('add', '-A') |
393 run('commit', '--no-verify', '-m', FREEZE + '.unindexed') | 435 run('commit', '--no-verify', '-m', FREEZE + '.unindexed') |
394 took_action = True | 436 took_action = True |
(...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
484 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f) | 526 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f) |
485 f.close() | 527 f.close() |
486 return ret | 528 return ret |
487 | 529 |
488 | 530 |
489 def is_dormant(branch): | 531 def is_dormant(branch): |
490 # TODO(iannucci): Do an oldness check? | 532 # TODO(iannucci): Do an oldness check? |
491 return branch_config(branch, 'dormant', 'false') != 'false' | 533 return branch_config(branch, 'dormant', 'false') != 'false' |
492 | 534 |
493 | 535 |
536 def is_unmerged(stat_value): | |
537 return ( | |
538 'U' in (stat_value.lstat, stat_value.rstat) or | |
539 ((stat_value.lstat == stat_value.rstat) and stat_value.lstat in 'AD') | |
540 ) | |
541 | |
542 | |
494 def manual_merge_base(branch, base, parent): | 543 def manual_merge_base(branch, base, parent): |
495 set_branch_config(branch, 'base', base) | 544 set_branch_config(branch, 'base', base) |
496 set_branch_config(branch, 'base-upstream', parent) | 545 set_branch_config(branch, 'base-upstream', parent) |
497 | 546 |
498 | 547 |
499 def mktree(treedict): | 548 def mktree(treedict): |
500 """Makes a git tree object and returns its hash. | 549 """Makes a git tree object and returns its hash. |
501 | 550 |
502 See |tree()| for the values of mode, type, and ref. | 551 See |tree()| for the values of mode, type, and ref. |
503 | 552 |
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
560 del_branch_config(branch, 'base') | 609 del_branch_config(branch, 'base') |
561 del_branch_config(branch, 'base-upstream') | 610 del_branch_config(branch, 'base-upstream') |
562 | 611 |
563 | 612 |
564 def repo_root(): | 613 def repo_root(): |
565 """Returns the absolute path to the repository root.""" | 614 """Returns the absolute path to the repository root.""" |
566 return run('rev-parse', '--show-toplevel') | 615 return run('rev-parse', '--show-toplevel') |
567 | 616 |
568 | 617 |
569 def root(): | 618 def root(): |
570 return config('depot-tools.upstream', 'origin/master') | 619 return get_config('depot-tools.upstream', 'origin/master') |
571 | 620 |
572 | 621 |
573 @contextlib.contextmanager | 622 @contextlib.contextmanager |
574 def less(): # pragma: no cover | 623 def less(): # pragma: no cover |
575 """Runs 'less' as context manager yielding its stdin as a PIPE. | 624 """Runs 'less' as context manager yielding its stdin as a PIPE. |
576 | 625 |
577 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids | 626 Automatically checks if sys.stdout is a non-TTY stream. If so, it avoids |
578 running less and just yields sys.stdout. | 627 running less and just yields sys.stdout. |
579 """ | 628 """ |
580 if not setup_color.IS_TTY: | 629 if not setup_color.IS_TTY: |
(...skipping 112 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
693 if dirty: | 742 if dirty: |
694 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd | 743 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd |
695 print 'Uncommitted files: (git diff-index --name-status HEAD)' | 744 print 'Uncommitted files: (git diff-index --name-status HEAD)' |
696 print dirty[:4096] | 745 print dirty[:4096] |
697 if len(dirty) > 4096: # pragma: no cover | 746 if len(dirty) > 4096: # pragma: no cover |
698 print '... (run "git diff-index --name-status HEAD" to see full output).' | 747 print '... (run "git diff-index --name-status HEAD" to see full output).' |
699 return True | 748 return True |
700 return False | 749 return False |
701 | 750 |
702 | 751 |
752 def status(): | |
753 """Returns a parsed version of git-status. | |
754 | |
755 Returns a dictionary of {current_name: (lstat, rstat, src)} where: | |
756 * lstat is the left status code letter from git-status | |
757 * rstat is the left status code letter from git-status | |
758 * src is the current name of the file, or the original name of the file | |
759 if lstat == 'R' | |
760 """ | |
761 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src') | |
762 | |
763 def tokenizer(stream): | |
764 acc = StringIO() | |
765 c = None | |
766 while c != '': | |
767 c = stream.read(1) | |
768 if c in (None, '', '\0'): | |
769 s = acc.getvalue() | |
770 acc = StringIO() | |
771 if s: | |
772 yield s | |
773 else: | |
774 acc.write(c) | |
775 | |
776 def parser(tokens): | |
777 END = object() | |
778 tok = lambda: next(tokens, END) | |
779 while True: | |
780 status_dest = tok() | |
781 if status_dest is END: | |
782 return | |
783 stat, dest = status_dest[:2], status_dest[3:] | |
784 lstat, rstat = stat | |
785 if lstat == 'R': | |
786 src = tok() | |
787 assert src is not END | |
788 else: | |
789 src = dest | |
790 yield (dest, stat_entry(lstat, rstat, src)) | |
791 | |
792 return dict(parser(tokenizer(run_stream('status', '-z', bufsize=-1)))) | |
iannucci
2016/06/16 00:24:20
why dict-ify this if you're just going to stream i
agable
2016/06/16 15:26:55
Done. Resulted in some more efficient code in stat
| |
793 | |
794 | |
703 def squash_current_branch(header=None, merge_base=None): | 795 def squash_current_branch(header=None, merge_base=None): |
704 header = header or 'git squash commit.' | 796 header = header or 'git squash commit.' |
705 merge_base = merge_base or get_or_create_merge_base(current_branch()) | 797 merge_base = merge_base or get_or_create_merge_base(current_branch()) |
706 log_msg = header + '\n' | 798 log_msg = header + '\n' |
707 if log_msg: | 799 if log_msg: |
708 log_msg += '\n' | 800 log_msg += '\n' |
709 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base) | 801 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base) |
710 run('reset', '--soft', merge_base) | 802 run('reset', '--soft', merge_base) |
711 | 803 |
712 if not get_dirty_files(): | 804 if not get_dirty_files(): |
(...skipping 191 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
904 ['HEAD']) | 996 ['HEAD']) |
905 | 997 |
906 | 998 |
907 def clone_file(repository, new_workdir, link, operation): | 999 def clone_file(repository, new_workdir, link, operation): |
908 if not os.path.exists(os.path.join(repository, link)): | 1000 if not os.path.exists(os.path.join(repository, link)): |
909 return | 1001 return |
910 link_dir = os.path.dirname(os.path.join(new_workdir, link)) | 1002 link_dir = os.path.dirname(os.path.join(new_workdir, link)) |
911 if not os.path.exists(link_dir): | 1003 if not os.path.exists(link_dir): |
912 os.makedirs(link_dir) | 1004 os.makedirs(link_dir) |
913 operation(os.path.join(repository, link), os.path.join(new_workdir, link)) | 1005 operation(os.path.join(repository, link), os.path.join(new_workdir, link)) |
OLD | NEW |