gitmergemoverebasecherry-pick

Change parent of a commit


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.


Solution

  • 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.