Yay, your diagram is accurate! 😀
Alas, there's no obvious tool in Git to achieve the result you want. The problem here is that rebase is just automated cherry-picking, and cherry-picking is about converting commits—and their snapshots—to change-sets and merging those changes with some other commit to make a new commit, which isn't want you want: you want to preserve the original snapshot.
Fortunately, there are several pretty-easy easy ways to do this. Unfortunately, some of them use at least one plumbing command, i.e., a not-meant-for-users, not shiny porcelain, internal Git command.
First, let's note that your own solution is correct:
Only solution preventing massive merge I found so far is
check out C
copy over all stuff from E to C
commit and get E'
which in actual Git commands is, e.g.:
$ git checkout -b new-branch master
$ git rm -r . # in case there are files in C that aren't in E at all
$ git checkout <hash-of-E> -- . # overwrite using E
$ git commit
This is actually not that bad, but it causes a lot of updates to the work-tree, which can be annoying if your next make
takes an hour, or whatever.
Easier way #1
The first easier way to do this is:
$ git checkout -b new-branch master
$ git read-tree -u <hash-of-E>
$ git commit
The read-tree
operation replaces your index contents with those taken from commit E. The -u
flag tells Git: As you do this index update, update the work-tree too: if a file is removed entirely from the index, remove it from the work-tree too, or if a file is replaced in the index, replace it in the work-tree too. This flag is not actually required, because git commit
is going to use what's in the index, but it's a good idea for sanity.
Easier way #2
The second easier method is this:
$ git commit-tree -p master -m "<message>" <hash-of-E>^{tree}
This prints out the new commit's hash ID; we then need to set something to point to this new hash ID:
$ git update-ref refs/heads/new-branch <hash-ID>
Or, in one line:
$ git update-ref refs/heads/new-branch $(git commit-tree -p master -m "<message>" <hash-of-E>^{tree})
Note that -m "<message>"
can be replaced with -F <file>
to read a message from a file, or even -F -
to read a message from stdin. You can then copy the commit message from commit E
using git log --no-walk --format=%B <hash-of-E>
and pipe that to the rest of the one-line command.
Be sure that new-branch
really is a new branch name, or if not, that it's the branch you want to re-set and is not the current branch, because git update-ref
does no error-checking by default.
Easier way #3
You can also do the job like this:
$ git checkout -b new-branch <hash-of-E> # now at E, with E in index and work-tree
$ git reset --soft master # make new-branch identify C, without
# touching index or work-tree
$ git commit -c <hash-of-E> # make new commit using E's message
This last method has the least work-tree churn, so is perhaps the best of the three. However, method #2 creates the new commit without touching anything, so if you don't actually want to be on the new branch, method #2 is perhaps the best.