【问题标题】:Who is `them` and `us` in a `git revert`?谁是“git revert”中的“他们”和“我们”?
【发布时间】:2021-01-02 13:47:49
【问题描述】:

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. 因为它没有明确谁是usthemGIT: How dangerous is "deleted by us" conflict?
  2. 因为它涵盖了mergerebase 而不是revert,并且git 经常使用相同的术语来表示相反 的事情,具体取决于操作:Who is "us" and who is "them" according to Git?
  3. 因为它没有提到“我们”和“他们” - Git - reverting a revert, conflicts

【问题讨论】:

  • 我不知道这一点,但它应该很简单,可以弄清楚:1)git show some_commit_hash 并查找添加/删除的文件。 2) 将这些与当前的 git status 相关联(添加/删除已反转,因为您正在还原)。 3) 利润。
  • 我仍然认为这是 Who is "us" and who is "them" according to Git? 的副本 - 如果与其他问题不同,您应该编辑该问题以包含 revert 的部分。
  • @pkamb,不,请参阅我的问题中“不重复”下的#2。我预见到了。
  • @GabrielStaples 我认为 pkamb 建议应该更新问题/答案以使其重复,而不是在此处添加新答案。
  • 如果您在这里得到一个好的答案,您可能会考虑编辑规范的问题/答案并将其作为副本关闭。 [死灵法师 x 52 :)]

标签: git merge-conflict-resolution git-revert


【解决方案1】:

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

我做了一些实验,观察到以下情况。

首先,我只手动解决了 both modified 文件 path/to/file2.h 中的冲突,这对于任何变基或合并冲突都是正常的。然后我添加了所有文件并完成了还原:

git add -A
git revert --continue

接下来,我观察到所有标有被他们删除的文件,以及所有标有我们添加的文件,在我的文件系统上存在/存在。因此,revert 没有删除它们。接下来,我想知道:哪个提交创建了这些文件?要查看此内容,请运行以下命令 (source):

git log --diff-filter=A -- path/to/file

这显示了 git log commit_hash 仅用于创建此文件的单个 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

所以,您可以看到 deleted by them 文件是由我恢复的提交添加的,这意味着恢复该提交将删除这些文件!所以,them 指的是被还原的提交,some_commit_hash,而us 指的是HEAD 的剩余提交。

冲突在于later_commit_hash 触及了这4 个“被他们删除”的文件,因此git revert some_commit_hash 不允许删除它们。而且,这两个“由我们添加”的文件在 some_commit_hash 之前并不存在,因此冲突在于它们在还原后不应该存在,但它们确实存在,因为它们是由 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 日更新:是的,看起来确实如此。这是我刚刚在this other answer here 下方留下的评论。我的评论似乎与上述观察完全相关:

我认为关于git revert 的关键点是,如果你有一个线性树...A--B--C--D(HEAD)D 是你当前的HEAD,然后你做一个git revert B,然后@ 987654364@,即您尝试还原的提交,成为当前的合并库,或此“合并”中的插槽 1,插槽 2 或“我们的”,成为 D/HEAD,插槽 3,或“他们的”,变为A,或者被还原的提交的父级,对吗?然后,执行低级“合并”,导致应用来自B..D 的所有更改,以及来自B..A 的所有更改,从而恢复B,对吗?这很难。

所以,这意味着这个“与some_commit_hash相反的短暂提交”实际上只是逆差异,或者是来自 some_commit_hash方向的差异,你正在恢复to 其父提交。现在,你有一个底层的 git 合并在后台进行,合并基础是 some_commit_hash 要恢复,“我们的”/“我们”是 HEAD,“他们的”/“他们”是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 所做更改的反向从而恢复此提交!

如果我把这一切都说清楚了,现在一切都说得通了!


我仍然对这个概念有点挣扎,但这就是它的要点。我认为恢复工作的确切机制将真正澄清这里的事情。 This answer 可能会提供更多见解,但我不明白。

我还刚刚在此处添加了一个答案,以澄清“我们”和“他们”对于我能想到的所有 4 个 git 操作可能会发生这种情况:git mergegit cherry-pick , git rebase, 和 git revert: Who is "us" and who is "them" according to Git?


(给自己的注释):

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

【讨论】:

  • git show --name-status some_commit_hash 对显示为 added by us 的 2 个文件有何看法?
  • @eftshift0,它们根本不存在,因为它们是由later_commit_hash添加的,它位于some_commit_hash之后。
  • 但是,当我执行 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 的确切含义)。跨度>
  • r100 表示内容没有变化。好的。让我消化这整件事。感谢您的信息。
【解决方案2】:

虽然这已经得到了很好的回答,但还有另一种方式来看待这一切。这就是 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 为完成此合并(将某些文件集作为动词合并)所做的是,对于所有三个提交中的每个文件HJ 和 @987654336 @,比较HJ 中的文件,看看我们 改变了什么,比较HL 中的文件,看看他们 改变了。然后 Git 组合这两组更改,将组合的更改应用到 H 中的任何内容。

提交H合并基础 提交,提交J 是“我们的”提交,而提交L 是“他们的”提交。任何差异,无论是“我们添加”的新文件,还是“他们删除”的文件,或者其他什么,都与提交 H 有关。

为了通过合并机制运行合并,Git 对以下内容进行了略微优化的提前版本:

  1. 设置:

    • 将合并基础提交 (H) 读取到插槽 1 的索引中
    • ours commit (HEAD = J) 读入插槽 2 的索引
    • theirs commit (L) 读入插槽 3 的索引
  2. 识别“相同的文件”。请注意,对每个文件都重复第 2 步和第 3 步。

    • 如果三个插槽中都有一个名为 F 的文件,那么它就是同一个文件
    • 否则,如果插槽 1 中有任何内容,请尝试猜测重命名,这会将插槽 1 中的合并基础文件绑定到插槽 2 中 不同名称 的我们或他们的文件,并且/ 或插槽 3;如果找不到需要重命名的文件,则我们和/或他们的一方删除了该文件;这些情况也可能导致高级别冲突,例如重命名/修改或重命名/删除,我们声明冲突并继续进行而不执行第 3 步
    • 否则(插槽 1 中没有内容,但插槽 2 和 3 中的内容)我们会发生添加/添加冲突:声明此特定文件存在冲突,然后继续进行而不执行第 3 步
  3. 短路简单案例,并通过低级合并处理困难案例:

    • 如果槽 1、2、3 中的 blob 哈希 ID 都匹配,则三个副本都相同;使用其中任何一个
    • 如果槽 1 中的 blob 哈希 ID 与槽 2 或 3 中的匹配,则说明有人没有更改文件而有人更改了;使用更改后的文件,即采用 不同 的文件
    • 否则,所有三个插槽都不同:通过更改块执行更改块行,低级合并
      • 如果在低级合并期间发生合并冲突,-X ours-X theirs 表示“使用我们的/他们的解决冲突”,其中我们是插槽 2 中的任何内容,而他们是插槽 3 中的任何内容
      • 请注意,这意味着无论没有冲突,例如,只有一个“边”更改了第 42 行,-X 扩展选项根本不适用,我们采取修改,不管是我们的还是他们的

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

因此 to mergemerge as a verb 在 Git 的索引中运行

上述所有操作都发生在 Git 的索引中,其副作用是将更新的文件留在工作树中。如果存在低级冲突,您的工作树文件将使用冲突标记和与索引槽 1(合并基)、2(我们的)或3(他们的)。

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

Cherry-pick 和 revert 使用合并机制

当我们运行 git cherry-pick 时,我们有一个如下所示的提交图:

...--P--C--...
   \
    ...--H   <-- somebranch (HEAD)

这里的字母 PC 代表任何父子提交对。 C 甚至可以是一个合并提交,只要我们使用-m 选项来指定要使用哪个父级。 (对于三个提交在图中的位置没有真正的限制:我用H 绘制它是P 之前的某个提交的子项,但它可以在P-C 对之后,如@例如 987654365@,或者如果您有多个不相交的子图,P-CH 提交之间可能根本没有关系。)

当我们运行时:

git cherry-pick <hash-of-C>

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

当我们运行时:

git revert <hash-of-C>

同样的事情发生了,除了这一次,commit C 是 slot 1 中的合并基础,commit P 是 slot 3 中的 --theirs commit。 slot 2 中的 --ours commit 来自 @ 987654388@像往常一样。

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

git cherry-pick stop..start

挑选工作首先使用拓扑上较旧的提交一次提交一个提交,而恢复工作一次提交一次使用拓扑上较新的提交。也就是说,给定:

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

git cherry-pick C..E 先复制 D,然后是 E,但 git revert C..E 先还原 E,然后是 D。 (提交C 不起作用,因为两点语法排除了从两点表达式左侧可到达的提交。有关更多信息,请参阅the gitrevisions documentation。)

rebase 是重复的樱桃采摘

rebase 命令通过重复运行git cherry-pick 来工作,之后 使用git checkout --detachgit switch --detach 进入分离的HEAD 模式。 (从技术上讲,它现在只是在内部执行此操作;在过去,git rebase 的一些基于 shell 脚本的版本确实使用了 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 已经生成了这个列表并进入了 detached-HEAD 模式,我们现在看起来像这样:

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

现在 Git 运行一个 git cherry-pick。它的参数是提交C 的哈希ID,这是要复制的第一个提交。如果我们看看上面的cherry-pick是如何工作的,我们会看到这是一个merge-as-a-verb操作,合并基础是C的父级,即提交B,当前或@987654424 @commit 是commit H,而要复制的或--theirs commit 是commit C。所以这就是为什么我们的和他们的似乎相反。

一旦这个樱桃挑选操作完成,我们现在有:

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

Git 现在继续复制提交 Dgit cherry-pick。合并基础现在是提交 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。所以再一次,在 rebase 期间,所有三个提交都是我们的——尽管合并冲突可能是在 H 上构建的结果。

当最后一个樱桃选择完成后,Git 通过将 name 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 存储库。)

【讨论】:

  • 这看起来是一个非常好的答案。我期待有机会时研究它。感谢您添加它。
  • 我把整个东西都染红了。谢谢你。您必须对git 源代码做出贡献才能获得这种深入的知识?这是非常有用的信息。对我来说也是如此:“默认情况下,我们可以将它们取回至少 30 天,之后git gc 可以随意收割它们,然后它们就真的消失了。”我从来不知道 git 会在多长时间内留下废弃的 HEADS/提交。
  • 我认为关于git revert 的关键点是,如果你有一个线性树...A--B--C--D(HEAD)D 是你当前的HEAD,然后你做一个@ 987654466@,然后是 B,即您尝试还原的提交,成为当前的合并基础,或此“合并”中的插槽 1,插槽 2 或“我们的”,成为 D/HEAD ,并且 Slot 3 或“他们的”成为A,或者被还原的提交的父级,对吗?然后,执行低级“合并”,导致应用来自B..D 的所有更改,以及来自B..A 的所有更改,从而恢复B,对吗?这很难。
  • 请查看我更新后的“结果和结论”部分of my answer here 并告诉我是否完全正确。
  • 我确实做出了贡献(最近,虽然不多)。你现在似乎已经明白了。至于“废弃”的提交会持续多久:它确实在 Git 的 reflogs 的控制之下。 reflog 条目使提交保持可达。运行 git gc 会运行 git reflog expire 以及由您的 GC 时间设置设置的到期时间。这就是 30 天和 90 天默认值的来源(另请参阅 git config 文档)。一旦 reflog 条目过期并且提交确实无法访问,[继续]
【解决方案3】:

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

  • ours/us 是当前HEAD(活动提交)的状态
  • theirs/them 是另一方的状态(提交被合并,提交被挑选/重新定位,或者在你的情况下,你想要恢复的提交的“反向”)

对于rebase 的一些额外说明(回答@GabrielStaples 的评论):

如果您在my/branch 上运行git rebase other/branchgit 将检查other/branch 的头部提交并开始在顶部重放一些提交。

如果发生冲突,由于签出的提交来自other/branchours 将大致代表other/branchtheirs 将是my/branch

这部分与直觉“ours应该是我的改变”相反,但它符合上面的描述:在冲突时,签出的提交是ours,另一边(提交是重播)是theirs

【讨论】:

  • 我不认为这是正确的:When you rebase, us refers the upstream branch, and them is the branch you're moving about. It's a bit counter-intuitive in case of a rebase.stackoverflow.com/a/21025695/4561887。因此,git rebase HEAD~3 意味着 usHEAD~3themHEAD,这与您的答案完全相反。这是因为从git 的角度来看(我认为?)它实际上只是在挑选下游提交,所以它已经检查了HEAD~3 所以它现在是它的活动HEAD,然后它继续到樱桃- 一次选择下游提交。
  • 关于rebase,它正确的。当你变基时,base branchHEAD(因此,我们)......你变基的将是the other branchezconflict.com/en/conflictsse12.html#x53-890001.7免责声明:我的资料,没有 cookie,没有跟踪,没有货币化)
  • 我已经支持您的答案,引用它并在我现在涵盖所有 4 个案例的答案中对其进行了阐述:stackoverflow.com/questions/21025314/…
  • @eftshift0,您的链接上似乎有一个标签 (#x53-890001.7),但它没有正确跳转到页面上的任何位置。您希望我关注该页面的哪个部分? (推荐:个人代码网站我强烈推荐你看看使用Github pages——你可以用markdown写——我自己也在设置)。
【解决方案4】:

嗯...revert 是一个非常特殊的情况。所以,想想一个正常的合并,与共同的祖先和一切,整个包,对吧?现在,整个事情就像合并 except (这是一个很大的例外)一样,合并引擎 forces the common ancestor 成为您要还原的修订版,而the other branch 是该修订的

【讨论】:

  • 我试图理解你在说什么,因为我真的认为你在这里有所作为,但我无法理解你的确切意思。不过,这种还原情况有些不同,我认为我的答案很接近,但也没有完全确定。
  • 您熟悉如何进行合并吗?像the common ancestor 和 2 个提示?如果你是,那么这只是一个简单的合并,你的分支就是你的分支,共同祖先固定到你正在恢复的修订,另一个分支是它的父。试着在你的脑海中描绘出来。如果您不熟悉这些概念,请访问我之前链接的同一站点并查看与 3 面硬币 相关的部分。
  • 是的,我不熟悉合并真正是如何在幕后工作的。我在这里看到你的链接 (ezconflict.com/en/conflictsse12.html#x53-890001.7)。我去看看。
  • 您是否在打开的项目中看到了这种冲突?我真的想看看发生了什么。
  • 而且它真的没有详细说明合并的工作原理......它至少告诉您合并考虑的三件事是如何的。 ... and 它恳求您使用 diff3 (这不会对您产生影响....您正在处理树冲突,而我还没有开始在那个部分......稍后会出现)。
猜你喜欢
  • 2014-01-28
  • 2019-05-03
  • 2018-05-27
  • 2014-10-23
  • 2016-03-19
  • 1970-01-01
  • 2018-01-06
  • 2011-01-26
  • 2011-02-26
相关资源
最近更新 更多