From 58742bc58aac4f3898b52c4df99ce3fd702ed665 Mon Sep 17 00:00:00 2001 From: Mike Frysinger Date: Mon, 8 Mar 2021 17:38:01 -0500 Subject: [PATCH] git-rb-catchup: more cleanups --- .bin/git-rb-catchup | 72 +++++++++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/.bin/git-rb-catchup b/.bin/git-rb-catchup index 3414c02..2cc5c53 100755 --- a/.bin/git-rb-catchup +++ b/.bin/git-rb-catchup @@ -1,21 +1,36 @@ #!/usr/bin/env python3 +# Distributed under the terms of the GNU General Public License v2 or later. -"""Helper to automatically rebase onto latest commit possible.""" +"""Helper to automatically rebase onto latest commit possible. + +Helpful when you have a branch tracking an old commit, and a lot of conflicting +changes have landed in the latest branch, but you still want to update. + +A single rebase to the latest commit will require addressing all the different +changes at once which can be difficult, overwhelming, and error-prone. Instead, +if you rebased onto each intermediate conflicting point, you'd break up the work +into smaller pieces, and be able to run tests to make sure things were still OK. +""" import argparse import subprocess import sys +from typing import List, Tuple, Union + + +assert sys.version_info >= (3, 7), f'Need Python 3.7+, not {sys.version_info}' -def git(args, **kwargs): +def git(args: List[str], **kwargs) -> subprocess.CompletedProcess: """Run git.""" kwargs.setdefault('check', True) kwargs.setdefault('capture_output', True) kwargs.setdefault('encoding', 'utf-8') + # pylint: disable=subprocess-run-check return subprocess.run(['git'] + args, **kwargs) -def rebase(target): +def rebase(target: str) -> bool: """Try to rebase onto |target|.""" try: git(['rebase', target]) @@ -29,15 +44,23 @@ def rebase(target): return False -def rebase_bisect(lbranch, rbranch, behind, leave_rebase=False): +def rebase_bisect(lbranch: str, + rbranch: str, + behind: int, + leave_rebase: bool = False, + force_checkout: bool = False): """Try to rebase branch as close to |rbranch| as possible.""" - def attempt(pos): + def attempt(pos: int) -> bool: target = f'{rbranch}~{pos}' - print(f'Rebasing onto {target} ', end='', flush=True) + print(f'Rebasing onto {target} ', end='') print('.', end='', flush=True) -# git(['checkout', '-f', target]) + # Checking out these branches directly helps clobber orphaned files, + # but is usually unnessary, and can slow down the overall process. + if force_checkout: + git(['checkout', '-f', target]) print('.', end='', flush=True) -# git(['checkout', '-f', lbranch]) + if force_checkout: + git(['checkout', '-f', lbranch]) print('. ', end='', flush=True) ret = rebase(target) print('OK' if ret else 'failed') @@ -71,14 +94,15 @@ def rebase_bisect(lbranch, rbranch, behind, leave_rebase=False): print('All caught up!') -def get_ahead_behind(lbranch, rbranch): +def get_ahead_behind(lbranch: str, rbranch: str) -> Tuple[int, int]: """Return number of commits |lbranch| is ahead & behind relative to |rbranch|.""" - output = git(['rev-list', '--left-right', '--count', f'{lbranch}...{rbranch}']).stdout + output = git( + ['rev-list', '--left-right', '--count', f'{lbranch}...{rbranch}']).stdout return [int(x) for x in output.split()] -def get_tracking_branch(branch): - """Return remote branch that |branch| is tracking.""" +def get_tracking_branch(branch: str) -> Union[str, None]: + """Return branch that |branch| is tracking.""" merge = git(['config', '--local', f'branch.{branch}.merge']).stdout.strip() if not merge: return None @@ -92,14 +116,16 @@ def get_tracking_branch(branch): return merge -def get_local_branch(): +def get_local_branch() -> str: """Return the name of the local checked out branch.""" return git(['branch', '--show-current']).stdout.strip() -def get_parser(): +def get_parser() -> argparse.ArgumentParser: """Get CLI parser.""" - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument( '--skip-initial-rebase-latest', dest='initial_rebase', action='store_false', default=True, @@ -108,13 +134,17 @@ def get_parser(): '--leave-at-last-failed-rebase', dest='leave_rebase', action='store_true', default=False, help='leave tree state at last failing rebase') + parser.add_argument( + '--checkout-before-rebase', dest='force_checkout', + action='store_true', default=False, + help='force checkout before rebasing to target (to cleanup orphans)') parser.add_argument( 'branch', nargs='?', help='branch to rebase onto') return parser -def main(argv): +def main(argv: List[str]) -> int: """The main entry point for scripts.""" parser = get_parser() opts = parser.parse_args(argv) @@ -129,7 +159,7 @@ def main(argv): rbranch = opts.branch else: rbranch = get_tracking_branch(lbranch) - print(f'Remote branch resolved to "{rbranch}"') + print(f'Tracking branch resolved to "{rbranch}"') ahead, behind = get_ahead_behind(lbranch, rbranch) print(f'Branch is {ahead} commits ahead and {behind} commits behind') @@ -141,12 +171,16 @@ def main(argv): git(['merge']) else: if opts.initial_rebase: - print(f'Trying to rebase onto latest {rbranch} ... ', end='', flush=True) + print(f'Trying to rebase onto latest {rbranch} ... ', + end='', flush=True) if rebase(rbranch): print('OK!') return 0 print('failed; falling back to bisect') - rebase_bisect(lbranch, rbranch, behind, leave_rebase=opts.leave_rebase) + rebase_bisect(lbranch, rbranch, behind, leave_rebase=opts.leave_rebase, + force_checkout=opts.force_checkout) + + return 0 if __name__ == '__main__': -- 2.39.2