I try to move a commit on top of another branch ignoring all changes done in target branch.
Current situation is:
master
↓
--A--B--C
\
D--E
I want to move commit E in front of C
master
↓
--A--B--C--E'
\
D--E
with E' being identical to E except parrent is C now instead of D (this means tree of E and E' should be identical).
To be more precides:
"git cat-file -p E" shows e.g.
tree b98c9a9f9501ddcfcbe02a9de52964ed7dd76d5a
parent D
"git cat-file -p E'" should show
tree b98c9a9f9501ddcfcbe02a9de52964ed7dd76d5a
parent C
I tried rebase with differnt parameters as well as cherry-pick but all of them finally try to merge any changes done in C into the new E' commit :(
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'
Now the trees are identical and the parents are differnt but there must be a simpler and much faster way, since all that has to be done is creating a simple commit-object with existing tree-object.
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.
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.
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.
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.