Git 使用分叉远程进行变基

Git rebase with forked remote

有一个主要的 git 回购;主要回购;主分支 - 我的远程。
我将其分叉到我的个人资料中:main-repo;主分支 - 起源

在分叉后添加了对远程的提交。
分叉后添加了对 origin 的提交。所以现在这是前一提交和后一提交。

目标:将 my-remote 重新设置为 origin。以这种方式,origin 应该首先从 my-remote 提交,然后再从 origin 提交。我知道这将创建新的提交 (SHA)。

我正在从我的个人资料中克隆。
git clone url2。 - 起源

将远程仓库添加为另一个远程仓库。
git remote add my-remote gitRepoToRemote

我在我的本地 master 分支上,它正在跟踪来源。
正在尝试变基。
git fetch --all.
git rebase my-remote/master.

有冲突。
手动解决。
然后 git add file-with-conflict.
继续变基 git rebase --continue.
git push origin HEAD:master.
获取错误。为什么在推送前解决冲突时会出现此错误?

 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'gitRepoToOrigin'
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

正如评论中暗示(但未说明)的那样,您将需要某种强制推动。原因并没有那么复杂,但是画出原因,与其说说而已,还是很有帮助的。这里的问题,用一种有趣的方式来说,是你有三个不同的存储库,都命名为 Bruce,一个新的提交命名为 Bruce,一个重写的新提交命名为 Bruce。您现在需要告诉 Bruce 忘记 Bruce,因为 Bruce 有了新的 Bruce 并且 Bruce 需要与 Bruce 匹配。

(以上有点矫枉过正,但也请参见had had had had had句。)

而不是称每个人和所有事物为“Bruce”,或者尝试给出每个提交的确切哈希 ID——这会导致类似(尽管不同)的问题,因为 人类 不擅长哈希 ID,将每个 ID 转换为他们头脑中类似“Bruce”的哔哔声——让我们将每个存储库绘制为一系列提交,其中提交由单个大写字母指定。我们首先从您所谓的“main-repo”开始,然后在您使用 git remote add:

后调用 my-remote
my-remote:

...--F--G--H   <-- master

名称 master 存在于此存储库中。该名称存储提交的哈希 ID H(一些丑陋的大哈希 ID)。右侧的提交 H 是最新的。它包含:

  • 每个文件的完整快照,以及
  • 包含早期提交的哈希 ID 的元数据 G

所以提交 H 向后链接到更早的提交 G。较早的提交 G 有每个文件的快照(来自较早的),当然,加上 still-earlier 提交 F 的哈希 ID。这一直重复到时间的开始:这就是 Git 存储提交的方式。

然后您使用 GitHub 的 fork 按钮制作了此存储库的 copy,我们将其称为 origin。此副本有自己的 b运行ch 名称,但与原始存储库共享实际提交1 在任何情况下,在 GitHub 上的这个副本中,name master 拥有相同的哈希 ID:

origin:

...--F--G--H   <-- master

然后您将 GitHub 分支克隆到您自己的计算机(我们将其称为“膝上型计算机”,即使它是台式机或桌边计算机,只是为了给它一个好记但独特的名称)。这复制了提交,但副本与原件相同——它们必须是;任何提交的任何部分在提交后都不能更改——然后将它们的 b运行ch 名称复制到 remote-tracking 名称,所以你有:

laptop:

...--F--G--H   <-- origin/master

你的 git clone 命令然后根据你的 -b master 参数,或者你缺少任何 -b 参数加上来自 GitHub 的建议,即您的 Git 在此处使用名称 master

laptop:

...--F--G--H   <-- master, origin/master

最后,在你的笔记本电脑上,你的 Git 运行 git checkout master 填充工作树(和 Git 的 index 虽然我们不会在这里讨论这个)从提交 H,使 master 成为 当前 b运行ch 并提交 H 当前提交,在laptop:

laptop:

...--F--G--H   <-- master (HEAD), origin/master

1他们是否共享原始底层提交或复制它们与我们并不真正相关。事实是提交是 100%,bit-for-bit 相同。这意味着只要 my-remoteorigin 都存储在同一台 物理机 上,它们就可以共享底层提交。如果它们存储在具有独立永久内存系统(旋转磁盘、闪存等)的独立物理机器上,他们将需要使用原始提交字节的副本,而不是真正共享底层存储,但我们不关心关于那个。


到目前为止的总结

截至此时(过去某个时间),所有三个存储库的内容看起来非常相似。您的笔记本电脑存储库中主要且可观察到的区别在于您拥有 origin/master 和提交的工作副本 H。除了这些差异(对于完成工作很重要,但在其他方面无关紧要),这三者看起来都是这样的:

...--F--G--H   <-- master

事情会随着时间而改变

接下来,您在笔记本电脑上进行了新的提交。我们将此提交称为 IH 之后的下一个字母。所以在 laptop 上我们现在有:

laptop:

...--F--G--H   <-- origin/master
            \
             I   <-- master (HEAD)

与此同时,有人在 GitHub 上为 my-remote 存储库向 master 发送了新提交。通常我会称这个提交为J,但没有明显的原因,我会在这里称它为K。所以 他们 有这个,我也会无缘无故地画得很奇怪:

my-remote:
             K   <-- master
            /
...--F--G--H

请注意名为 origin 的存储库还没有发生任何变化。

此时,您 运行 git push 在您的笔记本电脑上。无需过多担心您将 传递给 git push(如果有)的确切参数,这最终会发送一个提交,您的 lapto有,origin没有,要origin。所以现在,在 origin,你有:

origin:

...--F--G--H
            \
             I   <-- master

此推送成功后,您的笔记本电脑将更新其 origin/master。那是因为你的 git push 有你的笔记本电脑 呼叫 GitHub,通过互联网,要求 GitHub 连接到存储库 origin(哪个有效)。你的笔记本Git发送组成commitI的数据,然后询问GitHub设置他们的 master, 他们说可以, 所以你的笔记本电脑 Git 知道 origin Git 的 master 指向提交 I。所以现在 laptop 你有:

laptop:

...--F--G--H
            \
             I   <-- master (HEAD), origin/master

我们在这里绘制的图表中不需要所有这些小问题,但我是故意使用它们的。请注意,originlaptop 都不知道有关提交 K 的任何信息!同样,my-remote 从未听说过 commit I.

乐趣 (?) 开始

此时你开始做一些花哨的事情:

Added remote repo as another remote. git remote add my-remote gitRepoToRemote

您的 laptop Git 现在知道另一个 Git,名为 my-remote 存在 。它尚未连接到它。

git fetch --all

大多数人误用了--all,但在这里您已经正确地使用了它(恭喜!)。 --all 选项将您的 Git 指向 运行 git fetch 而不是 每个遥控器 .

这有点矫枉过正,因为这让你的 Git 调用了 origin my-remote,看看是什么新的,如果有的话,在这两个存储库中。 origin 中没有任何新内容, 也不可能 有任何内容,因为您将是唯一更新它的人。但没关系!但是,当您的 Git 调用名为 my-remote 的 Git 时,有些东西 新的:他们提交了 K,而您没有't.

你的 Git 因此 获得 提交 K 来自 my-remote,并将其插入你的存储库。然后你的 Git 创建了一个 remote-tracking 名字,my-remote/origin,因为这是 my-remote 的第一次联系。所以现在 laptop 有:

laptop:
             K   <-- my-remote/master
            /
...--F--G--H
            \
             I   <-- master (HEAD), origin/master

(现在你可以明白为什么我在绘图中保留扭结了)。

可以告诉你的Git告诉originGit关于提交K(发送K origin 在过程中2laptop 上的图可以看出,origin 会出现问题:名称 master 只能指向 一个 提交。您可以选择 提交 I 提交 K,但不能同时选择两者。要指向两个提交——以便您可以找到 I K——您至少需要 两个 名称。

您在 laptop 上的存储库 具有 必要的名称:my-remote/master 指向 Kmaster 指向至 Iorigin/master 也指向 I,所以你现在可以自由地让你的 master 指向 任何地方 :找到提交 K 你可以使用名称 my-remote/master,要查找 I,您可以使用名称 origin/master.


2因为originmy-remote的一个分支,origin理论上能够 更直接地提交K。理论上,GitHub 可以 ar运行为此。他们是否、如何以及何时这样做取决于他们:您的 Git 不需要知道任何相关信息。如果、如何以及何时 GitHub 添加 web 机制,而不是这种有点迂回的 fetch-and-push-again 方法,引入K 提交到 origin,同样取决于 GitHub。不过,从今天开始,您必须使用迂回方法。


变基

rebase 命令可以这样总结:我有一些我最喜欢的提交,但是这些提交有一些我 喜欢。我知道我不能 更改 任何现有提交的任何内容,所以我不想这样做,而是 copy 现有提交 new-and-improved个,把不喜欢的改了,喜欢的留了

让我们再看看此时 laptop 上的内容:

laptop:
             K   <-- my-remote/master
            /
...--F--G--H
            \
             I   <-- master (HEAD), origin/master

我们喜欢提交I的地方有两点:

  • 它没有来自提交 K
  • 的更新
  • 它在提交 H 之后出现,而不是在提交 K 之后出现。

我们喜欢它的是这些东西:

  • 它与提交 ​​H
  • 相比有一些变化
  • 它有一个精心制作的、令人愉快的详细提交消息解释为什么它有这些变化。

我们希望在进行 提交时保留这两件事,以修复我们 喜欢的两件事。我们用 git rebase 来做到这一点(在内部,它由 cherry-picking 我们想要复制的每个提交组成;只有一个,这就是为什么我们只是获得一个新的提交)。

无需过多担心 that 的进展情况——它有时会变得混乱:

Got conflicts. Resolved manually. then git add file-with-conflict. continue with rebase git rebase --continue.

但看起来你做的所有这些都是正确的——最终结果是一个新的提交:

laptop:
               J   <-- master (HEAD)
              /
             K   <-- my-remote/master
            /
...--F--G--H
            \
             I   <-- origin/master

提交,J,出现在现有提交 K 之后,并进行了正确的更改——根据 [=469] 的需要进行了修改=] 冲突——并带有正确的提交消息文本。所以 new commit J 就是我们想要的。它 完全废弃 旧提交 I

我们如何知道 J 取代了 I,而 I 不再有用? 这里唯一的答案是我们知道它是因为我们做到了Git不知道也不关心。只有 我们 知道并关心。我们的 Git,在 laptop,我们的 master 现在指向提交 J。提交 I 已被放弃,除了我们的 origin/master 仍然可以找到它。

我们现在要告诉 origin 做同样的事情

我们现在可以运行:

git push origin master

或:

git push origin HEAD:master

让我们的 Git 调用 GitHub,让他们连接到 origin 存储库,并向他们发送新的提交 J。这部分有效——但随后我们要求他们将他们的master设置为指向提交J他们拒绝.他们特别说不,错误说 如果我这样做,我将放弃一些现有的提交:

 ! [rejected]        master -> master (non-fast-forward)

non-fast-forward 消息意味着 我将丢失我的 master.

的一些提交

在这种情况下,他们将丢失的提交是提交 I。提交 I 提交 J 废弃。但是他们不知道。我们必须迫使他们放弃提交 I.

我们这样做的方法是使用各种强制选项之一(非常适合星球大战日):

git push --force origin master

或:

git push --force-with-lease origin master

它们之间是有区别的,但是 在这种特殊情况下origin 是“你的”分叉,由你控制,而不是 pushed-to由其他人)这并不重要。它们都像常规 git push 一样工作,除了最后,不是发送礼貌的请求:请,如果没问题并且没有丢失任何东西,请将您的 master 更新为记住现在提交 J,这种推送以 command 形式结束 update your master to point to commit J! Git 在 GitHub 将服从这个命令并在 origin 存储库中留下这个:

origin:
               J   <-- master
              /
             K
            /
...--F--G--H
            \
             I   ???

位于 GitHub 的 Git 最终会——这可能需要一天、一周或更长时间——运行 git gc 删除 提交 I,我们现在应该开始将其存储库绘制为:

origin:

...--F--G--H--K--J   <-- master

简化它。我们也可以开始用你的笔记本电脑这样做,除了我们需要 my-remote/master 指向 K,所以我们得到:

laptop:
                J   <-- master (HEAD), origin/master
               /
...--F--G--H--K   <-- my-remote/master

你的笔记本电脑 Git 实际上会挂起提交 I 相当长一段时间——默认情况下至少 30 天——然后才扔掉它,但由于它 不可见(在 git log 输出中)此时我们不必费心绘制它。