Git fu:确定 "behind its remote counterpart" 的提交

Git fu: identify commit(s) that are "behind its remote counterpart"

这应该很简单:

我正在尝试将一个分支(master)推到 Heroku 上的生产环境中:

$ git push production
To git@heroku.com:my-app.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'git@heroku.com:my-app.git'
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.

我意识到这意味着远程存储库中的代码比本地存储库中的代码 "newer"。推荐 git pull ... 但这是我们只推送到的 Heroku 存储库。

我也意识到我可以 push -f 强制更改,但我想了解为什么 git 会抛出此消息,特别是因为这是生产部署。

所以,我想深入了解 Git 认为的错误...我如何判断本地缺少哪些提交?当我将本地母版与生产母版进行比较时,我看到我将要推送的数十条(数百条?)差异线。我想看到的是 git 认为 存在于 production/master 而本地不存在的东西。

您组中是否有其他人同时推送,因此您落后于 Heroku 存储库?你能试试 "git fetch origin --dry-run" 看看会得到什么吗?如果您 git 获取 origin/master" 并将新的更改拉入 origin/master,您可以做一个 git 日志来列出 [=14] 的提示之间的区别=] 和您本地的跟踪主分支来查看增量,例如 "git log origin/master --not master"

我怀疑您在这里有一个或两个基本的误解导致您误入歧途。在 git 中,提交只是一个 object 标识源代码树的某个特定版本(当然,还有通常的提交消息和 parent 提交 ID 列表;最后一个很重要因为它们是 git 构造 "commit graph" 的方式——这导致下面的 "branches")。

这意味着,特别是 commit 不是 "ahead" 或 "behind" 任何其他东西。它就在那里,或不在那里:这就是它可能存在或不存在的全部。

在 git 中——显然这是通过 heroku 公开的——还有 "branch" 的概念。不幸的是,这个术语相当重载。它可以指代其中一项(或多项):

  • 提交图的一个子集("DAGlet")
  • 分支标签名称
  • 一个"remote branch"标签

另请参阅 this question 了解更多信息,以及一些漂亮的图形,说明分支标签如何指向 git 根据存储在每个分支中的 parent 提交 ID 构建的提交图的部分branch-tip 提交。

这些分支 标签 是 "ahead" and/or "behind" 它们的远程副本。

在这种情况下,您的 heroku 服务器上有一个共享的远程存储库(您称之为 production:那是 "remote name",而不是分支名称)。您在本地机器上也有自己的本地存储库(并且您的协作者在他们的机器上有 他们的 自己的本地存储库,所有这些都与单个共享的 heroku 存储库分开)。当某个合作者(为了方便起见我们称他为 Bob)进行了一些更改并推送时,他将他的提交提交给共享存储库——但是 you 没有它们,您只有 您的 提交:您之前共享的所有提交,以及您刚刚创建但尚未共享的一些新提交。

所以,现在 Bob 已经进行了一些新的提交并成功推送了它们。现在,他的回购和共享回购与您的提交基本相同,加上他的新提交。你没有 那些 新的,但你有自己的新的。也就是说,你有这样的东西:

A <- B <- C <- D <- F   <-- master

这里master是一个分支标签。和 AF(注意我跳过了 E;我们马上就会明白为什么)是 40 个字符的 SHA-1 提交 ID 的缩写,如果您看到运行git log。我在每个提交之间绘制了 backward-pointing 箭头,因为每个提交都记录了它的 parent ID(通过 40 个字符的 SHA-1 数字),所以我们可以说每个提交 "points to" 它自己的 parent.

Bob 和 heroku 改为:

A <- B <- C <- D <- E   <-- master

其中 E 是 Bob 推送的新提交。

Git 不了解或不关心 Bob(你也不必关心),它在你做 git push 时只知道它调用 heroku在 Internet-phone 上,发送您的提交 F,然后要求 heroku 的 git 将标签 master 指向提交 F.

如果 heroku-side git 这样做,远程存储库将最终拥有与您拥有的相同的提交链,其中 F 指向 D,它指向 C,依此类推。 Commit E 不再有任何人指向它,所以它丢失了。这就是远程 (heroku) git 告诉您的 git,然后您的 git 告诉您的内容:如果远程进行了更改,则至少有一个提交会丢失git 要求它制作(或者,使用 --force,您的 git 会命令它制作)。

(请注意,即使 heroku git 丢失了此提交,Bob 的 git 不会受到影响:他仍将有他的提交 E。但这意味着您将迫使 Bob 恢复他的工作,也许是通过告诉 heroku git 再次忘记您的工作并使用他的工作。这不是合作的方式。:-) )


至于如何处理,您有两个(或三个,取决于您的计算方式)选择:

  • 运行 对 Bob 的粗暴对待:用强制推送擦除他的提交
  • 协作:将您的工作合并或重新设置到 Bob 的工作中/之上

(如果你将最后一个拆分为"merge with"作为一个选项,将"rebase onto"作为第二个选项,你将得到三个选项)。

两个 "collaborate" 选项都要求您选择 Bob 的提交。您可以从 Bob 那里获得它,但此时从 heroku 获得它更容易。为此,您从 git fetch production 开始:这会获取 heroku 拥有但您没有的所有提交——在本例中意味着提交 E——并将它们复制到您自己的本地存储库,将它们放入在 "remote branch" 上。 (旁注:您可以 运行 只是 git fetch,没有远程名称,或者 git fetch --all;我在这里使用远程名称来明确说明,因为您使用它在 git push 命令中。)

因为您将 heroku remote 命名为 git production,此 "remote branch" 标签的名称。这实际上在您的存储库本地(采用经典 git-confuses-everyone 样式 :-))— 将是 production/master。在您的存储库中,我们可以绘制提交图的 more-complete 图片:

                   F   <-- master
                 /
A <- B <- C <- D
                 \
                   E   <-- production/master

您现在有两个标签都指向两个提交,这两个提交与提交 AD 的常见 branch-ancestry ("DAGlet") 不同。

此时你必须决定是否"merge"(使用两个 parents 进行新提交,以保留不同的历史记录)或 "rebase"(更改历史记录,将旧提交 F 复制到与 F 基本相同的新提交,但具有 parent,提交 E 而不是提交 D)。关于哪种方式是正确的方式存在很多哲学争论,但现在我们只说 "rebase is the right way",并说明变基,您可以通过 运行ning git rebase production/master.

要进行变基,git 所做的是将您的原始提交 F 与其 parent D 进行比较,以查看您的问题。然后它检查提交 E, 使 与您之前所做的相同 更改,并使用相同的 消息 [=176] 进行新的提交 F' =] 你之前也写过:

                   F
                 /
A <- B <- C <- D
                 \
                   E - F'

我特意去掉了这里的标签,因为在做出新的提交之后,git 做了最后一件事:它删除了你的 production 标签(指向 F) 并绘制一个新的 production 指向副本 F',否则与 F 相同。既然看不到原来的了,我们就把它抹掉,把图中向下的扭结理顺,画出这张新图:

A <- B <- C <- D <- E <- F'

也没有理由再叫它F',所以现在我们就叫它F,然后把标签放回去:

A <- B <- C <- D <- E <- F   <-- master, production/master

(两个标签现在都指向 F)。现在看起来好像你从未写过你的原创 F;它看起来像是在等待 Bob 完成他的工作,然后根据 Bob 的最新提交提交 F。而且,现在你可以将你的提交 F 推送到 heroku 远程,因为 F 的 parent 提交是 E,所以如果你得到那个 git使它的 master指向F,这次它不会丢失任何提交。


这两个步骤——git fetch production,然后是 git rebase production/master——可以用一个 git 命令完成,即 git pull --rebase production master。 (令人困惑的是——用户混淆是 git 的模式——这不会在任何地方使用远程分支标签 production/master。但是,当你使用这两个单独的命令时,你 必须 使用带有斜杠的远程分支标签。)我通常更喜欢这两个单独的命令,因为这意味着您可以看到在 开始变基之前 发生了什么。

一旦你完成了获取(但既不是合并也不是变基),看看他们有什么你没有,反之亦然,给git log 一个额外的参数:

$ git log master..origin/master

或:

$ git log origin/master..master

这个double-dot语法告诉git"find commits starting with the label on the right and working back through the commit graph, but then don't show commits that are found by starting with the label on the left."让我们再次回到中间图:

                   F   <-- master
                 /
A <- B <- C <- D
                 \
                   E   <-- production/master

如果我们从 production/master 开始,我们会得到提交 E。从那里向后工作,我们得到 D,然后是 C,等等。从 master 开始,我们得到 F,然后是 D,然后是 C,依此类推。因此 master..production/master 说 "show E, but then don't show D or C or any of those earlier commits":它们都可以通过从 F 返回来访问。因此,这向我们展示了我们从遥控器 git 中获取的内容,而没有向我们展示之前已经拥有的内容。特别是,它显示了我们的主人 "behind" 的提交,也就是他们 "ahead".

如果将两个点 production/master..master 的左侧和右侧反转,则告诉 git 到 select master 上的提交未在production/master。这是我们的 master "ahead" 或者他们的 "behind".

总结:使用git fetch然后双点语法来查看你有什么他们没有,反之亦然。使用 git rebase 将您拥有的复制到他们拥有的末尾。