如果我在过去有一个指向一个父结点的提交,但我想改变它所指向的父结点,我该怎么做呢?


当前回答

注意,在Git中更改一个提交要求后面的所有提交也必须更改。如果你出版了这部分历史,这是不鼓励的,有些人可能把他们的工作建立在历史上,那是在变革之前。

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

请注意! !这个解决方案不同于rebase解决方案,因为git rebase会重基/移植更改,而基于嫁接的解决方案只会按原样再现提交,不考虑旧父级和新父级之间的差异!

其他回答

几个小时前,@Jakub的回答确实帮助了我,当时我正在尝试和OP完全相同的事情。 然而,git replace -graft现在是关于移植的更简单的解决方案。此外,该解决方案的一个主要问题是过滤器分支使我失去了没有合并到HEAD分支中的每个分支。然后,git filter-repo完美无缺地完成了这项工作。

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

前段时间我也提出过类似的问题,所以完整的答案可以在这里找到。


更多信息:签出部分“重新嫁接历史”在文档

如果你发现你需要避免重设后续提交的基(例如,因为重写历史将是站不住脚的),那么你可以使用git replace(在git 1.6.5及更高版本中可用)。

# …---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.

建立上述替换后,对对象B的任何请求实际上都会返回对象C。C的内容与B的内容完全相同,除了第一个父元素(相同的父元素(除了第一个),相同的树,相同的提交消息)。

替换在默认情况下是活动的,但是可以通过使用git的——no-replace-objects选项(在命令名之前)或设置GIT_NO_REPLACE_OBJECTS环境变量来关闭替换。替换可以通过按refs/replace/*来共享(除了正常的refs/heads/*之外)。

如果你不喜欢提交(使用上面的sed完成),那么你可以使用更高级别的命令创建你的替代提交:

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

最大的区别是,如果B是合并提交,这个序列不会传播额外的父节点。

注意,在Git中更改一个提交要求后面的所有提交也必须更改。如果你出版了这部分历史,这是不鼓励的,有些人可能把他们的工作建立在历史上,那是在变革之前。

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

请注意! !这个解决方案不同于rebase解决方案,因为git rebase会重基/移植更改,而基于嫁接的解决方案只会按原样再现提交,不考虑旧父级和新父级之间的差异!

为了澄清以上的答案,并无耻地插入我自己的脚本:

这取决于你是想“rebase”还是“reparent”。正如Amber所建议的那样,rebase会围绕差异移动。Jakub和Chris建议的reparent在整个树的快照之间移动。如果你想重做,我建议使用git重做,而不是手动做这项工作。

比较

假设你有左边的图片,你想让它看起来像右边的图片:

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

重基和重父都将产生相同的图像,但C'的定义不同。当git rebase—to A B时,C'将不包含B引入的任何更改。当git reparent -p A时,C'将与C相同(除了B将不在历史中)。

使用git rebase。这是Git中通用的“获取提交并将其/它们放到不同的父(基)上”命令。

然而,有些事情是需要知道的:

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

也就是说,如果你目前在一个分支上有一些提交,你想要移动到一个新的父节点,它看起来像这样:

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

这将把当前分支中<old-parent>之后的所有内容移到<new-parent>之上。