Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(428)

Side by Side Diff: git_common.py

Issue 2052113002: Make git-freeze bail out if the user has too much untracked data. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | git_map.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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
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
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
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))
OLDNEW
« no previous file with comments | « no previous file | git_map.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698