Search code examples
gitworkflowrebase

Git rewriting history with rebase


I am currently working on a branch that looks like this

--A------B (master)
        / \
----C--D   E--F (feature_branch, HEAD)

I've been trying my best to fix it without success :( git-rebase <A_SHA1> doesn't seem to work at all. There are currently only two branches: master and feature_branch.

Is there even a way to make it look like this?

--A------B (master)
   \    / \
    C--D   E--F (feature_branch, HEAD)

Solution

  • Note that existing commits cannot be changed, so given:

    ...--α--A------B   <-- master
                  / \
     ....--γ--C--D   E--F   <-- feature_branch (HEAD)
    

    what you'll inevitably get from a rebase is, instead:

                C'-D'  E'-F'  <-- feature_branch (HEAD)
               /    \ /
              /___---B'
             //
    ...--α--A------B   <-- master
                  / \
     ....--γ--C--D   E--F   [abandoned]
    

    You can then forcibly move master to point to B' instead of B so that you have:

                C'-D'  E'-F'  <-- feature_branch (HEAD)
               /    \ /
              /___---B'  <-- master
             //
    ...--α--A------B
                  / \
     ....--γ--C--D   E--F   [abandoned]
    

    It's now possible to ignore the presence of B, γ, C, and so on entirely and pretend that C' is C, for instance. Note that commit γ has become unreachable unless α and γ are really the same commit.

    To achieve this using git rebase, you will want the somewhat-newfangled -r or --rebase-merges option:

    git checkout feature_branch   # if needed - you've drawn a detached HEAD
    git rebase -i -r --onto master <hash-of-γ>
    

    after which you will need to delete commits such as α and A from the list of commits to be pick-ed, as these commits are all currently on feature_branch as well as on master, through merge commit B. (Note that -r was new in Git 2.18. The -r option uses the interactive machinery to instruct Git to re-perform merges, which is what we will do below.)

    Overall, though, it's probably easier to achieve this using separate git cherry-pick and git merge commands:

    git checkout --detach <hash-of-A>    # note: master~1 probably finds commit A
    git cherry-pick <hash-of-C>          # make C'
    git cherry-pick <hash-of-D>          # make D'
    git merge --no-ff <hash-of-A>        # make new merge B'
    git branch -f master HEAD            # forcibly update master now
    git cherry-pick feature_branch~2     # make E'
    git cherry-pick feature_branch~1     # make F'
    git checkout -B feature_branch       # forcibly move feature_branch and re-attach HEAD
    

    If master~1 does identify commit A, you can use that in place of each literal hash here, and in that case, master^2^ will identify commit C and master^2 will identify commit D, so you can use that in place of those two literal hash IDs.