在我变基然后同步后,PR 中的历史记录加倍并显示不相关的更改

Abe*_*bel 3 git github rebase

今天早些时候我问了这个问题,现在我尝试了更多的事情,我可能会有更好的理解。

情况:

  • 我有一个 F# 的叉子
  • 我有一个当地人master和一个当地人workbranch,两者也在我的叉子里。
  • 有一个workbranch的PR
  • Workbranch 有 7 个文件已更改
  • 一个 PR 被合并到对我自己的工作有用的上游 master 中,所以我想重新调整我的分支
  • 我的工作分支比 master 落后 1 次提交。我想要最后一次提交,其中有 26 个已更改的文件
  • 我喜欢看到我的更改就像在 master 头之后一样(因此:rebase)

我执行的命令:

git checkout master
git fetch upstream
git merge upstream/master
git push origin/master

git checkout workbranch // up-to-date with origin
git rebase master
git rebase --continue   // after solving merge conflict
git pull .    // not sure why there were changes to be pulled, was this where I went wrong?
git push .
Run Code Online (Sandbox Code Playgroud)

之后,我看到的结果是:

  • 今天早些时候的 21 次提交
  • master 提交了 1 次包含这 26 个更改的文件
  • 另外 8 个提交与之前 21 个提交中的一些完全相等
  • 此变基的合并提交
  • diff 显示 33 个文件已更改

在 github.com 上的分支比较概述中,在我的 fork 中,我看到:

  • 今天早些时候的 21 次提交
  • 另外 8 个提交与上面类似
  • rebase 的合并提交
  • 此处的差异显示 7 个文件已更改

我期望看到的是我的原始提交,每个提交只有一次,不重复,合并提交,并且不应该有来自 master 的提交

我怀疑这与先拉后推有关。

tor*_*rek 6

这个是正常的。请注意,这是不可取,但正常\xe2\x80\x94这是一些人完全避免使用的原因之一git rebase

\n

长:为什么会出现这种情况

\n

首先记住 Git 提交是什么以及它的作用:

\n
    \n
  • 每次提交都会存储所有文件的快照,以及一些元数据:提交者的姓名和电子邮件地址、日期和时间戳等。

    \n
  • \n
  • 每个提交都有一个唯一的编号。这个数字不是一个简单的计数\xe2\x80\x94,它不是1、2、3等\xe2\x80\x94,而是一个大的、丑陋的、看起来随机的(但根本不是随机的)哈希ID。哈希 ID 是两个 Git 可以判断它们是否都有提交的方式,因为这个哈希 ID 在每个 Git中都是以相同的方式计算的。Git 中的计算方式都是相同的。如果他们的 Git 有提交,而您没有,则您的 Git 数据库中没有提交编号。如果您的 Git 有提交,而他们没有,则您的 Git 在其数据库中有提交编号(和提交),而他们没有。

    \n
  • \n
\n

此外, Git 存储库中的历史记录只是提交的集合。Git 通过在每个提交中存储提交号\xe2\x80\x94 和提交的父提交的哈希 ID\xe2\x80\x94 来完成此工作,或者对于合并提交,存储父提交(复数)。这些是在此提交之前的提交。

\n

如果我们忽略合并提交,我们会得到简单的、向后看的提交链,我们可以这样绘制:

\n
... <-F <-G <-H\n
Run Code Online (Sandbox Code Playgroud)\n

这里H代表链中最后一次提交的实际哈希 ID。H的元数据中,Git 存储了 commit 的实际哈希 ID G。因此,通过读取 的内容H,Git 可以找到 的提交号G,这让 Git 读取G包含 的提交号的提交F,依此类推。

\n

Git 中的分支名称仅保存提交号\xe2\x80\x94——链中最后一次提交的又大又难看的哈希 ID\xe2\x80\x94。因此,如果您的分支master有上述提交,我们可以这样绘制:

\n
...--F--G--H   <-- master\n
Run Code Online (Sandbox Code Playgroud)\n

我们实际上并不需要提交之间的向后指向的箭头,只要我们记住它们总是指向向后即可,因为任何提交中的任何内容都无法更改。其中包括父提交的哈希 ID。

\n

然而,分支名称和其他名称(例如远程跟踪名称)将移动。所以我们会画出他们的箭头来提醒我们。

\n

绘制您的设置

\n

我们可以这样画出你的初始情况:

\n
...--F   <-- master, origin/master, upstream/master\n      \\\n       G--H   <-- workbranch, origin/workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

现在我们更新我们的upstream/master,其中有一些新的提交,具有它们自己唯一的哈希 ID:

\n
git checkout master\ngit fetch upstream\n
Run Code Online (Sandbox Code Playgroud)\n

这给了我们:

\n
       I--J   <-- upstream/master\n      /\n...--F   <-- master (HEAD), origin/master\n      \\\n       G--H   <-- workbranch, origin/workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

git checkout步骤确保我们当前的分支是master,即我们正在使用 commit F。这就是为什么我们在这里将特殊名称HEAD附加到分支名称上master

\n

接下来,我们让 Git移动我们的名称master以指向我们刚刚获得的最后一个新提交upstream

\n
git merge upstream/master\n
Run Code Online (Sandbox Code Playgroud)\n

其产生:

\n
       I--J   <-- master (HEAD), upstream/master\n      /\n...--F   <-- origin/master\n      \\\n       G--H   <-- workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

请注意 now 如何master指向现有的 commit J。at 上的 Gitorigin甚至还没有提交I-J,而我们对它的记忆master在 our 中origin/master仍然指向 commit F

\n

最后,我们运行:

\n
git push origin master    # note: not origin/master\n
Run Code Online (Sandbox Code Playgroud)\n

这让我们的 Git 调用了位于 的 Git origin。这就是为什么这是origin master而不是origin/master:我们想要调用 Git origin,并根据我们的 master发送提交,这也是为什么最后一部分是master而不是origin/master。因此,我们将提交I-J(我们从upstreamvia获得upstreammaster)发送到origin,并要求将它们origin设置为指向提交。 masterJ

\n

假设他们服从,这就是我们在这个过程结束时在本地得到的结果:

\n
       I--J   <-- master (HEAD), origin/master, upstream/master\n      /\n...--F\n      \\\n       G--H   <-- workbranch, origin/workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,在这整个过程中没有任何提交发生变化。整个过程就是origin从其他存储库(位于 的那个)提交到特定的 Git 存储库(我们的,以及位于 的那个)upstream,并更新我们的分支名称 ( ) 和Git 中的master名称(我们的 Git 保留了一个内存)在我们的)中。masteroriginorigin/master

\n

(这一切都非常令人困惑:需要很长时间才能习惯所有的重复。我发现将每个存储库视为不同的“人”会有所帮助:上游先生了解提交I-J,然后我们了解它们,然后我们告诉起源先生。)

\n

Rebase 假装更改提交

\n

为了git rebase完成它的工作,它必须假装更改提交。这实际上是完全不可能的。相反,rebase 获取现有提交并使用它们来进行新的提交,这些提交略有不同,因此具有不同的提交编号。

\n

让我们重新绘制我们的最终情况,而不需要在 commit 后出现向上扭结F。我们可以随心所欲地绘制图表,只要我们能够从名称到提交,然后遵循内部向后指向的箭头即可。该git log --graph命令绘制一个图表,其中较新的提交位于图表顶部,但对于 StackOverflow,我更喜欢在右侧绘制较新的提交。

\n
...--F--I--J   <-- master (HEAD), origin/master, upstream/master\n      \\\n       G--H   <-- workbranch, origin/workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

我们想做的是假装我们G从 commit 中进行了提交J。当然,我们没有,但是git rebase可以:

\n
    \n
  • 使用 Git 的分离 HEAD模式将提交提取J到工作树;
  • \n
  • 用于在此处git cherry-pick复制提交G
  • \n
  • 再次使用git cherry-pick复制提交H;最后
  • \n
  • 强制名称标识workbranch上次复制的提交。
  • \n
\n

变基操作在每个步骤中都可能遇到障碍git cherry-pick,看起来您的操作曾经遇到过这样的问题。

\n

我们首先告诉 Git 提取提交H并附加到HEAD此处。这就是git rebase决定复制哪些提交的方式:它将查看HEAD. 所以我们运行:

\n
git checkout workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

这给了我们:

\n
...--F--I--J   <-- master, origin/master, upstream/master\n      \\\n       G--H   <-- workbranch (HEAD), origin/workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

同样,提交没有改变,但我们现在正在处理从 commit 中提取的文件H

\n

然后我们运行:

\n
git rebase master\n
Run Code Online (Sandbox Code Playgroud)\n

workbranchGit 现在列出了不在 上的提交的原始哈希 ID master。请注意,master包含提交...-F-I-J,结束于J,而workbranch包含提交...-F-G-H,结束于H。sF和更早的提交被取消,并且I-J提交根本不存在workbranch,因此这里要复制的提交列表只是GH

\n

(在您的情况下,有两个以上的提交需要复制,但结果应该足够清楚。)

\n

接下来,因为我们说过git rebase master,Git 对提交执行特殊的分离 HEAD 模式签出J

\n
...--F--I--J   <-- HEAD, master, origin/master, upstream/master\n      \\\n       G--H   <-- workbranch, origin/workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

现在,Git 使用git cherry-pick(或者或多或少等效的东西,取决于您的 Git 版本和传递给的标志git rebase)将commit 中所做的更改G复制到HEAD现在的位置。如果一切顺利,Git 会自行进行新的提交。要记住它是 的副本G,我们将其称为G'

\n
             G'  <-- HEAD\n            /\n...--F--I--J   <-- master, origin/master, upstream/master\n      \\\n       G--H   <-- workbranch, origin/workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

rebase 命令继续复制剩余的提交,给出:

\n
             G'-H'  <-- HEAD\n            /\n...--F--I--J   <-- master, origin/master, upstream/master\n      \\\n       G--H   <-- workbranch, origin/workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

现在所有提交都已成功复制(或者您已修复它们并git rebase --continue根据需要使用),Git 会将名称workbranch拉过来以指向H'提交,然后重新附加HEAD

\n
             G'-H'  <-- workbranch (HEAD)\n            /\n...--F--I--J   <-- master, origin/master, upstream/master\n      \\\n       G--H   <-- origin/workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

似乎两个现有提交以某种方式移动了,因为新提交具有相同的作者、时间戳和日志消息等。提交编号有什么不同,但谁真的会费心去查看那些又大又难看的哈希 ID?

\n

我们的 Git 故意忘记了workbranch曾经指向 commit 的地方H。相反,我们的workbranchnow 指向新的和改进的 commit H'。但请注意,我们的 Git 会记住 at 上的 Gitorigin记住 workbranch现有的提交H

\n

git push

\n

假设我们现在让 Git 通过 at 调用他们的Git origin,并向G'-H'他们发送提交:

\n
git push origin workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

他们会将G'H'放入自己的存储库中,至少是暂时的,然后考虑我们的请求,让他们更改名称workbranch指向 commit H'。但现在,他们会说不

\n

workbranch当我们礼貌地要求他们将他们的从他们的(和我们的)转移H到我们的(现在也是他们的)时H',他们说不因为如果他们这样做,他们会忘记如何找到 commit H。他们不知道H'是. 他们只知道,如果他们按照我们的要求去做,他们就会忘记。他们不会有一个仍然可以找到的名字。H HH

\n

所以,他们说不。

\n

git pull

\n

如果您现在运行git pull origin workbranch,或者甚至git pull不带任何参数,您现在可以让 Git 调用他们的 Git 并询问他们 workbranch. 他们会说:哦,当然,我的workbranch,它有这两个非常好的提交GH你喜欢它们吗? 如果你的 Git 已经扔掉了旧的G-H,它就会拿走这些副本。如果不是\xe2\x80\x94,你的 Git 肯定仍然有它们,因为你origin/workbranch一直记得它们\xe2\x80\x94,你的 Git 说它已经有了它们,但无论如何还是谢谢,现在你的 Git 知道它们workbranch要提交的点H。因此,如果需要的话,你的 Git 会更新你origin/workbranch的:origin/workbranchH

\n
             G'-H'  <-- workbranch (HEAD)\n            /\n...--F--I--J   <-- master, origin/master, upstream/master\n      \\\n       G--H   <-- origin/workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

现在你的Git 运行任何.git pull

\n

git pull命令实际上包含运行两个Git 命令:

\n
    \n
  • 第一个命令始终是git fetch. 这就是让你的 Git 调用他们的 Git 并询问他们的情况workbranch(也许还有他们的其他分支,这取决于你如何运行git fetch)的步骤。此步骤将带来他们拥有的、您没有的、您的 Git 需要的所有提交。然后,您的 Git 会origin/*在必要时更新您的姓名。

    \n
  • \n
  • 第二个命令默认为git merge. 合并在他们所说的分支的最后一次提交上运行

    \n
  • \n
\n

因此,在这里,您的 Git在 commit \xe2\x80\x94theirgit merge的哈希 ID 上运行,这是您的. 所以你的 Git 现在将你的提交与共享提交合并Hworkbranchorigin/workbranchH'H

\n
             G'-H'-M  <-- workbranch (HEAD)\n            /     /\n...--F--I--J  <- / -- master, origin/master, upstream/master\n      \\         /\n       G-------H   <-- origin/workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

你为了改进和丢弃旧的而制作的那些副本G-H仍然存在。旧的G-H也还在。新的合并提交M两个分支合并在一起。一个分支包含您认为已经摆脱的提交这一事实并不重要。提交仍然存在,合并会将它们合并。

\n

git push, 再次

\n

您的 Git 现在可以发送其 Git 提交M,即新的合并。如果他们进行了workbranch识别提交M,那么他们现有的提交仍然可以在G-H存储库中访问,因此他们对此感到满意。但此时,您已经复制了所有提交的副本(现在它们也将如此)。这根本不是你想要的。git rebase

\n

注意:成功git push将更新您以记住他们现在记住 commit 的origin/workbranch事实,所以现在绘图如下所示:workbranchM

\n
             G'-H'-M  <-- workbranch (HEAD), origin/workbranch\n            /     /\n...--F--I--J  <- / -- master, origin/master, upstream/master\n      \\         /\n       G-------H\n
Run Code Online (Sandbox Code Playgroud)\n

(我们或许可以通过将G-H线条移至绘图顶部来简化此操作,但我们不要这样做。)

\n

要解决该问题,您必须执行以下操作:

\n
    \n
  • 强制你的 Git 假装你M根本没有进行合并提交。
  • \n
  • 强制 Git 设置origin名称 workbranch以记住 commitH'而不是M
  • \n
\n

此时执行此操作的最简单的 Git 命令集是git reset --hardgit push --force。让我们看看它们是如何工作的。

\n

git reset --hard

\n

我们首先让Git忘记我们的提交M

\n
git checkout workbranch         # if needed - we're probably already there\n
Run Code Online (Sandbox Code Playgroud)\n

这确保了我们:

\n
             G'-H'-M  <-- workbranch (HEAD), origin/workbranch\n            /     /\n...--F--I--J  <- / -- master, origin/master, upstream/master\n      \\         /\n       G-------H\n
Run Code Online (Sandbox Code Playgroud)\n

现在在我们的存储库中。然后:

\n
git reset --hard HEAD~1\n
Run Code Online (Sandbox Code Playgroud)\n

HEAD~1符号意味着将第一个父级从 commit移M回到 commit H'。这使得我们的名字workbranch指向 commit H'。为了绘制这个,让我们将提交M向下移动到底行:

\n
             G'-H'  <-- workbranch (HEAD)\n            /    \\\n...--F--I--J   <- \\ -- master, origin/master, upstream/master\n      \\            \\\n       G-------H----M   <-- origin/workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

现在我们workbranch确定了 commit H',我们运行:

\n
git push --force origin workbranch\n
Run Code Online (Sandbox Code Playgroud)\n

这让我们的 Git 调用他们的 Git,在 处origin,告诉它提交H'\xe2\x80\x94 他们当然已经有了它,此时 \xe2\x80\x94 然后强制命令他们:将你的分支名称设置workbranch为指向犯罪H' (这来自--force,并取代了通常的礼貌请求。)

\n

假设他们服从\xe2\x80\x94,这部分由他们决定,但如果你可以控制这个存储库,只需确保你给自己强制推送权限\xe2\x80\x94,他们会将其移动到指向 workbranchcommit H',并且你的Git 会origin/workbranch相应地更新你的:

\n
             G'-H'  <-- workbranch (HEAD), origin/workbranch\n            /    \\\n...--F--I--J   <- \\ -- master, origin/master, upstream/master\n      \\            \\\n       G-------H----M   [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n

现在他们和你都没有可以用来查找commit 的名称,你甚至看不到它。一切都将如同从未存在过:M

\n
             G'-H'  <-- workbranch (HEAD), origin/workbranch\n            /\n...--F--I--J   <-- master, origin/master, upstream/master\n
Run Code Online (Sandbox Code Playgroud)\n

再说一次,这是 rebase 的正常现象

\n

变基的问题在于它通过将提交复制到新的和改进的提交来工作。

\n

Git 的一般问题是它不愿意放弃提交。它想要添加提交,而不是删除它们以支持新的和改进的提交。

\n

每个 Git 存储库都可以轻松添加新的提交。它不会那么容易忘记旧的。因此,要将这个特定的提交发送到origin,当origin记住您的旧提交H而不是新的和改进的时H',您必须强制推送。您可以使用--force-with-lease,它增加了一种安全检查,他们workbranch仍然记得H而不是其他提交。

\n

如果 Git 存储库还有其他用户origin,请记住他们也可能正在使用或添加originworkbranch。您应该确保所有这些用户都希望删除和替换提交。否则其他用户会对这种行为感到惊讶。

\n

避免变基完全可以避免这种意外,但最终这实际上取决于您和与您一起工作的任何人。如果你们都同意变基发生\xe2\x80\x94,一些提交可能会消失,并且如果它们应该保持消失,那么你不会将它们带回来\xe2\x80\x94,那么你可以这样工作。

\n