Change parent of a commit
Asked Answered
H

1

9

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.

Heeled answered 16/2, 2019 at 19:43 Comment(1)
Using git-reparent solves this problem directly.Tova
R
9

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.

Resting answered 16/2, 2019 at 20:15 Comment(3)
Thanks a lot, especially #2 is what I had in mind :) Just for completeness, I think there is an "m" missed in the command of #1 $ git read-tree -mu <hash-of-E> isn't it?Heeled
@Foobar: you can use -m. The result should act the same; the difference is (according to the documentation) just whether the stat info in the index is replaced.Resting
Regarding easier way #1: I get the following error: fatal: -u is meaningless without -m, --reset, or --prefixPlowshare

© 2022 - 2024 — McMap. All rights reserved.