git - 尽管没有进行任何更改,但 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' 显示了其他作者对分支的几次提交,这些提交没有出现在远程分支中,所以这条消息似乎很有意义,但我不明白我怎么能得到其他作者的提交在我的本地副本中,不在远程仓库中
我该如何深究?
解决方案
TL;DR 是Lasse V. Karlsen 在评论中的推测:
在您将它们放入本地存储库之后,也许有人强制推送到远程并从中删除这些提交?
可能是答案。跑:
git reflog origin/master
并在输出中查找字符串forced-update
;如果你看到这个,那就是发生的事情。
龙:这一切意味着什么
Git 本质上是关于提交的。提交是我们使用的存储单元。每个提交都有一个“真名”,在每个Git 中普遍相同,这是它的原始哈希 ID——一个大而难看的字母和数字字符串,例如b34789c0b0d3b137f0bb516b417bd8d75e0cb306
. 该哈希 ID 表示该提交,如果您有 Git 存储库的克隆,则您要么拥有该提交(即将发布的 Git 2.27 版本的一部分),要么没有——嗯,还没有。
但是,我们通常不会通过这些哈希 ID 来引用提交。它们又大又丑,无法记住或重新输入。我们喜欢使用名称:分支名称如master
、标签名称如v2.26.0
、1或远程跟踪名称如origin/master
.
为了使标签在所有情况下都能正常工作,我们必须保证永远不会更改某些标签所引用的哈希 ID。一些现代软件——特别是 Go 模块和 Go 的版本缓存——<a href="https://golang.org/ref/mod#versions" rel="nofollow noreferrer">依靠它来顺利运行(尽管你可以限制如果/必要的话,为了安全起见)。
不过,与标签名称相比,分支名称的问题在于,我们通过查找分支名称获得的提交哈希 ID 通常会更改。分支名称实际上在 Git 中定义为存储被视为分支一部分的最后一次提交的哈希 ID。
提交是不可变的,因为它们的真实姓名哈希 ID 是其内容的加密校验和。2 同时,每个提交都包含其每个直接前任或父提交的哈希 ID——通常只有一个,合并提交通常有两个。这些形式提交到有向无环图中的节点,通过使每个提交都充当由哈希 ID 命名的顶点和连接的单向边或弧,称为父节点。
只需通过将新提交添加到存储库中的所有提交集,就可以添加该图。也就是说,我们使用git checkout
orgit 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
来“删除”它们以实现大多数实际目的:master
H
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-J
master
master
H
git fetch origin
master
H
J
origin/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
.
8该git 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 次。
推荐阅读
- api - 如何在flutter中返回并显示计数结果json响应api
- reference - 引用和变量之间的关系
- go - 为什么我的 Gin and Go 测试没有通过?
- ms-access - 使用整数值更新 ms-access 导致算术溢出 将表达式转换为数据类型 int 的算术溢出错误
- flutter - 在标头中传递 JWT 时获取 403
- subquery - N1QL 值 NOT IN 子查询,它返回一个数组
- node.js - 如何在 Alpine Linux 中将 node.js 应用程序作为后台服务运行?
- git - git mv 整个 repo 上一层
- flutter - GestureDetector 在滚动时不接收事件
- typescript - typecript 承诺返回类型异步函数