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