Search code examples
gitmergebranchrebasecherry-pick

How can I move a commit back to a location in history before a branch was merged while leaving the branch intact?


After merging a branch into master, I realized that I ideally should have made a minor commit before the branch was merged, because it is directly related to the commit before the merge. I have tried to clarify what the desired outcome looks like in the simplified example below, where

  • B is the merge commit of X and Y into master,
  • C is a commit that relates to the merged content,
  • D does not touch any of the newly merged content and is directly related to A.

Current branch structure:

A----B-C-D
    /
 X-Y

The desired outcome is to move D right after A, while leaving the rest as is:

A-D----B-C
      /
   X-Y

Rebase after picking all commits and moving D after A:

A-D-X-Y-B-C

Rebase after dropping X and Y, and moving D after A:

A-D-B-C

How can I keep the branch in my history? Is rebase the wrong tool here? I have looked at cherry-pick, but I am not sure how I would use it to achieve the desired outcome.

In case it matters, the branch did not origin from master, but from an unrelated repo. It added a couple of new files while retaining their commit history from the previous repo. This is for a local repo where I am the only contributor.


Solution

  • You might want to do something like this:

    git rebase -i --rebase-merges A
    

    The rebase todo instructions will initially look like this

    label onto
    
    # Branch branch-name/master
    reset [new root]
    pick X
    pick Y
    label branch-name/master
    
    reset onto
    merge -C pick B
    pick C
    pick D
    

    Reorder the bottom part as in a standard rebase:

    reset onto
    pick D
    merge -C pick B
    pick C
    

    By adding --rebase-merges, you are asking Git to try to recreate any existing branches and maintain the merge structure when the rebase takes place.

    From the man pages:

    --rebase-merges[=(rebase-cousins|no-rebase-cousins)]

    By default, a rebase will simply drop merge commits from the todo list, and put the rebased commits into a single, linear branch. With --rebase-merges, the rebase will instead try to preserve the branching structure within the commits that are to be rebased, by recreating the merge commits. Any resolved merge conflicts or manual amendments in these merge commits will have to be resolved/re-applied manually.

    By default, or when no-rebase-cousins was specified, commits which do not have <upstream> as direct ancestor will keep their original branch point, i.e. commits that would be excluded by git-log2's --ancestry-path option will keep their original ancestry by default. If the rebase-cousins mode is turned on, such commits are instead rebased onto <upstream> (or <onto>, if specified).

    The --rebase-merges mode is similar in spirit to the deprecated --preserve-merges, but in contrast to that option works well in interactive rebases: commits can be reordered, inserted and dropped at will.

    It is currently only possible to recreate the merge commits using the recursive merge strategy; Different merge strategies can be used only via explicit exec git merge -s <strategy> [...] commands.

    See related StackOverflow topic here.