注意:在我写这个答案的时候,这个特殊的问题已经很老了。它是在第一个版本的 Git 版本发布前三年发布的,该版本修复了许多这些问题。 不过,似乎值得添加一个现代答案和解释器。 The existing accepted answer 建议运行 git fetch -p,1 这是一个好主意,尽管这些天不太经常需要。在 Git 版本 1.8.2 出来之前,它是非常必要的; Git 是在最初的问题发布三年后发布的。
1-p 或 --prune 选项不是必需的,仅在链接答案的括号中建议。请参阅下面较长的部分了解它的作用。
这实际上是如何发生的?
原来的问题是:
这实际上是如何发生的?
this 有问题的是,在git push origin master 之后,OP 运行git status 并看到消息On branch master 后跟Your branch is ahead of 'origin/master' by 1 commit. 要正确回答问题,我们需要把它分成几块。
首先,每个(本地)分支都有一个上游设置
这种说法实际上有点太强了。您自己的每个本地分支,在您自己的 Git 存储库中,可以有 一个设置,Git 将其称为 上游。或者,该分支可以有 no 上游。旧版本的 Git 并不太一致地将其称为 upstream 设置,但在现代 Git 中,它更加一致。我们还有 git branch --set-upstream-to 和 git branch --unset-upstream 用于设置或清除上游。
这些--set-upstream-to 和--unset-upstream 影响当前分支。当前分支是您的on,当git status 说on branch xyzzy 或它说的任何内容时。您选择这个分支带有git checkout 或——从 Git 2.23 开始——git switch。2无论你签出哪个分支,这就是你所在的那个。 3
如果你使用--unset-upstream,这删除当前分支的上游。没有上游,这会阻止关于领先或落后或分歧的信息。但是此消息是有用的,因此您可能不应该仅仅删除上游作为使其停止发生的一种方式。 (请随意忽略这条消息——毕竟它不是一个错误——如果你觉得它没有用处。)
如果您运行git branch --set-upstream-to=origin/xyzzy,则将当前分支的上游设置为origin/xyzzy。对于名为xyzzy 的分支,这将是典型的正确设置。某些创建分支的行为自动设置(通常是正确的)上游,而有些则没有,因此如果您使用自动设置正确上游的分支创建操作,则无需执行任何操作。如果您想要不同的上游,或者如果您使用了设置 no 上游的分支创建操作,则可以使用它来更改上游。
您可以将上游设置为:
- 您自己的另一个(本地)分支:
git branch --set-upstream-to=experiment 使您自己的本地 experiment 成为当前分支的上游;或
- 您的任何远程跟踪名称,例如
origin/main 或origin/master 或origin/xyzzy。这些是git branch -r 输出的名称。 Git 将这些远程跟踪分支名称称为(我喜欢在这里去掉“分支”这个词),我们稍后会详细讨论它们。
git status 打印的领先、落后、最新或分歧消息源自运行看起来有点神奇的命令:
git rev-list --count --left-right $branch...$upstream
其中$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可能在 no 分支上,Git 称之为 detached HEAD 模式。如果你使用git checkout,它可以让你突然进入这种模式(虽然它会打印一个很大的可怕警告,所以如果你没有看到可怕的警告,它没有这样做),但是如果你使用git switch,您必须 允许 带有 git switch --detach 的分离 HEAD 模式。这种模式没有任何错误,您只需要在进入该模式后小心,不要丢失您所做的任何新提交。如果您不小心,很容易丢失它们。在普通模式下,Git 不会像这样丢失新的提交。
如果您处于分离 HEAD 模式,则您没有上游(根据定义,因为您没有分支)并且此问题中的任何内容都不适用。
可达性
这部分有点技术性,我会将大部分内容外包给一个网站Think Like (a) Git。我将在这里总结一下:分支名称(如main 或xyzzy)和远程跟踪名称(origin/main,origin/xyzzy)是Git 如何发现 提交。 Git 是关于 commits 的。您的分支名称仅对查找您的提交很重要。当然,如果你找不到它们,你就有麻烦了,所以你的分支名称很重要。但关键是可达性,这是技术术语。
Git 存储库中的每个提交都有编号,带有一个大而难看的 hexadecimal 字母和数字字符串。这是提交的哈希 ID,它是 Git 真正找到提交的方式。
每个提交包含两件事:每个源文件的完整快照(以特殊的、压缩的、Git 化的和去重复的形式),以及有关提交本身的一些信息:元数据例如,告诉谁制造了它,何时以及为什么(他们的日志消息)。在元数据中,每个提交都包含一些较早提交的提交编号。这意味着一个提交可以找到另一个更早的提交。
正如Think Like (a) Git 所说,这有点像火车。一旦您在火车上,感觉很棒,在这种情况下,火车会自动带您倒退到所有较早的火车站点。但首先您必须找到到火车站的路。 Git 分支名称可以做到这一点:它保存您分支上 latest 提交的哈希 ID。
我们可以这样画:
... <-F <-G <-H <--branch
分支名称branch 保存最新 提交的哈希ID。我们说名称指向提交。无论真正的大丑哈希 ID 是什么,我们只是在这里使用字母 H 代替它。
H 是一个实际的提交,所以它有一个保存的快照——你的文件——和一些元数据。在元数据中,Git 保存了 earlier 提交的哈希 ID。我们将其称为较早的提交G。我们说H 指向 G。 Git 可以通过分支名称指针找到H,这使 Git 可以访问提交,包括元数据,所以现在 Git 具有早期提交 G 的哈希 ID。
G 当然也是一个实际的提交:它有一个保存的快照和一些元数据。在 G 的元数据中,Git 保存了较早提交 F 的哈希 ID。我们说G 指向F,现在Git 可以使用这个保存的哈希ID 找到F。
这将永远重复,或者更确切地说,直到我们进行第一次提交。该提交(大概我们在这里称之为A)不指向更早的提交,因为没有更早的提交。
可达性的这个概念基本上是对如果我们从提交H(通过分支名称branch 找到)开始并向后工作会发生什么的总结。我们到达commit H,它向后到达commit G,它又回到F,以此类推。
分支名称和远程跟踪名称
正如我们刚刚提到的,分支名称保存了某个提交的原始哈希 ID。这让 Git 找到那个提交。不过,branch 名称还有另一个特殊功能。
当您使用git checkout 或git switch 获得on 一个分支,然后进行新 提交时,Git 自动更新分支名称的存储哈希 ID。也就是说,假设我们有一系列这样的提交:
...--F--G--H <-- xyzzy (HEAD)
我们正在“打开”分支xyzzy,我想通过附加特殊名称HEAD 来表示它。当图中有多个分支名称时,这很有用。请注意,H 目前是 最新 提交。但现在我们将按照通常的方式制作另一个。
这个新的提交获得了一个新的、唯一的、又大又丑的十六进制哈希 ID,就像任何提交一样。 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)
这就是分支名称的工作方式。最后,这真的很简单:它只需要你同时考虑几件事。该名称找到提交。它找到 latest 提交。从那里开始,Git向后工作,因为每次提交都会找到一个较早的提交。
远程跟踪名称呢?好吧,这里的诀窍是您的 Git 与其他 Git 对话。 每个 Git 都有自己的分支名称。你有 你的master 或 main;他们有他们的。你有你的xyzzy 分支,他们也可以有他们的。
您的 Git可以随时、每次都调用他们的 Git,并询问他们的分支名称。但是,这不是很有效,而且如果您与 Internet 断开连接,也不会起作用。4 无论如何,Git 不会这样做。相反,当您的 Git 调用他们的 Git 并从他们那里获取所有分支名称和哈希 ID 的列表时,您的 Git 会获取这些名称和哈希 ID 并将它们存储在 您的 存储库。当您运行 git fetch 时会发生这种情况。5
虽然有问题。他们的main 或master,或者他们的xyzzy(如果有的话)并不一定意味着相同 提交与你的 main 或master或xyzzy。解决方案很简单:Git 只需将它们的 branch 名称转换为您的 remote-tracking 名称。
如果origin 的main 或master 或xyzzy 已移动,您只需运行git fetch 或git fetch origin,也可以使用--prune。你的 Git 调用他们的 Git。他们列出了他们的分支名称和提交哈希 ID。如有必要,您的 Git 会从他们那里获得任何新的提交:他们有的提交,而您没有。然后您的 Git 将它们的 branch 名称转换为您的 remote-tracking 名称并创建或更新您的远程跟踪名称以记住 their 分支名称指向的位置,此时您运行此git fetch。
如果您使用--prune,这将处理他们删除某些分支名称的情况。假设他们有一个名为oldstuff 的分支。你早点得到它,所以你的远程跟踪名称中有origin/oldstuff。然后他们删除了oldstuff,所以这次他们......只是不再拥有它了。如果没有--prune,你的 Git 会忽略它。你保留旧的origin/oldstuff,即使它现在已经死了。 有了--prune,你的 Git 会说:哦,呵呵,这现在看起来已经死了并把它删掉了:你的中的远程跟踪名称与其 分支 名称之一不对应的 Git,将被删除。
修剪选项可能应该一直是默认选项,但它不是,因此现在不能。6 但是,您可以将 fetch.prune 配置为 true 和 使它你的默认。
4现在 2021 年这种情况比 2010 年少了。2005 年 Git 首次发布时,这种情况要普遍得多。过去的情况是,例如,在飞往 Linux 会议的航班上,您无法以任何价格任何访问 Internet。
5选择使用哪些名称以及何时使用它们实际上是这里答案的一部分。它在 Git 中随着时间的推移发生了变化——并且仍在发生一点点变化——尽管仍然存在各种限制。不过,我们不会详细介绍所有细节。
6Git 通常非常重视向后兼容性。例如,从 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 可访问的提交。 the gitrevisions documentation 中描述的三点语法产生集合论中称为对称差分的东西。但是,在非数学术语中,我们可以将其视为进行两次提交可达性测试,
我们可以这样画:
I--J <-- xyzzy (HEAD)
/
...--G--H
\
K <-- origin/xyzzy
在这里,提交J 可以从您的分支名称xyzzy 访问,因为名称指向那里。提交I 可以从提交J 访问,因此它也很重要。这导致提交H——从图中可以看出,它有点特别。
同时,提交K 可以从您的远程跟踪名称origin/xyzzy 访问。提交H 可从K 访问。从后面的 commit H 开始,commit G 和 F 等也都可以访问。但是两个“铁轨”加入在提交 H:提交 H 和所有早期的提交都可以从两个名称访问。
这使得提交I-J 的特殊之处在于它们只能通过名称xyzzy 访问,而K 的特殊之处在于它只能通过名称origin/xyzzy 访问。三点表示法查找这些提交:只能从一个名称访问的提交,或者只能从另一个名称访问的提交。
如果我们将分支名称放在左侧,将其上游放在右侧,并使用三点表示法,我们将在这种情况下找到所有这三个提交。使用--count 使git rev-list 打印这个数字: 3. 使用--left-right 告诉git rev-list 更聪明,但是:它应该计算由于left 名称而被计算的提交数—— 当前分支 名称——以及由于 正确 之一(上游)而计算了多少次提交。因此,通过这两个选项以及三个点,我们得到:
2 1
作为输出,告诉我们xyzzy 上的两个提交不在origin/xyzzy 上,一个在origin/xyzzy 上的提交不在xyzzy 上。这些分别是提交J-和-I(xyzzy)和K(origin/xyzzy)。
如果没有 --count 选项,git rev-list 将列出哈希 ID,前缀为 <(左)或 >(右)符号。使用git log 而不是git rev-list,如:
git log --left-right xyzzy...origin/xyzzy
(再次注意三个点:请参阅gitrevisions 并搜索对称差异)我们将获得三个提交,再次以< 或> 为前缀(视情况而定)。
这是一种查看分支上的提交以及上游提交的简单方法。它通常对--decorate、--oneline 和--graph 更有用(在某些情况下,您可能还想添加--boundary)。
领先、落后、分歧或最新
所以,假设我们已经运行:
git rev-list --count --left-right $branch...$upstream
(或 - 再次参见 gitrevisions - 在此处右侧使用 $branch@{upstream})并得到我们的两个计数。这些可以是:
-
0 和 0:我们的分支名称和远程跟踪名称(或上游中的任何名称)指向 same 提交。没有人领先或落后。 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 现在会告诉我我的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——默认情况下礼貌地询问——如果他们愿意,请设置 他们的 branch 名称,通过其哈希 ID 引用最新提交,如 您的 分支名称中所示。这里没有远程跟踪的东西。您只是要求他们设置您正在使用的同名。
作为一般规则,如果您只是向他们的存储库添加新提交,这种礼貌的请求会成功。如果你要求他们“丢失”一些提交,它会失败:你会抱怨这是一个“非快进”。如果您是唯一一个向他们发送新提交的人,那么他们绝不应该以这种方式失去任何东西,所以这应该始终有效。8
如果推送失败,您的 Git 可以保持远程跟踪名称不变。你的 Git 从来没有从他们的 Git 那里得到信息,让你的 Git 更新它。但是如果推送 成功 ...好吧,他们只需将他们的 branch 名称设置为您的 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(用于开发)上。所以爱丽丝从她的origin/dev 中创建了自己的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 将提交 J 发送到 GitHub,如下所示:
I <-- dev
/
...--G--H
\
J ... polite request to set dev to J
如果 GitHub 会这样做,这将“丢失” Alice 的提交 I,因为 Git finds 提交是从名称开始并向后工作。所以他们以“不是快进”的抱怨拒绝了推动。
Bob 现在需要将提交 I 从 GitHub 拉到 Bob 自己的存储库中,以便 Bob 看到:
I <-- origin/dev
/
...--G--H
\
J <-- dev (HEAD) [Bob's computer]
Bob 应该使用git fetch 或git fetch origin 执行此操作,也许使用--prune(或将fetch.prune 设置为true)。 现在当 Bob 运行 git status 时,他将收到“发散”消息。
现在轮到 Bob,作为推动竞赛的失败者,想办法将他的的工作(提交 J)与 Alice 的(提交 I)结合起来。有不同的方法可以组合工作。两个主要的是git merge 和git rebase。我们不会在这里讨论谁应该做什么、何时以及为什么要做,只是当你认为自己完全领先于其他 Git 时,这是另一种可能会陷入“分歧”状态的事实。