我有一个文件myfile.txt
Line one
Line two
Line four
其中每一行已被添加到单独的提交中。
我编辑文件以添加“缺失”行,因此文件现在
Line one
Line two
Line three
Line four
此bash脚本设置存储库:
#!/bin/bash
mkdir -p ~/testrepo
cd ~/testrepo || exit
git init
echo 'Line one' >> myfile.txt
git add myfile.txt
git commit -m 'First commit'
echo 'Line two' >> myfile.txt
git commit -m 'Second Commit' myfile.txt
echo 'Line four' >> myfile.txt
git commit -m 'Third commit' myfile.txt
sed -i '/Line two/a Line three' myfile.txt
git commit --fixup=HEAD^ myfile.txt
历史记录如下所示
$ git --no-pager log --oneline
90e29ee (HEAD -> master) fixup! Second Commit
6a20f1a Third commit
ac1564b Second Commit
d8a038d First commit
我运行了一个交互式变基来将修正提交合并到“SecondCommit”中,但是它报告了一个合并冲突:
$ git rebase -i --autosquash HEAD^^^
Auto-merging myfile.txt
CONFLICT (content): Merge conflict in myfile.txt
error: could not apply 90e29ee... fixup! Second Commit
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 90e29ee... fixup! Second Commit
$ git --no-pager diff
diff --cc myfile.txt
index b8b933b,43d9d5b..0000000
--- a/myfile.txt
+++ b/myfile.txt
@@@ -1,2 -1,4 +1,7 @@@
Line one
Line two
++<<<<<<< HEAD
++=======
+ Line three
+ Line four
++>>>>>>> 90e29ee... fixup! Second Commit
- 为什么将链接地址信息提交从分支的HEAD移动到“第二次提交”和“第三次提交”之间的位置会产生合并冲突?
- 是否有一种方法可以执行重定基并避免冲突,或者自动解决冲突?
期望的历史是
xxxxxxx (HEAD -> master) Third commit
xxxxxxx Second Commit
d8a038d First commit
其中“第二次提交”如下所示:
diff --git a/myfile.txt b/myfile.txt
index e251870..802f69c 100644
--- a/myfile.txt
+++ b/myfile.txt
@@ -1 +1,3 @@
Line one
+Line two
+Line three
2条答案
按热度按时间xv8emn3q1#
TL; DR
你在这里遇到的基本上是合并的边缘情况。你只需要手动修复这些问题。你可能会想,当你没有运行
git merge
的时候,我为什么要谈论合并。关于这个问题,请参阅下面的详细答案。长
git rebase
所做的是 * 复制 *(一些)提交。当使用交互式rebase,git rebase -i
时,你可以修改复制过程。当使用--autosquash
时,Git自己修改复制过程。这种修改可能会导致你遇到的问题。即使没有任何修改,你仍然会遇到冲突。让我们来探索一下。关于提交
我们需要从提交的简要概述开始。每个提交:
每个提交表单中的父提交哈希ID都提交到一个向后看的链中,例如,如果我们用单个大写字母来代表哈希ID,我们会得到一个简单的线性提交链:
其中
H
代表提交链中 * 最后一个 * 提交的哈希ID,该提交包含快照和较早提交G
的哈希ID,我们说H
* 指向 *G
,G
依次指向F
,F
指向更早的提交。因为提交保存的是快照,而不是更改,所以我们需要让Git比较两个快照来发现更改,这就像玩spot the difference游戏一样。为此,我们可以运行
git diff
并给它两个原始提交哈希ID,或者我们可以对一个提交运行git show
,比较提交和它的(单)父提交。(对 * merge commits * 的影响比较复杂,因为它们是有两个或更多父提交的提交。)因为提交是通过哈希ID找到的,而哈希ID是加密校验和,所以我们不能修改任何现有提交的任何内容。如果某个提交在某些方面有缺陷,我们能做的最好的就是提取它,修复它,然后在Git中放入一个新的提交:不同的内容将为新的提交产生新的唯一的哈希ID。2现有的提交将保持不变。
因为一个提交包含其父提交的哈希ID,所以如果我们"改变"(即复制)任何一个提交,我们也会被迫"改变"(复制)所有 * 后续提交 *。因此,任何对提交的重新排序,或任何对 * 任何 * 提交的任何破坏性的修复--包括仅仅修复其日志消息--都会产生连锁React。这其实并不是什么大问题:大多数提交都很便宜。事实上,Git重用快照中的文件(去重),甚至删除整个快照,这意味着更改提交的一部分--比如日志消息--而不更改快照几乎不需要任何磁盘空间。1因此,我们通常不需要担心磁盘空间。
我们 * 确实 * 需要在重定基准时担心其他事情:特别是,我们必须担心其他Git仓库拥有这些提交的副本。但如果其他Git仓库没有这些提交,这种担心也就不重要了。总的来说,当我们只是使用私有仓库,或者当我们没有将提交发送给其他任何人时,重定基是非常安全的。即使整个过程出错,我们的 * 原始 * 提交仍然在Git中。(然而,它可以成为一个真正的苦差事,以 * 找到**原件 *。当你有47个人看起来很像,他们都声称是布鲁斯,哪个布鲁斯是 * 原始 * 布鲁斯?所以,如果你做这类事情,一定要仔细跟踪。)
1在此过程中完全放弃的任何提交往往会停留至少30天,但随后会自动清除。
简单看一下分支
一个 * 分支名称 * 主要只是保存了 * 某个链 * 中最后一个提交的哈希ID。也就是说,当我们有:
branch1
为我们做的是记住哈希IDH
,这样我们就不必记住它,也不必把它写在白板上,或者做其他事情。如果我们现在创建第二个分支名称branch2
,这个名称 * 也 * 指向提交H
:我们将特殊的名称
HEAD
附加到一个(且只有一个)分支名称上,以表示我们使用的是哪个名称,也就是哪个提交:现在我们做一些新的提交,第一个新的提交,我们称之为
I
,会指向当前最后一个提交H
,Git会把I
的哈希ID写入到HEAD
所附加的名字中:如果我们在
branch1
上进行第二次提交,然后在git checkout branch2
或git switch branch2
上附加HEAD
到branch2
,并使H
成为当前提交,我们得到:在当前的
branch2
上再提交两次,我们得到:合并
现在我们可以使用
git merge
。如果我们先使用git checkout branch1
,J
将是 * 当前提交 *,并且我们将使用git merge branch2
来合并工作和提交L
。如果我们只使用git merge branch1
,L
将是当前提交,并且我们将合并工作和提交J
。合并效果在这里基本上是对称的。但是最后的合并提交将扩展我们实际所在的分支,所以我们先来看看git checkout branch1
:Git现在会找到最佳的 shared commit--两个分支上的最佳提交--作为合并操作的 merge base。在这种情况下,最佳的共享提交是显而易见的:提交
G
和之前的所有提交都在两个分支上,但是H
更好,因为它更接近 end。为了合并工作,Git现在使用
git diff
来查找 * 修改 *。提交H
有一个快照,提交J
有一个快照,无论这两个提交之间有什么不同,好吧,这就是我们在branch1
上所做的:重复diff,但是使用提交
L
,另一个提交,这次,显示了 * 他们 *(好吧,我们)通过提交K
和L
改变了什么:合并过程--我喜欢称之为动词“合并”--现在“合并”了这两组更改。合并后的更改不仅执行我们所做的更改,还执行他们所做的更改。如果我们接触了某个文件,而他们没有,我们就得到我们的内容。如果他们接触了某个文件,而我们没有,我们就得到他们的内容。如果我们都接触了某个文件,Git也会尝试合并这些更改。
Git会将这些合并后的修改“应用”到“合并基础”提交中的任何内容,也就是说,假设文件
F
有100行,我们修改了第42行的内容,并在第50行添加了一行,这样文件F
现在有101行,假设他们修改了第99行的内容,Git可以:一切正常,Git会认为这是合并的正确结果。2
这个合并更改并将合并后的更改应用到合并库的过程,再次被我称为动词**merge,这将产生一组 merged files,如果没有冲突,这些合并文件就可以提交了。
合并工作实际上发生在Git的 index aka staging area 中,但我们在这里不会详细介绍。如果存在 merge conflict,Git会将所有三个输入文件保留在其索引中,并将其最大努力写入文件的工作树副本中。该工作树副本具有合并冲突标记。这会导致合并为动词过程失败。
对于
git merge
,如果merge-as-a-verb步骤成功,Git会继续进行一次 merge commit。合并提交几乎和常规提交完全一样:它有一个快照,就像任何提交一样,它有一个父提交,就像几乎所有的提交一样。但它还有一个 second 父提交。这就是为什么它是一个 *merge提交 *。这里使用了单词“merge”作为形容词,Git经常把这些提交称为 a merge。所以这就是我所有的 *merge作为名词 *。假设一切顺利,我们会得到:
合并
M
的 * 第一个父节点 * 应该是提交J
,因为branch1
,我们的HEAD
,刚才就在那里.合并M
的 * 第二个父节点 * 应该是提交L
.如果合并为动词的过程 * 失败 *,
git merge
会在中间停止,并留下一些混乱让您清理。对于那些从脚本或程序运行git merge
的程序,它也会以非零状态退出。2这是否真的 * 是 * 正确的是一个单独的问题,而不是Git真正关心的问题。Git只是遵循这些简单的文本替换规则。
使用
git cherry-pick
复制提交现在我们知道了分支,分支名和
git merge
是如何工作的,我们可以看看git cherry-pick
,它的功能是复制一个提交,通过找出提交 * 做什么 *,然后“再做一次”。也就是说,假设我们有这样一种情况:
我们现在正在处理
feature2
,突然我们注意到:* 嘿,如果我们在这里提交N
之后再提交J
,我们就可以完成了。* 理想情况下,我们会让某人把提交J
应用到提交H
上--可能是在一个新的分支上--和/或把提交J
合并到某个东西中,这样我们就可以更直接地使用它。我们只想把从I
到J
的 * 改变 * 成feature2
。我们可以运行:
看看有什么变化,然后自己对提交
N
中的内容做同样的修改,然后再做一个新的提交O
,但是为什么我们要费力地做这个复制呢,当我们有一台电脑可以做的时候?我们运行:而 Git 则会执行复制。如果一切顺利,它甚至会为我们复制
J
的提交消息,并创建一个新的提交。这个新的提交很像J
--比较N
与这个新的提交将显示与比较I
与J
相同的更改--所以不要调用新的提交O
,我们将其命名为J'
:你说的很好,但我们需要知道:**
git cherry-pick
实际 * 工作 * 的方式是运行Git合并机制。**它将提交I
(J
的父级)设置为合并基础,然后运行这两个git diff
命令:Git现在将这两组修改结合起来,保留我们的修改以跟上提交
N
,但添加它们的修改以获得提交J
的效果。提交I
甚至不在我们的分支上这一事实是无关紧要的。Git使用merge machinery 来进行这个复制-通常一切都运行得很好。运行了合并为动词的过程后,Git继续执行一个普通的单亲提交,这就是我们的
J'
,提交的 * 作者,作者日期和日志信息 * 都是从提交J
中复制过来的;我们成为提交者,并且新提交的提交日期是“现在”。但是:合并为动词的过程可能会失败,可能会有合并冲突,这就是你在
--autosquash
rebase中看到的。不使用修正或其他技巧的重定基准
我们已经准备好把碎片拼起来了。我们只需要知道一件事:
git rebase
通过复制提交来工作,就像使用git cherry-pick
一样。对于某些版本的git rebase
,Git实际上运行git cherry-pick
。到目前为止,最新版本的Git在rebase代码中内置了摘樱桃功能,因此它不必单独运行它。但效果是一样的。我们可以把它当作是摘樱桃。即使是修补和南瓜案件也是这样:它们只是改变了最后的创建新提交步骤。为了完成一个变基,Git首先列出所有要被复制的提交的提交哈希ID。这个列出过程比看起来要复杂得多,但我们可以忽略所有的复杂性,因为它们都不适用。在你的例子中,你有四个提交要担心,其中三个会被复制。我们把它画出来,我们把第一个命名为
A
,这是根提交一个稍微特殊的例子,一个没有父提交的提交。所以,这里是你所得到的:无论是否有
autosquash
正在运行,要执行git rebase -i
,Git首先列出要复制的每个提交。使用HEAD^^^
,你告诉Git not 要复制的提交从A
开始并向后运行。它 should 要复制的提交是那些从HEAD
(即master
)开始并向后运行的提交:D
、C
、B
和A
。在该列表中,我们去掉A
,然后返回,留下D
、C
和B
。B-C-D
的顺序复制这三个提交,这样就可以 * 工作 *。Git会将B
复制到一个新的改进的提交B'
,然后使用B'
作为C
的父提交来复制C
,再使用C'
来复制D
,以生成:每个复制步骤都像使用
git cherry-pick
一样,使用Git的 detached HEAD 模式。Git首先使用--detach
checkout 提交A
:现在运行
git cherry-pick
,哈希值为提交B
。3使用合并引擎将B
复制到B'
,将“合并基”设置为提交A
。Git比较提交A
,合并基,因为HEAD
要求使用A
。这表示不改变任何东西。然后Git比较提交A
(再次合并基址)来提交B
。这里说的是要做出导致提交B
的快照的更改。Git做出导致提交B
的快照的更改,并将这些提交为常规(非合并)提交B'
,重用B
的大部分元数据:现在Git选择提交
C
。提交B
是C
的父提交,所以它是强制合并的基提交。它与我们的HEAD
提交B'
完全匹配,所以 * 我们 * 没有要合并的更改;我们拾取 * 他们的 * 更改并提交,从而得到C
的精确副本C'
:我们用
D
重复这个过程,得到D'
,然后rebase执行最后一步,也就是把 namemaster
从提交D
中拉出来,粘贴到刚刚提交的最后一个文件中,然后重新附加HEAD
:这和我们之前画的是一样的,只是画得有点不同。
3 rebase命令在这里实际上很聪明:它意识到在提交
A
时复制B
会产生一个新的提交,除了日期和时间戳之外,它实际上是B
的“精确”副本。因此,它没有复制它,而是在适当的位置重用它。在极少数情况下,当您需要新的散列ID时-您可以强制git rebase
进行复制。为了便于说明,我们将假设git rebase
比较笨,或者您已经战胜了这种聪明,但是如果您深入研究rebase,知道它确实是这样的。压扁或修复
如果我们愿意,我们可以在复制过程中告诉
git rebase -i
将一个提交压缩到前一个提交中,我们只需要在git rebase -i
给我们编辑的指令表中用squash
替换pick
,假设我们对提交C
做了这样的操作:例如,在将B
复制到B'
之后,我们得到:Git将以和之前基本相同的方式处理
git cherry-pick
,导致下一个提交的是C'
,就像我们之前展示的那样。但是这个提交步骤并不是像平常一样提交,而是采取了两个特殊的动作:1.它把来自
B
(或B'
--它们是相同的)的提交信息写入一个临时文件,并添加来自C
的提交信息,还添加了一段文字说明这是两次提交的挤压,这就是Git在实际 * 写出 * 新提交之前启动编辑器时,在编辑器中看到的内容。A
作为其父提交,从而使C'
以B'
作为其父提交。此时的结果为:
其中
BC
有一个与提交C
匹配的快照,但提交消息是您在编辑文件时提供的。然后,Rebase可以像往常一样选择
D
,并像往常一样移动分支名称。如果你看不到被放弃的提交(包括被放弃的B'
),那么它可能不存在,4你只需要:而且我们也不需要引入其他被放弃的提交。
请注意,如果您在命令表中使用
fixup
而不是squash
,Git仍然会执行这个压缩 * 过程 *,只是不需要您 * 编辑新的提交消息 *。它不会将各个要压缩的提交/副本中的提交消息收集在一起,而是将修复消息完全删除。保留上一次提交的消息。(您可以合并修复和挤压:如果您有$S个挤压和$F个修复,则您编辑的组合邮件将包含所有$S个邮件,而不包含任何$F个邮件。)4由于rebase的聪明,它可能实际上并不存在。即使rebase只是直接重用commit
B
,这个过程也能工作。但是为什么我们会有冲突呢?
您添加了
--autosquash
。这使得git rebase
自动 * 移动 * 复制命令(然后也将一些替换为squash
或fixup
)。提交B
仍在原处,但提交D
,这是它的修正,移动到B
之后。CommitC
保留在末尾。Git正在执行以下操作:B
;那么D
作为带有修正的压缩,即,当我们使BD
作为新提交时丢弃D
的消息;那么C
。让我们看看复制
D
时得到了什么。就像我们之前做的一样。现在我们在提交
D
时运行git cherry-pick
。**这使用提交C
作为合并基础。*随着 * 我们的 * 更改,我们得到C
到B'
的差异。从
C
到B'
的差异表明要从文件的合并基础副本中 * 删除 * 行line four
;这一行应该是第三行。同时,C
到D
的diff表示要 * 替换 * 文件合并基础副本中的line four
行,以便读取line three
。在 * 两种 * 情况下,这一行都在line two
行之后。在提交
B'
的实际文件中, 在第2行之后没有一行是line two
*。Git不知道如何将其从阅读line four
改为读line three
,也不知道如何删除它,因为它根本不存在。Git对这个文件做了最好的处理。然后它失败了合并为动词的过程,停止重定基过程,并告诉您修复混乱。如果你将
merge.conflictStyle
设置为diff3
,5那么你的工作树副本不仅会包含Git由于某种原因无法合并的两个冲突的 changes,还会包含行的 merge base 版本。在这种情况下,这只会有一点点帮助,但可能已经足够了。一旦你修复了冲突--不管你选择用什么方式修复它--Git就会把你的结果当作“正确答案”,并使用你告诉Git的正确的读取方式来进行新的
BD
组合提交。Git现在应该选择提交
C
。这将运行一个合并,合并基础设置为提交B
。我们的提交是BD
,所以“我们改变的”是文件的B
副本与您所做的任何操作的差异。他们的提交是C
,因此,“他们改变了什么”是从B
到C
的差异,这意味着在第3行添加行“第4行”,在表示“第2行”的行(在第2行)和文件末尾之间。除非你让文件在两行之后结束,并且第二行阅读“line two”,否则Git很可能在合并“他们的”修改和你的修改时遇到问题,所以你会看到合并冲突。如果你 do 让文件在这里结束,Git会认为合并不需要任何东西,这会让
git rebase
有点困惑:它会告诉您似乎没有理由再选择提交C
,并迫使您选择是否使用git rebase --skip
跳过它。5使用
git config
。要为所有尚未设置它的存储库设置它,请使用git config --global
。我使用git config --global merge.conflictStyle diff3
全局设置它。6rqinv9w2#
正如@torek所指出的,这里您遇到了一个边缘情况,但是,如果您遇到了更一般的情况,即在第二次提交的第二行下面碰巧有一个锚行,那么一切都没有问题。
此时,
myfile.txt
看起来像然后,如果运行带有autosquash选项的rebase命令
将不存在合并冲突,并且你得到期望的提交历史。