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?
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:
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.
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.
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!
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-replacement-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.