问题描述
在us
期间,我无法理解them
和git revert
是谁在这些冲突中,所以我真的不知道这里发生了什么:
git revert some_commit_hash
然后git status
显示以下冲突:
deleted by them: path/to/file1.h
both modified: path/to/file2.h
deleted by them: path/to/file1.cpp
deleted by them: path/to/test_file1.cpp
added by us: path/to/file3.h
deleted by them: path/to/file4.h
added by us: path/to/file5.h
谁是“我们”?谁是“他们”?
更新:请注意,我要还原的提交是一个非常大的合并提交。
不重复:
- 因为它无法阐明谁是
us
和them
:GIT: How dangerous is "deleted by us" conflict? - 因为它涵盖了
merge
和rebase
但没有涵盖revert
,而git经常使用相同的术语来表示相反的内容,具体取决于操作:{{ 3}} - 因为它没有提及“我们”和“他们”-Who is "us" and who is "them" according to Git?
解决方法
发生冲突时,适用于所有情况的规则是:
-
ours
/us
是当前HEAD
(活动提交)的状态 -
theirs
/them
是另一端的状态(正在合并的提交,被挑选/重新设置的提交,或者在您的情况下是您想要的提交的“反向”还原)
对于rebase
(回答@GabrielStaples的评论)有一些额外的说明:
如果您在my/branch
上,并且运行git rebase other/branch
,则git
将检出other/branch
的头提交,并开始在顶部重播一些提交。
如果发生冲突,由于检出的提交来自other/branch
,因此ours
将大致代表other/branch
,而theirs
将是my/branch
。 / p>
这部分与直觉“ ours
应该是我的更改”相反,但它符合上面的描述:在发生冲突时,签出的提交为ours
,另一端(正在重播的提交)为theirs
。
TLDR;
跳到最底端以获取结果和结论。
详细信息:
关于:
然后
git status
显示以下冲突:deleted by them: path/to/file1.h both modified: path/to/file2.h deleted by them: path/to/file1.cpp deleted by them: path/to/test_file1.cpp added by us: path/to/file3.h deleted by them: path/to/file4.h added by us: path/to/file5.h
我做了一些实验,并观察了以下内容。
首先,我正常地手动解决了两个已修改的文件path/to/file2.h
中的冲突,这对于任何变基或合并冲突都是正常的。然后,我添加了所有文件并完成了还原:
git add -A
git revert --continue
接下来,我观察到所有标有删除的文件,以及所有标有由我们添加的文件,在我的文件系统上存在/存在。因此,还原没有删除任何文件。接下来,我想知道:哪些提交创建了这些文件?要查看此内容,请运行以下(source):
git log --diff-filter=A -- path/to/file
这仅显示创建该文件的单个git log commit_hash
的{{1}}。我一次一次 执行每个被他们删除或由我们添加的文件:
commit_hash
我发现,如上所述,其中4个文件是由我还原的提交添加的。 注意,这意味着它们是由提交git log --diff-filter=A -- path/to/file1.h # added by the commit I reverted
git log --diff-filter=A -- path/to/file1.cpp # added by the commit I reverted
git log --diff-filter=A -- path/to/test_file1.cpp # added by the commit I reverted
git log --diff-filter=A -- path/to/file3.h # added by a later commit
git log --diff-filter=A -- path/to/file4.h # added by the commit I reverted
git log --diff-filter=A -- path/to/file5.h # added by a later commit
本身添加的,而不是由我运行some_commit_hash
时创建的还原提交添加的。那么,如果我仍然将它们存在,为什么恢复该提交?好吧,事实证明,后来发生的提交(我们称为git revert some_commit_hash
,发生在later_commit_hash
之后,触及了所有6个文件,修改了其中4个并创建了2个。
将上述文件按删除的文件与我们添加的 的分组:
some_commit_hash
现在指示哪个文件是通过哪个提交添加的:
# deleted by them:
path/to/file1.h
path/to/file1.cpp
path/to/test_file1.cpp
path/to/file4.h
# added by us:
path/to/file3.h
path/to/file5.h
因此,您可以看到由他们删除的文件是我还原的提交中添加的,这意味着还原该提交将删除这些文件!因此,# deleted by them / added by the commit I reverted (`some_commit_hash`)
path/to/file1.h
path/to/file1.cpp
path/to/test_file1.cpp
path/to/file4.h
# added by us / added by a later commit (`later_commit_hash`)
path/to/file3.h
path/to/file5.h
指的是要还原的提交them
,而some_commit_hash
指的是us
处的其余提交。
冲突是HEAD
触摸了这4个“被他们删除”的文件,因此不允许later_commit_hash
删除它们。并且,这2个“由我们添加”文件在git revert some_commit_hash
之前不存在,因此冲突在于它们在还原后不应该存在,但它们确实存在,因为它们是由some_commit_hash
创建的
我解决的方法是手动删除所有这6个文件:
later_commit_hash
然后我将此更改作为新的提交提交>
rm path/to/file1.h
rm path/to/file1.cpp
rm path/to/test_file1.cpp
rm path/to/file3.h
rm path/to/file4.h
rm path/to/file5.h
但是,我可以将其重置回还原提交之前的位置,然后先还原git add -A
git commit
,然后再还原later_commit_hash
,然后有效地将这些更改回滚像这样:
some_commit_hash
结果和结论:
无论哪种情况,都要回答我自己的问题:
在git reset --hard HEAD~ # WARNING! DESTRUCTIVE COMMAND! BE CAREFUL.
git revert later_commit_hash
git revert some_commit_hash
# should result in no conflicts during both of those reverts now
期间:
- “我们” =键入并运行
git revert some_commit_hash
时当前已签出的提交(即:HEAD
),并且: - “他们” =您要还原的提交(相反或相反?);即:假设您运行命令
git revert some_commit_hash
,这是与some_commit_hash
相反的临时提交,以撤消some_commit_hash
的更改。
我仍在为这个概念而苦苦挣扎,但这就是要点。我认为,还原工作原理的确切机制将在这里澄清一些事情。 This answer可能会提供更多的见解,但我不理解。
我也刚刚在此处添加了一个答案,以澄清我想到的所有4个git操作的“我们”和“他们” :git revert some_commit_hash
,{ {1}},git merge
和git cherry-pick
:Who is "us" and who is "them" according to Git?
(自我注释):
需要看看:http://ezconflict.com/en/conflictsse12.html#x53-890001.7
,嗯... revert
是一种非常特殊的情况。那么,考虑一下普通的合并,与共同的祖先和所有东西,整个包,对吧?现在,整个工作就像合并 except 一样(而且很大),合并引擎强制 the common ancestor
是您要尝试的修订进行还原,并且the other branch
是该修订版的父级。
尽管已经很好地回答了这个问题,但是还有另一种方法可以查看全部内容。 Git本身就是这样看待它的。樱桃提取,合并,变基和还原这四个操作都使用相同的机器,--ours
和--theirs
标志分别为git checkout
,-X ours
和{ {1}}扩展选项,使用相同的内部代码引用相同的内容。我喜欢将这种机制称为作为动词进行合并,因为当合并必须进行真正的合并时,我们首先通过-X theirs
对其进行了介绍。
合并案例
进行真正的合并时,这些术语才有意义。我们从可以用这种方式说明的内容开始:
git merge
在这里,名称 I--J <-- ourbranch (HEAD)
/
...--G--H
\
K--L <-- theirbranch
选择提交ourbranch
,这是我们在分支上的提交(在这种情况下,这是两个这样的提交之一,尽管专门在我们自己的分支上的提交数量需要至少为 1才能强制进行真正的合并)。名称J
选择提交theirbranch
,这就是他们在其分支上的提交(还是两个之一,这里至少需要一个提交)。
Git为了进行合并(将其作为动词进行合并一些文件集)所做的工作是,对于所有三个提交文件L
,H
中的每个文件,和J
,将L
中的文件与H
中的文件进行比较,以查看我们发生了什么变化,并将J
中的文件与在H
中查看他们发生了什么变化。然后,Git将这两组更改组合在一起,并将组合的更改应用于L
中的所有内容。
提交H
是合并基础提交,提交H
是“我们的”提交,提交J
是“他们的”提交。任何差异,无论是“由我们添加”的新文件,还是“由他们删除”的文件,或其他有关提交L
的东西。
为了通过合并机制运行合并,Git进行了以下内容的略微优化:
- 设置:
- 将合并基本提交(
H
)读入插槽1的索引中 - 将
H
提交(ours
=HEAD
)读入插槽2的索引中 - 将
J
提交(theirs
)读入插槽3的索引中
- 识别“相同文件”。请注意,每个文件都重复执行步骤2和3。
- 如果所有三个插槽中都有一个名为 F 的文件,则该文件是同一文件
- 否则,如果插槽1中有任何内容,请尝试猜测重命名,该重命名会将插槽1中的合并基础文件与我们或他们的具有不同名称 的文件绑定,该文件位于插槽2中, /插槽3;如果找不到可用来重命名的文件,则我们和/或他们一方删除了该文件;这些情况也可能导致高层冲突,例如重命名/修改或重命名/删除,我们声明冲突并继续执行而不执行步骤3
- 否则(插槽1中什么都没有,但是插槽2和3中什么都没有)我们有一个添加/添加冲突:声明此特定文件有冲突,然后继续执行而不执行步骤3
- 短路简单情况,并通过低级别合并来进行困难情况:
- 如果插槽1、2和3中的Blob哈希ID都匹配,则所有三个副本都相同;使用其中任何一个
- 如果插槽1中的blob哈希ID与2或3中的blob哈希ID相匹配,则有人没有更改文件,有人更改了文件;使用更改后的文件,即采用不同 的文件
- 否则,所有三个插槽都不同:通过更改块,低级合并来执行更改的行块
- 如果在低级别合并期间存在合并冲突,则
L
或-X ours
的意思是“使用我们的解决冲突”,其中我们的冲突在插槽2中,而他们的冲突在插槽2中3 - 请注意,这意味着只要没有冲突,例如,只有一个“边”更改了第42行,
-X theirs
扩展选项根本就不适用,因此我们进行了修改,无论是我们的还是他们的
- 如果在低级别合并期间存在合并冲突,则
在此过程结束时,所有完全解析的文件都将移回到其正常的零插槽位置,同时删除插槽1、2和3条目。任何未解析的文件都将保留所有三个索引槽(在删除冲突和添加/添加冲突中,一些槽为空,但正在使用 some 非零阶段编号槽,这会将文件标记为已冲突) )。
因此,合并或合并为动词会在Git索引中起作用
以上所有操作均发生在Git的索引中,其副作用是将更新的文件保留在工作树中。如果存在低级冲突,则会在工作树文件上标记冲突标记,并在与索引插槽1(合并基准),2(我们的)或3(他们)。
最终,它总是归结为相同的等式:1 =合并基准,2 =我们的基准,3 =他们的基准。即使加载索引的命令不是-X
,也是如此。
樱桃选择和还原使用合并机制
运行git merge
时,我们将看到一个提交图,如下所示:
git cherry-pick
此处的字母...--P--C--...
\
...--H <-- somebranch (HEAD)
和P
代表任何一对父子提交。只要我们使用C
选项指定要使用的父对象,C
甚至可以是合并提交。 (对这三个提交在图中的位置没有真正的限制:我用-m
绘制了它,它是H
之前的某个提交的子对象,但也可以在{{1}之后}对,例如P
,或者,如果您有多个不相交的子图,则P-C
和...-E-P-C-F-G-H
提交之间可能根本没有关系。)
当我们跑步时:
P-C
Git将使用从H
回到git cherry-pick <hash-of-C>
的父链接自行定位提交P
。 C
现在充当合并基础,并且被读入索引槽1。P
充当P
提交,并被读取到索引槽3。我们当前的提交{{1} }是C
提交,并被读入索引槽2。合并机制现在运行,因此“我们的”提交是--theirs
,“他们的”提交是提交H
,其中合并基础-如果将--ours
设置为HEAD
,或者如果我们使用C
运行合并工具,则会显示为提交merge.conflictStyle
。
当我们跑步时:
diff3
发生相同的事情,除了这次,commit git mergetool
是插槽1中的合并基础,commit P
是插槽3中的git revert <hash-of-C>
提交。{{1} }像往常一样来自C
的插槽2中的提交。
请注意,如果您使用cherry-pick或还原一系列提交:
P
优先选择使用拓扑较旧的提交一次进行一次提交,而选择恢复的操作首先使用较新的拓扑提交一次进行操作。也就是说,给定:
--theirs
--ours
首先复制HEAD
,然后复制git cherry-pick stop..start
,但是...--C--D--E--...
\
H <-- HEAD
首先复制git cherry-pick C..E
,然后复制D
。 (提交E
不会起作用,因为两点语法会排除两点表达式左侧可到达的提交。有关更多信息,请参见the gitrevisions documentation。)
Rebase反复进行摘樱桃
rebase命令的工作方式是反复运行git revert C..E
,然后在之后使用E
或D
进入分离HEAD 模式。 (从技术上讲,它现在仅在内部执行;在过去,某些基于外壳脚本的C
确实使用了git cherry-pick
,尽管其哈希ID始终始终处于分离模式。)
当我们运行git checkout --detach
时,我们从以下内容开始:
git switch --detach
我们运行:
git rebase
首先要做的是 second ,这是进入分离的HEAD模式,HEAD提交是我们用git checkout
参数选择的提交。如果我们没有使用单独的git rebase
标志和参数,则 C--D--E <-- ourbranch (HEAD)
/
...--B--F--G--H <-- theirbranch
来自我们给出的一个参数,在这种情况下为git checkout ourbranch # if needed - the above says we already did that
git rebase theirbranch # or,git rebase --onto <target> <upstream>
。如果我们不使用单独的 --onto
参数,那么我们给出的一个参数(在本例中为--onto
)将同时用于两种目的。
Git也(首先,这就是上面第二个原因)列出了要复制的每个提交的原始哈希ID。这个列表比乍一看看起来要复杂得多,但是如果我们忽略了额外的复杂性,基本上是以下结果:
--onto
,在这种情况下,它们是提交theirbranch
,upstream
和theirbranch
的哈希ID:git rev-list --topo-order --reverse <hash-of-upstream>..HEAD
可以访问的三个提交,而C
不能访问的三个提交D
。
E
生成了此列表并进入了HEAD分离模式,我们现在看起来像这样:
ourbranch
现在,Git运行一个theirbranch
。它的参数是提交git rebase
的哈希ID,这是要复制的第一个提交。如果我们在上面查看cherry-pick的工作方式,我们将看到这是一个谓词合并操作,合并基础是 C--D--E <-- ourbranch
/
...--B--F--G--H <-- theirbranch,HEAD
的父级,即,提交git cherry-pick
,当前或C
提交是提交C
,而待复制或B
提交是提交--ours
。因此,这就是我们和他们的似乎相反的原因。
一旦完成摘樱桃操作,我们现在就有:
H
Git现在继续使用--theirs
复制提交C
。现在,合并基础为提交 C--D--E <-- ourbranch
/
...--B--F--G--H <-- theirbranch
\
C' <-- HEAD
,D
提交为提交git cherry-pick
,而C
提交为--ours
。这意味着我们的和他们的提交都是我们的,但是这次“我们的”提交是我们几秒钟(或毫秒)之前构建的!
它基于他们现有的提交C'
,但它是我们自己的提交--theirs
。如果我们遇到任何合并冲突,那么毫无疑问,它们是基于D
的结果,也许包括我们为制作H
手动执行的某种冲突解决方案。但是,从字面上看,所有三个输入提交都是我们的。索引插槽1来自提交C'
,索引插槽2来自提交H
,索引插槽3来自提交C'
。
完成所有操作后,现在的图片如下:
C
Git现在在提交C'
的哈希值上运行D
。合并基础为提交 C--D--E <-- ourbranch
/
...--B--F--G--H <-- theirbranch
\
C'-D' <-- HEAD
,而我们的提交和他们的提交分别为git cherry-pick
和E
。因此,再次说明,在重新设置期间,所有三个提交都是我们的—尽管合并冲突可能是基于D
构建的结果。
完成最后一个选择后,Git通过将 name D'
从旧提交E
中移出并将其粘贴到新提交{{1 }}:
H
我们现在恢复到正常的附加工作方式,因为ourbranch
从我们现在的位置开始(在提交E
处),并且向后工作,因此从不访问原始提交{{1 }},似乎我们已经以某种方式修改了原始的三个提交。我们还没有:它们仍然在我们的存储库中,可以通过特殊的伪引用E'
获得,也可以通过我们的引用日志获得。默认情况下,我们至少可以将它们退回30天,然后 C--D--E [abandoned]
/
...--B--F--G--H <-- theirbranch
\
C'-D'-E' <-- ourbranch (HEAD)
可以随时收割它们,然后然后真正消失了。 (好吧,只要我们不将它们git log
保留在仍保留它们的某些 other Git存储库中即可。)