首页 > 解决方案 > GIT:如何取消加入两个分支的合并

问题描述

不知何故,我设法生成了附加的合并图。提交的名称没有意义。这是一个游乐场 git。我最关心的是总体情况,尤其是本地master(pc图标)和origin master(有头像的那个)分支之间的关系。

我想要的是删除合并并获得一条直线路径,如下所示:

[add] add ...
add git ...
[add] add .. (near duplicate)
add git ...(near duplicate)
[]1.added
etc.

基本上我有两个合并的分支,但我想做一些事情(我想是)比如解锁它们,然后做一个变基。最终,我可以压扁它们。

此外,事实上,我不需要两个分支。它们实际上是相同的。但我很好奇我怎么能以一种同时保留两者的方式来处理它,我想在此过程中可以删除其中一个。

我已经尝试了一些东西,但是随机的,不值得一提。我真的不知道如何进行。

[更新]我正在寻找一个命令行/git命令答案,因为我想了解发生了什么。但是,我使用 VScode 进行编码,并且有各种存储库管理器(Gitkracken、Fork、Gitx、Github、Sourcetree)可供我使用,因此在这些情况下的答案将是一个开始的地方。

提前致谢...

重复提交

标签: gitmergegit-mergerebase

解决方案


基本上我有两个合并的分支,但我想做一些事情(我想是)比如解锁它们,然后做一个变基......

这绝对是可能的(尽管“解锁”在 Git 中不是一个东西)。不过,您可能需要一些基本的 Git 指令(正如这个“解锁”概念所建议的那样)。

在使用任何分布式版本控制系统时,有很多重要的事情需要牢记,尤其是当所讨论的 DVCS 是 Git 时。第一件事是它是分布式的,因此部分或全部部分的副本不止一份。这使得事情本质上变得复杂。我们需要一些方法来驯服和控制复杂性。

Git 在这里的选择是从commit的概念入手。提交是 Git 存在的理由。它们是它的基本存储单元。1 每个提交都有一个唯一的编号。如果那是一个简单的计数可能会很好:commit #1, commit #2, ... 但事实并非如此。相反,它是一个唯一的哈希 ID。这些哈希 ID 看起来是随机的,但实际上并不是随机的。事实上,如果我们可以提前预测您将在哪一秒进行新提交,并且知道您将在其提交消息中放入什么以及有关它的所有其他内容,我们就可以预测其哈希 ID。但我们当然不会也不能。

每个提交包含两件事:

  • 所有源文件的完整副本:快照,它是提交的主要数据;和
  • 一些元数据:有关提交的信息,例如提交的人员、时间以及有关提交原因的日志消息。

元数据的一个关键部分是每个提交都包含一些先前提交或提交的哈希 ID。也就是说,每个后来的提交都说“我之前的父提交是_____”(用哈希 ID 填空)。这将提交链接在一起,但仅向后指向。

一旦提交,就不能更改任何提交,甚至不能更改一位,因为它的哈希 ID 是其所有位的加密哈希。也就是说,您可以从存储库中取出现有的提交,对其大惊小怪,然后保存一个的提交,但是对它的任何更改都会导致保存一个新的不同的提交,这只会添加到存储库中。现有的提交仍然存在,并且仍然没有改变,在其原始哈希 ID 下。换句话说,一个提交一出生就被永远冻结。这意味着不能修改父提交以保存其子的哈希 ID。孩子知道他们的父母(在您创建孩子时就存在),但父母永远不知道他们的孩子(父母出生时孩子还没有出生)。

最后,这也意味着要记住一个提交,我们只需要记住链中的最后一个链接。也就是说,如果我们绘制一系列提交,使用大写字母代表真正的哈希 ID,我们会得到如下所示的内容:

A <-B <-C   <--master

名称会记住 最后一次提交master的哈希 ID ,. 我们说名称指向。Commit包含一个快照加上元数据,并且在元数据中,记住了 commit 的 hash ID ,所以我们说指向。同样,记住 commit 的哈希 ID 。Cmaster CCCBCBBA

提交A有点特别,因为它是有史以来的第一次提交。它没有更早的提交要记住,因此它没有保存的父级。Git 将此称为根提交,这意味着我们可以停止向后看。

要添加一个的提交,我们从最后一个开始——在本例中是C提取它的文件。提交中的文件是一种特殊的、只读的、仅限 Git 的、冻结和压缩的格式,2所以要对提交进行任何实际工作,我们必须先将其提取。提取提交C后,Git 知道当前提交是C. 然后我们做我们通常的事情并进行新的提交:

A--B--C   <-- master
       \
        D

新的提交D指向C(这应该是一个箭头,但是箭头太难画了,所以我用连接线代替了大部分)。然后git commit执行它的魔术:它将D的哈希 ID 写入name master,因此master现在指向D

A--B--C
       \
        D   <-- master

(现在我们可以理顺线条:不再需要图形中的扭结)。


1提交可以进一步分解,就像原子可以分解成质子、中子和电子一样,但是一旦你将它们分解,它们就不再是原子的了,以一种巧妙的方式。

2我喜欢将这些冻结的、Git 化的文件称为 freeze-dried”。因为它们冻结的——事实上,它们是经过哈希处理的,就像提交一样——的提交可以共享先前提交中现有的冻结文件。那就是Git 存储库不会很快膨胀的原因之一:大多数新提交主要重用以前提交的所有文件。

由于没有散列的 Git 对象可以更改,因此继续重用现有对象是完全安全的。提交总是获得唯一的 ID,因为它们具有时间戳和父链接等。您可以重复使用提交 ID 的唯一方法是制作相同的快照,具有相同的父母同时- 到完全相同的秒 - 就像制作早期快照时一样。因此,如果您今天制作昨天制作的相同快照,时间设置回昨天,重新使用昨天的日志消息和昨天的所有其他内容,您将再次获得相同的提交......这就是您昨天做的,有什么问题吗?

有一种方法可以通过脚本在多个分支上同时进行多个提交。如果你开始这些分支指向同一个提交,这会使它们指向同一个最终提交——这起初令人惊讶,但并没有被破坏。

由于鸽巢原理,哈希冲突也存在理论上的问题,但在实践中从未发生过。另请参阅新发现的 SHA-1 冲突如何影响 Git?


分支名称只是指向现有提交的指针

这一切都意味着分支名称本身实际上做的很少。他们所做的一件事就是记住一些提交的哈希 ID。由于哈希 ID 又大又丑,而且人类无法记住,这实际上非常有用。这不是很多工作

在 Git 中,您可以拥有任意数量的分支名称,它们都指向同一个提交。您还可以随时移动您的任何分支名称,只要每个分支名称都指向您确实拥有的一个提交。所以如果我们有:

A--B--C--D   <-- master

我们可以通过运行添加更多名称D,例如:

git branch dev

让我现在这样画:

A--B--C--D   <-- master (HEAD), dev

HEAD在括号中添加了特殊名称,附加到 name master。这是 Git 在现实中所做的示意图:Git 将分支的名称即存储master在它用于3HEAD的文件中,以将特殊名称“附加”到分支名称。这就是 Git 知道你在哪个分支上的方式——然后是分支名称本身,在这种情况下,Git 也知道你在哪个提交上。master

现在让我们做一个新的提交,并调用它E。Git 会照常写出快照和元数据。由于当前提交是DE其父级将是D。然后,当 Git 将提交保存E到 all-commits 数据库时,Git 会将E的哈希 ID 写入HEAD附加到的任何分支名称中,在本例中为master,给我们:

           E   <-- master (HEAD)
          /
A--B--C--D   <-- dev

HEAD仍然附加到master,但现在master指向链的最后一个提交,即E. 名称dev仍指向D; A通过提交D现在在两个分支上;并且提交E仅在master.

这是 Git 中的日常开发:

  • 选择要附加HEAD到的分支,该分支选择其提示提交
  • 从该提交中提取所有文件,以便我们可以使用 / 处理它们
  • 做我们通常做的事情
  • 进行新提交:打包 Git索引4中的任何内容以进行新提交,其父项是当前提交,然后更新当前分支名称以指向新提交。

通过这样做,随着时间的推移,分支会增长——一次提交一个。


3 Git 实际上为此使用了一个文件,至少在今天是这样。但是,不能保证有一天它不会改变方法:通常,如果您正在编写低级脚本,您应该HEAD使用提供的程序进行读写:git rev-parsegit symbolic-refgit update-ref等等;或git branch类似的东西,用于更正常的日常使用。

4 Git 也称为staging area的索引在此答案中没有正确解决,但它git commit确实有效。虽然索引在冲突合并期间发挥了扩展的作用,但它的主要功能是充当您想要放入下一次提交的文件的保存区域。它开始匹配当前提交中的文件副本。

从技术上讲,索引保存的是哈希 ID,而不是实际的文件副本。但是除非并且直到您开始使用git update-indexand git ls-files --stage,否则您可以将索引视为保存每个文件的预冻干副本。


合并(真正的合并)

最终,我们可能会有这样的事情:

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

我们现在想要feature分支(实际上是 commit L,加上我们向后处理的历史记录L, K, H,等)合并到G当前master分支中,即 , Jthen I, thenH等等G

为了完成这个合并,我们将运行git merge feature. Git 将定位不是一个,不是两个,而是三个提交:

  • 提交 #1 将是合并基础,但在我们到达那里之前,让我们定位 #2 和 #3。
  • 提交 #2 是当前提交,这非常简单:它是HEAD, 即J.
  • 提交 #3 也很简单:它就是我们命名的那个。我们说过git merge feature,名称feature指向L,所以 commit #3 是 commit L

然后,合并基础是最好的共享(公共)提交,我们从两个技巧开始并向后工作来找到它。在这种情况下,很明显:两个分支上的最佳提交是H.

合并现在通过比较所有三个提交的快照来进行。(请记住,每个提交都有所有文件的完整快照。)比较HvsJ告诉 Git 我们在 () 分支上所做更改master;比较HvsL告诉 git 他们在 ( ) 分支上所做更改feature。合并现在简单或复杂地组合这两个更改,将合并的更改应用于合并基础中的快照H,如果一切顺利,则创建一个新的提交,Git 称之为合并提交

新的合并提交几乎以通常的方式进行:索引内容的快照、日志消息和基于当前分支的父级。这个合并提交的特别之处在于它也有第二个父级。合并的第二个父项是您合并的提交——在本例中为 commit L。因此,如果一切顺利,Git 会M自己进行这个新的合并提交:

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

提交M点返回到和,但除此之外, J L任何其他提交相同。注意当前分支名称master现在如何指向最后一次提交M;但还要注意如何M返回到 J L以便所有这些提交现在都在master

快进“合并”

如果可以的话,该git merge命令可以并且默认情况下会做一些根本不是合并的事情。假设我们有:

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

如果我们运行git merge dev,Git 会像往常一样找到三个感兴趣的提交:#2 是HEADwhich is H,#3 是 from devwhich is J,合并基础是两个分支上最好的共享提交,H再次是 ...。

如果我们让 Git 将快照中H的快照与中的快照进行比较H,会有什么不同?(这是一个简单的练习。想一想。我们必须更改哪些文件才能从保存在H中的文件变为 中的文件H?)

由于从Hto没有任何变化H,我们将得到的唯一变化是那些从Hto J——--theirs集合——如果我们进行真正的合并的话。我们可以强制 Git 进行真正的合并,如果我们这样做了,Git 将尽职尽责地将无更改与更改合并并进行新的合并提交M

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

如果我们运行,我们将得到它git merge --no-ff dev。但默认情况下,Git 会说:将无与物结合会得到某种东西;应用某些东西来H获取快照J;所以让我们重用现有的提交J 运行git merge devorgit merge --ff-only dev将执行快进而不是合并,给我们:

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

实际上,只需检查提交J并移动master到指向J. (像往常一样,特殊名称HEAD仍然附加。)

壁球合并

您还可以使用git merge --squash. 在这里,Git 完成了完全合并的大部分常见动作。这意味着它适用于类似快进的情况,但也适用于真正的类似合并的情况:

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

Git 将像往常一样进行比较和组合——如果我们有这样的简单结果,则与往常一样:

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

——然后准备好进行新的提交以保存合并快照。但是,Git 并没有将新提交作为合并提交,而是假装你告诉了它--no-commit,从而抑制了提交。然后你必须自己运行git commit,当你这样做时,Git 会使用单亲进行普通提交:

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

例如,Seasy-merging commit 产生的“squash merge”快照在哪里J,或者:

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

真正合并并用作合并基础的S“壁球合并”快照在哪里。JLH

请注意,在这两种情况下,“压扁”一侧的任何提交都不再有用。当我们 squash-mergedfeature时,commitsK-L做了一些事情,但是 commitS同样的事情,不管是什么,commit J。我们不再需要提交K-L

你得到的是合并壁球或变基的结果

我们还没有介绍变基——我们马上就会到那里——但让我们看看这个:

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

如果我们愿意,我们现在可以运行git merge feature(尽管这通常不是一个好主意)。Git 将比较HvsS以查看我们更改了什么,以及HvsL以查看它们更改了什么。然后,Git 将尽其所能组合这两组更改。

由于S已经包含H-vs-L更改,如果我们幸运(或者不幸?),没有冲突,Git 意识到它可以完全忽略H-vs-L部分并仅使用H-vs-S部分。或者,也许我们会遇到一些冲突。我们何时以及是否发生冲突取决于H-vs-J部分是什么,但没有得到任何冲突是很常见的。也许我们手动解决一些冲突;M无论哪种方式,我们都会继续进行新的合并提交,即使按字母顺序排列,我也会调用SM

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

我们现在在图中有了这个合并气泡,并且冗余提交K-L作为 merge 的第二个父级M

我们稍后会看到如何完全摆脱M

变基

git rebase命令通过复制提交来工作。我在一开始就提到不可能更改任何提交,但您可以取出一个提交(或比较两个提交),对文件大惊小怪,然后进行的提交。我们可以使用此属性将提交复制到新的和改进的版本。

让我们从:

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

提交I并且J非常好,但是如果我们让 Git 找出从to所做的更改,并将相同的更改应用于 中的快照呢?让我们分离,让它直接指向这个新的提交之后:HIL HEADL

                I'  <-- HEAD
               /
...--G--H--K--L   <-- master
         \
          I--J   <-- feature

提交I'是我们的副本I——这就是我们称之为它的原因I'——因为我们让 Git 复制提交消息和所有内容。

I原始和副本之间的区别在于,I'I'具有L作为其父级和不同的快照,因此与其父级进行比较 得到的结果与与其父级进行比较的结果相同。I'L IH

此复制过程由git cherry-pick. 5 Cherry-pick 是 Git 的通用“复制提交”操作,在内部,它使用与 full 相同的引擎git merge,但您大多可以将其视为“复制提交”。6 复制I到 后I',我们现在需要复制JJ'

                I'-J'  <-- HEAD
               /
...--G--H--K--L   <-- master
         \
          I--J   <-- feature

现在,既然I'-J'是我们新的和改进的提交,我们希望我们的 Git放弃原始提交,转而支持这些新提交。为了实现这一点,我们的 Git 将简单地feature从提交中剥离标签J并使其指向J'。完成后,我们的 Git 可以重新附加 HEAD到分支名称feature

                I'-J'  <-- feature (HEAD)
               /
...--G--H--K--L   <-- master
         \
          I--J   [abandoned]

由于我们通过从分支名称开始查找提交,找到其存储的哈希 ID,然后查找提交,所以当我们查看这个存储库时,看起来我们以某种方式更改了两个提交。而不是J然后I,我们看到J'然后I'。但如果我们仔细观察,我们会发现这些是不同的哈希 ID。


5某些形式的git rebase真正、实际运行git cherry-pick。其他人(主要是旧形式的变基)没有,但非常接近地模拟它。

6例外情况是在复制过程中遇到合并冲突,但我们不会在这里讨论。


分布式存储库

回到开头,我提到要记住的最重要的事情是 Git 是分布式的,并且存储库的副本不止一个。

在我们的例子中,假设我们有本地 Git,在我们的机器上,另一个 Git 在 GitHub 上。(在某种程度上,另一个 Git 在哪里并不重要——GitHub、Bitbucket、GitLab、企业服务器等等:它们的工作方式几乎相同,因为它们在某个 IP 地址后面都有一个 Git。最大的区别是托管公司通过网站添加自己的用户界面,并且网络界面不同。)

不管怎样,我们让我们的 Git 通过 URL 调用他们的Git——无论“他们”是谁,该 URL 转换为我们提供给服务器的一些 IP 地址和路径名。Git 将此 URL 存储在一个名称下,Git 将其称为远程. 任何遥控器的标准名字都是origin,所以我们将在这里使用它作为名字。

由于 Git over atorigin 一个 Git 存储库,因此它有自己的分支名称。我们的分支名称,在我们的 Git 中,是我们的。他们的就是他们的。他们不需要匹配!特别是,当我们向分支添加提交时,我们将“领先于”他们的分支。

让我们从在我们的机器上根本没有 Git 存储库开始(也许我们必须买一台新的笔记本电脑,或其他什么)。我们将git clone 他们的Git 存储库:

git clone <url>

我们计算机上的 Git 将创建一个新的、完全空的存储库,并添加名称origin以存储 URL。然后它会调用他们的 Git 并让他们列出他们的分支名称,以及这些分支名称选择的提交的哈希 ID。他们将提议为这些分支发送这些提示提交。

对于每个提交哈希 ID,我们的 Git 会说:是的,我想要那个提交。假设这是 commit Hon master。他们有义务提供该提交的父级, G。我们的 Git 会检查:我有那个父提交吗? 当然,我们 Git 的对象数据库是空的,所以我们不要。所以我们也会要求G。他们的 Git 会提供F,我们会接受它,依此类推,最后,我们将获得他们拥有的每一个提交(好吧,除了任何被遗弃的提交,如果他们有的话——有时他们会这样做!)。

现在我们将拥有:

...--G--H

在我们的提交数据库中。但是我们还没有任何名称。我们已经完成了从他们那里获得的提交——他们只有master和提交H及其历史,我们得到了所有这些——所以我们的 Git 与他们的 Git 断开了连接。现在,我们的 Git 获取了它们所有的分支名称,也就是master,并通过将我们的远程名称 、 放在前面来重命名每个分支,并origin用斜杠将它们分开:

...--G--H   <-- origin/master

这些origin/*名称是我们 Git 的远程跟踪名称。他们为我们记住了他们的Git分支名称。

对于它的最后一招,我们的git clone跑步git checkout master。我们实际上还没有分支master但是如果您要求 Git 检查您没有的分支,您的 Git 将尝试从相应的远程跟踪名称创建该分支。我们确实有origin/master并且它选择了 commit H,所以我们的 Git 创建了我们的 master指向H,并附加了我们的HEAD那里:

...--G--H   <-- master (HEAD), origin/master

我们git clone的现在完成了。

如果我们现在创建新的提交,它们会以通常的方式添加:

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

我们现在可以使用git push. 当我们这样做时,我们选择两件事:

  • 要发送哪些提交,以及
  • 设置哪个分支名称

如果我们运行git push origin master,我们将选择 commit发送J(因为我们的名字选择了 commit )和要设置的名字(因为我们说)。masterJmastermaster

如果我们愿意,我们可以运行git push origin master:dev发送 J并要求他们设置他们的dev而不是他们的master。你通常不会这样做——更典型的是,你会dev先创建自己的,然后你有Jon dev,然后git push origin dev——但它作为示例很有用。我们发送我们拥有的提交(可能他们没有),然后我们git push要求他们设置他们的 分支名称。与我们的 Git 不同,它们在这里没有远程跟踪名称!git clone远程跟踪名称是和的一个属性git fetch

为了发送它们J,我们必须先发送它们I。我们也会提供给他们H,但他们已经有了,所以他们说不,谢谢,我有那个。这让我们的 Git 压缩得非常好(我们知道他们也有提交H 所有早期的提交!)当我们发送它们IJ. 然后我们要求他们设置他们的分支名称。

如果服务器端存储库是共享的——如果我们不是唯一使用它的人——他们master可能在我们上次与他们交谈后获得了新的提交。例如,也许其他人跑git push origin master了。所以我们发送他们I-J,如果他们有:

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

我们要求他们将其master指向J,他们可能会说ok, done。他们现在有:

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

他们的存储库中。我们的 Git 将相应地更新我们的origin/master。但如果他们有:

...--G--H--K   <-- master
         \
          I--J

他们服从了我们的礼貌要求,他们最终会得到:

          K   [abandoned]
         /
...--G--H--I--J   <-- master

因为任何Git查找提交的方式都是从末尾开始并向后工作。结局是现在J,谁的父母是I,谁的父母是H。没有办法从Hto K:箭头都是单向的,指向后面。所以在这种情况下,他们会说不,我不会设置我的master.

您的 Git 会将其作为错误呈现给您:

  ! rejected (non-fast-forward)

这意味着您必须从他们那里获得他们的新提交,并将其合并到您的工作中,例如,通过或。git mergegit rebase

或者,您可以向他们发送命令,而不是礼貌的请求:将您的设置masterJ 如果他们服从这个命令,他们失去 commit K。很有可能您再也无法从他们那里取回它了。制造的人K可能会生气(但是——无论如何,你可以希望——制造的人K仍然在他们的克隆中拥有它)。

拉取请求和 GitHub 的可点击按钮

拉取请求不是 Git 的东西,而是 GitHub 和其他托管服务提供商提供的东西。它们为您提供了一种在他们所谓的分叉存储库中进行合并的方法。(fork 实际上只是一个添加了一些特殊功能的克隆,其中最大的是这些拉取请求。)

合并 PR 时,GitHub 提供了三个选项。一个是直git merge的,即使快进是可能的,也会进行真正的合并。一种称为“变基和合并”,git rebase即使没有必要,也总是将所有提交复制到新链,然后对新链进行快进式合并。最后一个叫做“squash and merge”,相当于运行git merge --squash

由于 GitHub 的 squash 和 rebase 样式合并总是产生新的哈希 ID,您现在可以遇到我们之前观察到的相同问题,即 squash 后合并。

删除合并(或任何其他提交)

您自己的存储库中,您可以完全控制所有分支名称。您可以使任何分支名称指向任何提交。

那么,假设你有这个:

          I--M   <-- master (HEAD)
         /  /
...--G--H--I'  <-- origin/master

I您之前的原始提交在哪里,您master将其发送给某个将其复制到I并将其放在他们的 master. 你的origin/master仍然指向这个副本I';你master指向你的合并M,谁的第一个父母是I,谁的第二个父母是I'

如果你git fetch origin; git merge origin/master或者你只是git pullwhich 运行,你会得到这个git fetch origin master; git merge FETCH_HEAD。问题再次是,无论出于何种原因,运行的人都origin决定复制您的提交。

如果您想放弃合并M,现在可以运行:

git reset --hard HEAD^        # or HEAD~1 or HEAD~

这将破坏任何未提交的工作,因此请确保您没有任何工作!reset除了它所做的所有其他事情(在这种情况下破坏未提交的工作)之外,该操作还表示要移动当前分支名称。当前分支名称(现在)master将选择的新提交是您在命令行中命名的提交。

您可以使用始终有效的原始哈希 ID:只需将其从git log输出中删除,并且您说过我希望我当前的分支名称选择该 commit。或者,您可以使用名称:例如,分支名称选择名称指向的提交。在这里,我们使用HEAD,表示当前的提交,但随后添加一个后缀:^,表示第一个父级,或者~1,表示倒数一个 first-parent,这是同一个东西。

这意味着 Git 会找到 merge M,然后查看它的第一个父级,即I. 这就是我们所说的git reset --hard,所以我们最终会得到:

            __M   [abandoned]
           /  /
          I  /  <-- master (HEAD)
         /  /
...--G--H--I'  <-- origin/master

画起来有点难——commitM仍然存在,但是没有人指向它,所以我们找不到它。把它从图中拿出来,结果就更清楚了:

          I   <-- master (HEAD)
         /
...--G--H--I'  <-- origin/master

请注意,这是可行的,因为我们从未M向任何其他 Git 提交过提交。只有我们 master知道如何找到 commit M。我们可以重置它,它不会回来。

如果我们在完成后确实发送M到其他 Git,例如 via git push origin master他们将有 commit M。我们可以尝试将它从我们的Git 中重新设置,这会工作一段时间,但是origin/master在我们的存储库中,以及它们 master它们的克隆中,仍然会有合并提交M。为了摆脱它,我们必须说服他们也改变他们 master

一般来说,一旦你共享了一个提交,你就会从其他所有 Git 中再次获得它。Git 是为添加提交而不是删除提交而构建的;默认共享操作是添加到我的收藏,如果合适则合并


推荐阅读