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(message, *args): |
| 229 print >> sys.stderr, textwrap.dedent(message % args) |
| 230 sys.exit(1) |
| 231 |
226 | 232 |
227 def branch_config(branch, option, default=None): | 233 def branch_config(branch, option, default=None): |
228 return config('branch.%s.%s' % (branch, option), default=default) | 234 return get_config('branch.%s.%s' % (branch, option), default=default) |
229 | 235 |
230 | 236 |
231 def branch_config_map(option): | 237 def branch_config_map(option): |
232 """Return {branch: <|option| value>} for all branches.""" | 238 """Return {branch: <|option| value>} for all branches.""" |
233 try: | 239 try: |
234 reg = re.compile(r'^branch\.(.*)\.%s$' % option) | 240 reg = re.compile(r'^branch\.(.*)\.%s$' % option) |
235 lines = run('config', '--get-regexp', reg.pattern).splitlines() | 241 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)} | 242 return {reg.match(k).group(1): v for k, v in (l.split() for l in lines)} |
237 except subprocess2.CalledProcessError: | 243 except subprocess2.CalledProcessError: |
238 return {} | 244 return {} |
239 | 245 |
240 | 246 |
241 def branches(*args): | 247 def branches(*args): |
242 NO_BRANCH = ('* (no branch', '* (detached from ') | 248 NO_BRANCH = ('* (no branch', '* (detached from ') |
243 | 249 |
244 key = 'depot-tools.branch-limit' | 250 key = 'depot-tools.branch-limit' |
245 limit = 20 | 251 limit = get_config_int(key, 20) |
246 try: | |
247 limit = int(config(key, limit)) | |
248 except ValueError: | |
249 pass | |
250 | 252 |
251 raw_branches = run('branch', *args).splitlines() | 253 raw_branches = run('branch', *args).splitlines() |
252 | 254 |
253 num = len(raw_branches) | 255 num = len(raw_branches) |
| 256 |
254 if num > limit: | 257 if num > limit: |
255 print >> sys.stderr, textwrap.dedent("""\ | 258 die("""\ |
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 get_config(option, default=None): |
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 |
285 def config_list(option): | 287 def get_config_int(option, default=0): |
| 288 assert isinstance(default, int) |
| 289 try: |
| 290 return int(get_config(option, default)) |
| 291 except ValueError: |
| 292 return default |
| 293 |
| 294 |
| 295 def get_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 if any(is_unmerged(s) for s in stat.itervalues()): |
| 325 die("Cannot freeze unmerged changes!") |
| 326 |
| 327 key = 'depot-tools.freeze-size-limit' |
| 328 MB = 2**20 |
| 329 limit_mb = get_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 if untracked_size > limit_mb: |
| 336 die("""\ |
| 337 You appear to have too much untracked+unignored data in your git |
| 338 checkout: %d/%dMB. |
| 339 |
| 340 Run `git status` to see what it is. |
| 341 |
| 342 In addition to making many git commands slower, this will prevent |
| 343 depot_tools from freezing your in-progress changes. |
| 344 |
| 345 You should add untracked data that you want to ignore to your repo's |
| 346 .git/info/excludes |
| 347 file. See `git help ignore` for the format of this file. |
| 348 |
| 349 If this data is indended as part of your commit, you may adjust the |
| 350 freeze limit by running: |
| 351 git config %s <new_limit> |
| 352 Where <new_limit> is an integer threshold in megabytes.""", |
| 353 untracked_size, limit_mb, key) |
| 354 |
313 try: | 355 try: |
314 run('commit', '-m', FREEZE + '.indexed') | 356 run('commit', '-m', FREEZE + '.indexed') |
315 took_action = True | 357 took_action = True |
316 except subprocess2.CalledProcessError: | 358 except subprocess2.CalledProcessError: |
317 pass | 359 pass |
318 | 360 |
319 try: | 361 try: |
320 run('add', '-A') | 362 run('add', '-A') |
321 run('commit', '-m', FREEZE + '.unindexed') | 363 run('commit', '-m', FREEZE + '.unindexed') |
322 took_action = True | 364 took_action = True |
(...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
409 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f) | 451 ret = run('hash-object', '-t', kind, '-w', '--stdin', stdin=f) |
410 f.close() | 452 f.close() |
411 return ret | 453 return ret |
412 | 454 |
413 | 455 |
414 def is_dormant(branch): | 456 def is_dormant(branch): |
415 # TODO(iannucci): Do an oldness check? | 457 # TODO(iannucci): Do an oldness check? |
416 return branch_config(branch, 'dormant', 'false') != 'false' | 458 return branch_config(branch, 'dormant', 'false') != 'false' |
417 | 459 |
418 | 460 |
| 461 def is_unmerged(stat_value): |
| 462 return ( |
| 463 'U' in (stat_value.lstat, stat_value.rstat) or |
| 464 ((stat_value.lstat == stat_value.rstat) and stat_value.lstat in 'AD') |
| 465 ) |
| 466 |
| 467 |
419 def manual_merge_base(branch, base, parent): | 468 def manual_merge_base(branch, base, parent): |
420 set_branch_config(branch, 'base', base) | 469 set_branch_config(branch, 'base', base) |
421 set_branch_config(branch, 'base-upstream', parent) | 470 set_branch_config(branch, 'base-upstream', parent) |
422 | 471 |
423 | 472 |
424 def mktree(treedict): | 473 def mktree(treedict): |
425 """Makes a git tree object and returns its hash. | 474 """Makes a git tree object and returns its hash. |
426 | 475 |
427 See |tree()| for the values of mode, type, and ref. | 476 See |tree()| for the values of mode, type, and ref. |
428 | 477 |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
480 run('rebase', '--abort') | 529 run('rebase', '--abort') |
481 return RebaseRet(False, cpe.stdout) | 530 return RebaseRet(False, cpe.stdout) |
482 | 531 |
483 | 532 |
484 def remove_merge_base(branch): | 533 def remove_merge_base(branch): |
485 del_branch_config(branch, 'base') | 534 del_branch_config(branch, 'base') |
486 del_branch_config(branch, 'base-upstream') | 535 del_branch_config(branch, 'base-upstream') |
487 | 536 |
488 | 537 |
489 def root(): | 538 def root(): |
490 return config('depot-tools.upstream', 'origin/master') | 539 return get_config('depot-tools.upstream', 'origin/master') |
491 | 540 |
492 | 541 |
493 def run(*cmd, **kwargs): | 542 def run(*cmd, **kwargs): |
494 """The same as run_with_stderr, except it only returns stdout.""" | 543 """The same as run_with_stderr, except it only returns stdout.""" |
495 return run_with_stderr(*cmd, **kwargs)[0] | 544 return run_with_stderr(*cmd, **kwargs)[0] |
496 | 545 |
497 | 546 |
498 def run_stream(*cmd, **kwargs): | 547 def run_stream(*cmd, **kwargs): |
499 """Runs a git command. Returns stdout as a PIPE (file-like object). | 548 """Runs a git command. Returns stdout as a PIPE (file-like object). |
500 | 549 |
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
537 return ret, err | 586 return ret, err |
538 | 587 |
539 | 588 |
540 def set_branch_config(branch, option, value, scope='local'): | 589 def set_branch_config(branch, option, value, scope='local'): |
541 set_config('branch.%s.%s' % (branch, option), value, scope=scope) | 590 set_config('branch.%s.%s' % (branch, option), value, scope=scope) |
542 | 591 |
543 | 592 |
544 def set_config(option, value, scope='local'): | 593 def set_config(option, value, scope='local'): |
545 run('config', '--' + scope, option, value) | 594 run('config', '--' + scope, option, value) |
546 | 595 |
| 596 |
| 597 def status(): |
| 598 """Returns a parsed version of git-status. |
| 599 |
| 600 Returns a dictionary of {current_name: (lstat, rstat, src)} where: |
| 601 * lstat is the left status code letter from git-status |
| 602 * rstat is the left status code letter from git-status |
| 603 * src is the current name of the file, or the original name of the file |
| 604 if lstat == 'R' |
| 605 """ |
| 606 stat_entry = collections.namedtuple('stat_entry', 'lstat rstat src') |
| 607 |
| 608 def tokenizer(stream): |
| 609 acc = StringIO() |
| 610 c = None |
| 611 while c != '': |
| 612 c = stream.read(1) |
| 613 if c in (None, '', '\0'): |
| 614 s = acc.getvalue() |
| 615 acc = StringIO() |
| 616 if s: |
| 617 yield s |
| 618 else: |
| 619 acc.write(c) |
| 620 |
| 621 def parser(tokens): |
| 622 END = object() |
| 623 tok = lambda: next(tokens, END) |
| 624 while True: |
| 625 status_dest = tok() |
| 626 if status_dest is END: |
| 627 return |
| 628 stat, dest = status_dest[:2], status_dest[3:] |
| 629 lstat, rstat = stat |
| 630 if lstat == 'R': |
| 631 src = tok() |
| 632 assert src is not END |
| 633 else: |
| 634 src = dest |
| 635 yield (dest, stat_entry(lstat, rstat, src)) |
| 636 |
| 637 return dict(parser(tokenizer(run_stream('status', '-z', bufsize=-1)))) |
| 638 |
| 639 |
547 def squash_current_branch(header=None, merge_base=None): | 640 def squash_current_branch(header=None, merge_base=None): |
548 header = header or 'git squash commit.' | 641 header = header or 'git squash commit.' |
549 merge_base = merge_base or get_or_create_merge_base(current_branch()) | 642 merge_base = merge_base or get_or_create_merge_base(current_branch()) |
550 log_msg = header + '\n' | 643 log_msg = header + '\n' |
551 if log_msg: | 644 if log_msg: |
552 log_msg += '\n' | 645 log_msg += '\n' |
553 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base) | 646 log_msg += run('log', '--reverse', '--format=%H%n%B', '%s..HEAD' % merge_base) |
554 run('reset', '--soft', merge_base) | 647 run('reset', '--soft', merge_base) |
555 run('commit', '-a', '-F', '-', indata=log_msg) | 648 run('commit', '-a', '-F', '-', indata=log_msg) |
556 | 649 |
(...skipping 102 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
659 return None | 752 return None |
660 return ret | 753 return ret |
661 | 754 |
662 | 755 |
663 def upstream(branch): | 756 def upstream(branch): |
664 try: | 757 try: |
665 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name', | 758 return run('rev-parse', '--abbrev-ref', '--symbolic-full-name', |
666 branch+'@{upstream}') | 759 branch+'@{upstream}') |
667 except subprocess2.CalledProcessError: | 760 except subprocess2.CalledProcessError: |
668 return None | 761 return None |
OLD | NEW |