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

After I rebase and then sync, history doubles in the PR and shows unrelated changes

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

情况:

我执行的命令:

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 .

这之后,我看到的结果是:

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

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

我怀疑是pull-before-push的问题

这是正常的。请注意,这不是 理想的 ,但很正常——这是一些人完全避免使用 git rebase 的原因之一。

Long:为什么会这样

首先记住什么是 Git 提交是什么:

  • 每次提交都会存储所有文件的快照,以及一些元数据:提交人的姓名和电子邮件地址、date-and-time 戳记等。

  • 每个提交都有一个唯一的编号。这个数字不是一个简单的计数——它不是 1、2、3 等等——而是一个又大又丑的 random-looking(但根本不是随机的)哈希 ID。哈希 ID 是两个 Git 可以判断它们是否都有提交的方式,因为此哈希 ID 在 every Git 中的计算方式相同。如果他们的 Git 有提交而你没有,那么你的 Git 在其数据库中没有提交编号。如果您的 Git 有提交而他们没有,那么您的 Git 在其数据库中有提交编号(和提交),而他们没有。

此外,Git 存储库中的 历史记录 只是一组提交。 Git 通过在每个提交中存储提交的 parent 提交的提交编号(哈希 ID)来解决这个问题,或者对于合并提交,父提交(复数)。这些是此提交之前的提交。

如果我们忽略合并提交,我们会变得简单,backwards-looking 提交链,我们可以这样绘制:

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

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

A branch name in Git 简单地保存 last 提交的提交号——丑陋的大哈希 ID在链中。因此,如果您的分支 master 具有上述提交,我们可以这样绘制:

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

我们真的不需要 backwards-pointing 箭头 提交之间,只要我们记住它们总是指向后方,因为任何提交中的任何内容都不可能变了。这包括父提交的哈希 ID。

分支名称和其他名称,例如 remote-tracking 名称,但是,移动。所以我们会画出他们的箭头来提醒我们。

绘制您的设置

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

...--F   <-- master, origin/master, upstream/master
      \
       G--H   <-- workbranch, origin/workbranch

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

git checkout master
git fetch upstream

这给了我们:

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

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

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

git merge upstream/master

产生:

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

请注意 master 现在如何指向现有提交 Jorigin 上的 Git 甚至还没有提交 I-J,我们对其 master 的记忆,在我们的 origin/master 中,仍然指向提交 F.

最后,我们运行:

git push origin master    # note: not origin/master

我们 Git 在 origin 呼叫 Git。这就是为什么这是 origin master 而不是 origin/master:我们想在 origin 调用 Git,并根据 our[=446= 发送提交] master,这也是为什么最后一部分是master而不是origin/master的原因。因此,我们将提交 I-J(我们通过 upstreammasterupstream 获得)发送到 origin,并要求 origin 设置 他们的 master 指向提交 J.

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

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

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

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

Rebase 假装更改提交

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

让我们 re-draw commi 后没有 up-kink 的最终情况F。我们可以随心所欲地绘制图表,只要我们可以从名称到提交,然后遵循内部 backwards-pointing 箭头。 git log --graph 命令绘制了一个图,其中较新的提交朝向图的 top,但对于 Whosebug,我更喜欢将较新的提交绘制在右侧。

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

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

  • 使用 Git 的 分离 HEAD 模式提取提交 J 到 work-tree;
  • 使用git cherry-pick复制提交G到这里;
  • 再次使用git cherry-pick复制提交H;最后
  • 强制使用名称 workbranch 来标识 last-copied 提交。

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

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

git checkout workbranch

这给了我们:

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

同样,提交没有改变,但我们现在正在处理从提交H.

中提取的文件

然后我们运行:

git rebase master

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

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

接下来,因为我们说过 git rebase master,Git 对提交 J:

进行特殊的 detached-HEAD 模式检出
...--F--I--J   <-- HEAD, master, origin/master, upstream/master
      \
       G--H   <-- workbranch, origin/workbranch

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

             G'  <-- HEAD
            /
...--F--I--J   <-- master, origin/master, upstream/master
      \
       G--H   <-- workbranch, origin/workbranch

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

             G'-H'  <-- HEAD
            /
...--F--I--J   <-- master, origin/master, upstream/master
      \
       G--H   <-- workbranch, origin/workbranch

现在所有提交都已成功复制(或者您已修复它们并根据需要使用 git rebase --continue),Git 将名称 workbranch 拉到指向 H' 提交,并且 re-attaches HEAD:

             G'-H'  <-- workbranch (HEAD)
            /
...--F--I--J   <-- master, origin/master, upstream/master
      \
       G--H   <-- origin/workbranch

似乎 两个现有的提交似乎以某种方式移动了,因为新的提交具有相同的作者和 time-stamp 以及日志消息等等。提交 numbers 有什么不同,但谁真的会 look 看那些又大又丑的哈希 ID?

我们的 Git 故意 忘记了 workbranch 曾经指向提交 H。相反,我们的 workbranch 现在指向 new-and-improved 提交 H'。但是请注意,我们的 Git 记得 origin 处的 Git 有 他们的 workbranch 记住现有的提交 H.

git push

假设我们现在 Git 调用 他们的 Git,在 origin,并发送提交 G'-H'给他们:

git push origin workbranch

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

当我们礼貌地要求他们将他们的 workbranch 从他们的(和我们的)H 移到我们的(现在也是他们的)H' 时,他们说 no 因为如果他们这样做,他们会忘记如何找到提交 H。他们不知道 H' 是 new-and-improved 替代 H。他们只知道,如果按照我们的要求去做,他们就会忘记 H。他们将没有仍然可以找到的名字 H.

所以,他们说不。

git pull

如果你现在 运行 git pull origin workbranch,甚至 git pull 没有参数,你现在可以 Git 调用他们的 Git 并询问他们关于 他们的 workbranch。他们会说:哦,当然,我的 workbranch,它上面有这两个非常好的提交 GH,你喜欢吗? 如果您的 Git 已经扔掉了旧的 G-H,它会占用这些副本。如果没有——你的 Git 肯定还有它们,因为你的 origin/workbranch 一直记得它们——你的 Git 说它已经有了它们,但无论如何谢谢,现在你的 Git 知道他们的 workbranch 指向提交 H。因此,如果需要,您的 Git 会更新您的 origin/workbranch(不需要,因为您的 origin/workbranch 已经还记得 H):

             G'-H'  <-- workbranch (HEAD)
            /
...--F--I--J   <-- master, origin/master, upstream/master
      \
       G--H   <-- origin/workbranch

现在 你的 Git 运行任何git pull的后半部分。

git pull命令实际上由运行ning tw组成 Git 命令:

  • 第一个命令总是git fetch。这就是让您的 Git 调用他们的 Git 并询问他们的 workbranch 的步骤(也许还有他们的其他分支,具体取决于您 运行 git fetch).此步骤带来了他们拥有的任何提交,而您没有,而您的 Git 将需要这些提交。然后,您的 Git 会在必要时更新您的 origin/* 名称。

  • 第二个命令默认为git merge。他们所说的任何提交上的合并 运行s 是 他们的 分支的最后一次提交。

所以在这里,你的 Git 运行s git merge 在提交 H 的哈希 ID 上——他们的 workbranch,这是你的 origin/workbranch.所以你的 Git 现在 合并 你的提交 H' 与共享提交 H:

             G'-H'-M  <-- workbranch (HEAD)
            /     /
...--F--I--J  <- / -- master, origin/master, upstream/master
      \         /
       G-------H   <-- origin/workbranch

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

git push,再次

您的 Git 现在可以发送他们的 Git 提交 M,新的合并。如果他们使他们的 workbranch 标识提交 M,他们现有的提交 G-H 仍然可以在 他们的 存储库中访问,因此他们对此感到满意。但是在这一点上,您复制了 git rebase 复制的所有提交(现在它们也会)。这根本不是你想要的。

注意:成功的 git push 将更新您的 origin/workbranch 以记住他们的 workbranch 现在记住提交 M 的事实,所以现在绘图看起来像这样:

             G'-H'-M  <-- workbranch (HEAD), origin/workbranch
            /     /
...--F--I--J  <- / -- master, origin/master, upstream/master
      \         /
       G-------H

(我们可以通过将 G-H 线移动到绘图的顶部来简化它,但我们不要那样做。)

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

  • 强迫你的 Git 假装你根本没有进行合并提交 M
  • 强制 Git 在 origin 设置 his workbranch 名称以记住提交 H' 而不是 M.

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

git reset --hard

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

git checkout workbranch         # if needed - we're probably already there

这确保我们有:

             G'-H'-M  <-- workbranch (HEAD), origin/workbranch
            /     /
...--F--I--J  <- / -- master, origin/master, upstream/master
      \         /
       G-------H

现在在我们的存储库中。那么:

git reset --hard HEAD~1

HEAD~1 符号表示 向后移动一个 first-parent,从提交 M 到提交 H'。这使得我们的名字 workbranch 指向提交 H'。为了绘制它,让我们将提交 M 向下移动到底行:

             G'-H'  <-- workbranch (HEAD)
            /    \
...--F--I--J   <- \ -- master, origin/master, upstream/master
      \            \
       G-------H----M   <-- origin/workbranch

既然我们的 workbranch 识别了提交 H',我们 运行:

git push --force origin workbranch

这有我们的 Git 调用他们的 Git,在 origin,告诉它关于提交 H' — 他们当然已经有了它,在这一点上—然后强制命令它们:设置你的分支名称 workbranch 指向提交 H'!(这来自 --force,并采用通常礼貌请求的地方。)

假设他们服从——那部分由他们决定,但如果你可以控制这个存储库,只要确保你给自己 force-push 权限——他们将移动 他们的 workbranch 指向提交 H',你的 Git 将相应地更新你的 origin/workbranch

             G'-H'  <-- workbranch (HEAD), origin/workbranch
            /    \
...--F--I--J   <- \ -- master, origin/master, upstream/master
      \            \
       G-------H----M   [abandoned]

既然他们和你都没有 名称查找 提交 M,你甚至不会看到它。一切都会像从未存在过一样:

             G'-H'  <-- workbranch (HEAD), origin/workbranch
            /
...--F--I--J   <-- master, origin/master, upstream/master

同样,这对于 rebase

来说是正常的

变基的问题是它通过复制提交到new-and-improved提交来工作。

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

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

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

避免 rebase 可以完全避免这种意外,但最终这完全取决于您和与您一起工作的任何人。如果你们都同意变基发生——一些提交可以消失,你不会把它们带回来我他们应该留下来[=44​​6=]——然后你就可以这样工作了。