首页 > 解决方案 > 尽管没有进行任何更改,但 Git 本地主机仍处于领先地位

问题描述

抱歉,如果这很明显,我不是 git 专家。

今天早上我进入了我的本地仓库并做了以下事情:

$ git fetch
$ git checkout master

并收到一条我以前见过很多次的消息:

Switched to branch 'master'
Your branch is ahead of 'origin/master' by 8 commits.
  (use "git push" to publish your local commits)

问题是我从未对该分支进行任何更改。我尝试了 git pull 但它只是响应“已经是最新的”。

'Git log origin/master..master' 显示了其他作者对分支的几次提交,这些提交没有出现在远程分支中,所以这条消息似乎很有意义,但我不明白我怎么能得到其他作者的提交在我的本地副本中,不在远程仓库中

我该如何深究?

标签: git

解决方案


TL;DR 是Lasse V. Karlsen 在评论中的推测

在您将它们放入本地存储库之后,也许有人强制推送到远程并从中删除这些提交?

可能是答案。跑:

git reflog origin/master

并在输出中查找字符串forced-update;如果你看到这个,那就是发生的事情。

龙:这一切意味着什么

Git 本质上是关于提交的。提交是我们使用的存储单元。每个提交都有一个“真名”,在每个Git 中普遍相同,这是它的原始哈希 ID——一个大而难看的字母和数字字符串,例如b34789c0b0d3b137f0bb516b417bd8d75e0cb306. 该哈希 ID 表示提交,如果您有 Git 存储库的克隆,则您要么拥有该提交(即将发布的 Git 2.27 版本的一部分),要么没有——嗯,还没有。

但是,我们通常不会通过这些哈希 ID 来引用提交。它们又大又丑,无法记住或重新输入。我们喜欢使用名称:分支名称如master、标签名称如v2.26.01远程跟踪名称origin/master.

为了使标签在所有情况下都能正常工作,我们必须保证永远不会更改某些标签所引用的哈希 ID。一些现代软件——特别是 Go 模块和 Go 的版本缓存——<a href="https://golang.org/ref/mod#versions" rel="nofollow noreferrer">依靠它来顺利运行(尽管你可以限制如果/必要的话,为了安全起见)。

不过,与标签名称相比,分支名称的问题在于,我们通过查找分支名称获得的提交哈希 ID 通常会更改。分支名称实际上在 Git 中定义为存储被视为分支一部分的最后一次提交的哈希 ID。

提交是不可变的,因为它们的真实姓名哈希 ID 是其内容的加密校验和。2 同时,每个提交都包含其每个直接前任或提交的哈希 ID——通常只有一个,合并提交通常有两个。这些形式提交到有向无环图中的节点,通过使每个提交都充当由哈希 ID 命名的顶点和连接的单向边或弧,称为父节点。

只需通过将新提交添加到存储库中的所有提交集,就可以添加该图。也就是说,我们使用git checkoutorgit switch选择某个分支名称作为当前分支,并选择它的最后一次提交——存储分支名称中的哈希 ID——作为当前提交。然后我们像往常一样做我们的工作,Git 打包一个新的快照和一组新的元数据来进行新的提交。新提交的父级是当前提交。3 现在新提交存在并安全地存储在存储库中,4 Git 将新提交的新的唯一哈希 ID 写入当前分支名称,以便分支自动指向(新的)最后一次提交。

这就是分支的增长方式,一次提交一次,它为分支名称的移动提供了“正常方向”:

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

变成:

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

当我们添加新的 commit 时I,就会变成:

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

当我们添加 commitJ等等。

我们实际上无法从该图中删除提交,但我们可以强制任何分支名称向后移动,就像它原来的那样。例如,在添加提交I和之后,我们可以通过强制名称再次保存提交的哈希 IDJ来“删除”它们以实现大多数实际目的:masterH

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

由于 Git 通常通过使用 name来查找master提交,然后通过 commit-to-commit 父链接向后工作,这指向向后,Git 不能再找到commit J,至少不能以 name 开头master。最终,我们的 Git 将I-J真正地提交提交——不是立即进行,并且我们可以通过隐藏的名称找到它们,正如我们将看到的,但最终是.

您自己的 Git远程跟踪名称,例如origin/master,是您的 Git 对其他 Git 分支名称的记忆。也就是说,当您第一次克隆存储库或用于git fetch更新克隆时,您的 Git 通过 Git 保存在名称下的 URL 调用其他 Git。我们称这个名字——在这种情况下origin——一个远程,我们提供这个名字作为 URL 的简写:

git fetch origin

我们的 Git 调用他们的 Git,从他们那里获取他们的分支名称和 last-commit-hash-ID 的列表,并从他们那里获取他们拥有的、我们不需要的、我们需要的任何提交。例如,如果我们有,此时:

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

我们可以调用他们的 Git,也许他们 master现在指向新的提交J

...--F--G--H--I--J   <-- master [in the Git at origin]

我们的 Git 检查,发现我们没有带有 hash ID 的提交J,并从他们的 Git 获取该提交。这也带来了提交I,因为我们总是得到我们没有的一切,5然后更新我们的——我们origin/master他们的 master. 所以现在我们有,在我们自己的本地存储库中:

...--F--G--H   <-- master
            \
             I--J   <-- origin/master

在这一点上,他们的主人是2 ahead——两个提交之前——我们的master. 我们现在可以采取行动将这些提交 , 添加I-J到我们自己的master. 我们可以通过将名称 master“forward”滑动到 commit来让 Git 做到这一点J

...--F--G--H
            \
             I--J   <-- master, origin/master

如果我们用 做这个git merge,Git 称之为快进合并。不涉及实际的合并:Git 实际上只是直接检出 commitJ并将名称更新master为指向 commit J。(还有其他方法可以快进任何分支名称:如果签出提交,并且这不是我们当前的分支,则名称只是“向前滑动”。)

但正如我们上面提到的,我们可以强制一个分支名称“向后移动”,以便“删除”——嗯,某种删除——提交。假设有人在我们添加到我们自己的之后,在另一个 Git上执行此操作。 他们的存储库现在指向 commit 。6 当我们的 Git 通过运行调用他们的 Git 时,我们会看到他们的名字是 commit ,而不是 commit 。他们没有的提交给我们,所以我们的 Git现在更新我们的,给我们这个:I-JmastermasterHgit fetch originmasterHJorigin/master

...--F--G--H   <-- origin/master
            \
             I--J   <-- master

我们现在2 ahead属于他们,就好像我们做出了承诺一样I-J

如果我们被允许,git push origin master我们可以很容易地把这两个提交放回去,只需git push origin master现在运行。如果他们的 Git 真的丢弃了提交I-J,那么现在他们又回来了。如果没有,他们已经有了I-J,我们git push只是让他们快进他们的名字master指向 commit J

I-J无论谁从 Git 存储库中“删除”提交,origin他们这样做都是有原因的。是故意的吗?这是一个错误吗?我们没有办法知道——好吧,我们有一个办法:问他们! 他们知道;我们只能猜测,所以问他们。

但是,如果我们的 GitI-J在某个时候从他们那里获取并相应地更新了我们自己的,我们就可以说这发生了origin/master。这就是我提到的那些隐藏名字的来源。


1在 Git 的 Git 存储库中,v2.26.0表示 commit 274b9cc25322d9ee79aa8e6d4e86f0ffe5ced925,尽管名称实际上解析为标记对象哈希 ID adf6396efeb4e8c12fb07174b4074c4031b2c460,. 然而,标签的全部意义在于我们不需要知道这些。简单、易读、易于理解的名称v2.26.0就足够了。

2实际上,所有 Git 对象都是如此——它并不特定于提交对象。另请参阅新发现的 SHA-1 冲突如何影响 Git?

3如果新提交是合并提交,则其第一个父提交是当前提交。剩下的父母——这就是它成为合并提交的原因;这里通常只有一个其他提交——可以是任何现有提交的哈希 ID,但每一个都必须某个现有提交的哈希 ID。

4请注意,工作树不在存储库中——从某种意义上说,它是一个独立的实体,位于存储库旁边——并且您的文件在提交之前是不安全的。

5除了 Git 所谓的克隆。让我们在这里忽略它们。

6他们的存储库可能实际保存提交,也可能不保存I-J,这取决于他们的 Git 以多快的速度丢弃无法访问的提交。可达性是这里的一个关键概念;有关(更多)关于此的更多信息,请参阅Think Like (a) Git


Reflogs 保留名称的先前值

每当我们的 Git 更新我们的任何名称时——<code>masterorigin/master甚至是标签名称——我们的 Git 将(可选,但默认情况下对我们来说是启用的)将名称的旧值保存在日志中。此日志是给定名称的引用日志。7 这意味着我们——我们origin/master对他们所在位置的记忆master,每次运行时git fetch都会更新——跟踪他们master 过去在哪里git fetch

假设有人故意或意外地强制origin' 名称master向后移动——在上面的示例中从提交J到提交H。进一步假设我们运行了一个git fetch捕获指向J之前提交的名称,现在我们运行git fetch并且名称指向H. 我们的 Git 将以origin/master非快进方式强制更新我们的。8

事后,我们现在可以在 reflog 中查找origin/master

$ git reflog origin/master

就我而言,我可以使用我的 Git 存储库来执行此操作,因为pu分支一直都是这样移动的:

$ git reflog origin/pu
dfeb8fbf42 (origin/pu) refs/remotes/origin/pu@{0}: fetch: forced-update
fc307aa377 refs/remotes/origin/pu@{1}: fetch: forced-update
5525884f08 refs/remotes/origin/pu@{2}: fetch: forced-update
...

这里的“强制更新”部分意味着他们在他们的分支上有一些提交,我们能够使用我们自己的远程跟踪名称来获取和记住这些提交,但随后他们决定将这些提交推到一边。我们的 Git 选择了他们的新pu名称并相应地更新了我们的名称origin/pu


7还有一个特殊名称的 reflog HEAD,但在这种特殊情况下它没有重要功能,因为我们不能“开启”远程跟踪名称,例如origin/master.

8git fetch命令在其输出中报告了这一点,尽管许多人没有注意到它。您将在每行中看到三个指示符:一个前导加号、三个点而不是两个点,以及附加的“强制更新”字样:

   b34789c0b0..07d8ea56f2  master     -> origin/master
   232c24e857..5cccb0e1a8  next       -> origin/next
 + fc307aa377...dfeb8fbf42 pu         -> origin/pu  (forced update)
   139372b246..6b33381979  todo       -> origin/todo

请注意pu分支是如何以非快进方式移动的,并且git fetch已经告诉了我们 3 次。


推荐阅读