"""Helper to rebase all local branches."""
import argparse
+import functools
import os
from pathlib import Path
import re
import sys
+# Not sure if newer features are used.
+assert sys.version_info >= (3, 6), f'Python 3.6+ required but found {sys.version}'
+
+
PROG = os.path.basename(__file__)
BRACKET = _combine(Terminal.BOLD, Terminal.FG_BLUE)
+def fatal(msg):
+ """Show an error |msg| then exit."""
+ print(f'{Color.BAD}{PROG}: error: {msg}{Color.NORMAL}', file=sys.stderr)
+ sys.exit(1)
+
+
def git(args, **kwargs):
"""Run git."""
kwargs.setdefault('check', True)
- kwargs.setdefault('capture_output', True)
+ kwargs.setdefault('stdout', subprocess.PIPE)
+ kwargs.setdefault('stderr', subprocess.STDOUT)
kwargs.setdefault('encoding', 'utf-8')
#print('+', 'git', *args)
return subprocess.run(['git'] + args, **kwargs)
+@functools.lru_cache(maxsize=None)
+def checkout_is_dirty():
+ """Determine whether the checkout is dirty (e.g. modified files)."""
+ output = git(['status', '--porcelain']).stdout
+ return any(x for x in output.splitlines() if '?' not in x[0:2])
+
+
+@functools.lru_cache(maxsize=None)
def rebase_inprogress():
"""Determine whether a rebase is already in progress."""
output = git(['rev-parse', '--git-path', 'rebase-merge']).stdout.strip()
# Skip if rebase is in progress.
if rebase_inprogress():
- print(f'{Color.BAD}{PROG}: skipping due to active rebase{Color.NORMAL}')
- return 1
+ fatal('skipping due to active rebase')
# Switch to the top dir in case the working dir doesn't exist in every branch.
topdir = git(['rev-parse', '--show-toplevel']).stdout.strip()
curr_state = None
branch_width = 0
+ local_count = 0
for line in state:
head, worktreepath, branch, tracking, ahead_behind = line.split('|')
branch_width = max(branch_width, len(branch))
if head == '*':
curr_state = branch
+ if not (worktreepath and worktreepath != topdir):
+ local_count += 1
+ # If there are no branches to rebase, go silently.
+ if not local_count:
+ return 0
if not curr_state:
- print('Unable to resolve current branch', file=sys.stderr)
- return 1
+ fatal('unable to resolve current branch')
+ switched_head = False
branches = {}
for line in state:
- head, worktreepath, branch, tracking, ahead_behind = line.split('|')
+ _, worktreepath, branch, tracking, ahead_behind = line.split('|')
# If it's a branch in another worktree, ignore it.
if worktreepath and worktreepath != topdir:
print('Up-to-date!')
continue
elif not ahead:
- git(['update-ref', f'refs/heads/{branch}', tracking])
- print('fast forwarded')
+ # If we haven't switched the checkout, update-ref on current HEAD
+ # will get us into a dirty checkout, so use merge to handle it.
+ if switched_head or curr_state != branch:
+ git(['update-ref', f'refs/heads/{branch}', tracking])
+ print('fast forwarded [updated ref]')
+ else:
+ result = git(['merge', '-q', '--ff-only'], check=False)
+ if result.returncode:
+ print(f'{Color.BAD}unable to merge{Color.NORMAL}\n' + result.stdout.strip())
+ else:
+ print('fast forwarded [merged]')
+ continue
+
+ if checkout_is_dirty():
+ print(f'{Color.BAD}unable to rebase: tree is dirty{Color.NORMAL}')
continue
print(f'rebasing [{ahead_behind}] ', end='', flush=True)
git(['checkout', '-q', branch])
+ switched_head = True
if opts.catchup:
print()
result = git(['rb-catchup'], capture_output=False, check=False)
else:
print('OK!')
- git(['checkout', '-q', curr_state])
+ if switched_head:
+ git(['checkout', '-q', curr_state])
return 0