Setting git parent pointer to a different parent
Asked Answered
T

5

283

If I have a commit in the past that points to one parent, but I want to change the parent that it points to, how would I go about doing that?

There answered 28/9, 2010 at 6:52 Comment(0)
I
492

Using git rebase. It's the generic "take commit(s) and plop it/them on a different parent (base)" command in Git.

Some things to know, however:

  1. Since commit SHAs involve their parents, when you change the parent of a given commit, its SHA will change - as will the SHAs of all commits which come after it (more recent than it) in the line of development.

  2. If you're working with other people, and you've already pushed the commit in question public to where they've pulled it, modifying the commit is probably a Bad Idea™. This is due to #1, and thus the resulting confusion the other users' repositories will encounter when trying to figure out what happened due to your SHAs no longer matching theirs for the "same" commits. (See the "RECOVERING FROM UPSTREAM REBASE" section of the linked man page for details.)

That said, if you're currently on a branch with some commits that you want to move to a new parent, it'd look something like this:

git rebase --onto <new-parent> <old-parent>

That will move everything after <old-parent> in the current branch to sit on top of <new-parent> instead.

Ithnan answered 28/9, 2010 at 7:1 Comment(9)
I'm using git rebase --onto to change parents but it seems that I end up with only a single commit. I put up a question on it: stackoverflow.com/questions/19181665/…Notornis
Aha, so THAT is what --onto is used for! The documentation has always been completely obscure to me, even after reading this answer!Ammeter
This line: git rebase --onto <new-parent> <old-parent> Told me everything about how to use rebase --onto that every other question and document I've read so far failed to.Phenolic
For me this command should read: rebase this commit on that one, or git rebase --on <new-parent> <commit>. Using the old parent here doesn't make sense to me.Hilel
@kopranb The old parent is important when you want to discard some of the path from your head to the remote head. The case you are describing is handled by git rebase <new-parent>.Unsubstantial
To put it another way, --onto and your old parent are important if you are rebasing because your upstream rebased, or you are changing upstreams, rather than just following the upstream forward.Unsubstantial
while doing so git rebase --onto remotes/origin/feature/new_parent remotes/origin/ I'm getting this invalid upstream remotes/origin/. So I tried git rebase --onto remotes/origin/feature/new_parent/my_branch remotes/origin/my_branch but then it giving Does not point to a valid commit: remotes/origin/feature/new_parent/my_branch Any suggestion what is wrong here ? @IthnanHypogynous
make sure you add origin in front, something like this: git rebase --onto origin/<new_branch> origin/<old_branch>Ironsmith
In my case, I had pushed to remote branch and even raised an MR. Doing this and pushing it was rejected because parent branch was more updated. I simply had to delete the remote branch with git push --delete origin branchname and then pushed againBabbling
R
36

If it turns out that you need to avoid rebasing the subsequent commits (e.g. because a history rewrite would be untenable), then you can use the git replace (available in Git 1.6.5 and later).

# …---o---A---o---o---…
#
# …---o---B---b---b---…
#
# We want to transplant B to be "on top of" A.
# The tree of descendants from B (and A) can be arbitrarily complex.

replace_first_parent() {
    old_parent=$(git rev-parse --verify "${1}^1") || return 1
    new_parent=$(git rev-parse --verify "${2}^0") || return 2
    new_commit=$(
      git cat-file commit "$1" |
      sed -e '1,/^$/s/^parent '"$old_parent"'$/parent '"$new_parent"'/' |
      git hash-object -t commit -w --stdin
    ) || return 3
    git replace "$1" "$new_commit"
}
replace_first_parent B A

# …---o---A---o---o---…
#          \
#           C---b---b---…
#
# C is the replacement for B.

With the above replacement established, any requests for the object B will actually return the object C. The contents of C are exactly the same as the contents of B except for the first parent (same parents (except for the first), same tree, same commit message).

Replacements are active by default, but can be turned of by using the --no-replace-objects option to git (before the command name) or by setting the GIT_NO_REPLACE_OBJECTS environment variable. Replacements can be shared by pushing refs/replace/* (in addition to the normal refs/heads/*) .

If you do not like the commit-munging (done with sed above), then you could create your replacement commit using higher level commands:

git checkout B~0
git reset --soft A
git commit -C B
git replace B HEAD
git checkout -

The big difference is that this sequence does not propagate the additional parents if B is a merge commit.

Response answered 28/9, 2010 at 9:7 Comment(3)
And then, how would you push the changes to an existing repository ? It seems to work locally, but git push does not push anything after thatSeabrook
@BaptisteWicht: The replacements are stored in a separate set of refs. You will need to arrange to push (and fetch) the refs/replace/ ref hierarchy. If you just need to do it once, then you can do git push yourremote 'refs/replace/*' in the source repository, and git fetch yourremote 'refs/replace/*:refs/replace/*' in the destination repositories. If you need to do it multiple times, then you could instead add those refspecs to a remote.yourremote.push config variable (in the source repository) and a remote.yourremote.fetch config variable (in the destination repositories).Response
A simpler way to do this is to use git replace --grafts. Not sure when this was added, but its whole purpose is to create a replacement that is the same as commit B but with the specified parents.Chinchilla
D
34

Note that changing a commit in Git requires that all commits that follow it alse have to be changed. This is discouraged if you have published this part of history, and somebody might have build their work on history that it was before change.

Alternate solution to git rebase mentioned in Amber's response is to use grafts mechanism (see definition of Git grafts in Git Glossary and documentation of .git/info/grafts file in Git Repository Layout documentation) to change parent of a commit, check that it did correct thing with some history viewer (gitk, git log --graph, etc.) and then use git filter-branch (as described in "Examples" section of its manpage) to make it permanent (and then remove graft, and optionally remove the original refs backed up by git filter-branch, or reclone repository):

echo "$commit-id $graft-id" >> .git/info/grafts
git filter-branch $graft-id..HEAD

NOTE !!! This solution is different from rebase solution in that git rebase would rebase / transplant changes, while grafts-based solution would simply reparent commits as is, not taking into account differences between old parent and new parent!

Divulsion answered 28/9, 2010 at 8:40 Comment(4)
From what I understand (from git.wiki.kernel.org/index.php/GraftPoint), git replace has superseded git grafts (assuming you have git 1.6.5 or later).Laomedon
@Thr4wn: Largely true, but if you want to rewrite history then grafts + git-filter-branch (or interactive rebase, or fast-export + e.g. reposurgeon) is the way to do it. If you want/need to preserve history, then git-replace is far superior to grafts.Overstrain
@JakubNarębski, could you explain what you mean by rewrite vs preserve history? Why can't I use git-replace + git-filter-branch? In my test, filter-branch seemed to respect the replacement, rebasing the entire tree.Sealey
@cdunn2001: git filter-branch rewrites history, respecting grafts and replacements (but in rewritten history replacements will be moot); push will result in non fasf-forward change. git replace creates in-place transferable replacements; push will be fast-forward, but you would need to push refs/replace to transfer replacements to have corrected history. HTHOverstrain
A
30

To clarify the above answers and shamelessly plug my own script:

It depends on whether you want to "rebase" or "reparent". A rebase, as suggested by Amber, moves around diffs. A reparent, as suggested by Jakub and Chris, moves around snapshots of the whole tree. If you want to reparent, I suggest using git reparent rather than doing the work manually.

Comparison

Suppose you have the picture on the left and you want it to look like the picture on the right:

                   C'
                  /
A---B---C        A---B---C

Both rebasing and reparenting will produce the same picture, but the definition of C' differs. With git rebase --onto A B, C' will not contain any changes introduced by B. With git reparent -p A, C' will be identical to C (except that B will not be in the history).

Alderson answered 2/10, 2013 at 17:40 Comment(2)
+1 I have experimented with the reparent script; it works beautifully; highly recommended. I would also recommend creating a new branch label, and to checkout that branch before calling (as the branch label will move).Malonylurea
It's also worth noting that: (1) the original node remains intact, and (2) the new node does not inherit the original node's children.Malonylurea
C
7

Definitely @Jakub's answer helped me some hours ago when I was trying the exact same thing as the OP.
However, git replace --graft is now the easier solution regarding grafts. Also, a major problem with that solution was that filter-branch made me loose every branch that wasn't merged into the HEAD's branch. Then, git filter-repo did the job perfectly and flawlessly.

$ git replace --graft <commit> <new parent commit>
$ git filter-repo --force

I've made a question like this some time ago, so the complete answer can be found here.


Fore more info: checkout the section "Re-grafting history" in the docs

Corley answered 19/6, 2020 at 22:23 Comment(1)
git filter-repo requires 2.22 or laterFinegrained

© 2022 - 2024 — McMap. All rights reserved.