繁体   English   中英

如何通过提交分配从拉取请求中删除某些文件?

[英]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 --设置上游源 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。 我在拉取请求中想要的最终 output 将是

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时,您将合并的提交是来自某个提交链的提交,以该链中的最后一个提交结束。

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

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

然后,分支名称只需提供链中最后一次提交的原始 hash ID,Git 将从中找到所有以前的提交:

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

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

  • 首先分叉和/或克隆一些存储库,以便您获得其他人拥有的所有提交
  • 创建一个新的分支名称,以便您有一个指向最后一个提交的名称,这也是他们的提交之一;
  • 进行新的提交,以便您的分支名称前进。

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

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

...--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包含所有文件,因为所有提交始终具有每个文件的完整快照,但提交F中的文件提交E中的所有文件相同,除了您更改的三个文件。 (Git 巧妙地对相同的文件进行重复数据删除,这样也不会占用太多空间。)

现在提交F存在,您可以进行另一个新的提交G

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

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

最后,添加提交H

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

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

这让我们再次提出您的问题:

...我将如何更新拉取请求,以便不包含file2.txt

Git 基于提交,而不是文件,您的 PR 说请合并提交H 要更改此设置,您必须:

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

永远不可能改变任何关于任何 commit的东西,所以第一个想法是正确的。

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

不过,您的首要任务是提出新的提交 您不希望file2.txt更改,但它在提交F (与提交E )中有所不同,因此提交F本身在某种程度上是不好的。 这意味着您需要一个新的替换提交F 我们称这个F'表示它很像F ,但它会有不同的原始 hash ID。

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

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

现在我们将运行,比如说, git cherry-pick -n并给 Git 提交F的 hash 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的更新版本,但从名称origin/somebranch file2.txt的提交E回到了原始 file2.txt .

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

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

提交G只影响file1.txt ,所以我们可以直接复制它,使用git cherry-pick ,它不仅会找出它改变了什么并应用它,而且还会进行新的提交,重新使用原始提交的消息:

       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副本,当然,这也会迫使我们复制它——但是任何迫使我们复制提交的东西都会强制整个事情。 请注意,当我们使用cherry-pick“复制” G时,Git 会将G中的快照F中的快照进行比较,以查看发生了什么变化 由于这对提交中的 file2.txt 没有更改,因此file2.txt不会更改G'F'中的file2.txt 所以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 ,所有(全部?!)我们要做的就是让名称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   ???

FGH链会发生什么变化,Git 过去通过查看名称my-fancy-new-feature来查找? 答案是:什么都没有发生 它还在那里 只是现在,它没有使用。 这些不是您要查找的droids提交,因此我们只需确保这些不是我们找到的提交。

我们现在在这个存储库中在本地拥有正确的提交。 现在我们必须将它们带到托管站点,并让托管站点更新拉取请求。 To do that on GitHub, we just push the new commits to GitHub, telling the Git over on GitHub to replace the FGH commits in its repository with our new F'-G'-H' chain.

Git in general is greedy for commits, so if we just run a regular git push origin my-fancy-new-feature , they—the Git over on GitHub, operating on your repository over there—will reject our attempt to do this . 他们会说,实际上,不! 如果我这样做,我会失去 FGH 链! (与我们自己的存储库一样,提交不会消失,只是无法再通过名称my-fancy-new-feature找到它们。但这足以让他们拒绝请求。)您可能会得到一个建议,你从 GitHub 中pull (即获取和合并)提交:他们没有意识到他们首先是从你那里得到的,你告诉他们这些是新的和改进的替代品,所以你应该抛弃旧的,转而使用这些新的和改进的

为了他们意识到这一点,您需要某种强制推动(不是星球大战风格的“强制”,而只是常规的英语含义)。 Git 有几种,你可以在这里使用它们中的任何一种,但是--force-with-lease有一个安全功能(在这里不应该重要:如果是这样,则某些事情不按计划进行,并且安全功能检测到)并且通常是 go 的方式。

使这变得容易(ish)

上面的序列中有很多 Git 命令,其中很多很棘手(由于多种原因,我没有显示完整的命令)。 我们可以使用git rebase -i将其减少到更少的复杂命令。 不过还是有一点点小技巧。

跑步:

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

是我们如何开始的。 rebase 在当前分支上运行,因此我们首先检查my-fancy-new-feature (您可以在此处使用git checkoutgit switch ,或者如果您已经使用它,则什么也不做)。

rebase 的作用是:

  • 列出要复制的提交(哈希 ID);
  • 使用 Git 的分离 HEAD模式开始复制;
  • 开始采摘樱桃。

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

通常,当我们有一些我们最喜欢的提交时,我们会使用变基,但是对于那些我们喜欢的提交有一些东西。 由于任何现有提交都无法更改,因此 rebase 通过复制提交来工作。 在我们提交新副本之前,可以随时更改新副本。

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

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

并将其复制到:

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

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

因此,交互式变基,而不是仅仅计划所有的挑选然后开始它们,而是写出一个指令表 本说明书列出了樱桃精选,每个樱桃都使用“ pick ”一词:

pick hash1  subject
pick hash2  subject
pick hash3  subject

然后,一旦编写了指令表, git rebase -i在指令表上打开一个编辑器,以便我们可以更改命令

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

将第一个pick更改为edit后,Git 将挑选第一个提交,然后停止让我们修复它。 这里有一件特别棘手的事情: 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在这里的工作方式相同,因此您可以使用其中任何一个)。

一旦我们修复了可编辑的提交,我们告诉 Git 恢复变基:

git rebase --continue

这将完成所有剩余的樱桃选择,然后重新排列分支名称并将我们的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 而不关闭和重新打开”现在已经完成。

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


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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM