]> git.wh0rd.org - home.git/blobdiff - .bin/git-rb-catchup
git-rb-catchup: more cleanups
[home.git] / .bin / git-rb-catchup
index 3414c0200b86fdb8e963d19a0cd24212709df8fb..2cc5c5381616d3e91c45d10c3350b65d3709dc96 100755 (executable)
@@ -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__':