注意:在我撰写此答案时,这个特定的问题已经相当老了。它是在 Git 发布修复许多这些问题的版本之前三年发布的。然而,添加现代答案和解释似乎是值得的。现有的被接受的答案建议运行git fetch -p
,1这是一个好主意,虽然现在不那么经常需要。Git 1.8.2 版本发布之前,它是更加必要的;那个版本发布于原始问题之后三年。
1-p
或--prune
选项不是必需的,仅在链接的答案中建议使用。请参阅下面更长的部分以了解其作用。
这是如何发生的?
最初的问题是:
这是如何发生的?
所谓的“这”是指在执行git push origin master
后,OP运行git status
并看到消息On branch master
,后面跟着Your branch is ahead of 'origin/master' by 1 commit.
要正确回答这个问题,我们需要把它分成几个部分。
首先,每个(本地)分支都有一个上游设置
这个说法实际上有点过于强硬了。在你自己的Git仓库中,每个你自己的本地分支可以有一个Git称之为上游的设置。或者,该分支可能没有上游。旧版本的Git在称之为上游设置方面不太一致,但在现代Git中,它更加一致。我们还有git branch --set-upstream-to
和git branch --unset-upstream
用于设置或清除上游。
这些--set-upstream-to
和--unset-upstream
会影响当前分支。当前分支是你使用git checkout
或自Git 2.23以来使用的git switch
2选择的分支。无论你检出了哪个分支,那就是你所在的分支。3
如果使用--unset-upstream
,这将删除当前分支的上游。没有上游,这会停止有关超前或落后或分歧的消息。但是,这条消息旨在有用,因此您可能不应该只删除上游以使其停止发生。(如果您不觉得它有用,可以随意忽略该消息,毕竟它不是一个错误。)
如果运行git branch --set-upstream-to=origin/xyzzy
,则将当前分支的上游设置为origin/xyzzy
。对于名为xyzzy
的分支,这将是典型的正确设置。某些创建分支的操作会自动设置(通常正确的)上游,而有些则不会,因此如果您使用了自动设置正确上游的分支创建操作,则无需执行任何操作。如果您需要不同的上游,或者如果您使用了未设置任何上游的分支创建操作,则可以使用此选项更改上游。
你可以将上游设置为以下内容:
- 自己的另一个本地分支:
git branch --set-upstream-to=experiment
将你自己的本地 experiment
设置为当前分支的上游;或者
- 你的任何一个远程跟踪名称,例如
origin/main
或者 origin/master
或者 origin/xyzzy
。这些是由 git branch -r
输出的名称。Git 将它们称为 远程跟踪分支名称(我喜欢在这里省略“分支”一词),我们稍后会详细讨论。
git status
打印的 ahead、behind、up-to-date 或 diverged 消息是从运行下面这个看起来有点神奇的命令中得出的:
git rev-list --count --left-right $branch...$upstream
这里的$branch
是当前分支名称,$upstream
是从其上游设置中获取的字符串(来自上面的git branch --set-upstream-to
)。两个名称之间有三个点,--count
、--left-right
和三个点都是必需的,以便让git rev-list
输出这两个数字。
如果您的Git版本为2.23或更高版本,则建议使用
git switch
,因为它避免了一些棘手的
git checkout
行为,这些行为通常会让初学者陷入麻烦(甚至会让Git专家出错)。但是,如果您习惯于使用
git checkout
,则可以继续使用,因为它仍然受支持。实际问题基本上是
git checkout
过于强大,可能会意外破坏工作。新的
git switch
有意减少了功能,不会这样做;“故意破坏我的工作”操作已移至
git restore
。
在Git中,可能处于无分支状态,即Git称之为“游离HEAD”模式。如果使用
git checkout
,它可能会突然将您置于此模式下(尽管会打印一个大而可怕的警告,因此如果您没有看到可怕的警告,则没有这样做),但如果使用
git switch
,则必须使用
git switch --detach
来允许游离HEAD模式。这种模式没有任何问题,只需要小心,一旦进入该模式,不要丢失任何新提交。如果不小心就容易丢失它们。在正常模式下,Git不会像这样丢失新提交。
如果您处于游离HEAD模式,则没有上游分支——根据定义,因为您没有分支——并且此问题中的任何内容都不适用。
可达性
这部分内容比较技术性,我将大部分转交给网站Think Like (a) Git。但是我会在这里概括一下:像
或者这样的分支名称和origin/main
,origin/xyzzy
这样的远程跟踪名称是Git用来找到提交的方式。Git的核心在于提交。你的分支名称仅在查找你的提交时起作用。当然,如果你无法找到它们,那就有麻烦了,所以你的分支名称很重要。但关键是可达性,这是一个技术术语。
Git存储库中的每个提交都有一个编号,由一串大而且丑陋的十六进制字母和数字组成。这是提交的哈希ID,也是Git实际查找提交的方式。
每个提交包含两个内容:每个源文件的完整快照(以特殊压缩、Git格式化和去重的形式)和一些关于提交本身的信息:例如,元数据告诉我们谁在何时以及为什么(他们的日志信息)。在元数据中,每个提交都保存了先前某些提交的提交编号。这意味着一个提交可以找到另一个早期的提交。
如Think Like (a) Git所述,这有点像一辆火车。当你在火车上时,它会向后自动带你到所有先前停靠站。但首先你要找到去火车站的路。Git分支名称也将完成这个任务:它保持了你的分支上最新提交的哈希ID。
我们可以像这样画出来:
... <-F <-G <-H <--branch
分支名称branch
保存着最新提交的哈希ID。我们称这个名称指向该提交。无论实际上是什么样的大而丑陋的哈希ID,我们在此处使用字母H
来代替它。
H
是一个实际的提交,因此它有一个保存的快照——您的文件和一些元数据。在元数据中,Git保存了一个早期提交的哈希ID。我们将那个早期提交称为G
。我们说H
指向G
。Git可以通过分支名称指针找到H
,这给了Git访问提交的权限,包括元数据,因此现在Git具有早期提交G
的哈希ID。
当然,G
也是一个实际的提交:它有一个保存的快照和一些元数据。在G
的元数据中,Git保存了早期提交F
的哈希ID。我们说G
指向F
,现在Git可以使用这个保存的哈希ID找到F
。
这将一直重复下去,或者说,直到我们到达有史以来的第一个提交。该提交(在这里我们可能称其为A
)不向后指向早期提交,因为没有早期提交。
这个可达性的概念基本上是从我们从分支名称branch
找到提交H
开始,向后工作所发生的事情的总结。我们到达提交H
,它向后指向提交G
,后者又指向F
,依此类推。
分支名称和远程跟踪名称
正如我们刚才提到的,一个分支名称保存了某个提交的原始哈希ID。这使Git能够找到该提交。然而,关于分支名称还有另一个特殊功能。
当您使用git checkout
或git switch
进入一个分支,并进行新的提交时,Git会自动更新分支名称存储的哈希ID。也就是说,假设我们有一系列提交,就像这样:
...--F--G--H <-- xyzzy (HEAD)
我们正在分支{{xyzzy}}上,我喜欢将特殊名称{{HEAD}}附加到它上面表示。当图表中有多个分支名称时,这很有用。请注意,{{H}}目前是最新的提交。但现在我们会按照通常的方式再做一个提交。
这个新的提交会得到一个新的、唯一的、又大又丑的十六进制哈希值,就像任何提交一样。Git确保新的提交向后指向提交{{H}},因为那是我们用来创建新提交的提交。我们将使用字母{{I}}表示这个新提交。让我们把它画出来:
...--F--G--H <-- xyzzy (HEAD)
\
I
这张图片实际上是中间提交状态:Git已经完成了I
,但还没有完成git commit
操作。现在要问自己一个问题:我们如何才能以后找到提交I
?我们需要它的哈希ID。那么我们可以把哈希ID存储在哪里呢?
如果你说:分支名称中,那么你就是对的。实际上,就Git而言,正确的分支名称是你当前所在的分支。在这个图示中,我们已经将HEAD
附加到该分支上。因此,作为git commit
的最后一部分,Git会将I
的哈希ID写入名称xyzzy
中。这使得它指向提交I
,如下所示:
...--F--G--H
\
I <-- xyzzy (HEAD)
现在图中没有弯曲的原因,所以我们可以将其拉直:
...--F--G--H--I <-- xyzzy (HEAD)
这就是分支名称的工作原理。最终它非常简单:只需要同时理解几个概念。名称找到提交。它找到最新提交。从那里开始,Git向后工作,因为每个提交都会找到一个更早的提交。
那么远程跟踪名称呢?这里的诀窍在于你的Git与其他Git进行通信。每个Git都有自己的分支名称。你有你的master或main;他们有他们的。你有你的xyzzy分支,他们也可以有自己的分支。
你的Git可能一直打电话给他们的Git,并询问他们的分支名称。然而,这样并不高效,如果你断网了就行不通。无论如何,Git不会这样做。相反,当你的Git调用他们的Git并从他们那里获取所有分支名称和哈希ID的列表时,你的 Git会将这些名称和哈希ID存储在你的存储库中。当你运行git fetch时,就会发生这种情况。
但是存在一个问题。他们的main或master,或者如果他们有一个xyzzy,并不一定意味着与你的main或master或xyzzy相同的提交。然而,解决方案很简单:Git只需将他们的分支名称转换为你的远程跟踪名称。
如果origin的main或master或xyzzy已移动,则只需运行git fetch或git fetch origin,可能还要使用--prune。你的Git调用他们的Git。他们列出他们的分支名称和提交哈希ID。如有必要,您的Git从他们那里获取任何新的提交:他们拥有的,你没有的提交。然后,你的Git将他们的分支名称转换为你的远程跟踪名称,并创建或更新你的远程跟踪名称,以记住在你运行此git fetch时他们的分支名称指向的位置。
如果你使用--prune
,这将处理他们删除一些分支名称的情况。假设他们有一个名为oldstuff
的分支。您之前获取了它,因此在您的远程跟踪名称中有origin/oldstuff
。然后他们删除oldstuff
,所以这一次他们...就没有了。如果没有--prune
,您的Git会忽略它。即使它现在已经死了,您仍将保留您的旧origin/oldstuff
。使用--prune
,您的Git会说:哦,嗯,这看起来已经失效了并将其修剪掉:在您的 Git中的远程跟踪名称不对应于其中一个他们的分支名称,就会被删除。
< p >修剪选项可能应该始终是默认值,但它不是,因此现在无法成为默认值。6 但是,您可以配置fetch.prune
为true
并使它成为您的默认值。
4在2021年,这种情况比2010年时更少见。2005年Git首次发布时,这种情况普遍存在。例如,在前往Linux会议的航班上,你以任何价格都无法获得互联网访问。
5选择采用哪些名称,以及何时采用它们,实际上是答案的一部分。在Git中,这已经随着时间的推移而发生了变化,并且仍在略微变化中,尽管仍存在各种限制。我们不会详细介绍所有细节。
6Git通常非常重视向后兼容性。例如,它花费了1.x到2.0的转换来将push.default
默认设置从matching
更改为simple
。
如何使用git rev-list
获取这两个数字
之前我提到过,git status
打印的ahead、behind、up-to-date或diverged信息是通过运行以下命令得到的:
git rev-list --count --left-right $branch...$upstream
这里git rev-list
的作用是计算可访问提交的数量。在gitrevisions文档中描述的三点语法产生了集合论中所谓的对称差异。虽然不是数学术语,但我们可以将其视为进行两次提交可达性测试,可以像这样绘制:
I--J <-- xyzzy (HEAD)
/
...--G--H
\
K <-- origin/xyzzy
在这里,提交J
可以从你的分支名称xyzzy
到达,因为该名称指向那里。提交I
可以从提交J
到达,因此也计入其中。这将导致回到提交H
——正如您从图表中看到的那样,这有点特殊。
同时,提交K
可以从您的远程跟踪名称origin/xyzzy
到达。提交H
可以从K
到达。从提交H
开始,提交G
、F
等等都是可达的。但是两个“铁路轨道”在提交H
处汇合:提交H
和所有更早的提交都可以从两个名称到达。
这使得提交I-J
很特殊,因为它们仅从名称xyzzy
可达,而提交K
很特殊,因为它仅从名称origin/xyzzy
可达。三个点符号找到这些提交:仅从一个名称或仅从另一个名称可达的提交。
如果我们将分支名称放在左侧,将其上游放在右侧,并使用三个点符号,我们将找到此情况下的所有三个提交。使用--count
使git rev-list
打印此数字:3。使用--left-right
告诉git rev-list
变得更加智能:它应该计算由左侧名称——当前分支名称——计算了多少提交,以及由右侧名称——上游计算了多少提交。因此,在两个选项和三个点的情况下,我们得到:
2 1
作为输出,告诉我们在xyzzy
上有两个提交不在origin/xyzzy
上,而在origin/xyzzy
上有一个提交不在xyzzy
上。这些是分别在xyzzy
上的J
和I
提交以及在origin/xyzzy
上的K
提交。
如果没有--count
选项,git rev-list
将列出哈希ID,并加上左侧的<
或右侧的>
符号。使用git log
代替git rev-list
,如下所示:
git log --left-right xyzzy...origin/xyzzy
(请注意三个点:查看gitrevisions并搜索Symmetric Difference) 我们将看到显示的三个提交,再次根据需要加上<
或>
前缀。
这是一种简单的方法,可以看到哪些提交在您的分支上,哪些提交在上游分支上。 通常与--decorate
、--oneline
和--graph
一起使用更有用(在某些情况下,您可能还想添加--boundary
)。
提前、落后、分叉或最新
因此,假设我们运行了:
git rev-list --count --left-right $branch...$upstream
(或者-再次查看gitrevisions-在这里使用$branch@{upstream}
)并获得我们的两个计数。它们可以是:
0
和0
:我们的分支名称和我们的远程跟踪名称(或上游中的任何内容)指向相同的提交。没有人领先或落后。 git status
命令将显示Your branch is up to date with '<upstream>'
。
非零,零:当前分支上有未在上游分支上的提交。上游分支上没有未在当前分支上的提交。因此,我们的分支领先于上游。
零,非零:当前分支上没有未在上游分支上的提交,但上游分支上有一些未在当前分支上的提交。这意味着我们的分支落后于上游。
非零,非零:这就像我上面画的图表。当前分支及其上游同时领先和落后于彼此。 git status
命令将使用单词diverged
。
现在我们将回到最初的问题。假设当前分支的上游是一个远程跟踪名称。请注意,git rev-list
得到的计数是基于我们远程跟踪分支名称中的内容。
这实际上是如何发生的?
在OP的情况下,只有一个人正在创建新提交并使用git push
将其发送。如果我是唯一的人,我可能会从GitHub git clone
一些东西,然后进行一两个新的提交和git push origin master
。
在现代Git中,git status
会告诉我我已经更新了。在非常旧的Git版本中,git status
现在会告诉我我的master
领先于origin/master
。原因很简单:在旧时代,git push
未能更新origin/master
。运行git fetch origin
或者只是git fetch
让你自己的Git调用GitHub上的Git,读取他们的信息,并意识到你的git push
已经起作用了。
当你运行git push
时,你的Git会调用另一个Git。然后,你的Git向其他Git提供他们没有但需要完成git push
的任何新提交。他们接收这些提交并将它们放在某个地方7。然后,你的Git会礼貌地询问他们的Git(默认情况下)是否可以将分支名称设置为最新提交的哈希ID,就像在你的分支名称中看到的那样。这里没有远程跟踪的内容。你只是要求他们设置与你使用的相同的名称。
一般来说,如果你只是向他们的存储库添加新的提交,则此类礼貌请求将成功。如果你要求他们“删除”某些提交,则会出现“非快进”的错误。如果你是唯一一个向他们发送新提交的人,则他们永远不会有任何损失,因此这应该始终有效8。
如果推送失败,则你的Git保持远程跟踪名称不变是适当的。你的Git从他们的Git那里没有得到更新它所需的信息。但是,如果推送成功...他们只是将他们的分支名称设置为你的Git要求使用的哈希ID。因此,现在你的Git知道他们的分支名称指向哪里。你的Git应该更新你的远程跟踪名称。
在旧的Git版本中,你的Git只是没有费心做这件事。在Git 1.8.2版本中,Git作者终于解决了这个问题:成功的git push
将使你的Git根据他们的Git同意的更新来更新远程跟踪名称。因此,这种情况不再经常发生。
在过去,他们直接将它们放入存储库中。在现代Git中,他们将其放入隔离区,仅在实际接受新提交时才将其迁移到存储库中。
当然,像GitHub这样的地方也提供了诸如“受保护的分支”之类的功能,例如拒绝每个推送。我们还可以发明更复杂的场景,比如当您有多台计算机并忘记通过计算机A创建和推送新提交,现在尝试从计算机B推送时。
如果你不是唯一一个进行git push
的人怎么办
假设Alice和Bob都克隆了某个GitHub存储库。开发在这个存储库中发生在分支dev
(代表develop)上。因此,Alice从她的origin/dev
创建了自己的dev
分支:
...--G--H <-- dev (HEAD), origin/dev [Alice's computer]
鲍勃同样也自己制作{{他的}}dev
:
...
GitHub仓库dev
也以H
结尾(它们没有origin/dev
:GitHub仓库不需要远程跟踪名称)。
Alice进行了新的提交,我们称之为I
,并像这样在Alice的计算机上绘制:
I <
/
...
同时,Bob进行了一次新的提交,我们称之为J
:
...
\
J <
现在Alice和Bob都尝试执行git push origin dev
。其中一个人先到达了目的地,可能是Alice。
...--G--H--I <-- dev [GitHub's systems]
Bob发送提交J
,在GitHub上看起来像这样:
I <-- dev
/
...--G--H
\
J ... polite request to set dev to J
如果GitHub这样做,就会“丢失”Alice的提交I
,因为Git通过从名称开始向后工作来查找提交。因此,他们拒绝了带有“不是快进”的投诉的推送。
Bob现在需要将提交I
从GitHub中拉入自己的存储库,以便Bob看到:
I <
/
...
\
J <
Bob应该使用git fetch
或git fetch origin
来完成此操作,也许使用--prune
(或将fetch.prune
设置为true
)。现在,当Bob运行git status
时,他将收到“diverged”消息。
现在由Bob作为推送竞赛的输家,需要想办法将他的工作(提交J
)与Alice的工作(提交I
)结合起来。有不同的方法来合并工作。两种主要的方法是git merge
和git rebase
。我们不会在这里讨论谁应该在何时以及为什么做什么,只是提到这是另一种可能遇到“diverged”状态的方式,即当您认为自己严格领先于其他Git时。
push
操作的?你的远程和分支配置设置是什么? - CB Baileygit status
不会检查远程仓库,但git pull
会。如果您有一个用于推送的跟踪分支的存储库,git push
将在推送成功后更新您的本地跟踪分支以反映远程分支的新状态。这就是为什么我问问者的配置情况,因为如果没有正确地进行操作,那么可能存在配置错误。 - CB Baileygit status
?真的吗?我的git status
从来没有告诉我我的分支超前了多少...你有传递额外的参数吗? - hasengit status
不会去远程仓库检查远程分支是否有更新,它会告诉你本地分支相对于你 本地存储的 远程跟踪分支落后了多少。问题在于正常的git push
(以及 fetch 和 pull)应该会更新远程跟踪分支,但是对于提问者来说,这似乎没有起作用。要查看原因,我们需要查看正在使用的git push
的确切形式和本地存储库的配置,但由于提问者已经接受了答案,现在我无法看到这种情况发生。 - CB Bailey