[英]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 --force
或git 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
的副本匹配; 它的所有其他文件都与提交F
和E
的文件匹配。
最后,添加提交H
:
...--D--E <-- origin/somebranch
\
F--G--H <-- my-fancy-new-feature (HEAD)
在提交H
中,您已将file3.txt
替换为修改后的文件; file1.txt
和file2.txt
匹配提交G
中的副本,依此类推。
这让我们再次提出您的问题:
...我将如何更新拉取请求,以便不包含
file2.txt
?
Git 基于提交,而不是文件,您的 PR 说请合并提交H
要更改此设置,您必须:
H
,或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 status
和git diff --cached
将显示我们现在保留了file1.txt
和file3.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-feature
或git 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 的方式。
上面的序列中有很多 Git 命令,其中很多很棘手(由于多种原因,我没有显示完整的命令)。 我们可以使用git rebase -i
将其减少到更少的复杂命令。 不过还是有一点点小技巧。
跑步:
git switch my-fancy-new-feature
git rebase -i origin/somebranch
是我们如何开始的。 rebase 在当前分支上运行,因此我们首先检查my-fancy-new-feature
(您可以在此处使用git checkout
或git switch
,或者如果您已经使用它,则什么也不做)。
rebase 的作用是:
完成后,它通过将分支名称移动到复制的提交的最后一个(在我们的例子中为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.