X-Git-Url: https://git.wh0rd.org/?a=blobdiff_plain;f=.bin%2Fgit-rb-all;h=a2f7922a9c84358ed6edca4bcb612b3fcfd0a547;hb=fb5d2cb632ad05e57c8b257f5a3c85bb75c8ac31;hp=d2c6737b95c1f9a3bb6ca4f784106654124e40a9;hpb=65a6ea8481d328dfa05a55d5c0b0adbbcbe76e35;p=home.git diff --git a/.bin/git-rb-all b/.bin/git-rb-all index d2c6737..a2f7922 100755 --- a/.bin/git-rb-all +++ b/.bin/git-rb-all @@ -3,6 +3,7 @@ """Helper to rebase all local branches.""" import argparse +import functools import os from pathlib import Path import re @@ -10,6 +11,10 @@ import subprocess 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__) @@ -44,22 +49,39 @@ class Color: HILITE = _combine(Terminal.BOLD, Terminal.FG_CYAN) BRACKET = _combine(Terminal.BOLD, Terminal.FG_BLUE) + @classmethod + def good(cls, msg): + return cls.GOOD + msg + cls.NORMAL + + @classmethod + def bad(cls, msg): + return cls.BAD + msg + cls.NORMAL + def fatal(msg): """Show an error |msg| then exit.""" - print(f'{Color.BAD}{PROG}: error: {msg}{Color.NORMAL}', file=sys.stderr) + print(Color.bad(f'{PROG}: error: {msg}'), 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() @@ -73,6 +95,13 @@ def rebase_inprogress(): return False +@functools.lru_cache(maxsize=None) +def cherry_pick_inprogress(): + """Determine whether a cherry-pick is in progress.""" + output = git(['rev-parse', '--git-path', 'CHERRY_PICK_HEAD']).stdout.strip() + return Path(output).exists() + + class AppendOption(argparse.Action): """Append the command line option (with no arguments) to dest. @@ -109,10 +138,6 @@ def main(argv): parser = get_parser() opts = parser.parse_args(argv) - # Skip if rebase is in progress. - if rebase_inprogress(): - 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() os.chdir(topdir) @@ -124,7 +149,7 @@ def main(argv): # ||s-stash|| state = git( ['for-each-ref', '--format=%(HEAD)|%(worktreepath)|%(refname:short)|%(upstream)|%(upstream:track,nobracket)', - 'refs/heads/*']).stdout.splitlines() + 'HEAD', 'refs/heads/*']).stdout.splitlines() curr_state = None branch_width = 0 @@ -140,12 +165,15 @@ def main(argv): if not local_count: return 0 if not curr_state: - fatal('unable to resolve current branch') + # Are we in a detached head state? + if not git(['symbolic-ref', '-q', 'HEAD'], check=False).returncode: + fatal('unable to resolve current branch') + curr_state = git(['rev-parse', 'HEAD']).stdout.strip() - is_dirty = None + 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: @@ -162,22 +190,39 @@ def main(argv): m = re.search(r'behind ([0-9]+)', ahead_behind) behind = int(m.group(1)) if m else 0 if not behind: - print('Up-to-date!') + print(Color.good('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(Color.bad('unable to merge') + '\n' + result.stdout.strip()) + else: + print('fast forwarded [merged]') continue - if is_dirty is None: - output = git(['status', '--porcelain']).stdout - is_dirty = any(x for x in output.splitlines() if '?' not in x[0:2]) - if is_dirty: - print(f'{Color.BAD}unable to rebase: tree is dirty{Color.NORMAL}') + # Skip this ref if tree is in a bad state. + if rebase_inprogress(): + print(Color.bad('skipping due to active rebase')) + continue + if cherry_pick_inprogress(): + print(Color.bad('skipping due to active cherry-pick')) + continue + if checkout_is_dirty(): + print(Color.bad('unable to rebase: tree is dirty')) continue print(f'rebasing [{ahead_behind}] ', end='', flush=True) - git(['checkout', '-q', branch]) + result = git(['checkout', '-q', branch], check=False) + if result.returncode: + print(Color.bad('unable to checkout') + '\n' + result.stdout.strip()) + continue + switched_head = True if opts.catchup: print() result = git(['rb-catchup'], capture_output=False, check=False) @@ -185,11 +230,12 @@ def main(argv): result = git(['rebase'] + opts.git_options, check=False) if result.returncode: git(['rebase', '--abort']) - print(f'{Color.BAD}failed{Color.NORMAL}\n' + result.stdout.strip()) + print(Color.bad('failed') + '\n' + result.stdout.strip()) else: - print('OK!') + print(Color.good('OK')) - git(['checkout', '-q', curr_state]) + if switched_head: + git(['checkout', '-q', curr_state]) return 0