如何保存正在进行的 Git 变基操作?

23

我正在进行一个包含多个冲突的大型"变基中"。

我想把这个过程搁置一下,尝试使用另一种方法解决这个问题。

是否有办法保存一个正在进行中的变基,以便稍后完成?


1
我会这样做。将更改存储,从中创建一个新分支,例如分支2,然后应用存储以便您可以在该分支上工作。在初始分支上,我会采取另一种方法。 - Andrei T
2
@AndreiT:不幸的是,您无法隐藏冲突的合并(因为每个隐藏都是一堆提交,而在冲突时无法提交)。 - torek
3个回答

20

如果您在rebase的冲突合并中卡住了,那么您可能会感到困惑。下面是为什么,如何以及您可以做什么。

Rebase = 重复cherry-pick

基本上,在Git中进行rebase操作只是一系列cherry-pick操作。我们从这样的东西开始:

...--A1--A2--A3--A4--A5   <-- branchA
          \
           B1--B2--B3   <-- branchB

我们希望最终得到:

...--A1--A2--A3--A4--A5   <-- branchA
          \           \
           \           B1'-B2'-B3'  <-- branchB
            \
             B1--B2--B3   [abandoned]
我们(或Git)实现这一点的方式是使用git cherry-pick或类似方法将现有提交B1复制(转换为补丁并应用)到A5之后,并将B2复制到B1'之后,以此类推。
交互式变基逐步运行git cherry-pick来执行每个“pick”操作。非交互式变基有几个选项,包括运行git cherry-pick
在挑选提交时,如果在应用过程中出现冲突,Git可以使用三方合并。但这可能会导致失败和冲突。这会停止变基。或者,当使用交互式变基时,您可以选择“编辑”提交,在这种情况下,Git会挑选该提交,然后停止变基。无论哪种情况,Git都会留下足够的信息,以便您稍后恢复变基。
冲突在索引中
提醒一下,Git的“索引”是您构建“下一个”提交的位置。通常情况下,每个要提交的文件都有一个索引条目,因此如果您的下一个提交只由三个名为READMEfileotherfile的文件组成,则会有三个索引条目。
请注意,索引与“工作树”是分开的,后者包含正常的非Git格式文件。您可以编辑这些文件、编译它们、使用它们来提供网页服务等,与索引和存储库文件的内部Git格式不同。(工作树还可以保存未跟踪的文件,在变基期间并不重要。)
在冲突合并期间,每个索引条目都会显示其各自的“插槽”。每个条目最多有四个插槽,它们是编号的。插槽零保存正常的未冲突文件(如果存在的话),否则为空。插槽1-3(如果正在使用)保存必须解决的三个冲突部分。1它们分别是“基础”版本(来自“合并基础”)、“本地”或--ours版本以及另一个或--theirs或有时称为“远程”版本。您的任务是编辑该文件的工作树版本,解决冲突,然后git add结果。这将把调整后的工作树版本复制到索引中的插槽零中,并清除插槽1-3条目。现在该文件已解决并准备好提交。

1因此,要么插槽0被占用且1-3为空,要么插槽0为空且插槽1-3被占用。有一些奇怪的情况,其中插槽1、2和/或3也可以为空,例如如果您遇到修改/删除冲突或添加/添加冲突,但通常情况是“0为空表示1-3已满”,反之亦然。


但只有一个索引

短语“the” index暗示只有一个索引,这基本上是正确的。

因为未合并的状态在这个(“the”)索引中,而且只有一个索引,任何需要使用“the”索引的其他内容都无法进行,直到您完成解决冲突(然后提交更改)。

如果您愿意,可以只是git add未修复/未解决的项目并git commit结果,以摆脱冲突。但是这样做的缺点是Git不会保留哪些文件有冲突:您将删除插槽1-3条目,Git会认为您已经完成了所有工作。

您可以保存索引 - 它是一个普通的文件;您可以将其复制到.git/index之外的其他地方。但是,由于它是一个具有各种特殊内部用途的二进制文件 - 索引也称为“缓存”,它会为了速度而缓存内部文件系统数据,因此这实际上并不安全。(如果Git有一种方法可以“导出”索引状态,然后稍后再“导入”它,以便您真正可以保存和恢复合并冲突状态,那将是很好的。但是Git没有这个功能。)

因此,出于安全考虑,建议完成解决此冲突的合并状态。或者,如果您尚未开始解决,请不要开始:那么就没有要保存的工作了。

您现在所处的位置

假设您从上面的“分支B”的rebase中开始,并且当前卡在复制提交B2的中间,还有一些未解决的冲突。下面是您当前实际拥有的内容:

...--A1--A2--A3--A4--A5   <-- branchA
          \           \
           \           B1'  <-- HEAD
            \
             B1--B2--B3   <-- branchB

索引处于冲突状态。您还有一个“分离的HEAD”:Git正在以此方式构建新的提交链。名称HEAD指向所有已完成的提交。

如果您已经解决了一些工作,请完成它(因为保存未解决状态太困难了),或者至少记录下未解决的内容(因为您可以将文件添加到下一次提交中),然后运行git commit来创建提交B2'

...--A1--A2--A3--A4--A5   <-- branchA
          \           \
           \           B1'-B2'  <-- HEAD
            \
             B1--B2--B3   <-- branchB

如果您没有进行任何解决工作,则没有实际的工作可保存,因此不要运行git commit。但是无论如何现在是创建指向与HEAD指向相同提交的分支或标记名称的时候了:

$ git branch saveme    # or git tag saveme

现在你有这个:

...--A1--A2--A3--A4--A5   <-- branchA
          \           \
           \           B1'-B2'  <-- HEAD, saveme
            \
             B1--B2--B3   <-- branchB

现在你只需要:

$ git rebase --abort

这将使Git停止rebase尝试并返回到branchB

...--A1--A2--A3--A4--A5   <-- branchA
          \           \
           \           B1'-B2'  <-- saveme
            \
             B1--B2--B3   <-- HEAD->branchB

现在你已经保存了到目前为止所做的所有工作,可以回去稍后重试rebase。你有您(或Git)对 B1' 做出的决议,如果您提交了B2',则也有您对其所做出的决议。它们分别是提交 saveme~1saveme,或者如果只有一个提交,则只是提交 saveme


在你描述的四个插槽中,我可以看到合并基础版本来自哪里,'ours'和'theirs'都很明显。但是“正常的未冲突文件”是什么,它来自哪里? - dumbledad
1
@dumbledad:有史以来第一个没有冲突的版本是由您(或其他人)创建的。如果存在冲突,则槽0为空闲状态:即不存在未冲突的版本。现在轮到解决冲突(使用工作树版本和/或三个槽1-2-3版本),然后在您想要的任何内容上运行git add,将其写入槽0作为正常的未冲突版本。然后它就会进入下一个提交,因此每个提交中的内容也都是未冲突的。啊,我看到措辞问题了,我会修复的。 - torek
非常感谢您提供这个出色的答案。 - Profpatsch
我通常会在长篇的 Git 答案中感到恍惚,因为它们通常充满了无意义的炫耀术语,旨在展示作者有多聪明。然而,这篇文章真的很好,用恰到好处的修饰词来添加重要的上下文信息,却没有无意义的内容。 - reergymerej

3

我想到了一个可行的方法,但有点取巧:

将仓库重新克隆到另一个目录中,保留 正在进行中的变基


2
这确实有效:它可以让你得到一个克隆版本,该版本具有自己的索引和工作树,并且新克隆版本中的索引和工作树与正在进行的变基操作的索引和工作树是独立的。另一个非常方便的技巧(自Git 2.5以来就可用,但在2.6之前我不太信任它)是git worktree add,它可以创建一个具有自己独立索引的新工作树。然而,有一个限制:每个添加的工作树必须位于它自己的分支上。正在进行的变基操作没有分支(分离的HEAD),但一旦完成变基操作,它将尝试返回到其原始分支,因此在这里要小心。 - torek
但对于大型存储库,这可能会占用太多空间。 - dumbledad
2
@dumbledad 我简直无法想象一个仓库如此之大,以至于您不能同时在磁盘上拥有2个副本。也许如果该仓库有大量的媒体文件,我可以理解这一点。那时,也许是时候重新考虑将它们与代码一起进行版本控制了。 - Zach Lysobey

2
正如torek所提到的,rebase只是一系列cherry-pick。如果你在一个冲突的rebase过程中(也就是说已经发生了一些cherry-pick),而你需要放弃它,执行git rebase --abort
稍后你可以使用git reflog命令查看过去git事件的历史记录。这将显示一个列表,其中你将看到中止事件和所有先前的cherry-pick事件逐个列出。
每个事件都有一个哈希值。现在你可以通过git reset --hard <hash-from-reflog-list>跳转到中止前的最后一步。当然,你不会处于rebase模式,但现在你可以通过普通的cherry-pick继续rebase。
请注意!这也会回滚你之前进行的所有其他git更改。

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