[PATCH] contrib/rebase-catchup: helper for updating old branches

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



For people who want to rebase their work onto the latest branch
(instead of merging), but there's many conflicting changes.  This
allows you to address those conflicts one-by-one and work through
each issue instead of trying to take them all on at once.

Signed-off-by: Mike Frysinger <vapier@xxxxxxxxxx>
---
If there's no interest in merging this into contrib, then this is more spam,
and anyone interested can use https://github.com/vapier/git-rebase-catchup

 contrib/rebase-catchup/README.md             |  61 ++++++
 contrib/rebase-catchup/git-rebase-catchup.py | 187 +++++++++++++++++++
 2 files changed, 248 insertions(+)
 create mode 100644 contrib/rebase-catchup/README.md
 create mode 100755 contrib/rebase-catchup/git-rebase-catchup.py

diff --git a/contrib/rebase-catchup/README.md b/contrib/rebase-catchup/README.md
new file mode 100644
index 000000000000..083d2012f833
--- /dev/null
+++ b/contrib/rebase-catchup/README.md
@@ -0,0 +1,61 @@
+# Rebase Catchup
+
+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.
+
+## Example
+
+Let's say you have a branch that is currently 357 commits behind.  When you try
+rebasing onto the latest, it hits a lot of conflicts.  The tool will bisect down
+to find the most recent commit it can cleanly rebase onto.
+
+```sh
+$ git rebase-catchup
+Local branch resolved to "s-logs"
+Tracking branch resolved to "origin/master"
+Branch is 2 commits ahead and 357 commits behind
+Trying to rebase onto latest origin/master ... failed; falling back to bisect
+Rebasing onto origin/master~178 ... failed
+Rebasing onto origin/master~267 ... failed
+Rebasing onto origin/master~312 ... failed
+Rebasing onto origin/master~334 ... failed
+Rebasing onto origin/master~345 ... OK
+Rebasing onto origin/master~339 ... OK
+Rebasing onto origin/master~336 ... failed
+Rebasing onto origin/master~337 ... OK
+Rebasing onto origin/master~336 ... failed
+Found first failure origin/master~336
+```
+
+Now you know the first conflicting change is `origin/master~336`.  Rebase onto
+that directly and address all the problems (and run tests/etc...).  Then restart
+the process.
+
+```sh
+$ git rebase origin/master~336
+... address all the conflicts ...
+$ git rebase --continue
+$ git rebase-catchup
+Local branch resolved to "s-logs"
+Tracking branch resolved to "origin/master"
+Branch is 2 commits ahead and 335 commits behind
+Trying to rebase onto latest origin/master ... failed; falling back to bisect
+Rebasing onto origin/master~167 ... OK
+Rebasing onto origin/master~83 ... OK
+Rebasing onto origin/master~41 ... failed
+Rebasing onto origin/master~62 ... OK
+Rebasing onto origin/master~51 ... OK
+Rebasing onto origin/master~46 ... OK
+Rebasing onto origin/master~43 ... failed
+Rebasing onto origin/master~44 ... failed
+Rebasing onto origin/master~45 ... OK
+Rebasing onto origin/master~44 ... failed
+Found first failure origin/master~44
+```
+
+Now you're only 44 commits behind.  Keep doing this until you catchup!
diff --git a/contrib/rebase-catchup/git-rebase-catchup.py b/contrib/rebase-catchup/git-rebase-catchup.py
new file mode 100755
index 000000000000..2cc5c5381616
--- /dev/null
+++ b/contrib/rebase-catchup/git-rebase-catchup.py
@@ -0,0 +1,187 @@
+#!/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.
+
+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: 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: str) -> bool:
+    """Try to rebase onto |target|."""
+    try:
+        git(['rebase', target])
+        return True
+    except KeyboardInterrupt:
+        git(['rebase', '--abort'])
+        print('aborted')
+        sys.exit(1)
+    except:
+        git(['rebase', '--abort'])
+        return 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: int) -> bool:
+        target = f'{rbranch}~{pos}'
+        print(f'Rebasing onto {target} ', end='')
+        print('.', end='', flush=True)
+        # 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)
+        if force_checkout:
+            git(['checkout', '-f', lbranch])
+        print('. ', end='', flush=True)
+        ret = rebase(target)
+        print('OK' if ret else 'failed')
+        return ret
+
+    # "pmin" is the latest branch position while "pmax" is where we're now.
+    pmin = 0
+    pmax = behind
+    old_mid = None
+    first_fail = 0
+    while True:
+        mid = pmin + (pmax - pmin) // 2
+        if mid == old_mid or mid < pmin or mid >= pmax:
+            break
+        if attempt(mid):
+            pmax = mid
+        else:
+            first_fail = max(first_fail, mid)
+            pmin = mid
+        old_mid = mid
+
+    if pmin or pmax:
+        last_target = f'{rbranch}~{first_fail}'
+        if leave_rebase:
+            print('Restarting', last_target)
+            result = git(['rebase', last_target], check=False)
+            print(result.stdout.strip())
+        else:
+            print('Found first failure', last_target)
+    else:
+        print('All caught up!')
+
+
+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
+    return [int(x) for x in output.split()]
+
+
+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
+
+    remote = git(['config', '--local', f'branch.{branch}.remote']).stdout.strip()
+    if remote:
+        if merge.startswith('refs/heads/'):
+            merge = merge[11:]
+        return f'{remote}/{merge}'
+    else:
+        return merge
+
+
+def get_local_branch() -> str:
+    """Return the name of the local checked out branch."""
+    return git(['branch', '--show-current']).stdout.strip()
+
+
+def get_parser() -> argparse.ArgumentParser:
+    """Get CLI parser."""
+    parser = argparse.ArgumentParser(
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter)
+    parser.add_argument(
+        '--skip-initial-rebase-latest', dest='initial_rebase',
+        action='store_false', default=True,
+        help='skip initial rebase attempt onto the latest branch')
+    parser.add_argument(
+        '--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: List[str]) -> int:
+    """The main entry point for scripts."""
+    parser = get_parser()
+    opts = parser.parse_args(argv)
+
+    lbranch = get_local_branch()
+    print(f'Local branch resolved to "{lbranch}"')
+    if not lbranch:
+        print('Unable to resolve local branch', file=sys.stderr)
+        return 1
+
+    if opts.branch:
+        rbranch = opts.branch
+    else:
+        rbranch = get_tracking_branch(lbranch)
+    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')
+
+    if not behind:
+        print('Up-to-date!')
+    elif not ahead:
+        print('Fast forwarding ...')
+        git(['merge'])
+    else:
+        if opts.initial_rebase:
+            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,
+                      force_checkout=opts.force_checkout)
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))
-- 
2.30.0




[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]

  Powered by Linux