【问题标题】:Merging two GIT commits on the same branch在同一分支上合并两个 GIT 提交
【发布时间】:2020-01-28 22:21:16
【问题描述】:

我重新创建了一个我遇到过几次的问题。这是基本情况:

创建一个文件file1.txt,内容如下:

你好,
欢迎来到我的档案。
再见。

$ git add file1.txt
$ git commit -m “Initial commit”

向 file1.txt 添加第二个正文行。 **注意:添​​加此行时,“不小心”删除了“Welcome to my file”。

你好,
这是第二行。
再见。

$ git add file1.txt
$ git commit -m “Added second line”


$ git log
commit ccd8.. (HEAD -> master)
Author: ____
Date:   Tue Jan 28 11:50:11 2020 -0800

Added second line

commit 6d83..
Author: ___
Date:   Tue Jan 28 11:49:36 2020 -0800

Initial commit

合并这两个提交的最佳方法是什么?目标是生成文件 file1.txt,其内容为:

你好,
欢迎来到我的档案。
这是第二行。
再见。

到目前为止我尝试的是:

$ git checkout 6d83..
$ git branch tmp
$ git checkout master
$ git merge tmp

但我收到“已经是最新的”消息。 git rebase 是这里最好的选择吗?为什么创建一个临时分支然后合并不起作用?

【问题讨论】:

  • 与您的情况不完全相同,但this 可能会有所帮助。这是同样的问题,但问题分布在两个文件中。
  • 此外,您收到的最新消息是由于 git 跟踪提交,而不是实际内容。因此,由于 master 和 temp 分支都包含完全相同的提交,因此可以确定实际上没有发生任何新事情。
  • 而你不能用 git 做你想做的事情的原因是 git 提交被设计为不可变的。 IE。如果提交的内容发生更改,则提交 ID 必须更改。由于该规则,您无法在不创建新提交的情况下更改现有数据。做你想做的最简单的方法是rebase

标签: git


【解决方案1】:

这里的问题是,就Git而言,deleting你删除的那一行是正确答案

请记住,Git 的基本存储单元是提交。每个提交都有:

  • 一些数据:所有文件的快照;和
  • 一些元数据:关于提交的信息。这包括制作者、制作时间(日期和时间戳)以及原因(您或提交者的日志消息)。不过,对 Git 来说最后也是最重要的元数据是 parent 提交哈希 ID。

每个提交都有一个唯一的哈希 ID。这个哈希 ID 在你提交的那一刻被分配给提交。从那时起,该哈希 ID 将保留给 that 提交。只有该提交可以具有该 ID。1

同时,正如我们刚刚提到的,每个提交都可以在其元数据中存储一个哈希 ID。从技术上讲,每个提交都可以存储 Git 想要的任意数量的哈希 ID,但它们必须是已经存在的提交的哈希 ID。2 大多数提交只存储一个其他提交哈希 ID:父(单数) ) 的提交。 (合并提交存储两个,这就是使它们合并提交的原因,并且某人在一个新的、完全空的存储库中进行的第一次提交,不能有父提交——没有更早的提交可以引用——所以它只是没有。 t.)

那么,在你的情况下,你可能有一些更早的提交,或者没有。我们将只绘制一个假设您这样做的图表:

... <-F <-G <-H

哈希 ID 为 HH 代表真实哈希 ID,看起来是随机的)的提交会记住其父级的哈希 ID,先前存在的提交 G,它会记住它的F,等等。这些嵌入在每个提交的元数据中的向后指向的箭头是 Git 查找提交的方式——除了提交 H 本身,它是 last 提交。 p>

Git 查找任何分支的 last 提交的方式是分支 name,例如 master,保存提交的哈希 ID。所以为了使绘图更完整,让我们把它画进去。因为任何提交在我们完成后都不会改变,我们可以偷懒并停止绘制 那些 箭头作为箭头,只要我们记得他们向后指:

...--F--G--H   <-- master

现在,让我们进行添加这个新文件file1.txt 的新提交。提交H 根本没有file1.txt — 它有一些其他文件,但没有file1.txt。我们git add file1.txt 并运行git commit 并提供日志消息。 Git 创建一个新的提交,它会获得一个新的唯一的大丑陋哈希 ID,但我们将称之为 I。 Git 将父级设置为H,以便I 指向H

...--F--G--H   <-- master
            \
             I

然后,作为git commit 的最后一步,Git 将I 的实际哈希ID 写入名称master

...--F--G--H
            \
             I   <-- master

(没有理由继续在单独的行上绘制I,所以我们不会。)

现在您编辑该文件,并按照通常的过程进行新的提交J。提交JI 作为其父级,Git 将J 的哈希ID 写入名称master

...--F--G--H--I--J   <-- master

这里没有什么可以合并,也就是说,你不能使用git merge 来做你想做的事。你有一个线性的提交链,以J 结尾。从J 回到I,从I 回到H,以此类推。


1从某种意义上说,哈希 ID 是在您提交之前 保留给该提交的——除了哈希 ID 本身取决于 确切的时间你做到了,一直到第二个。因此,如果您提前一秒或一秒后提交,它将具有不同的哈希 ID。在任何情况下,哈希 ID 都是唯一的:只有提交可以具有该哈希 ID。

如果 Git 无法提供唯一的哈希 ID,它不会让您进行提交!这实际上从未发生过,尽管这是理论上的可能性。另见How does the newly found SHA-1 collision affect Git?

2我们即将创建的新提交的哈希 ID 取决于其父提交的哈希 ID。因此,即使我们计算出如果其父提交是现有提交 X 的新提交将具有什么哈希 ID,对于任何 X,如果我们随后插入 this 在创建之前将提交的元数据散列到其中,它毕竟会获得一个 不同的 散列 ID。因此,提交不能可能引用自身,也不能允许在其中放置一些随机垃圾。因此,每次提交总是指一些较早的提交。

更简单地说,给定一个提交,你可以在时间上倒退到它的父级......但你只能在时间上倒退。你不能转发到它未来的孩子。

因此,您不能更改任何提交,也不能删除任何较早的提交而不删除所有后来的提交。 (Git 使删除提交变得特别困难。与 Mercurial 相比,您在其中运行 hg strip -r &lt;rev&gt; 并删除该提交 它的所有子项。您仍然无法选择子项,但是很容易取消提交。)


合并

Git 中的合并通常发生在我们有多个分支名称 时。让我们回到我们刚刚提交H 作为master 的最后一次提交的情况。 (我们可以使用git reset --hard HEAD~2 来实现这一点——这使得master 再次直接指向H,并且还设置了工作区——Git 的索引,以及我们可以看到文件的工作树——以反映提交@再次返回 987654364@。IJ 将继续存在,默认情况下,至少可以再检索 30 天。但我们会假装我们根本没有创建过 IJ。)所以我们有这个:

...--G--H   <-- master

现在我们将创建一个或两个新分支。当我们这样做时,我们需要在我们的绘图中再添加一件事。如果只有一个分支名称master,那可能就是我们正在使用的分支。但是如果我们添加了dev 作为第二个名字呢?我们使用的是哪个名称

Git 对此的回答是使用特殊名称HEAD。这个特殊名称通常附加到您的一个分支名称。 (它只能附加到一个或没有:永远不会超过一个。)我们将添加第二个分支名称 dev,但将 HEAD 附加到 master

...--G--H   <-- master (HEAD), dev

现在我们将以通常的方式创建新的提交 IJ。让我们把它们画进去:

          I--J   <-- master (HEAD)
         /
...--G--H   <-- dev

注意dev 没有移动:它仍然指向现有的提交H。名称 master 现在指向新提交 J

现在,让我们在 dev 上创建两个提交。我们从git checkout dev 开始。这会将我们的 HEAD 附加到 dev,并提取提交 H 的内容以使用/打开:

          I--J   <-- master
         /
...--G--H   <-- dev (HEAD)

存储库中的 commits 没有改变!但是我们看到和使用的文件有,当前分支dev当前提交H3现在我们又做了两个新的提交。任何数字都是允许的,但两个使说明更容易:

          I--J   <-- master
         /
...--G--H
         \
          K--L   <-- dev (HEAD)

现在我们可以运行git merge。我们选择一个要使用的分支——git checkout mastergit checkout dev——然后我们运行 git merge 并为其指定另一个分支的名称。4 让我们使用git checkout mastergit merge dev 所以HEAD 和当前提交标识 J 而不是 L:5

          I--J   <-- master (HEAD)
         /
...--G--H
         \
          K--L   <-- dev

Git 现在必须找到 both 分支上的 best 提交。在这种情况下,这很明显:它是提交H。我们从J 后退两步到达那里,我们从L 后退两步到达那里。如果底部的链更长,我们将不得不返回 3 或 4 步或许多步,但只要我们能够 to 提交H,提交H将是最好的共享提交。

Git 将这个共享的、最佳的提交称为 合并基础,我们和他们都从该提交开始。 合并基础提交是合并的关键。您(或 Git)通过查看图表来找到它,该图表显示了提交是如何连接的。

Git 现在将运行两个 git diff 操作:

  • git diff --find-renames <em>hash-of-H hash-of-J,找出在master我们 发生了什么变化,因为共享提交H;和
  • git diff --find-renames <em>hash-of-H hash-of-L,找出他们在dev 上更改了什么,因为共享提交H

git merge 所做的是合并这些更改,然后将合并后的更改应用到提交H 中的快照——合并基础。这样我们就可以保留更改并添加更改。

这也是为什么合并大多是对称的。如果我们检查了dev,即提交L,然后运行git merge master,Git 仍然会发现共同提交H 作为合并基础。它将运行相同的两个git diff 命令(以另一个顺序,但谁在乎呢?)。然后它将这些差异组合成一个大的组合集,并将它们应用于来自提交H 的快照。结果是一样的。

如果我们的更改和它们的更改以某种方式重叠,Git 将声明一个合并冲突。在这种情况下,Git 不会自行完成合并。它会给你留下一个必须用手清理的烂摊子。没关系:您只需清理它,git add,然后提交(或运行 git merge --continue)即可完成工作。

为了完成这项工作,Git 将进行一个新的提交——我们称之为M,用于合并,因为我们巧妙地将之前的每个提交标记为HL——并更新当前分支名称像往常一样,我们检查的任何一个分支现在都以新的合并提交M 结束。为了将其标记为合并提交,Git 将其 两个 父级设置为 J,然后是 L,因为我们在开始时位于 J。所以我们可以得出结果:

          I--J
         /    \
...--G--H      M   <-- master (HEAD)
         \    /
          K--L   <-- dev

我们进行了合并。合并时的快照是将来自H-vs-J 和来自H-vs-L 的组合更改应用到H 的结果。合并的 父母 像往常一样是上一个提交,以及我们在运行 git merge dev 时选择的另一个提交。

既然存在这种合并,尝试将L 甚至K 合并到master 中是不可能的。原因是LM 之间最好的共享 提交是提交L ...这已经是M 历史的一部分。如果我们沿着 bottom 行从M 后退,我们会到达L。历史记录(在 Git 中由提交组成,包括它们的连接)表明 L 已在此处合并。


3当你问 Git:HEAD 中有什么内容?你有两种表达方式。您可以问 Git:HEAD 中的 分支名称 是什么? 或者,您可以问:HEADcommit 做了什么选择? 两个不同的问题得到两个不同的答案。在“分离的 HEAD”模式下,HEAD 没有附加到任何分支名称,第一个给你一个错误而不是答案。第二个问题几乎总是有效。

Git 还具有 未出生分支 的概念,当您开始使用一个完全没有提交的新的、完全空的存储库时,它需要它。在这种情况下,HEAD 存在,并且拥有一个分支名称,但分支名称本身 存在并且无效。所以在这种特殊情况下,你可以问关于 HEAD 的“什么名字”问题,但不能问“什么 ID”问题:与分离的 HEAD 设置相反。

4事实上,git merge 通过提交哈希 ID 工作,所以我们可以给它任何我们想要的提交的哈希 ID。但通常我们——人类——按名字工作。

5合并结果一般每路都是一样的,只是先列出哪个父级。但是,如果我们对git merge 使用特定的标志参数,则合并结果可能会有所不同。


樱桃采摘

我们可以做一些事情。给定任何提交链——不管有没有像这样的分叉:

          o--P--C--o--o   <-- branch1
         /
...--o--o
         \
          o--o--H   <-- branch2 (HEAD)

或只是一个线性链,如:

...--o--o--P--C--o--o--H   <-- branch (HEAD)

我们可以挑选出一些提交C,一个其父是P的孩子,并在其上运行git cherry-pick。 (通常你会在这里使用 C 的哈希 ID。)这样做会迫使 Git:

  • 找到提交PC的父级:这很容易,因为C在其中包含P的哈希ID;
  • P 视为合并基础,将C 视为“他们的”提交,将当前提交H(由HEAD 选择)视为“我们的”提交,以及像往常一样进行全面的三向合并。

所以 Git 现在将比较 PC 以查看“他们”做了什么,比较 PH 以查看我们做了什么,并将这两组更改结合起来。然后,Git 会将合并的更改应用到P 中的快照。如果一切顺利,Git 将使用 C 的原始提交消息将生成的文件作为新的快照C'(提交C副本)提交,以此类推。它不会使这是一个合并提交,而只是一个普通的提交:

          o--P--C--o--o   <-- branch1
         /
...--o--o
         \
          o--o--H--C'  <-- branch2 (HEAD)

或:

...--o--o--P--C--o--o--H--C'  <-- branch (HEAD)

从另一个分支中挑选提交往往更有意义,如上图所示;但是您可以从自己的历史记录中挑选一个提交,以重新应用相同的更改。如果在CC' 之间的某个提交 是在C 中发生的任何事情un-did 的提交,这将特别有用。6


6Git 有一个命令,git revert,可以进行此类提交。您将它指向某个孩子,Git 会执行与樱桃选择相同的三向合并,除了 merge base 这次是 C,而“他们的”提交是 @ 987654488@。 (和往常一样,ours / HEAD 提交是 HEAD 提交。) 练习:尝试按此顺序获取 CP 的差异。如果您按顺序将这组更改与CHEAD 结合起来会发生什么?


请注意,所有这些操作都针对整个提交

您一开始想对一个文件大惊小怪。但是Git 在这里所做的一切——或者我们已经展示了Git 所做的——都是基于整个commits。那是因为提交确实是 Git 中的基本单元。确实,提交存储文件,但 Git 并不是真正关于 files。 Git 是关于提交。文件只是使提交有用的东西。

可以从单个提交中提取单个文件,并处理它们:例如git diff,给定两个文件的名称,可以区分这两个文件。但这是使用 Git 的一种非典型方式。 Git 适用于一次提交操作。

【讨论】:

    【解决方案2】:

    您不能像上面尝试的那样通过合并自动执行此操作。但是假设你已经正确配置了你最喜欢的差异编辑器,这将让你在提交之前手动修复文件访问以前的内容。在你的主分支上:

    git difftool ccd8:file1.txt file1.txt
    

    一旦正确修复并保存,退出编辑器后

    git add file1.txt
    

    如果还没有push,可以修改之前的commit

    git commit --amend
    

    或者用恢复的线创建一个全新的

    git commit -m "Recovered line"
    

    【讨论】:

      【解决方案3】:

      没有办法在 git 中做你想做的事情。如果有的话,它会导致合并冲突。

      【讨论】:

      • 我真的很了解它背后的原因(如果你知道的话)。由于 git 如何跟踪内容,这在技术上是不可能的吗?还是只是周围没有现成的工具?
      • 我“想要”一个合并冲突,这样我就可以接受这两行。但是目前,它甚至不允许合并。
      • 合并冲突有什么问题?它们只是需要人工判断才能正确应用的更改。
      • 在这种情况下,整个合并是一个冲突。这没有任何意义。
      【解决方案4】:

      一个简单的方法是git checkout -p @^ file.txt,它会发现你的工作树版本和祖父母版本之间的每一个差异,并提供应用它,你可以编辑提供的帅哥。

      Cherrypick 通常只是 diff 的简写 |申请 -3,如果你想恢复 @^ 的所有更改,你也可以尝试 git diff @^!|git apply -3,这可能会给你留下一些需要解决的冲突,但不要害怕那些,他们是罕见但正常。使用好的合并/差异工具进行练习。我喜欢 vimdiff,解决琐碎的冲突非常快。像争夺一个新的标志位之类的事情通常需要几秒钟才能解决。

      【讨论】:

        猜你喜欢
        • 2012-03-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-03-23
        • 2014-07-17
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多