推送到远程失败,因为 "tip of your current branch is behind its remote counterpart"

Pushing to remote fails because "tip of your current branch is behind its remote counterpart"

$ git config pull.ff only
$ git pull
Already up to date
$ git checkout EditReadMe1
Switched to branch 'EditReadMe2'
$ git rebase master
Current branch EditReadMe2 is up to date
$ git push myremote EditReadMe2
To https://github.com/[redacted repo]-playground
 ! [rejected]         EditReadMe2 -> EditReadMe2 (non-fast-forward)
error: failed to push some refs to 'https://github.com/[redacted repo]-playground'
hint: Updates were rejected because the tip of your current branch is behind
hint: it's 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

我的朋友正在尝试帮助我学习解决拉取请求中的冲突。他创建了一个存储库。我克隆了它,并创建了一个名为“EditReadMe2”的 b运行ch。我将“EditReadMe2”推送到存储库,他创建了一个冲突。

我原来是运行下面的命令

git checkout master
git pull
git checkout EditReadMe2
git rebase master

它警告我存在冲突,我已解决,但是当我尝试推送 EditReadMe2 时,它给了我错误。

我再次输入命令以在附图中显示我的终端,因为我不知道当我第二次 pull 和 rebase 时 b运行ch 怎么可能落后,它告诉我一切都是最新的,但它仍然失败。

强制推送解决了问题,但我想知道如何在不使用 --force 的情况下做到这一点。

命令序列不完整。
git checkout EditReadMe2 之后你需要执行另一个 git pull.

git pull 更新当前分支的工作索引,而不是所有 local 分支的索引。
当您发出变基命令时,您正在将更新的 master 变基到您的“旧”EditReadMe2 分支。

总之git rebase这样用还是可以的


一个提示:
如果你切换到 master, git pulling, 切换回 EditReadMe2 only 为 rebase 的目的, 你可以使用以下顺序和保存几个命令:

假设您在 EditReadMe2

git pull
git rebase origin/master

变基通常会产生这个结果——这需要使用--force——因为变基用新的和改进的[=216]替换一些现有的提交=]1 提交。要真正理解这是如何工作的,您需要了解 Git 如何使用和查找提交,以及 git push 和其他命令如何工作。这有点棘手!先看一下my long answer to How to delete all unpushed commits without deleting local changes,想知道画的是什么样的:

...--G--H   <-- master
         \
          I   <-- feature (HEAD)

可能是这个意思。特别是,您需要记住这些字母如何代表原始哈希 ID,每个提交如何向后指向其父提交,以及 b运行ch 名称如何指向 latest 提交/包含在 b运行ch.


1至少,我们希望它们有所改进。


正在设置

现在假设我们有一系列提交,这些提交本身没有缺陷——我们真的不需要修复它们中的任何东西——但它们是早些时候做出的,就像这样:

...--G--H   <-- master
         \
          I--J   <-- feature

(没有附加的 HEAD 表示我们不关心在这一点之前签出了哪一个)。我们 运行 git checkout mastergit switch 主人,然后 git pull 或类似的,并获得一个新的 master 提交,给我们这个:

...--G--H--K   <-- master (HEAD), origin/master
         \
          I--J   <-- feature, origin/feature

我们还添加或更新了这些远程跟踪名称origin/masterorigin/feature。它们是我们Git对一些otherGit的b运行ch名字的记忆。我们的名称 origin/master 标识提交 K,现在我们自己的 b运行ch 名称 master 也是如此;我们的名字 origin/feature 告诉我们在 origin 上,他们有我们的 b运行ch 名字 feature 的副本,它也标识了提交 J,就像我们的 feature。 (也许他们在早些时候我们 运行 git push origin feature 时得到了它。)

下一部分很重要:提交哈希 ID——这些大写字母代表的大丑陋的字母和数字串——相同跨两个存储库。 b运行ch 名称不需要,但在这种特殊情况下,它们现在也是如此。

Rebase 通过复制提交来工作

在此设置中,我们确定我们的功能的缺陷是它基于提交 H,而最新的提交现在是提交 K。我们希望 feature b运行ch 基于提交 K。为此,我们 运行:

git switch feature       # or git checkout feature

给我们:

...--G--H--K   <-- master, origin/master
         \
          I--J   <-- feature (HEAD), origin/feature

其次是:

git rebase master

rebase 命令列出了 b运行ch feature 不是 master 上的那些提交的原始哈希 ID ].在这种情况下,这是提交 IJ 的哈希 ID。 (请注意,提交 H 和更早的提交在 both b运行ches 上。)然后,Git 使用其特殊的“分离 HEAD”模式启动使用提交 K,在 master:

的末尾
...--G--H--K   <-- HEAD, master, origin/master
         \
          I--J   <-- feature, origin/feature

Git 应用我们在提交 I 中所做的并从中进行新的提交。这个新提交有一个新的、不同的哈希 ID,但重新使用了来自 I 的作者姓名和日期和时间戳,并重新使用了来自 I 的提交消息,因此commit 看起来非常像 commit I。换句话说,它是提交 Icopy2 我们称这个新副本为 I':

             I'  <-- HEAD
            /
...--G--H--K   <-- master, origin/master
         \
          I--J   <-- feature, origin/feature

已成功将 I 复制到 I',Git 现在以相同的方式复制 J,结果:

             I'-J'  <-- HEAD
            /
...--G--H--K   <-- master, origin/master
         \
          I--J   <-- feature, origin/feature

复制过程现在已经完成——没有更多的提交要复制——所以 rebase 执行它的最后一步,即将名称 feature 从它用来命名的提交中拉出来并使其指向最后复制的提交,在本例中为 J':

             I'-J'  <-- feature (HEAD)
            /
...--G--H--K   <-- master, origin/master
         \
          I--J   <-- origin/feature

如图所示,在这最后一步中,Git 重新连接 HEAD,这样我们就回到了使用连接的 HEAD 的正常模式,即在 b运行ch.

请注意,这里的两个原始提交不再可以使用名称 feature 找到。如果我们没有名字 origin/feature 记住 other Git 的 feature,我们将完全放弃这两个提交。但是我们的Git记得他们的Git记得使用他们的名字feature提交J .

无论哪种情况,请注意我们所做的事情。 我们抛弃了,或者至少试图抛弃旧的提交,支持这些新的和改进的提交。我们仍然可以通过我们的 origin/feature 名称访问旧的提交,因为我们记住 origin 上的 Git 是通过 its b运行ch name [= 记住提交 J 35=].


2如果愿意,您可以使用 git cherry-pick 自己复制任何提交。 rebase 所做的是分离你的 HEAD,然后执行一组自动的 cherry-picks,然后是这个 b运行ch-name 动作,类似于 git resetgit branch -f .在旧版本的 Git 中,rebase 可以默认为替代策略,该策略实际上不是 运行 git cherry-pick,但这些细节通常并不重要。


git push 的工作原理

git push 命令的工作原理是让您的 Git 调用其他 Git。另一个 Git 也有提交和 b运行ch 名称。他们的 b运行ch 名称不需要与您的 b运行ch 名称匹配,但如果不匹配,事情就会变得非常混乱,因此大多数人在这里将他们的 b运行ch 名称设为相同.

他们的 Git 列出了你的 Git,他们的 b运行ch 名称和提交哈希 ID。3 这可以让您的 Git 找出您有哪些提交,而他们没有,他们需要。您的 Git 然后通过它们的哈希 ID 将这些提交发送到它们的 Git。除了这些提交,您的 Git 还会发送它们 Git 需要的任何其他内部对象。

发送正确的对象后,您的 Git 现在会发送一个或多个礼貌的请求或命令。礼貌的请求有这样的形式: 请,如果可以,请将您的名字 ______(填写一个 b运行ch 或标签名称)设置为 ______(填写一个hash ID). 命令有两种形式之一: 我认为你的名字 ______ (填一个 b运行ch 或标签名) 设置为 ______(填写一个哈希ID)。如果是,请将其设置为 ______! 或: 将您的姓名 ______ 设置为 ______!

礼貌的请求表单将要求他们将他们的feature设置为指向提交J',我们的J副本用作 J 的新改进版本。 他们,然而,他们不知道这是一个新的和改进的副本——他们只能说我们要求他们 扔掉 提交 IJ,并让他们的名字 feature 记住提交 J'。他们说不!他们说如果我这样做,我会失去一些提交。

这就是我们希望他们做的:丢失提交 IJ,用新的和改进的替换它们。要让他们这样做,我们必须向他们发送命令。

如果我们使用 git push --force-with-lease,我们将向他们发送条件命令:我认为您的 feature 标识提交 J;如果是这样,让它识别 J' 如果他们接受这个命令并执行它,我们和他们将有提交 I'-J' 并且我们现在可以像这样绘制我们的存储库:

             I'-J'  <-- feature (HEAD), origin/feature
            /
...--G--H--K   <-- master, origin/master
         \
          I--J   [abandoned]

--force-with-lease 选项通常是执行此操作的正确方法 if 完全允许这样做。 执行这会迫使在另一个 Git 存储库中使用 feature b运行ch 的任何其他人更新 他们的 b运行 ches 使用新的和改进的提交。一般来说,在开始以这种方式重新定基之前,让每个人都同意 feature 可能会以这种方式重新定基是个好主意。你需要做的是弄清楚“每个人”是谁。如果那只是你自己,你只需要同意你自己。如果是你和六个同事,先征得同事的同意。

使用 git push --force 而不是 --force-with-lease,省略了安全检查:它只是向另一个 Git 发送命令 set your feature 没有任何有条件的“我认为”部分。如果您的 Git 与他们的 Git 是最新的,那么您的 origin/feature 和他们的 feature 都可以识别提交 J,这是可以的。但是,如果就在您完成工作并准备推送之后,其他人 添加了一个新提交 L 到 [=275= 中的 feature ] 在 origin 结束?你的强制推送也会告诉 Git 放弃 提交。

force-with-lease 选项更好,因为你的 Git 会告诉另一个 Git 你相信他们的 feature 识别提交 J,而不是提交L。他们会说:糟糕,不,我现在是 L 而你的 git push --force-with-lease 会失败。您现在可以 git fetch,看到有一个新的提交 L,并修复您的 rebase 以复制提交 L,然后再次尝试您的 git push --force-with-lease 现在您的 origin/feature 说提交 L.


3这里的确切机制是为Git智能协议v2重写的,它在Git 2.26中首次默认开启。我不会详细介绍,但在早期的 v2 协议中有一个小但令人讨厌的小错误,您的 Git 有时会推送太多对象。此错误已在 Git 2.27 中修复。如果你有 2.26 并且推送花费的时间太长,你可以使用 git -c protocol.version=0 push ... 解决它,或者只是升级。