能否修改父提交记录而不改变当前提交记录中的文件?

3
有时我想将现有的提交分为两个,以便进行代码审查或更清晰的版本历史。然而,在拆分提交时,你可能会发现某些不一致性或者你需要修改新父提交来维护构建。但是,我经常需要做很多手动工作来跟踪/维护我已编写的最终代码。
在不修改最终提交树哈希的情况下,是否可以运行一个rebase?
例如,我们有三个提交:ABC: 这是提交 A
# test_file.py
# this is commit A
does_stuff()

这是一个新提交 B

# test_file.py
# this is commit B
does_stuff()

这是一个新的提交C

# test_file.py
# this is commit C
does_stuff()

原始树的外观类似于

... -- C -- A

但是我们希望将A“分割”成两个提交

... -- C -- B -- A

当我们使用交互式变基创建B时,A的注释也会更新,显示# this is commit B(假设除了这个文件外还有一些其他更改)。然而,我们希望保留原来的注释。注意:A的哈希值会因为有不同的父节点而发生变化,但其树哈希应该与之前相同。
1个回答

3

任何提交都不可能被修改。相反,你可以将一些现有的提交复制到一个新的提交集合中。这其实是好消息,因为这意味着原始的提交仍然可用。

当我们使用交互式变基创建B时,A的注释也将更新以显示# this is commit B(假设除了这个文件之外还有其他几个更改)。

在你展示的具体例子中,不应该这样:你应该会得到合并冲突。

对于其他情况,当然,你是正确的。

但是,我们想保留注释不变。注意:A由于具有不同的父项而具有不同的整体哈希,但其树哈希应保持与之前相同。

记住,你从以下内容开始:

... -- C -- A
我会将其描述为以下形式:

...--C--A   <-- branchname (HEAD)

为了表示某个名为 branchname 的现有分支指向提交 C,并且 HEAD 已经附加到 A。您随后运行了 git rebase -i <hash-of-C> 或类似命令。这会给您列出一些要做的事情,您选择“编辑”A。Git 现在:
  • detaches HEAD to the target of the rebase:

           A   <-- branchname
          /
    ...--C   <-- HEAD
    
  • Copies commit A (using an exact copy / fast-forward, in this case, so that it re-uses A itself; you can disable this if you like with --no-ff, although in the end it makes no difference):

           A   <-- HEAD, branchname
          /
    ...--C
    

    or:

           A   <-- branchname
          /
    ...--C--A'   <-- HEAD
    

    (using --no-ff to force a copy).

此时您需要做一些更改,然后运行git addgit commit --amend将当前提交推到一边,并使HEAD指向新的提交B,其父节点是C。假设您没有使用--no-ff选项;则结果如下:

       A   <-- branchname
      /
...--C--B   <-- HEAD

(如果您使用了--no-ff,那么另外一个没有名称的A'将被保留约一个月后进行垃圾回收。然后我们必须将下一个副本命名为A"以区分它们,因此让我们假设您没有使用--no-ff。)
现在,您想要从提交A获取文件和提交信息,并创建一个新的提交。由于branchname仍然指向原始提交A,所以只需执行以下操作:
$ git checkout branchname -- .     # assumes you're at the top level of your repo
$ git commit -C branchname         # or -c if you want to edit it again

现在您拥有的是:
       A   <-- branchname
      /
...--C--B--A'  <-- HEAD

在这一点上,您可以使用git rebase --continue完成rebase操作。由于没有需要复制的提交,您已经完成了rebase操作中最后一个提交A的复制,因此这是rebase操作的最后一步,它将从原始提交链中剥离分支名称,并将其移动到指向与HEAD相同的提交,同时重新附加HEAD

       A   <-- ORIG_HEAD
      /
...--C--B--A'  <-- branchname (HEAD)

作为副作用,rebase 会设置 ORIG_HEAD 来记住 branchname 原来的指向位置,这样就可以轻松地确保一切都正常工作并且你已经到达了期望的状态:
git diff ORIG_HEAD HEAD

如果出现错误,您可以使用git reset --hard ORIG_HEAD命令进行重置,结果如下:

       A   <-- branchname (HEAD)
      /
...--C--B--A'  <-- ORIG_HEAD

请注意,其他命令(包括git reset)会设置ORIG_HEAD(这就是为什么它们在此处交换的原因)。最终,这两个提交中的一个将被完全放弃,除了reflog条目外,当这些条目过期时,不可访问的提交将真正消失。这些提交的默认到期时间是30天。

很好,我不确定是否有比手动重新应用连续提交更清洁的解决方案。git checkout branchname -- .足以处理单个提交,对于多个提交来说,看起来有些棘手。 - Mezuzza
是的,这确实更加棘手。从分支末端返回的时候有一个方便的符号表示:你可以在任何有效的提交表达式后面加上一个帽子和数字,例如 branch^1,来选择该提交的第 n 个父提交。当然,在线性链中只有一个父提交。数字默认为 1,因此 branch^^ 是父提交的父提交,即向后两步,而 branch^^^ 是向后三步,以此类推。为了缩写这个过程,你可以使用波浪号和数字:branch~3 = branch^^^branch~7 = branch^^^^^^^,等等。但这仍然需要在脑海中计算和向后工作。 - torek
这些内容都在gitrevisions文档中有详细说明。请注意,上面提到的“任何有效提交表达式”技术上是错误的:存在一些搜索表达式将会使用插入符或波浪号(以及数字如果存在)。不过这应该是很明显的。 - torek
是的,我实际上一直使用 HEAD~,因为 zsh 会在不使用额外引号的情况下使用扩展通配符消耗 ^ - Mezuzza

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