git:对“非快进”错误的基本误解

tom*_*tom 4 git

当我自己工作时,不了解 git 的基础知识是可以的,但是现在我正在和另一个人一起工作,并且我们每个人都提交拉取请求以让他们合并,这开始成为一个问题。

工作流程:我写在我的“作者”分支。当准备好进行审查时,我提交了一个拉取请求,我的编辑器将其合并到 master 中。当她对我有意见时,她提交了她的 Editor 分支的 pull request,我将它们合并到 master 中。

今天,我收到了一个完全令人恼火的循环错误,我不明白我被要求做什么。

thomas@trigger ‹ author ?? › : ~/pm/wip [1] % git push To https://github.com/mathpunk/punk-mathematics-text.git ! [rejected] Editor -> Editor (non-fast-forward) ! [rejected] author -> author (non-fast-forward) error: failed to push some refs to 'https://github.com/mathpunk/punk-mathematics-text.git' To prevent you from losing history, non-fast-forward updates were rejected Merge the remote changes (e.g. 'git pull') before pushing again. See the 'Note about fast-forwards' section of 'git push --help' for details.

所以,我尝试 git pull 并得到这个:

thomas@trigger ‹ author ?? › : ~/pm/wip
[130] % git pull
You asked me to pull without telling me which branch you
want to merge with, and 'branch.author.merge' in
your configuration file does not tell me, either. Please
specify which branch you want to use on the command line and
try again (e.g. 'git pull <repository> <refspec>').
See git-pull(1) for details.

If you often merge with the same branch, you may want to
use something like the following in your configuration file:
    [branch "author"]
    remote = <nickname>
    merge = <remote-ref>

    [remote "<nickname>"]
    url = <url>
    fetch = <refspec>

See git-config(1) for details.
Run Code Online (Sandbox Code Playgroud)

与此问题相关的问题在 stackoverflow 上很常见,但显然我对 git 的基本概念不够了解,无法理解它们。也可能我们的工作流程很疯狂:我们都不是开发人员。我接下来可以尝试什么?

tor*_*rek 5

这是我认为 git 文档非常糟糕的领域之一。一切都指向你,git pullgit pull 只是一个建立在几个底层项目之上的便捷方法,而底层项目对于理解这一点至关重要。Git 让批判性理解的部分“泄漏”,同时试图假装它们无关紧要。

顺便说一下,这里是实际的基本元素:

  • 当您与某人共享内容时,会涉及多个独立的存储库 (repos)。至少有“你的”和“他们的”,而且经常有第三个(例如,在 github 上)你们都用来完成共享。在这种情况下,模型是这个第三方(github)处理所有复杂的身份验证内容(https、ssh 等),因此您不必这样做。

  • 要从其他存储库获取内容,您可以使用git fetch.

  • 要将东西交付给其他一些仓库,您可以使用git push.

换句话说,它的反义词push实际上不是pull,而是fetch

参考规格

为了使这两个操作(push 和 fetch)起作用,git 使用了它所谓的“refspecs”。请记住,在推送和获取时,涉及两个存储库。

Refspecs 通常看起来像一个单一的分支名称。然而,refspec 最简单的“真实”版本实际上是两个用冒号分隔的分支名称:

master:master
Editor:Editor
author:author
Run Code Online (Sandbox Code Playgroud)

左侧和右侧命名了两个存储库中的分支。对于 a push,左侧的名称是您的仓库中的分支,右侧的名称是其仓库中的分支。对于 a fetch,左侧的名称是其仓库中的分支,右侧的名称是您的仓库中的分支。

这是模型再次变得有点奇怪的地方,并且是非对称的。Git 相信(几乎可以说是相信任何事情)当你fetch 时,你他们可能都在做工作;但是当你push 时,只有应该做任何工作。(这有很好的理由,我不会深入讨论,因为这已经很长了。:-))

为了使这一切正常,fetch提供了分支重命名。相反取“自己的”分支(的masterauthor等)直接连接到“你”的分支,这将使其非常难以进入的工作,你自做的最后一取,您将获取“自己”的东西是什么混帐所谓的“远程分支”。


获取和“远程分支”

尽管名称为“远程分支”,但“远程分支”实际上是本地事物。就此而言,“远程”也是如此。“远程”是您在本地配置的名称,例如origingithub。与此“远程”相关联的是一个 URL,例如https://github.com/mathpunk/punk-mathematics-text.git. 还有一条fetch线。现在不要担心fetch生产线的机制(一旦创建,它通常可以正常工作);只知道这就是 git 知道在获取时使用什么“远程分支名称”的方式。

在某种程度上,您确实必须担心遥控器的实际名称。通常的默认名称是origin但您在执行git remote add命令时选择该名称。远程名称成为“远程分支”名称的一部分。具体来说,远程名称位于分支名称之前。

因此,假设你会git fetch origin从github上带来的东西了,“远程分支名称”将是origin/masterorigin/Editororigin/author

如果您git fetch github从 github 中带来东西,“远程分支名称”将改为github/master, github/Editor, 和github/author

在所有情况下,您只需命名远程,并fetch带来所有分支,但重命名它们。通过省略 refspec,您可以使用该fetch行中的默认值。

如果您添加分支名称(git fetch origin author例如),git 会通过使用同一fetch行重命名传入分支将其转换为“真实”refspec 。实际上,git fetch origin author变成git fetch origin author:origin/author. 他们的分支名称,在左边,author变成你的“远程分支名称” origin/author,在右边。

(这里的想法是,你可以添加多个不同的遥控器。如果是你,你的编辑器,你的出版商都希望直接与对方,而不是与第三方像github上分享,你可以有2个遥控器,命名editorpublisher例如,并且您将获得“远程分支名称”,例如editor/Editor一个远程和publisher/Editor另一个。如果您使用像 github 这样的单个共享站点,那么所有这些都是毫无意义的复杂化。)


好的,回到fetchpush。当您git fetch origin使用远程origin名称时,您可以使用“他们的”分支,但将它们放在您的origin/*“远程”分支下。这使他们的工作与您的工作分开。(当然,在某些时候您需要将这些结合起来;我们稍后会谈到。)

push但是,当您使用“推送”时,使用“远程分支”的概念。您只需直接推送到他们的分支。因此,如果您的 repo 中有一些更改,在您的 branch 中author,并且您想推送这些更改,您只需git push origin author:author. origin这里的部分再次是远程名称,最后一部分是像往常一样的 refspec,命名您的分支 ( author) 然后他们的分支 (也只是author)。

如果您在push命令中包含分支名称,则此处缺少分支重命名显示为:git push origin author"means" git push origin author:author您的分支名称 ,author,在左侧,被简单地复制以用作author右侧分支名称,。


审查

快速回顾的时间:

  1. 你设置了一个“远程”
  2. 你用来fetch进入你的“远程分支”,和
  3. 您将其用于push本地分支机构到其本地分支机构。

想一想。请注意,缺少一个步骤。

您如何将他们的工作(在第 2 步之后)现在列在您的远程分支机构中,转移到您自己的本地分支机构中?

这是进来的地方git merge,因此也是git pull进来的。

这也是您的问题标题中的项目出现的地方。快进或非快进是 git 中“标签移动”的属性。

要真正理解这一点,我们必须再走一小段路,讨论 git 的提交和分支模型。


提交图

每个提交都有一个保证唯一的标识符(SHA-1,9afc317...数字)。您或其他任何人都不会创建具有该编号的任何不同提交,但是如果您或其他任何人能够设法准确地重新创建该提交,您将获得相同的编号。(这对于获取很重要。)

每个提交还包含 - 间接地,通过引用 - 一个完整的独立实体,“树”。该树是该提交中所有文件的集合。然而,提交并不是完全独立的:它有一个或多个“父”提交。这些决定了提交历史,从而“建立”了实际的分支结构。

(在许多 - 甚至大多数 - 其他版本控制系统中,树不是独立的:VCS 必须通过父提交和子提交来提取树,和/或进行新的提交。但在 git 中,每棵树是独立的;它只需要通过父/子排序来比较两棵树,或者确定提交历史。)

给定一个提交,git 找到它的父提交,以及它们的父提交,等等,并构建一个“提交图”:

        C - F
      /       \
A - B           G   <-- master
      \       /
        D - E
Run Code Online (Sandbox Code Playgroud)

这是一个包含 7 个提交的存储库的图,所有提交都在一个名为master. A是最初的提交(A这里代表一些丑陋的独特 SHA-1 数字),B自此发生了一些变化A(比较两棵树AB会显示变化),然后某人——或者可能是两个“某人”——做了什么称为“分支”:C根据 commitB创建提交,并根据 commit创建D提交B

之后,有人E基于DF基于C.

最后,有人将这两个分支合并起来进行合并提交,即 commit G。CommitG有其(两个)父项,即FE。事实上,它有两个父级的事实使它成为“合并提交”。

当所有这些都发生在一个存储库中时,它就足够简单了。一个“某人”,即使用存储库的人A,在 branch 上进行了 commits B, 和Con branch master,然后可能创建了一个从 commit 开始的命名分支B

git checkout -b sidebranch master~1
Run Code Online (Sandbox Code Playgroud)

并进行了提交DE。然后他们回到master

git checkout master
Run Code Online (Sandbox Code Playgroud)

并提交F,然后运行:

git merge sidebranch
Run Code Online (Sandbox Code Playgroud)

创建 commit G。在此之后,他们可以删除分支sidebranch,因为提交G(现在是提示提交master)指向 commitE和 commit F

相同的模式,但会发生在自己的回购当两个你,工作,和“他们”,在他们的工作,使提交。假设你正在工作master并且你已经提交A并且B

A - B   <-- master
Run Code Online (Sandbox Code Playgroud)

此时你将你的工作推送到共享点(github),使其具有AB。他们克隆了这个存储库,给了他们第三个存储库,其中包含 github 共享点和两个提交A以及B.

现在你在你的 repo 中工作并创建 commit C。他们在他们的内部工作并创建DE,在你推C送到 github之前,他们将他们的D和推E送到 github:

[你:]

        C   <-- master
      /
A - B
Run Code Online (Sandbox Code Playgroud)

[他们和github:]

A - B
      \
        D - E   <-- master
Run Code Online (Sandbox Code Playgroud)

此时,假设您使用git fetch github. 记住,fetch重命名“他们的”分支,所以结果是这样的:

        C   <-- master
      /
A - B
      \
        D - E   <-- github/master
Run Code Online (Sandbox Code Playgroud)

Git的能做到这一点,因为每次提交具有独特的SHA-1,因此它知道你AB他们AB相同的,但你C是从他们的不同DE

此时,您可以创建 commit F,这使您master指向最新的提交:

        C - F   <-- master
      /
A - B
      \
        D - E   <-- github/master
Run Code Online (Sandbox Code Playgroud)

现在,如果你想分享你的工作,这是当你最好git push github......但问题是,你的主人有提交A - B - C - F,同时提交DE是,从你的角度来看,只有上github/master

如果您将您的推master送到 github 并使 githubmaster指向 commit F,则提交D并将E丢失。(“他们”,无论他们是谁,仍然会拥有它们,您仍然会拥有它们,但已命名github/master,因此可以解决此问题,但很痛苦。)

解决方案是让修补它,以便“他们的”提交DE, 也在您的master. 一种简单的方法是让您合并您的工作和他们的工作,给出:

        C - F
      /       \
A - B           G   <-- master
      \       /
        D - E   <-- github/master
Run Code Online (Sandbox Code Playgroud)

快进

请注意,master每次进行新提交时,您的分支标签 是如何“向前移动”的?

您所做承诺Fmaster,其中用于点提交C,向前来点新的承诺,F

然后,你做合并提交Gmaster,其中用于点F,向前来点新的承诺,G

在您构建分支时,标签会沿着分支“向前移动”。

假设我们有另一个标签——另一个分支名称——指向(比如说) commit B,一直以来,我们还没有移动:

      ..............<-- br
     .
    .   C - F
    v /       \
A - B           G   <-- master
      \       /
        D - E
Run Code Online (Sandbox Code Playgroud)

我们现在可以让 git “br向前滑动标签”,并“快速”完成——一次完成,一路提交G

git checkout br
git merge --ff-only master
Run Code Online (Sandbox Code Playgroud)

当我们要求 git 进行合并时,如果我们告诉它--ff-only(仅快进),它将查看是否有办法将标签从它现在指向的任何提交向前滑动到目标提交,在这种情况下G。(名称master指向提交,G因此合并选择提交G作为快进目标。)在这种特殊情况下,实际上有两种方法可以做到,B-C-F-G或者B-D-E-G;任何一个都足以允许这种“快进”。

(使用--ff-only,如果分支标签不能快进,合并请求会被简单地拒绝。没有--ff-only,git 将尝试创建一个新的、实际的合并提交,以便标签可以向前移动。使用--no-ffgit merge将创建一个合并,即使已经可以进行快进。默认情况下,根本没有选项,如果可能,则快进,否则进行新的合并提交。)

push 需要快进属性

如果您要求 git 推送我们的 new master,这是允许的,因为这符合“快进”测试。当我们进行推送时,我们会告诉 github:“请提交CFG,然后将您的标签master(我们称之为github/master)从提交移动E到提交G”。有从E到的路径G吗?有,所以是允许的。


git pull实际上,所有操作都是 run git fetch,然后是 run git merge

不幸的是,这意味着您确实需要了解以上所有内容才能真正理解git pull.

不过这里有几条大皱纹。首先,我一直在使用git fetch origingit fetch github以上。换句话说,我一直在命名一个遥控器。当你这样做时,遥控器从哪里来git pull

答案是它来自您的配置。存储库中的每个分支都可以命名一个远程:

$ git config branch.author.remote github
Run Code Online (Sandbox Code Playgroud)

现在分支的“远程”authorgithub.

其次,如果您运行git merge,您必须告诉它要合并的内容。当您这样做时,合并名称从何而来git pull

答案再次是它来自配置。每个分支都可以命名一个上游合并分支:

$ git config branch.author.merge author
Run Code Online (Sandbox Code Playgroud)

Git 将merge与结合起来remote,因此在这两个git config命令之后,git pull基本上是git merge github/author.

我说“基本上”是因为还有另一个问题:在旧版本的 git 中,pullfetch以不更新远程分支名称的方式运行。相反,它使用一个特殊FETCH_HEAD文件。(在较新版本的 git 中,它仍然使用FETCH_HEAD但它也更新了远程分支名称。)

最后,有一个非常大的问题:您可以配置git pull为使用git rebase而不是git merge. 但是这个答案现在已经足够完整了;我不打算深入这些细节。

  • +1。注意:关于 `git pull` 的复杂性,这就是为什么这些天有一个 `git update` 提议……:http://www.spinics.net/lists/git/msg230598.html (2认同)