将git父指针设置为不同的父节点

281
如果我有一个提交记录,指向一个父节点,但是我想要改变它所指向的父节点,我应该如何操作?
5个回答

487

使用git rebase。这是Git中的通用命令,可以将提交(commit)移动到不同的父级(base)。

但需要知道以下几点:

  1. 由于提交(commit)包含其父级(parent)信息,因此当您更改给定提交的父级时,它的SHA值将发生更改,并且线性开发中位于其后面(比其更近的)所有提交的SHA值也将发生更改。

  2. 如果您正在与他人合作并且已经将要修改的提交公开推送到其他用户已经拉下来的地方,那么修改提交可能是一个糟糕的想法™。这是由于#1所述的原因,其他用户的存储库将遭遇的混乱结果,因为它们的“相同”提交的SHA值不再匹配于你们的存储库中的SHA值。(有关详细信息,请参见链接手册的“从上游重置恢复”部分。)

话虽如此,如果您当前在一条具有一些提交的分支上并希望将其移动到新父级,那么看起来会像这样:

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

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


我正在使用git rebase --onto来更改父级,但似乎最终只得到一个提交。我在这个问题上提出了一个问题:http://stackoverflow.com/questions/19181665/git-rebase-onto-results-on-single-commit - Eduardo Mello
11
啊哈,原来“--onto”是用来做这个的!即使在阅读了这篇答案后,文档对我来说仍然完全晦涩难懂! - Daniel C. Sobral
19
这行代码:“git rebase --onto <new-parent> <old-parent>”,告诉了我如何使用“rebase --onto”命令,这是其他所有我之前读过的问题和文档都没有解释清楚的。 - jpmc26
4
对我而言,这个命令应该是“将此提交在那一个的基础上变基”,或者是“git rebase --on <new-parent> <commit>”。在这里使用旧的父提交对我来说没有意义。 - Samir Aguiar
1
@kopranb 当您想要从本地头部到远程头部中丢弃某些路径时,旧的父提交记录很重要。您所描述的情况可以通过 git rebase <新父提交记录> 来处理。 - clacke
显示剩余4条评论

36

如果你需要避免重新定位后续提交(例如因为历史重写是不可行的),你可以使用 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 push之后没有推送任何内容。 - Baptiste Wicht
2
@BaptisteWicht:替换内容存储在单独的一组引用中。您需要安排推送(和获取)refs/replace/引用层次结构。如果您只需要执行一次,则可以在源代码库中执行git push yourremote 'refs/replace/*',并在目标代码库中执行git fetch yourremote 'refs/replace/*:refs/replace/*'。如果您需要多次执行,则可以将这些refspecs添加到remote.yourremote.push配置变量(在源代码库中)和remote.yourremote.fetch配置变量(在目标代码库中)。 - Chris Johnsen
3
使用git replace --grafts是一个更简单的方法。不确定这是什么时候被添加的,但它的整个目的是创建一个替代品,与提交B相同,但具有指定的父项。 - Paul Wagland

34
注意,在Git中更改提交需要更改其后的所有提交。如果您已经发布了此部分历史记录,并且有人可能已经基于更改之前的历史记录构建了他们的工作内容,则不建议这样做。
Amber's response中提到的git rebase的替代方案是使用grafts机制(请参阅Git Glossary中的Git grafts的定义以及Git Repository Layout文档中.git/info/grafts文件的文档),以更改提交的父项,请使用一些历史查看器(gitkgit log --graph等)检查它是否做了正确的事情,然后使用git filter-branch(如其手册的"Examples"部分所述)使其永久(然后删除嫁接点,可选地删除由git filter-branch备份的原始引用或重新克隆存储库):

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


3
据我所了解(来自https://git.wiki.kernel.org/index.php/GraftPoint),假设您使用的是1.6.5或更高版本的Git,`git replace`已经取代了git grafts。 - Alexander Bird
4
大部分是正确的,但如果您想要"重写历史",那么可以使用嫁接(grafts)+ git-filter-branch(或交互式变基(interactive rebase),或快速导出+例如reposurgeon)来实现。如果您想/需要"保留历史记录",则git-replace比嫁接更为优越。 - Jakub Narębski
@JakubNarębski,您能解释一下重写保留历史记录的含义吗?为什么我不能使用git-replace + git-filter-branch?在我的测试中,filter-branch似乎尊重替换,重新定位整个树。 - cdunn2001
1
@cdunn2001:git filter-branch 重写历史,尊重嫁接和替换(但在重写的历史中,替换将无效);推送将导致非快进更改。git replace 创建就地可转移的替换;推送将是快进的,但您需要推送 refs/replace 来传输替换以纠正历史。希望对你有所帮助。 - Jakub Narębski

30

为了澄清以上答案并且厚颜无耻地推销我的脚本:

这取决于您是想“rebase”还是“reparent”。如Amber所建议的那样,rebase会移动diffs。如JakubChris所建议的那样,reparent会移动整个树的snapshots。如果要进行reparent,请使用git reparent而不是手动操作。

比较

假设您有左侧图片,并希望它看起来像右侧图片:

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

变基和父提交重定向都会产生相同的结果,但是C'的定义不同。使用git rebase --onto A BC'将不包含由B引入的任何更改。使用git reparent -p AC'将与C完全相同(除了B不在历史记录中)。


1
+1 我已经尝试过 reparent 脚本,它运行得非常好;强烈推荐。我还建议创建一个新的 branch 标签,并在调用之前 checkout 该分支(因为分支标签会移动)。 - Rhubbarb
值得注意的是:(1)原始节点保持不变,(2)新节点不继承原始节点的子节点。 - Rhubbarb

7

当我和提问者尝试着做相同的事情时,@Jakub的答案确实帮了我几个小时。
然而,git replace --graft 现在是关于编接的更简单的解决方案。此外,那个解决方案的一个主要问题是 filter-branch 让我失去了每个没有合并到 HEAD 分支的分支。然后,git filter-repo 完美无缺地完成了工作。

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

我之前曾经提出过一个类似的问题,完整的答案可以在这里找到。


更多信息:请查看文档中的“重新嫁接历史”部分。


1
git filter-repo 需要 2.22 或更高版本。 - George

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接