为什么我的推送中突然出现合并提交?

Int*_*eXX 5 git git-merge git-revert team-explorer git-commit

好吧,我好像把一些事情搞砸了。

直到最近,我曾经能够进行合并提交,然后推送到原点,而无需显示单独的提交。现在,我在管道中只能看到合并提交:

管道在之后提交

在此开始之前,仅手动提交被推送到原点(或至少显示为这样):

管道提交之前

这是行为更改后的团队资源管理器(VS 2019 v16.6.5):

团队探索者

...这是我当地的分支机构历史:

分行历史

看到变化了吗?

a13adadf这一切都是在我恢复提交、修复并重新发布之后开始的。现在我遇到了某种奇怪的分支效应,我不知道如何让事情回到之前的状态。(我尝试研究这个问题,但是在搜索与 相关的任何内容时,信噪比非常低merge commit。)

如何让我的存储库“忽略”(即停止显示)合并提交?

(注意:我是唯一参与此存储库的开发人员。)

tor*_*rek 11

您之前可能执行过快进操作。如果条件正确,该git merge命令将执行此操作而不是合并:

\n
    \n
  1. 快进必须是可能的。
  2. \n
  3. 您需要避免--no-ff选项,这会禁用快进。
  4. \n
\n
\n

a13adadf这一切都是在我恢复提交、修复并重新发布之后开始的。

\n
\n

这一定已经创建了一个分支。这个词\xe2\x80\x94“分支”有一个问题,即\xe2\x80\x94,它会让你误入歧途,但你在问题中显示的图形片段表明这实际上是发生的事情。

\n
\n

如何让我的存储库“忽略”(即停止显示)合并提交?

\n
\n

如果您只是想避免显示它们,您的查看器可能有一些选项可以执行此操作。

\n

如果您想回到不创建\xe2\x80\x94之前的情况\xe2\x80\x94,您需要消除您创建的分支。

\n

Long:这是怎么回事(以及为什么“分支”这个词有问题)

\n

首先要记住的是 Git 的核心就是提交。刚接触 Git 的人,甚至那些已经使用它很长时间的人,常常认为 Git 是关于文件或分支的。但事实并非如此:它与提交有关。

\n

每次提交都有编号,但这些数字不是简单的计数。相反,每次提交都会获得一个看似随机的\xe2\x80\x94,但实际上根本不是随机的\xe2\x80\x94哈希 ID。这些东西又大又难看,Git 有时会缩写它们(例如您的a13adadf),但其中每一个都是某个 Git 对象的数字 ID\xe2\x80\x94,在本例中,用于 Git 提交。

\n

Git 有一个包含所有对象的大型数据库,可以通过 ID 进行查找。如果您给 Git 一个提交编号,它就会通过 ID 找到该提交的内容。

\n

提交的内容分为两部分:

\n
    \n
  • 首先,有 Git 知道的所有文件的快照。这往往是大多数提交的大部分,除了一件事:文件以一种特殊的、只读的、仅限 Git 的、压缩的和去重复的格式存储。当您进行新提交时,其中大多数文件与先前的某些提交基本相同,新提交实际上不会再次存储文件。它只是重新使用现有的文件。换句话说,特定文件的特定版本会在许多提交重新使用它时进行摊销。重复使用是安全的,因为文件是只读的。

    \n
  • \n
  • 除了保存的快照之外,每个提交还存储一些元数据:有关提交本身的信息。这包括提交者的姓名和电子邮件地址,以及一些日期和时间信息等。值得注意的是,每个提交的元数据还存储供 Git 使用的提交号\xe2\x80\x94 以及该特定提交之前的提交的哈希 ID\xe2\x80\x94 。Git 将其称为父提交,或者对于合并提交,称为提交的父级。

    \n
  • \n
\n

这样做的目的是让 Git 能够向后工作。这就是 Git 的工作方式,倒着看。如果我们有一长串提交,全部连续,如下所示:

\n
... <-F <-G <-H\n
Run Code Online (Sandbox Code Playgroud)\n

其中H代表链中最后一次H提交的实际哈希 ID,Git 将从 commit 开始,从其对象数据库中读取它。在 commit 中H,Git 会找到所有保存的文件,以及之前提交的哈希 ID GG如果 Git 需要它,Git 将使用此哈希 ID从对象数据库中读取提交。这为 Git 提供了较早的快照,以及更早提交的哈希 ID F

\n

如果 Git 需要,Git 将使用哈希 ID F(存储在 中G)来读取F,当然还F包含另一个父哈希 ID。所以通过这种方式,Git 可以从最后一次提交开始向后工作。

\n

这就给 Git 带来了一个问题:它如何快速找到链中最后一次提交的哈希 ID?这就是分支名称的用武之地。

\n

分支名称仅保存最后一次提交的哈希 ID

\n

考虑到上面的\xe2\x80\x94,并且故意变得有点懒,并将从提交到提交的连接绘制为一条线,而不是从子项到父项的箭头\xe2\x80\x94,我们现在可以像master这样绘制分支:

\n
...--F--G--H   <-- master\n
Run Code Online (Sandbox Code Playgroud)\n

名称 master仅包含现有提交的实际哈希 ID H

\n

让我们添加另一个名称 ,develop也包含哈希 IDH,如下所示:

\n
...--F--G--H   <-- develop, master\n
Run Code Online (Sandbox Code Playgroud)\n

现在我们有一个小问题:我们要使用哪个名称?在这里,Git 使用特殊名称HEAD来记住要使用哪个分支名称,所以让我们稍微更新一下绘图:

\n
...--F--G--H   <-- develop, master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

这表示之后的结果git checkout master当前分支名称是 now master,并master选择 commit H,因此这就是我们正在使用的提交(以及我们也在使用的分支名称)。

\n

如果我们git checkout develop现在运行,Git 将切换到该分支。该名称仍然标识 commit H,因此没有其他需要更改的内容,但现在我们有:

\n
...--F--G--H   <-- develop (HEAD), master\n
Run Code Online (Sandbox Code Playgroud)\n

如果我们现在进行新的提交,Git 将:

\n
    \n
  • 打包它所知道的所有文件(这是 Git 的索引暂存区域的用武之地,但我们在这里不会介绍它);
  • \n
  • 添加适当的元数据,包括您作为作者和提交者的姓名以及“现在”作为时间戳,但重要的是,使提交成为新提交的H级;
  • \n
  • 使用所有这些来进行新的提交,我们将其称为I.
  • \n
\n

Git 还会做一件事,但现在让我们画出这一部分。结果是:

\n
...--F--G--H\n            \\\n             I\n
Run Code Online (Sandbox Code Playgroud)\n

那两个名字呢?还有一件事:Git 会将I哈希 ID 写入当前名称。如果是develop,我们得到这个:

\n
...--F--G--H   <-- master\n            \\\n             I   <-- develop (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,该master名称保持不变,但名称develop已移动以指向最新的提交。

\n

当两个名称标识相同的提交时,任一名称都会选择该提交

\n

请注意,最初,当masterdevelop都选择 commit 时H,从某种意义上说,您与 一起使用哪一个并不重要git checkout。无论哪种方式,您都会将提交H作为当前提交。但是当你进行新的提交时,现在就很重要了,因为 Git 只会更新一个分支名称。没有人知道新提交的哈希 ID 是什么(因为它部分取决于您进行提交的确切时间),但一旦提交,develop将保留该哈希 ID(如果develop是当前名称)。

\n

请注意,如果您现在git checkout master进行另一次新提交,则名称master将是这次更新的名称:

\n
...--F--G--H--J   <-- master (HEAD)\n            \\\n             I   <-- develop\n
Run Code Online (Sandbox Code Playgroud)\n

不过,我们暂时假设您还没有这样做。

\n

快进

\n

记住前面的图片,让我们git checkout master现在运行,然后返回使用 commit H

\n
...--F--G--H   <-- master (HEAD)\n            \\\n             I   <-- develop\n
Run Code Online (Sandbox Code Playgroud)\n

在这种状态下,我们git merge develop现在就跑吧。

\n

git mergeGit 将执行它对\xe2\x80\x94执行的操作(请参阅下面的 \xe2\x80\x94),并发现合并基础是 commit H,这也是当前提交。另一个提交I位于提交之前H。这些是 Git 可以进行快进操作的条件。

\n

快进并不是真正的合并。发生的情况是 Git 对自己说:如果我进行了真正的合并,我会得到一个快照与 commit 匹配的提交I。因此,我会走捷径,只需在拖动名称的同时检查提交即可。Imaster 结果如下:

\n
...--F--G--H\n            \\\n             I   <-- develop, master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

现在没有理由在绘图中保留扭结\xe2\x80\x94我们可以将其全部排成一排。

\n

真正的合并

\n

有时,上述那种快进而不是合并的技巧根本不起作用。假设您从以下内容开始:

\n
...--G--H   <-- develop, master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

并进行两个新的提交I-J

\n
          I--J   <-- master (HEAD)\n         /\n...--G--H   <-- develop\n
Run Code Online (Sandbox Code Playgroud)\n

现在您git checkout develop再进行两次提交K-L

\n
          I--J   <-- master\n         /\n...--G--H\n         \\\n          K--L   <-- develop (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

此时,无论您给 哪个名称git checkout,如果您git merge使用另一个名称运行,则无法从前进JL,反之亦然。从J,您必须备份到I,然后转到共享提交H,然后才能前进到K然后L

\n

因此,这种合并不能是快进操作。相反,Git 会进行真正的合并。

\n

为了执行合并,Git 使用:

\n
    \n
  • 当前 ( HEAD) 提交:让我们先这样Jgit checkout master
  • \n
  • 您命名的另一个提交:让我们使用git merge develop选择提交L
  • \n
  • 以及 Git 自己发现的另一次提交。
  • \n
\n

最后\xe2\x80\x94 或者实际上,第一个\xe2\x80\x94commit 是合并基础,合并基础是根据称为最低公共祖先的图形操作来定义的,但简短且易于理解的版本是Git 工作从两个提交向后查找最佳共享的共同祖先。在本例中,这就是 commit H:两个分支的分歧点。虽然 commitG和更早的提交也可以共享,但它们不如 commit 好H

\n

所以 Git 现在将:

\n
    \n
  • 将合并基础H快照与HEAD/J快照进行比较,看看我们更改了什么master
  • \n
  • 将合并基础H快照与其他/L快照进行比较,看看它们发生了什么变化develop;和
  • \n
  • 合并两组更改,并将它们应用到合并基础快照。
  • \n
\n

这是合并的过程,或者作为动词合并。如果可以的话,Git 会自己完成所有这些工作。如果成功,Git 将进行一次新的提交,我们将其称为M

\n
          I--J\n         /    \\\n...--G--H      M   <-- master (HEAD)\n         \\    /\n          K--L   <-- develop\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,新提交M指向提交. 事实上,这就是使这个新提交成为合并提交的原因。因为快进实际上是不可能的,所以 Git必须进行此提交,才能实现合并。J L

\n

您最初进行的是快进

\n

你一开始是这样的情况:

\n
...--G--H   <-- master, develop (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

然后产生:

\n
...--G--H   <-- master\n         \\\n          I   <-- develop (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

您使用git checkout master; git merge develop或类似的方式获得:

\n
...--G--H--I   <-- master (HEAD), develop\n
Run Code Online (Sandbox Code Playgroud)\n

之后您可以重复该过程,首先创建develop,然后创建两个develop master命名新提交J

\n
...--G--H--I--J   <-- master (HEAD), develop\n
Run Code Online (Sandbox Code Playgroud)\n

但此时你做了一些不同的事情:你git revertmaster.

\n

git revert命令进行新的提交。新提交的快照就像前一个快照一样,只是回退了一个提交,所以现在您拥有:

\n
                K   <-- master (HEAD)\n               /\n...--G--H--I--J   <-- develop\n
Run Code Online (Sandbox Code Playgroud)\n

中的快照K可能与中的快照匹配I(因此它重新使用所有这些文件),但提交号是全新的。

\n

从这里开始,您git checkout develop编写了一个比 更好的提交J,我们可以将其称为L

\n
                K   <-- master\n               /\n...--G--H--I--J--L   <-- develop (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

然后你就回去master跑了git merge develop。这一次,Git必须进行新的合并提交。所以它就这么做了:

\n
                K--M   <-- master (HEAD)\n               /  /\n...--G--H--I--J--L   <-- develop\n
Run Code Online (Sandbox Code Playgroud)\n

现在,当您返回develop并进行新的提交时,您会得到相同的模式:

\n
                K--M   <-- master\n               /  /\n...--G--H--I--J--L--N   <-- develop (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

当您切换回masterand时git merge develop,Git 必须再次进行新的合并提交。快进是不可能的,相反你会得到:

\n
                K--M--O   <-- master (HEAD)\n               /  /  /\n...--G--H--I--J--L--N   <-- develop\n
Run Code Online (Sandbox Code Playgroud)\n

对此你能做些什么

\n

假设您现在运行git checkout develop && git merge --ff-only master. 第一步选择develop作为当前分支。第二个要求与 合并master。这个额外的标志--ff-only告诉 Git:但只有当你可以快进时才这样做。

\n

(我们已经相信 Git 可以快进地做到这一点,所以这个--ff-only标志只是一个安全检查。不过,我认为这是一个好主意。)

\n

由于可以快进,您将得到:

\n
                K--M--O   <-- master, develop (HEAD)\n               /  /  /\n...--G--H--I--J--L--N\n
Run Code Online (Sandbox Code Playgroud)\n

请注意名称如何develop向前移动,指向 commit O,而不添加新的合并提交。这意味着您所做的下一次提交 develop作为O其父提交,如下所示:

\n
                        P   <-- develop (HEAD)\n                       /\n                K--M--O   <-- master\n               /  /  /\n...--G--H--I--J--L--N\n
Run Code Online (Sandbox Code Playgroud)\n

如果您现在git checkout master; git merge develop将获得快进,两个名称都标识新的提交P,并且您将回到提交develop允许快进的情况。

\n

develop请注意,通过这样做,您实际上是在声称您根本不需要该名称

\n

如果你的工作模式是:

\n
    \n
  • 进行新的提交
  • \n
  • 向前拖动master以匹配
  • \n
\n

那么您需要做的就是在 上进行新的提交master

\n

对另一个名称进行新的提交本质上并没有什么问题,如果这只是您有时的工作模式,那么这可能是一个好习惯:使用大量分支名称稍后会对您有所帮助,并且养成在之前创建新名称的习惯开始工作是好的。不过,您可能需要考虑使用比 更有意义的名称develop

\n

无论如何,请注意,Git 在这里关心的是提交。分支名称只是让 Git 帮助您查找特定提交的方式:每个名称找到的提交就是您使用该名称进行工作的点。实际的分支(如果有的话)是您所做的提交的函数。

\n

换句话说:要使提交形成分支,您需要分支名称,但仅拥有分支名称并不会使提交形成分支。 那是:

\n
...--F--G--H   <-- master\n            \\\n             I--J   <-- develop\n
Run Code Online (Sandbox Code Playgroud)\n

为您提供两个“最后”提交,但以 commit 结尾的单个线性链J。在一种意义上,有两个分支,其中一个以 结束H,其中一个以 结束J,但在另一种意义上,只有一个分支,以 结束J。我们可以添加更多名称,指向现有的提交:

\n
...--F   <-- old\n      \\\n       G--H   <-- master\n           \\\n            I--J   <-- develop\n
Run Code Online (Sandbox Code Playgroud)\n

现在有三个名称(和三个“最后”提交),但存储库中的实际提交集并未更改。我们只是F单独画了一条线,以便让名字old指向它。

\n