git-revise icon indicating copy to clipboard operation
git-revise copied to clipboard

Add flag to update other local branches affected by the rewrite

Open lilyball opened this issue 5 years ago • 10 comments

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

lilyball avatar Aug 14 '19 19:08 lilyball

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.

odnoletkov avatar Aug 14 '19 21:08 odnoletkov

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.

lilyball avatar Aug 14 '19 23:08 lilyball

This sounds like a useful feature to have, but it would have to be opt in, and not allowed in interactive mode.

Manishearth avatar Aug 14 '19 23:08 Manishearth

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 *)

lilyball avatar Aug 15 '19 00:08 lilyball

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.

odnoletkov avatar Aug 15 '19 20:08 odnoletkov

I wrote a script on top of git-revise to support a similar workflow: git-branchless

krobelus avatar Aug 16 '20 09:08 krobelus

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

lilyball avatar Sep 01 '20 20:09 lilyball

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

Sarcasm avatar Nov 07 '22 09:11 Sarcasm

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.

arxanas avatar Nov 07 '22 20:11 arxanas

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.

Manishearth avatar Nov 07 '22 21:11 Manishearth