git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
* [PATCH] contrib/rebase-catchup: helper for updating old branches
@ 2021-03-08 23:03 Mike Frysinger
  2021-03-08 23:38 ` Junio C Hamano
  0 siblings, 1 reply; 3+ messages in thread
From: Mike Frysinger @ 2021-03-08 23:03 UTC (permalink / raw)
  To: git

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@gentoo.org>
---
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


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* Re: [PATCH] contrib/rebase-catchup: helper for updating old branches
  2021-03-08 23:03 [PATCH] contrib/rebase-catchup: helper for updating old branches Mike Frysinger
@ 2021-03-08 23:38 ` Junio C Hamano
  2021-03-09  1:03   ` Mike Frysinger
  0 siblings, 1 reply; 3+ messages in thread
From: Junio C Hamano @ 2021-03-08 23:38 UTC (permalink / raw)
  To: Mike Frysinger; +Cc: git

Mike Frysinger <vapier@gentoo.org> writes:

> 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.

I wonder how well this compares or complements with Michael
Haggerty's "git imerge".

> 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

The thinking during the past several years is that the Git ecosystem
and userbase have grown large enough, and unlike our earlier years,
individual add-on's like this (and "imerge" I mentioned earlier) can
thrive without being in-tree to gain an undue exposure boost over
its competitors, so I doubt that adding more stuff to contrib/ would
be a good direction to go in the longer term.

>  create mode 100755 contrib/rebase-catchup/git-rebase-catchup.py

And it does not help for this to be written in Python (which we've
been moving away from).

Having said all that, the end-user community may benefit from having
a well curated set of third-party tools advertised at a single
autoritative-sounding place, but it is dubious that contrib/
subdirectory of my tree should be such a place (I won't be its
curator anyway).

Perhaps there is an opening for non-code contribution for those
without much programming experience but with great organizational
skills.

Thanks.

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [PATCH] contrib/rebase-catchup: helper for updating old branches
  2021-03-08 23:38 ` Junio C Hamano
@ 2021-03-09  1:03   ` Mike Frysinger
  0 siblings, 0 replies; 3+ messages in thread
From: Mike Frysinger @ 2021-03-09  1:03 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git

On 08 Mar 2021 15:38, Junio C Hamano wrote:
> Mike Frysinger <vapier@gentoo.org> writes:
> > 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.
> 
> I wonder how well this compares or complements with Michael
> Haggerty's "git imerge".

thanks, hadn't heard of that before

> > 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
> 
> The thinking during the past several years is that the Git ecosystem
> and userbase have grown large enough, and unlike our earlier years,
> individual add-on's like this (and "imerge" I mentioned earlier) can
> thrive without being in-tree to gain an undue exposure boost over
> its competitors, so I doubt that adding more stuff to contrib/ would
> be a good direction to go in the longer term.

i'm totally fine with "no".  thanks for the info.
-mike

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2021-03-09  1:04 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-03-08 23:03 [PATCH] contrib/rebase-catchup: helper for updating old branches Mike Frysinger
2021-03-08 23:38 ` Junio C Hamano
2021-03-09  1:03   ` Mike Frysinger

Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).