git-revise
git-revise copied to clipboard
Add flag to update other local branches affected by the rewrite
Sometimes when I'm rewriting history, I've got multiple branches that contain the rewritten commits. For example:
A---B---C---D---E
\ \ \
master featA featB
This is common if I'm working on a feature, and decide that I want to publish a subset of commits to PR right now while I continue to work on the rest.
With this setup, if I rewrite commits B or C while featB
is checked out, then featB
diverges from featA
and I have to go force-update my featA
branch to point to the equivalent rewritten commit.
What I'd really like is a way to ask git-revise
to just deal with this for me.
Non-interactive mode
In non-interactive mode, git-revise
should print a warning to stdout if any local branches contain the rewritten commits but aren't being updated by this command. That way I can decide whether I should undo my change, or update the branch.
It should also support a flag that lets me list additional branches (besides HEAD) that should be rewritten as needed. This flag should take a glob string, so I can pass *
to mean "rewrite any affected branches". I should be able to specify it multiple times. Alternatively it could take a fixed string and have a separate flag to mean "rewrite all branches", but I figure the glob string is a bit simpler.
This is somewhat related to the existing --ref
flag, but that flag only identifies a single ref, and it's also got usability issues (apparently I need to list the entire ref)
With the above example, this would let me say something like git revise --rewrite-additional-local-branches \* $B
while featB
is checked out and it would rewrite both featA
and featB
for me (this flag is too verbose but it's just for illustrative purposes).
Interactive mode
In interactive mode, a new command ref
should be added that lets me create or update a ref. In interactive mode it should behave as though --rewrite-additional-local-branches \*
is implicitly specified if the flag isn't otherwise listed. When constructing the todo list, it should insert a ref
command for every matching branch that points to a commit in the todo list, except of course for the working ref (HEAD
or --ref
) because that will always point to the final result. The --rewrite-additional-local-branches
flag would default to *
because I can always delete refs I don't want to update from the todo list before committing it.
When --autosquash
is specified, any refs that point to a fixup or squash commit will not move with the fixup or squash commit, they will end up pointing to whatever non-fixup/squash commit was behind it. For example, if my initial todo list is
pick 5152786f70f2 b
pick 736a81426962 c
pick c08daa5ef3d5 fixup! b
ref refs/heads/featA
pick ce817c55b54c d
pick e7b6185cdd59 e
then autosquash will turn that into
pick 5152786f70f2 b
fixup c08daa5ef3d5 fixup! b
pick 736a81426962 c
ref refs/heads/featA
pick ce817c55b54c d
pick e7b6185cdd59 e
If the ref
command is used to list the working ref (HEAD
or the --ref
value), this will do nothing because that ref will always be updated with the final result of the revise; listing this should be legal but it may be worth printing a warning about the ref
command being ignored (and why).
Isn't that too broad of a task for this utility? You may also want to resolve the same problem for other history rewriting, not only for git-revise – for the usual 'git rebase -i' for example. I think it shouldn't be too hard to write a generic wrapper utility that can do this:
$ git with-updating-refs ref1 ref2 refN -- rebase <rebase args>
Multi-step rewriting such as interactive rebase may be a problem but for this case updating others branches can be performed post factum based on reflog information.
How would git with-updating-refs
possibly work? There's no way for it to figure out what new ref the old ref maps to unless it's involved in the rewriting (even just saying "do a match on the commit message" fails if there are duplicate messages or if I modify the commit message during the rewrite).
As for too broad of a task, I disagree. This utility rewrites history. If I have two branches that both contain a given commit, and I'm rewriting the commit, why would I only want to update one of the branches? In fact, I think my proposal doesn't go quite far enough; by default it should rewrite all local branches that contain the commit rather than using a single "working ref", but this model doesn't work in interactive mode so I'm not proposing it.
And yes I agree that git rebase -i
should handle this too, but it's a much harder sell to convince git to change the behavior of a core tool. At least with git rebase -i
I can use exec
lines to update refs by hand.
This sounds like a useful feature to have, but it would have to be opt in, and not allowed in interactive mode.
Why "not allowed in interactive mode"? I gave a whole description for how the interactive mode version would work. The non-interactive mode version as described is opt-in; the interactive mode one isn't, but could be just by removing the recommendation of defaulting --rewrite-additional-local-branches
to *
in interactive mode (I suppose we could make that a config value, revise.defaultRewriteLocalBranchesGlobPattern
[needs better name!], so I can set it to *
)
How would
git with-updating-refs
possibly work? There's no way for it to figure out what new ref the old ref maps to unless it's involved in the rewriting (even just saying "do a match on the commit message" fails if there are duplicate messages or if I modify the commit message during the rewrite).
There are other ways to identify rewritten commits, even built-in in Git. For example patch ids – git help patch-id
– this concept is used internally in git-cherry-pick
and git-rebase
to resolve this problem (matching commits in rewritten histories). Of course patch itself can change during rewriting. But such utility can combine multiple heuristics to match commits between two histories – patch id, commit title, position between other matched commits etc.
I wrote a script on top of git-revise
to support a similar workflow: git-branchless
patch ids are not reliable enough for a feature like this. Especially because git-revise very explicitly has features that break patch ids (such as merging/squashing two commits together, which will break heuristics based on metadata too).
Since Git 2.38 rebase has a new option --update-refs
that git-revise could mimick eventually?
- https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---update-refs
Since Git 2.38 rebase has a new option --update-refs that git-revise could mimick eventually?
I think that's the best approach. In fact, I don't think it even needs to be opt-in. My git-branchless (unrelated to the above project, which is now called git-branchstack
) and Jujutsu both rebase descendant branches. From user feedback so far, it seems like there's basically no case in which you wouldn't want to rebase the descendant branches. The only complication is how to resolve merge conflicts:
- Presumably,
git rebase --update-refs
waits until the rebase has concluded to confirm any branch updates, after any merge conflicts have been resolved (on-disk). - Jujutsu handles this by storing merge conflicts in the commits themselves, so the rebase operation always succeeds (and the descendant commit might have conflicts).
- git-branchless will attempt rebase descendant commits automatically. If the rebase would fail due to a merge conflict, it defers it until the next time you run
git restack
; or, you can pass the-m
/--merge
flag to resolve the conflicts (on-disk) now.
The latter seems most appropriate for git-revise
.
Isn't that too broad of a task for this utility? You may also want to resolve the same problem for other history rewriting, not only for git-revise – for the usual 'git rebase -i' for example.
The canonical way to transmit rewrite information is via the post-rewrite
hook. git rebase
already invokes this with sufficient information to figure out where to move the branches, even in the presence of squashes/fixups. In fact, if git-revise
were to invoke the post-rewrite
hook, then git-branchless
would be informed of the rewrites and serve as that very tool: you could run git restack
after the fact to restore the branch structure.
FWIW this is basically similar to hg evolve
's behavior
Technically git-revise
could potentially actually be changed to handle revises that change the base as well; it would be tricky and it loses some of the benefits of in-memory revise, but it can be done.
It's very useful for working with patch stacks.