为什么Git不自动快进?

4
当我执行 git pull、add、commit、push 时,会出现以下情况。
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'http://foo:8080/tfsdev/foo/_git/Development.Services'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

当我在git pull命令中添加了-ff选项时,它可以运行。我以为默认情况下是快进模式,为什么会出现这种情况?

你确定在 pull 和 push 之间没有提交到远程仓库吗? - jwodder
1个回答

23
当我在git pull后面添加-ff,它就会执行。我以为快进是默认的,为什么会这样?
你在混淆许多不同的概念...这并不奇怪,因为git pull也混淆了许多不同的概念,试图变得方便,但大多数情况下却让人感到困惑。(好吧,还有时候很方便 :-))
使用git pull通常会运行git merge,它通常会进行快速向前合并而不是合并。这将使您的分支与通过git fetch从远程获取的内容保持同步,以便您添加的提交也只会添加到它们的提交(而不是替换或删除)。但是,git push不是git pull的相反,而是git fetch的相反:它不进行任何合并。
当您执行pull + do stuff(提交或其他操作)+ push序列时,您正在与其他人竞争,他们也正在执行pull-commit-push序列。谁赢了,谁就可以推送。这一部分确实如此简单。问题在于当你输掉比赛时!
当您不是唯一在运行的人时,赢得这场比赛很容易。(如果没有其他人将提交推送到该分支,则您始终可以赢得比赛。)这里发生的事情是您不是唯一在运行的人,而且您输了。
完整的答案不幸的是复杂的,而且您没有展示足够的内容来回答这个具体的情况。因此,让我们看一下真正发生了什么。
始终跟踪您当前的分支
如果git状态说on branch master,或者git branch打印* master,那么您当前的分支就是master。大多数Git命令都与您当前的分支一起使用,git pull也不例外。
git pull命令只是运行两个其他的Git命令。第一个始终是git fetch。第二个,您可以更改。
首先运行的命令是 git pull 中的 git fetch 。 它运行了一个有点受限制 git fetch ,告诉 git fetch 只带来所需的提交和其他内容以更新您的当前分支。 如果您只运行 git fetch ,Git将带来所有东西,这可能需要更长的时间,但意味着您已经与所有内容保持最新。 git fetch 命令是与您当前分支工作的常规Git规则的例外。 在某些情况下,它确实会查看您的当前分支,但 git fetch 通常要做的是更新您的远程跟踪分支。 在大多数情况下,您只有一个名为 origin 的所谓远程,运行 git fetch 会从 origin 获取所有内容并更新您的 origin /master origin /develop 和其他 origin / 分支。 这些是 origin 远程的远程跟踪分支。
因此,如果您自己运行 git fetch ,它将更新所有 origin / * 远程跟踪分支。 如果您运行 git pull ,它将以一种只基于当前分支更新一个远程跟踪分支的方式运行 git fetch 。 这些更新始终是安全的:它们将新定义的提交带入您的存储库,对存储库中的所有现有提交没有影响。
为了让人困惑, git fetch 将其所有更新编写到名为 FETCH_HEAD 的文件中,您会在git fetch文档中看到此名称的提及。但是,它会更新您的远程跟踪分支,除非您使用非常旧的Git版本(旧于Git版本1.8.4)。 对于那些具有古老Gits(1.7.x)的人,从 origin 手动 git fetch 会更新它们,但由 git pull 运行的更新则不会更新它们。
然后, git pull 会运行其他一些东西。
这里情况有点混乱。`git pull`的第二部分是`git merge`或`git rebase`中的任意一个,即使`git rebase`通常更好,但默认情况下是`git merge`。你必须提前选择`git pull`应该使用哪个命令,但哪个命令才是最好的呢?唯一确定的方法是查看即将到来的内容,如果您拾取新的提交,则可以使用`git log`和其他Git命令查看它们。
然而,要查看即将到来的内容,您必须先把它带进来。这就是`git fetch`的作用。`git fetch`步骤将新提交带入,并安全地将它们放在远程跟踪分支的后面。现在您可以决定是否合并或变基。
`git pull`命令会让您在查看内容之前做出决策。因此,请选择您想要它执行的操作(合并或变基),然后拉取,它将获取然后执行该操作。要使其执行变基,运行`git pull --rebase`,或设置当前分支以使`git pull`默认执行变基。
(此设置是每个分支的专属设置,因此您必须为每个分支设置它。也有一个配置项告诉Git自动为创建的每个分支设置"变基模式",但在我看来,自己运行`git fetch`然后自己运行第二个命令更容易,并且在出问题时更少混淆。这样可以让您有机会查看已获取的内容,同时也更加清晰。)
当第二个命令是`git merge`时,假设`git pull`执行的第二个操作是`git merge`,这将对当前分支起作用。但是,`git merge`本身有些复杂。
当您合并时,您选择某个提交--通常是分支名称,而且很常见地使用远程跟踪分支名称,例如`origin/master`--然后请求Git解决几个问题。这个合并的目标很容易说明:**合并将您完成的工作与其他人完成的工作结合起来,创建一个包含两组工作的新提交**。
为了弄清楚你和他们(不管他们是谁)都做了什么,Git 需要找到“合并基础”,这个合并基础或多或少是你和他们共享的第一个提交。这意味着现在是时候绘制提交图的一部分了。以下是一些可能性。
快进式“合并”
...--o--*     <-- master
         \
          o   <-- origin/master

o代表提交,右侧是新的提交。我用*标记了合并基础提交。在这里,你的master指向合并基础提交,而origin/master指向一个更新的提交。

在这种情况下,git merge能够执行这种"快进"操作。也就是说,因为合并基础已经是master的末端,实际上不需要进行合并。Git可以将名称 master 向下和向前滑动,以便它指向与origin/master相同的提交:

...--o--o
         \
          o   <-- master, origin/master

现在我们也可以将这条线条拉直,只留下如下内容:
...--o--o--o  <-- master, origin/master

如果你现在运行 git push origin master,基本上什么都不会发生,因为在你这一端没有新内容。最后一个提交是你从 origin 已经获取的。

真正的合并

但是如果我们有以下情况呢?

...--o--*--o   <-- master
         \
          `-o    <-- origin/master

在这里,Git需要进行一次真实的合并操作。Git将提交*与您的master提交(顶部行中最右边的o)以及您的origin/master提交(底部行的o)进行比较。也就是说,Git会运行两次git diff。然后它会尝试组合这两组更改,如果成功,将创建一个新的“合并提交”类型的提交:

...--o--*--o--o   <-- master
         \   /
          `-o    <-- origin/master

这个提交已经准备好推送到origin,与其他人同时试图将其推送到origin竞争。

或者可能根本没有要做的事情

你甚至可以从这开始:

...--o--o--*   <-- master, origin/master

现在没有需要合并的内容了: masterorigin/master 这两个名称指向同一个提交,所以合并基础提交已经是 master 分支的末端(也是 origin/master 的末端)。现在没有需要合并的内容,什么都不做后,也没有需要推送的内容。你甚至不必尝试比赛,更不用赢它。

现在你也可以创建新的提交记录

现在你的 masterorigin/master 已经一致,你可以创建新的提交记录。当你这样做时,你的 master 将会“超过” origin/master

...--o--o   o--o   <-- master
         \ /
          o   <-- origin/master

git push 的作用

前面我已经提到,push 不是 pull 的反义词,而是 fetch 的反义词。

当你运行 git push 时,你的 Git 会拨通另一个 Git。在这种情况下,它会连接到远程仓库 origin 上的 Git。然后,它会提交任何新的提交,并发送请求:"请将您的 master 设置为最新的提交"。

换句话说,我们要求它们的 Git 改变他们的 master,即我们称之为 origin/master 的东西,以匹配我们的 master

看看上面那些图示。当我们进行快进操作时,我们把我们的 master 移动到了一个我们已经从 origin/master 中选择的提交中。如果我们没有做任何事情,就什么也没做。但不管哪种情况,都没有新的提交:没有需要推送的内容。如果我们仍然尝试推送,我们将向其他 Git 发出这样的信息:"这里没有新内容,但请将您的 master 设置为指向这个特定的提交,就像我们的一样"。

如果我们确实创建了新的提交——正如我们执行有实际操作的合并时所做的那样,或者当我们创建新的提交时——我们会将它们交给他们,然后请求他们将他们的 master 设置为与我们的 master 相同。

当然,我们也将与其他人竞赛同时推送。

如果没有其他人赢得了推送竞赛,他们的 master 仍然与我们的 origin/master 匹配。我们会请求推送,并且——嗯,只需查看上面的图示。将 origin/master (我们对其的名称)向前移动以匹配我们的 master,这是我们之前看到的一些快进操作中的一个!

如果推送将进行快进,则通常会成功。

但是,如果其他人赢了怎么办?在我们运行了 git fetchgit merge 之后,我们花了一些时间进行提交,而与此同时,其他人已经在我们之前进行了推送,会发生什么呢?

好吧,现在我们有了这个:

(我将底部的 o 向下移动,只是为了能够指向 origin/master。)

...--o--o   o--o   <-- master
         \ /
          o   <-- origin/master

但那并不完全正确,因为他们他们的master上有新的提交。 他们拥有其他人推送的提交。 我们需要运行git fetch来获取它们。 让我们这样做,现在 - 现在git fetch使用新提交更新了我们的origin/master - 现在我们有:

...--o--o   o--o   <-- master
         \ /
          o-----o   <-- origin/master

现在,为了推送,我们必须合并或变基。我将画出合并的情况。(请注意,合并基础是底部行中最左边的提交。)
...--o--o   o--o--o   <-- master
         \ /     /
          *-----o   <-- origin/master

如果我们进行这个合并,现在我们就可以推送了,因为现在我们的 master 已经"快进"到他们的 master 前面了。
当然,在我们合并的同时,还会与其他可能正在尝试推送的人竞争。如果我们输掉比赛,可能需要再次获取和合并。
(在所有这些情况下,git rebase 可能比 git merge 更好。请参见其他关于 rebase 的 StackOverflow 回答。)

哇,这真是详细!我需要读几遍才能完全理解,但这确实消除了一些困惑。 - user1869558
非常棒的答案!我需要几遍阅读才能完全掌握这些概念。非常感谢! - TetraDev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接