在 `git revert` 中,`them` 和 `us` 指的是谁?

6

在进行git revert期间,我无法理解这些冲突中的usthem是谁,所以我真的不知道这里发生了什么:

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

"我们"是谁?"他们"是谁?

更新:请注意,我要撤销的提交是一个非常大的合并提交。


非重复项:

  1. 因为它没有澄清谁是“我们”和“他们”:GIT:“被我们删除”的冲突有多危险?
  2. 因为它涵盖了mergerebase,但没有涉及revert,而Git经常使用相同的术语来表示相反的事情,具体取决于操作:根据Git,谁是“我们”,谁是“他们”?
  3. 因为它没有提到“我们”和“他们”- Git - 撤消撤消,冲突

我并不是立即知道这个,但应该很容易弄清楚:1)使用 git show some_commit_hash 命令查看添加/删除的文件。2)将它们与当前的 git status 相对应(添加和删除相反,因为你正在还原)。3)获得收益。 - 0x5453
1
我仍然认为这是Git中“我们”和“他们”是谁?的重复问题 - 如果与其他问题不同,您应该编辑该问题以包括有关revert的部分。 - pkamb
2
如果您在这里得到了一个好的答案,您可能考虑编辑规范的问题/答案并将此问题作为重复关闭。[Necromancer x 52 :)] - pkamb
此外,我不确定将别人的问题塑造成扩展自己问题的最佳做法。我想双方都有争议,但我通常反对让别人的问题超出提问者最初的问题范围。他或她标记了一个不涵盖我的问题的答案,因此显然提问者当时的问题与我现在的问题不同。 - Gabriel Staples
另一个问题问:“在Git rebase之后,以及其他情况下…… - pkamb
显示剩余3条评论
4个回答

5

当发生冲突时,适用于所有情况的规则是:

  • ours/us 表示当前 HEAD 的状态(即活动提交)
  • theirs/them 表示另一方的状态(正在合并的提交,正在复制/变基的提交,或在您的情况下是要还原的提交的“反向”)

在进行rebase时,有些额外的澄清(回答@GabrielStaples'的评论):

如果您在my/branch上,并运行git rebase other/branchgit将检出other/branch的头提交,并开始重播一些提交。

如果发生冲突,由于已检出的提交来自other/branch,因此ours大致表示other/branch,而theirs将是my/branch

这一部分与“ours应该是我的更改”的直觉相反,但它符合上述描述:在冲突发生时,已检出的提交是ours,另一方(正在重播的提交)是theirs


我认为这不正确:“当您进行变基时,us指的是上游分支,them是您正在移动的分支。在变基的情况下,这有点违反直觉。” https://dev59.com/D2Ei5IYBdhLWcg3wseKA#21025695。因此,“git rebase HEAD〜3”意味着“us”是“HEAD〜3”,而“them”是“HEAD”,这与您的答案陈述的完全相反。这是因为从git的角度来看(我想?),它实际上只是挑选下游提交,因此它已经检出了HEAD〜3,所以现在它是其活动的HEAD,然后继续逐个挑选下游提交。 - Gabriel Staples
你在评论的第二部分给出了解释,我编辑了我的答案以突出显示在rebase情况下ours/theirs规则的应用。 - LeGEC
关于rebase,它是正确的。当您进行rebase时,“基础分支”是在“HEAD”上的内容(因此是我们)...而您正在rebase的内容将是“另一个分支”。http://ezconflict.com/en/conflictsse12.html#x53-890001.7(**免责声明**:这是我的材料,没有cookies,没有跟踪,没有货币化) - eftshift0
我已经给你的答案点了赞,引用了它并在我的回答中加以阐述,涵盖了所有关于Git中“我们”和“他们”的情况。现在可以在这里查看:https://dev59.com/D2Ei5IYBdhLWcg3wseKA#63911630 - Gabriel Staples
@eftshift0,你的链接似乎有一个标签(#x53-890001.7),但它没有正确地跳转到页面上的任何位置。你想让我关注页面的哪个部分?(建议:我强烈推荐你使用Github pages来创建个人代码网站--你可以使用markdown编写--我也在设置我的)。 - Gabriel Staples

3
尽管已经有很好的回答了,但还有一种方法可以看待这个问题。这就是Git本身的看法。所有四个操作——cherry-pick、merge、rebase和revert——都使用相同的机制,并且--ours--theirs标志、git checkout命令中的-X ours-X theirs扩展选项最终引用相同的内容,使用相同的内部代码。我喜欢将这个机制称为“合并作为动词”,因为我们首先是通过git merge来介绍它的,当合并必须进行真正的合并时。

合并情况

在进行真正的合并时,这些术语是有意义的。我们从以下方式开始说明:

          I--J   <-- ourbranch (HEAD)
         /
...--G--H
         \
          K--L   <-- theirbranch

在这里,名称ourbranch选择提交J,这是我们在我们的分支上的提交(在这种情况下有两个这样的提交,尽管独占我们自己的分支的提交数量仅需至少为1,即可强制进行真正的合并)。名称theirbranch选择提交L,这是他们在他们的分支上的提交(同样是两个之一,在这里至少需要一个提交)。
Git执行合并来“合并”一些文件。对于所有三个提交HJL中的每个文件,Git会将HJ中的文件进行比较以查看我们改变了什么,将HL中的文件进行比较以查看他们改变了什么。然后,Git组合这两组更改,将组合的更改应用于H中的任何内容。
提交H是合并基础提交,提交J是“我们”的提交,提交L是“他们”的提交。任何“差异”,无论是我们“添加的新文件”,还是他们“删除的文件”,或者其他任何差异,都是相对于提交H而言的。
为了通过合并机制运行合并,Git执行了以下略微优化的预处理:
  1. 初始化:

    • 将基础合并提交(H)读入到索引的第一个插槽中
    • 将我们的提交(HEAD = J)读入到索引的第二个插槽中
    • 将他们的提交(L)读入到索引的第三个插槽中
  2. 识别"相同文件"。注意步骤2和3对每个文件都要重复执行。

    • 如果所有三个插槽中都有名为F的文件,则它是相同的文件
    • 否则,如果插槽1中有任何东西,则尝试猜测重命名,这会将位于插槽1中的合并基文件与位于插槽2和/或3中的不同名称的我们或他们的文件联系起来;如果找不到重命名的文件,则我们的和/或他们的一方删除了此文件;这些情况可能导致高级冲突,例如重命名/修改或重命名/删除,在这种情况下,我们宣布存在冲突并继续执行步骤3
    • 否则(插槽1中没有任何内容,但插槽2和3中有内容),我们有一个添加/添加冲突:宣布此特定文件存在冲突,并继续执行步骤3
  3. 短路易于处理的情况,并使用低级别合并处理困难情况:

    • 如果插槽1、2和3中的blob哈希值都匹配,则所有三个副本相同;使用任何一个
    • 如果插槽1中的blob哈希值与插槽2或3中的哈希值匹配,则有人没有更改该文件,而有人更改了该文件;使用更改的文件,即使用不同的文件
    • 否则,所有三个插槽都不同:按块更改进行按块低级别合并
      • 如果在低级别合并期间出现合并冲突,则-X ours-X theirs表示"使用ours/theirs解决冲突",其中ours是插槽2中的内容,theirs是插槽3中的内容
      • 请注意,这意味着无论是否存在冲突,例如只有一侧更改了第42行,扩展选项-X都根本不适用,我们将采用修改,无论其是否属于我们或他们

在此过程结束时,任何完全解决的文件都将移回其正常的零插槽位置,插槽1、2和3的条目将被删除。任何未解决的文件都保留了所有三个索引插槽(在删除冲突和添加/添加冲突中,某些插槽为空,但使用某些非零阶段号插槽,这标记该文件存在冲突)。

因此,合并或作为动词的合并操作在Git的索引中进行

所有上述操作都发生在Git的索引中,带来的副作用是将更新的文件留在您的工作目录中。如果存在低级冲突,则会使用冲突标记标记您的工作树文件以及对应于位于索引槽1(合并基础)、2(我们的)或3(他们的)文件副本所在行的各个部分。

最终,这总是归结为相同的公式:1 = 合并基础,2 = 我们的,3 = 他们的。即使加载索引的命令不是git merge,这也适用。

Cherry-pick和revert使用合并机制

当我们运行git cherry-pick时,我们拥有如下提交记录图:

...--P--C--...
   \
    ...--H   <-- somebranch (HEAD)
PC代表任何父子提交对。即使是合并提交,只要使用 -m 选项来指定使用哪个父节点也可以。(在图形中三个提交的位置没有实际的限制:我将其画为 H 是在某个在 P 之前的提交的子节点,但它可以在 P-C 对之后,例如...-E-P-C-F-G-H,或者可能根本没有 P-CH 提交之间的关系,如果您有多个不相交的子图。)
当我们运行以下命令时:
git cherry-pick <hash-of-C>

Git会自行查找提交记录P,使用从CP的父链接。现在,P充当合并基础,并被读入索引槽1。C充当--theirs提交,并被读入索引槽3。我们当前的提交H--ours提交,并被读入索引槽2。现在运行合并机制,所以“我们”的提交是HEAD,“他们”的提交是提交C,并且合并基础 - 如果将merge.conflictStyle设置为diff3或使用git mergetool来运行合并工具,则会显示- 是提交P

当我们运行以下命令时:

git revert <hash-of-C>

同样的事情发生了,只不过这一次,提交C是槽1中的合并基础,提交P是槽3中的--theirs提交。槽2中的--ours提交与通常一样来自HEAD

请注意,如果您在一系列提交上使用cherry-pick或revert:

git cherry-pick stop..start

挑选特定提交(cherry-picking)是逐个提交进行操作,优先选择拓扑上较老的提交;而还原撤销(reverting)是逐个提交进行操作,优先选择拓扑上较新的提交。也就是说,假设有如下提交:

...--C--D--E--...
 \
  H   <-- HEAD

git cherry-pick C..E会先复制D,然后再复制E,但git revert C..E会先撤销E,然后再撤销D。(因为两个点语法排除了从两个点表达式左侧可达的提交。有关更多信息,请参阅gitrevisions文档。)

变基是重复使用cherry-pick

变基命令通过在进入分离头状态之前运行git checkout --detachgit switch --detach,然后重复运行git cherry-pick来工作。(实际上,现在它只是在内部这样做;在旧版本的一些基于Shell脚本的git rebase中,确实使用了git checkout,不过哈希ID总是进入分离模式)。

当我们运行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>

这个命令的第一(实际上是第二)步是进入到分离 HEAD 模式,HEAD 提交记录是我们使用 --onto 参数所选定的提交记录。如果我们没有使用单独的 --onto 标志和参数,则 --onto 是我们所提供的一个参数,即 theirbranch。如果我们没有使用单独的 upstream 参数,则我们所提供的一个参数(在本例中为 theirbranch)将用于两个目的。

Git 还会(首先执行此操作),列出要复制的每个提交记录的原始哈希 ID。这个列表比起初看起来更加复杂,但是如果我们忽略额外的复杂性,它基本上是以下结果:

git rev-list --topo-order --reverse <hash-of-upstream>..HEAD

在这种情况下,CDE 的哈希 ID 是需要翻译的内容:这三个提交是从 ourbranch 能够到达但无法从 theirbranch 到达的提交。
使用 git rebase 生成此列表并进入分离 HEAD 模式后,现在我们看起来像这样:
       C--D--E   <-- ourbranch
      /
...--B--F--G--H   <-- theirbranch, HEAD

现在Git运行一个`git cherry-pick`命令。其参数是要复制的第一个提交的哈希值C。如果我们看一下cherry-pick的工作方式,我们会发现这是一个将合并作为动词操作,其中合并基础是提交C的父节点,即提交B;当前或`--ours`提交是提交H;将要复制或`--theirs`提交是提交C。所以这就是为什么我们的和他们的似乎被颠倒了。
然而,一旦此cherry-pick操作完成,我们现在具有:
       C--D--E   <-- ourbranch
      /
...--B--F--G--H   <-- theirbranch
               \
                C'  <-- HEAD

现在 Git 使用 git cherry-pick 命令复制提交 D。合并基础现在是提交 C--ours 提交是提交 C'--theirs 提交是提交 D。这意味着我们的提交中有"ours"和"theirs"两个部分,但这次"ours"提交是我们几秒钟(或毫秒)之前构建的!

它基于现有的提交 H,属于他们,但是它是我们的提交 C'。如果我们遇到任何合并冲突,这无疑是由基于 H 造成的,可能包括我们手动执行的某种冲突解决来生成 C'。但是,非常明显,所有三个输入提交都是我们自己的。索引槽 #1 是来自提交 C,索引槽 #2 来自提交 C',索引槽 #3 来自提交 D

完成后,我们的情况现在如下:

       C--D--E   <-- ourbranch
      /
...--B--F--G--H   <-- theirbranch
               \
                C'-D'  <-- HEAD

现在Git会在提交E的哈希值上运行git cherry-pick。合并基准是提交D,我们和他们的提交分别是D'E。因此,在变基期间,所有三个提交都是我们的——尽管合并冲突可能是建立在H的基础上的结果。

当最后一个cherry-pick完成后,Git会通过将名称为ourbranch从旧的提交E中拔出,并粘贴到新的提交E'上来完成变基:

       C--D--E   [abandoned]
      /
...--B--F--G--H   <-- theirbranch
               \
                C'-D'-E'  <-- ourbranch (HEAD)

我们现在回到了普通的头部附着模式,因为git log从我们当前的提交E'开始向后工作,从未访问原始提交C,所以似乎我们已经修改了原来的三个提交。但实际上并不是这样,他们仍然存在于我们的存储库中,并通过特殊的伪引用ORIG_HEAD和reflogs可用。默认情况下,至少可以在30天内恢复它们,之后git gc将可以自由地清除它们,然后它们就真的被删除了。(只要我们没有将它们git push到其他仍在保留它们的Git存储库中。)


这看起来像是一个非常棒的答案。我期待有机会学习它。谢谢你的添加。 - Gabriel Staples
我已经阅读了整个内容。谢谢。您一定为git源代码做出了贡献,才能获得这种深入的知识吧?这对我来说非常有用:"我们可以默认在至少30天内找回它们,之后git gc将自由地收割它们,然后它们就真的消失了。"我从未知道git会保留多久被遗弃的HEADS/commits。 - Gabriel Staples
关于 git revert,我认为最重要的一点是,如果你有一个线性树 ...A--B--C--D(HEAD),其中 D 是当前的 HEAD,并且你执行了 git revert B,那么你试图还原的那个提交 B 就会成为当前的合并基础,或者说是这个“合并”的 Slot 1,而 Slot 2 或者说是“我们”的就变成了 D/HEAD,Slot 3 或者说是“他们”的就变成了 A,也就是被还原的提交的父提交,对吗?然后,进行低级别的“合并”,结果就是应用了 B..D 的所有更改,以及 B..A 的所有更改,从而实现了还原 B,对吗?这很困难。 - Gabriel Staples
1
我确实做出了贡献(最近,虽然不多)。你现在似乎已经弄清楚了。至于“废弃”的提交会保留多长时间:这实际上是由Git的reflogs控制的。reflog条目使提交可达。运行git gc会运行,除其他外,git reflog expire,其到期时间由您的GC时间设置设置。这就是30天和90天默认值的来源(也请参阅git config文档)。一旦reflog条目过期并且提交真正无法访问,[继续] - torek
一旦过期,打包对象可以通过“git repack”从包中弹出,松散对象可以通过“git prune”移除。由于git gc在各种间隔时间运行这两个命令,因此此时现在未受保护的提交变得不安全,但实际上它们何时消失更加随机,因为gc进程依赖于.git子目录中的累积。它可能触发快速,也可能需要几个月的时间。 - torek
显示剩余2条评论

2

简介

请直接跳到最后查看结果和结论。

详情:

相关内容:

Then git status shows the following conflicts:

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中的both modified文件中的冲突,这与任何rebase或merge冲突的正常处理方式相同。然后我添加了所有文件并完成了还原:

git add -A
git revert --continue

接下来,我注意到所有标记为他们删除的文件以及所有标记为我们添加的文件,在我的文件系统中都存在。因此,撤销操作没有删除它们。接下来,我想知道:这些文件是哪个提交创建的?要查看这个信息,请运行以下命令(来源):
git log --diff-filter=A -- path/to/file

这显示了仅针对创建此文件的单个commit_hashgit log commit_hash。我为每个由他们删除或我们添加的文件逐个执行此操作:

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

我发现如上所示的4个文件是由我撤销的提交添加的。请注意,这意味着它们是由提交some_commit_hash本身添加的,而不是由我运行git revert some_commit_hash时创建的撤销提交。那么,如果我撤销了该提交,为什么它们仍然存在呢?原来,稍后的提交(我们将其称为later_commit_hash),它发生在some_commit_hash之后,触及了这6个文件中的所有文件,修改了其中的4个文件并创建了2个文件。
让我们按照由它们删除由我们添加的组别对上述文件进行分组:
# 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 的其余提交。

冲突是因为 later_commit_hash 修改了这 4 个 “被他们删除”的文件,所以不允许使用 git revert some_commit_hash 删除它们。而且,在 some_commit_hash 之前并不存在这 2 个 “我们添加的” 文件,所以冲突是它们在撤消后不应存在,但它们确实存在,因为它们是由 later_commit_hash 创建的。

我的解决方法是手动删除所有这些 6 个文件:

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 命令期间:

  1. "us" = 当前检出的提交(即:在键入并运行 git revert some_commit_hash 命令时的 HEAD),以及:
  2. "them" = 您正在还原的提交的相反面或相反面;即:它是某个短暂的提交,其与 some_commit_hash 相反,以撤消 some_commit_hash 的更改,假设您运行 git revert some_commit_hash 命令。

2020年1月7日更新:是的,这确实是它。以下是我刚刚在这个其他答案下面留下的评论。我的评论似乎与上述观察完全一致:

对于我来说,关于 git revert 的关键是,如果你有一个线性树 ...A--B--C--D(HEAD),其中 D 是当前的 HEAD,并且你执行 git revert B,那么被还原的提交 B 将成为当前的合并基础,或者说是“合并”的 Slot 1,而 Slot 2 或“我们”的则变成了 D/HEAD,Slot 3 或“他们”的则变成了 A,即被还原的提交的父级,正确吗?然后进行低级别的“合并”,从而应用所有来自 B..D 的更改,以及所有来自 B..A 的更改,从而还原 B,对吗?这很难。

所以,这意味着“与some_commit_hash相反的短暂提交”实际上只是相反的差异,或者是从some_commit_hash方向到其父提交进行还原。现在,在底层git合并中进行了低级别合并,其中合并基础是some_commit_hash,要还原,“ours”/“us”是HEAD,“theirs”/“them”是some_commit_hash的父提交,也就是some_commit_hash~。当git执行此低级别合并时,从some_commit_hashHEAD的差异(即等效于git diff some_commit_hash..HEAD)捕获了所有新内容,而从some_commit_hash到其父提交的差异(即等效于git diff some_commit_hash..some_commit_hash~)捕获了由提交some_commit_hash所做更改的相反变化从而还原了此提交!

如果我理解正确,现在一切都很清楚了!


我仍然有点困惑这个概念,但这就是要点。如何还原的确切机制会使事情更加清晰,我认为这里的答案可能会提供更多见解,但我不理解。

我还添加了一个答案来澄清“我们”和“他们”在所有可能发生此情况的4个git操作中的含义:git mergegit cherry-pickgit rebasegit revert根据Git,谁是“我们”,谁是“他们”?


(自己的笔记):

需要查看:http://ezconflict.com/en/conflictsse12.html#x53-890001.7


git show --name-status some_commit_hash 对于那两个显示为 added by us 的文件有什么说法? - eftshift0
@eftshift0,它们根本不存在,因为它们是由later_commit_hash添加的,而该哈希值在some_commit_hash之后。 - Gabriel Staples
当我执行 git show --name-status later_commit_hash 命令时,我看到了以下内容:R100 path/to/file3_old_name.h path/to/file3.hR100 path/to/file5_old_name.h path/to/file5.h,这表明它们都被重命名了(虽然我不知道 R100 具体是什么意思)。 - Gabriel Staples
r100 表示内容没有改变。好的,让我消化一下这整个东西。谢谢提供信息。 - eftshift0

0

嗯... revert 是一个非常特殊的情况。所以,想象一下正常的合并,有共同的祖先和所有东西,整个包裹,对吧?现在,整个过程的工作方式与合并相同除了(这是一个很大的例外),合并引擎强制共同祖先成为您尝试还原的修订版本,并且另一个分支是该修订版本的父级


我正在努力理解你所说的话,因为我真的认为你在这方面有所发现,但我无法确切理解你的意思。不过,这种还原情况与以往不同,我认为我的答案接近了,但还没有完全掌握。 - Gabriel Staples
你是否熟悉合并是如何进行的?比如说,“共同祖先”和两个分支的情况?如果你熟悉,那么这只是一个简单的合并,你的分支就是你的分支,共同祖先被“固定”到你要还原的版本上,另一个分支是它的父级。试着在脑海中画出来。如果你不熟悉这些概念,请前往我之前提供链接的网站,并查看与“三面硬币”相关的部分。 - eftshift0
是的,我不太熟悉合并在底层实际上是如何工作的。我看到你这里的链接(http://ezconflict.com/en/conflictsse12.html#x53-890001.7)。我会去查看一下的。 - Gabriel Staples
你在一个公开项目中看到了这个冲突吗?我真的很想知道发生了什么。 - eftshift0
1
它并没有详细解释合并的工作方式... 至少告诉你要考虑哪3个方面进行合并.... 并且强烈建议您使用_diff3_ (这对您没有影响... 因为您正在处理树冲突,而我还没有开始讲解该部分......稍后再说)。 - eftshift0
你是在一个公开项目中看到这个冲突吗?我真的很想知道发生了什么。不好意思,这是一个私有项目,我不能分享。 - Gabriel Staples

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