如何从大量提交的拉取请求中删除某些文件?

How to remove certain files from a pull request with allot of commits?

概括地说,假设我有一个包含以下目录的项目。在推送并执行拉取请求后,我将如何最终删除 file2.txt?

app/someFolder
  - file1.txt
  - file2.txt
  - file3.txt

假设我的提交是这些

Commit 1       
  file1.txt
    Hello World
  file2.txt
    Cool, Superb
  file3.txt
    December 2

git 添加 .
git提交-m“提交1”
git push --set upstream origin someBranchOnRemote

 Commit 2  
   file1.txt
     Hello World
     Boss Bass

git 添加 .
git提交-m“提交2”
git推

 Commit 3
   file3.txt
     December 3

git 添加 .
git提交-m“提交3”
git推

所以如果我要执行拉取请求,文件将如下所示

file1.txt
  Hello World
  Boss Bass
file2.txt
  Cool, Superb
file3.txt
  December 3

现在我该如何更新拉取请求,这样我就可以不包含 file2.txt?假设散列是 hash1、hash2 和 hash3。我在拉取请求中想要的最终输出是

file1.txt
  Hello World
  Boss Bass
file3.txt
  December 3

TL;DR:您需要 git rebase -i 后跟 git push --forcegit push --force-with-lease。但请阅读以下内容。

首先,旁注:Git 本身没有“拉取请求”;这些是某些托管网站的功能,例如 GitHub 和 Bitbucket。他们倾向于在每个托管站点上以类似的方式工作,但是每个站点在这里都有自己的怪癖和行为。您可能需要针对您使用的任何托管站点调整此答案。

除此之外,PR 是您向某人提出的请求,要求他们合并(或“获取并合并”=“拉取”)您所做的一些提交。在 Git 中,你并没有真正合并一个 分支: 你实际上合并了 提交 。当您 运行 git merge 时,您将合并的提交是来自某个提交链的提交,以该链中的 last 提交结束。

即:提交表单链。每个提交 in 一条链都会记住其前一个提交的原始哈希 ID。我们说提交 指向 它的父提交,我们可以这样画:

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

A 分支名称 然后简单地提供链中 last 提交的原始哈希 ID,Git将找到所有以前的提交:

...--E--F--G--H   <-- branch

当您去 提出 拉取请求时,您:

  • 首先分叉 and/or 克隆一些存储库,这样您就可以获得其他人拥有的所有 提交
  • 创建一个新的分支名称,以便您有一个指向 last 提交的名称,该提交也是 their 提交之一;
  • 进行新的提交,以便您的分支名称前进。

例如,假设他们的提交通过(然后停止)我在上面绘制为 E 的提交。 (顺便说一下,我只是因为懒惰才停止在提交之间绘制箭头:提交总是指向后方,所以任何时候你看到一条连接的“线”,它实际上是一个 backwards-pointing“箭头”。)

也就是说,他们在他们的存储库中有一些提交序列:

...--D--E   <-- somebranch

您现在在您的存储库中拥有:

...--D--E    <-- origin/somebranch

你创建了一个新分支名称指向提交E:

...--D--E    <-- my-fancy-new-feature, origin/somebranch

现在您在这个新分支“上”进行新提交

...--D--E    <-- origin/somebranch
         \
          F   <-- my-fancy-new-feature (HEAD)

这是影响三个文件的“哈希 1”或“提交 1”。提交 F 中有 所有文件 ,因为所有提交总是有每个文件的完整快照,但是文件 in 提交F 与提交 E 中的所有文件都相同,除了您更改的三个文件。 (Git 巧妙地 de-duplicates 相同的 文件,因此这也不会占用太多 space。)

现在提交 F 存在,您进行另一个新提交 G:

...--D--E    <-- origin/somebranch
         \
          F--G   <-- my-fancy-new-feature (HEAD)

这是您的“提交 2”,它仅更改文件 file1.txt。提交 G 仍然有每个文件,只是它的副本 file2.txt 与提交 F 的副本匹配;它的 file3.txt 副本与提交 F 的副本相匹配;及其所有其他文件与提交 F E.

的文件相匹配

最后,您添加提交 H:

...--D--E    <-- origin/somebranch
         \
          F--G--H   <-- my-fancy-new-feature (HEAD)

在提交 H 中,您已将 file3.txt 替换为修改后的文件; file1.txtfile2.txt 匹配提交 G 中的副本,依此类推。

这让我们又回到了你的问题:

... how would I update the pull request so I can have file2.txt not be included?

Git 基于 commits,而不是文件,你的 PR 说 please merge commit H.要更改此设置,您必须:

  • 以某种方式更改提交 H,或
  • 更改 PR,使其列出一些其他提交哈希 ID,而不是 H

关于任何提交,几乎不可能改变任何,所以第一个想法是正确的。

是否可以更改 PR 以使其列出其他提交,这取决于托管站点。如果托管站点特别令人讨厌,您可能必须 关闭 此 PR,稍后再打开一个新的。但是 GitHub 至少可以让你很简单地更新 PR。

不过,您的首要任务是提出新提交。您不想更改 file2.txt,但它在提交 F 中有所不同(与提交 E 相比),因此提交 F 本身在某些方面是不好的。这意味着您需要一个新的替代提交 F。让我们称它为 F' 来表示它与 很像 F,但它将具有不同的原始哈希 ID。

为了获得提交 F',我们想要“复制”提交 F 而不是完全提交。我们将从检查提交 E 开始。我们可以创建另一个新的分支名称,但我们也可以使用 Git 的“分离 HEAD”模式,如下所示:

...--E   <-- HEAD, origin/somebranch
      \
       F--G--H   <-- my-fancy-new-feature

现在我们 运行, 说git cherry-pick -n 并给出 Git 提交 F 的哈希 ID,或类似的东西:例如 my-fancy-new-feature~2。 Git 将复制 F 的效果但尚未提交任何内容——我们将有一些正在进行的工作可以提交——现在我们有机会 撤消 更改为 file2,例如 git restore:

git restore -SW --source=origin/somebranch file2.txt

快速 git statusgit diff --cached 将显示我们现在保留了 file1.txtfile3.txt 的更新版本,但又回到了原来的 file2.txt 来自提交 E 由名称 origin/somebranch.

找到

我们现在可以运行git commit来制作F':

       F'  <-- HEAD
      /
...--E   <-- origin/somebranch
      \
       F--G--H   <-- my-fancy-new-feature

提交G只影响file1.txt,所以我们可以直接复制它,用git cherry-pick,这不仅会弄清楚它改变了什么并应用它,而且还会使一个新的提交,re-using 原始提交的消息:

       F'-G'  <-- HEAD
      /
...--E   <-- origin/somebranch
      \
       F--G--H   <-- my-fancy-new-feature

您可能想知道为什么我们 G 复制到 G',而不是仅使用 G 本身。答案很简单:关于提交G 的任何事情都不会改变。从 G 出来的箭头指向 F,是 G 的一部分。改变不了!提交 G 将永远指向提交 F,永远不会提交 F'。所以我们不得不复制G.

此外,提交 G 中包含错误的 file2.txt 副本,当然,这也会迫使我们复制它——但是 anything这迫使我们复制提交,迫使整个事情发生。请注意,当我们使用 cherry-pick 进行“复制”G 时,Git 会将 in G 中的快照与 [=38] 中的快照进行比较=] 看看 改变了什么 。由于file2.txt在这个pair-of-commits中没有改变,Git不会改变file2.txtG'F' 中。因此 G' 将具有与 F' 相同的 file2.txt,而 F' 将具有与 E 相同的 file2.txt

现在,出于同样的原因,我们需要复制 H,我们可以再使用一个 git cherry-pick 命令来完成。结果是:

       F'-G'-H'  <-- HEAD
      /
...--E   <-- origin/somebranch
      \
       F--G--H   <-- my-fancy-new-feature

现在我们有了 commits,所有(所有?!)我们要做的就是获得 name my-fancy-new-feature 指向 H' 而不是 H。我们可以通过多种方式做到这一点,例如 git checkout -B my-fancy-new-featuregit switch -C my-fancy-new-feature。这里的最终结果将是:

       F'-G'-H'  <-- my-fancy-new-feature (HEAD)
      /
...--E   <-- origin/somebranch
      \
       F--G--H   ???

F-G-H 链发生了什么变化,Git 过去常常通过查看名称 my-fancy-new-feature 来查找?答案是:没有发生。它仍然在那里。只是现在,它闲置了。 These aren't the droids commits you're looking for,所以我们只是确保这些不是我们 找到 .

的提交

我们现在在 this 存储库中有正确的本地提交。现在我们必须将它们带到托管站点,并让托管站点更新拉取请求。要在 GitHub 上执行此操作,我们只需将新提交推送到 GitHub,告诉 Git 在 GitHub 上 replace F-G-H 使用我们的新 F'-G'-H' 链在其存储库中提交。

Git 通常是贪婪的提交,所以如果我们只是 运行 一个普通的 git push origin my-fancy-new-feature,他们——Git 在 GitHub 上,在您的存储库上操作 — 将 拒绝我们这样做的尝试 。实际上,他们会说 不!如果我这样做,我将失去 F-G-H 链!(与我们自己的存储库一样,提交不会 消失 ,它们只是不会' name my-fancy-new-feature 再也找不到了。但这足以让他们拒绝请求。)你可能会得到一个建议,你 pull (即,获取和合并)来自 GitHub 的提交:他们没有意识到他们首先从你那里得到了它们,而你告诉他们 这些是新的和改进的替换 所以你应该放弃旧的,转而使用这些 new-and-improved 的 .

他们意识到,你需要某种forced-push(不是星球大战风格的“力量”,而是常规的English-language意思). Git 有几种,你可以在这里使用它们中的任何一种,但是 --force-with-lease 有一个安全功能(在这里应该无关紧要:如果是这样,有些东西已经消失了 not-according-to-plan,并且安全功能检测到)并且通常是要走的路。

让这一切变得简单(大概)

上面的序列中有很多 Git 命令,其中很多都很棘手(出于多种原因我没有显示完整的命令)。我们可以使用 git rebase -i 将其减少到较少数量的 much-less-tricky 命令。不过还是有点棘手。

运行:

git switch my-fancy-new-feature
git rebase -i origin/somebranch

我们就是这样开始的。 rebase 在 当前分支 上运行,因此我们首先检查 my-fancy-new-feature(您可以在此处使用 git checkoutgit switch,如果你已经在上面了)。

rebase 做的是:

  • 列出要复制的提交(哈希 ID);
  • 使用Git的detached HEAD模式开始复制;和
  • 开始cherry-picking。

完成后,它通过将分支名称移动到最后的 copied 提交(H' 在我们的例子中)。这样就可以自动完成很多艰苦的工作。

一般来说,当我们有一些我们最喜欢喜欢的提交时,我们会使用变基,但是有一些关于那些提交我们喜欢。由于任何现有提交都不能 更改 ,因此变基通过 复制 提交来工作。新副本 可以 在我们提交之前进行更改。

interactive rebase 特别为我们提供了更多改变的机会。一个普通的 rebase 只是复制所有东西而不给我们修复东西的机会,这对于 移动 提交很有用——对于像这样的链:

          A--B--C   <-- topic
         /
...--o--o--o--o   <-- mainline

并将其复制到:

          A--B--C   ???
         /
...--o--o--o--o   <-- mainline
               \
                A'-B'-C'  <-- topic

以便现在提交出现在主线的 end,而不是从较早的点发芽。这不是我们想要的:我们想要更改其中一个提交中的一些文件

因此,交互式变基不是仅仅计划所有 cherry-pick 然后启动它们,而是写出 指令 sheet。这条指令 sheet 列出了 个 cherry-pick,每一个都使用单词 pick

pick hash1  subject
pick hash2  subject
pick hash3  subject

然后,一旦编写了指令 sheet,git rebase -i 将在指令 sheet 上打开一个编辑器,这样我们就可以 更改命令.

在我们的例子中,我们不想只选择提交 #1 as-is。我们希望有机会 改变 它。所以我们将pick改为edit。我们确实想按原样选择#2 和#3,所以我们将保留它们。然后我们写出指令sheet并退出编辑器,1到return到cherry-pick动作

将第一个 pick 更改为 edit,Git 将 cherry-pick 第一次提交,然后停下来让我们修复它。这里有一件事特别棘手:Git 实际上 做了 此时的临时提交,所以当我们修复它时,我们必须 运行 git commit --amend.2 我们现在可以像我之前描述的那样做 git restore,然后 运行 git commit --amend:

git restore -SW --source origin/somebranch file2.txt
git commit --amend

(注意:--source=origin/somebranch--source origin/somebranch 在这里的工作方式相同,因此您可以使用任何一个)。

一旦我们完成修复 edit-able 提交,我们告诉 Git 恢复变基:

git rebase --continue

这将完成所有剩余的 cherry-picks,然后 re-arrange 分支名称和 re-attach 我们的 HEAD 到我们的分支,现在我们有了我们通缉:

       F'-G'-H'  <-- my-fancy-new-feature (HEAD)
      /
...--E   <-- origin/somebranch
      \
       F--G--H   ???

我们现在准备 运行:

git push --force-with-lease origin my-fancy-new-feature

如果我们正在谈论 GitHub,“不关闭更新 PR 并且 re-opening”现在已经完成。

我们总共使用了五六个 Git 命令,大约是我们之前需要的一半,除了交互式变基“编辑”步骤之外,我们不需要做任何棘手的事情。这里的其他一切都非常简单。


1有些编辑器不会 退出: 你早点启动它们,然后它们就会永远闲逛。示例包括 Emacs、Sublime 和 Atom 的许多案例。如果您正在使用这些编辑器之一,则必须使它们与 Git 良好交互;这是特定编辑器的问题,但现在大多数编辑器都有一个 --wait 标志,可以安排所有这些工作正常进行。

2--amend标志似乎改变了一个提交,这与上面我们不能改变任何提交的说法相矛盾。这里的肮脏秘密是 --amend 不会 更改提交。相反,它只是让 又一次 提交。因此,当我们使用 --edit 时,我们会生成额外的、毫无意义的“垃圾”提交。但是 Git 中的提交是如此之小和廉价,以至于这样做比避免它更好。 Git 最终会自行清理,尽管这通常需要一个多月的时间。 Git 做的清理/清洁工作有点慢,但在 5 分钟内清扫一个月的垃圾,而不是立即清理所有垃圾,实际上是一个非常实用的权衡。

根据您的要求,我会删除该文件,从中重新提交并推送到您的分支。这样 PR 中将有第 4 次提交删除文件,最终结果将如您所说。