从旧 git 提交中删除一些文件
Remove few files from old git commit
我正在尝试删除我不小心包含在我的旧提交(不是以前或最后一次提交)中但 运行 与我想保留的其他文件发生大冲突的文件。我只想删除不需要的文件并在特定提交中保留我想要的文件。我正在使用 VS2022。
假设我的本地功能分支 MyBranch
有提交:A -> B -> C -> D -> E
。所有提交也被推送到远程 MyBranch
分支。
提交 C
有 file1, file2, file3 and file4
。我只想删除不需要的文件 2、3、4,并将 file1 保留在 C
本地和远程分支中。 MyBranch
是我的私人功能分支,除了我没有其他人在使用它。如果我恢复提交 C
,file1
有很多合并冲突。我想知道是否有办法在本地重写历史记录并更新远程,就好像 MyBranch
从未包含不需要的文件 2、3、4。谢谢
TL;DR
使用 git rebase -i
和 git push --force-with-lease
或类似的。
长
任何东西,甚至 Git 本身,都不能更改任何 现有的 提交。但这里并没有失去一切,这只是意味着你的工作更复杂。
你画了一组提交我会这样改写:
...--o--● <-- main
\
A -> B -> C -> D -> E <-- MyBranch, origin/MyBranch
重要的是要意识到连接提交的箭头——就像你绘图中的 A to B
——都是向后并且是[=]的一部分275=] 稍后 提交。这是必要的,因为一旦提交 A
,就无法更改。它包含一个向后指向 b运行ch 上的最后一次提交的箭头,您从那里开始了 b运行ch MyBranch
——我在上面使用 ●
的那个——和它将永远向后指向该提交。所以更准确的绘图看起来像这样:
...--o--● <-- main
\
A <-B <-C <-D <-E <-- MyBranch, origin/MyBranch
(我们很懒惰,没有正确绘制早期提交的箭头,部分原因是我的 up-and-left 箭头像 ↖︎
和 ⬉
和 ⇱
只是看起来有点蹩脚)。除了这些 backwards-pointing 来自每个提交的“箭头”之外,每个提交都包含 每个 文件的完整快照,1 所以很可能您在提交 C
中添加的您不想要的文件也存在于提交 D
和 E
.
中
无论如何,这里的最终问题是您确实不能更改提交C
。它将始终拥有这些文件并始终指向 B
。提交 D
将始终拥有它所拥有的一切,并将始终指向 C
,而提交 E
将始终拥有 它 拥有的一切,并将总是指向 D
。但是...如果您提取提交 C
、 修复问题 并进行 新的和改进的 提交会怎么样。我们将此称为 new-and-improved 提交 C'
,并将其绘制在:
...--o--● <-- main
\
A--B--C--D--E <-- MyBranch, origin/MyBranch
\
C' <-- improved-branch
现在我们想采用现有提交 D
并非常相似地改进它:新提交 D'
,我们现有 D
的副本应该对 C'
做任何事情D
对 C
做了,应该向后指向 C'
:
...--o--● <-- main
\
A--B--C--D--E <-- MyBranch, origin/MyBranch
\
C'-D' <-- improved-branch
我们再次重复提交 E
得到:
...--o--● <-- main
\
A--B--C--D--E <-- MyBranch, origin/MyBranch
\
C'-D'-E' <-- improved-branch
然后我们去掉名称 improved-branch
并使名称 MyBranch
找到提交 E'
而不是查找提交 E
:
...--o--● <-- main
\
A--B--C--D--E <-- origin/MyBranch
\
C'-D'-E' <-- MyBranch
1每次提交:
- 是编号的,有一个又大又丑的random-looking(但完全不是运行dom),cryptographic-checksum哈希ID;
- 是不可变的;
- 包含两件事:每个文件的完整快照和一些元数据。
元数据提供诸如提交作者的姓名和电子邮件地址以及提交时间的 date-and-time 之类的信息。它包括他们(您)当时写的日志消息。而且,为了制作这些“箭头”,每个提交都有一个 先前提交哈希 ID 的列表 。大多数提交在此列表中只有一个条目,这是我们从提交中出来的“箭头”:哈希 ID 允许 Git 使用 this 提交来查找 上一个提交。
由于每次提交都包含每个文件的完整快照——文件内容de-duplicated在提交内和提交之间——Git可以简单地比较 A
和B
中的快照,例如,查看哪些文件相同,哪些不同。 Git 然后仅向您显示 不同的 文件,并通过计算 git diff
来向您显示,而不是向您显示每个提交中每个文件的全部内容.但是那个差异不是提交 存储 的内容。它实际上有每个文件的完整副本(de-duplication 处理明显的反对意见,这会太快填满你的磁盘)。
用 git rebase
走到这一步
实际复制提交的命令(例如,将 E
变成 E'
)是 git cherry-pick
,但我们必须多次使用它——在本例中是三次。我们在这里想要 Git 的强大工具,那就是 Git 的 interactive rebase。我们运行:
git switch MyBranch # or git checkout MyBranch, if/as needed
然后:
git rebase -i HEAD~3 # 3 here means "count back 3 times from `E`
这会调出一条指令 sheet,其中包含三个 pick
命令。这些对应于 运行ning git cherry-pick
,这是 Git 的 built-in 命令,用于制作一些提交的副本。我们不想要 C
的 完整 副本,所以我们必须将第一个 pick
命令更改为 edit
,然后写出这个指令集并退出编辑器,2 这使得 git rebase
开始整个过程并执行第一个 cherry-pick,但随后停止修改。我们现在可以 运行:
git rm file2 file3 file4
然后:
git commit --amend
(这有点谎言,2 但让我们 C'
)然后:
git rebase --continue
这完成了工作——剩下的两个提交仍然被标记为 pick
——并得到了我们想要的结果:
...--o--● <-- main
\
A--B--C--D--E <-- origin/MyBranch
\
C'-D'-E' <-- MyBranch
2对于某些编辑器,您不最后 退出 编辑器,您只需让编辑器向 Git 发回信号,表明指令 sheet 已完成。详细信息取决于您的编辑器。 git commit
命令会打开一个编辑器供您编写提交日志消息,其工作方式相同,因此无论您为此使用什么——只要它不是 -m
或其他东西——都会也在这里工作。
3Git 无法更改任何现有提交,git commit --amend
也不例外。这就是为什么 --amend
是一个谎言。 --amend
所做的是:
- 无论我们现在在哪里,都进行新的提交,但是
- 不是让新提交指向 当前 提交,而是让它向后指向当前提交的 parent(s) .
此外,git rebase -i
如果可能的话,它会“作弊”,而不是实际上 复制 一个提交,如果可能的话。所以当我们把pick
改成edit
写出指令退出的时候,Git其实也懒得去copyC
.它只是让我们进入“分离的 HEAD”模式,C
是 当前提交 ,像这样:
...--o--● <-- main
\
A--B D--E <-- MyBranch, origin/MyBranch
\ /
C <-- HEAD
我们的 git commit --amend
使用 Git 的 索引 又名 暂存区 中的任何内容,所以 git rm file2 file3 file4
更新它,然后我们 运行 git commit --amend
命令。这使得新的 C'
与 C
具有相同的父级 - 即 B
- 并将 HEAD
指向 C'
:
...--o--● <-- main
\
A--B--C--D--E <-- MyBranch, origin/MyBranch
\
C' <-- HEAD
当我们 运行 git rebase --continue
时,Git 从指令中停止的地方继续。这还有两个 pick
命令,用于 D
和 E
,因此 rebase 现在对这些执行正常的 cherry-picks:此处不允许使用快捷方式。所以在 cherry-picking 序列的末尾,rebase 有:
...--o--● <-- main
\
A--B--C--D--E <-- MyBranch, origin/MyBranch
\
C'-D'-E' <-- HEAD
现在 rebase 已成功到达指令末尾,它将 b运行ch 名称 MyBranch
从它之前所在的位置(提交 E
)中拉出并粘贴它在这里(在 E'
),然后是“re-attaches” Git 的 HEAD
:
...--o--● <-- main
\
A--B--C--D--E <-- origin/MyBranch
\
C'-D'-E' <-- MyBranch (HEAD)
这就是我通常画这些东西的方式。
您自己的版本库现已修复; GitHub 还没有
您现在在您的 存储库中拥有您想要的提交(加上一些您不想要的,但您对此无能为力)。您现在需要将 new 提交发送到 GitHub,以便他们可以将它们放入 他们的 存储库(您控制的那个在那边)。它们还不存在。
通常你会 运行:
git push origin MyBranch
这会让你的 Git 调用他们的 Git,枚举你有但他们没有的任何提交——在本例中是 C'
、D'
、和 E'
——然后发送这些提交并要求他们设置他们的名字 MyBranch
,你的 Git 记得是 origin/MyBranch
.
如果你现在这样做,你会看到提交 do 被发送,但是 GitHub 拒绝请求到更新名称 MyBranch
:
! [rejected] MyBranch -> MyBranch (non-fast-forward)
这是 Git 的说法,“他们抱怨说,如果他们遵从您的礼貌请求更新他们的 MyBranch
,他们最终会丢失一些提交”。他们将丢失的提交当然是提交C-D-E
:正是您希望他们丢失的提交。
为了 使 他们放弃那些提交,您需要使用 git push
的“强制”变体之一,而不是发送 请,如果可以,请更新您的姓名MyBranch
请求,您发送更新您的姓名MyBranch
! 立即,该死! 命令。那是 git push --force
.
为了更小心——在这种情况下你不需要,但通常明智的做法是小心使用像 --force
这样的锋利锯子——你可以使用 --force-with-lease
。这发送,而不是礼貌的请求 或 一个压倒一切的命令,一个妥协: I think your b运行ch name MyBranch
identifies commit _______(用 E
的散列 ID 填空)。如果我是对的,将其更改为 _______(用另一个哈希 ID 填充空白,这次是 E'
),即使在 b运行 的末尾丢失了提交通道让我知道您是否这样做了。 他们现在将进行此项检查。请注意,您的 Git 根据您的 origin/MyBranch
名称为 E
提供散列 ID,并根据您 运行 的事实为 E'
提供散列 ID:
git push --force-with-lease origin MyBranch
也就是说,此处的名称 MyBranch
提供了两个哈希 ID:一个是直接提供的,另一个是通过您 Git 查找该名称的 origin/
变体。
使用 --force-with-lease
解决 shared GitHub(或其他站点)存储库出现的问题,多个人可能会向其推送提交.如果 其他人 在您修复 C-D-E
成为 C'-D'-E'
时添加了提交 F
,您的 git push --force-with-lease origin MyBranch
将会失败,因为您的Git 将发送 E
的哈希 ID,而他们现在实际上持有 F
的哈希 ID。然后你可以 运行 git fetch
到获取新的提交并将它们 git cherry-pick
到您更新的 b运行ch 并再次尝试 --force-with-lease
。
由于没有其他人写入此 GitHub 存储库,因此您不需要 --force-with-lease
,但了解一下也很好。
我正在尝试删除我不小心包含在我的旧提交(不是以前或最后一次提交)中但 运行 与我想保留的其他文件发生大冲突的文件。我只想删除不需要的文件并在特定提交中保留我想要的文件。我正在使用 VS2022。
假设我的本地功能分支 MyBranch
有提交:A -> B -> C -> D -> E
。所有提交也被推送到远程 MyBranch
分支。
提交 C
有 file1, file2, file3 and file4
。我只想删除不需要的文件 2、3、4,并将 file1 保留在 C
本地和远程分支中。 MyBranch
是我的私人功能分支,除了我没有其他人在使用它。如果我恢复提交 C
,file1
有很多合并冲突。我想知道是否有办法在本地重写历史记录并更新远程,就好像 MyBranch
从未包含不需要的文件 2、3、4。谢谢
TL;DR
使用 git rebase -i
和 git push --force-with-lease
或类似的。
长
任何东西,甚至 Git 本身,都不能更改任何 现有的 提交。但这里并没有失去一切,这只是意味着你的工作更复杂。
你画了一组提交我会这样改写:
...--o--● <-- main
\
A -> B -> C -> D -> E <-- MyBranch, origin/MyBranch
重要的是要意识到连接提交的箭头——就像你绘图中的 A to B
——都是向后并且是[=]的一部分275=] 稍后 提交。这是必要的,因为一旦提交 A
,就无法更改。它包含一个向后指向 b运行ch 上的最后一次提交的箭头,您从那里开始了 b运行ch MyBranch
——我在上面使用 ●
的那个——和它将永远向后指向该提交。所以更准确的绘图看起来像这样:
...--o--● <-- main
\
A <-B <-C <-D <-E <-- MyBranch, origin/MyBranch
(我们很懒惰,没有正确绘制早期提交的箭头,部分原因是我的 up-and-left 箭头像 ↖︎
和 ⬉
和 ⇱
只是看起来有点蹩脚)。除了这些 backwards-pointing 来自每个提交的“箭头”之外,每个提交都包含 每个 文件的完整快照,1 所以很可能您在提交 C
中添加的您不想要的文件也存在于提交 D
和 E
.
无论如何,这里的最终问题是您确实不能更改提交C
。它将始终拥有这些文件并始终指向 B
。提交 D
将始终拥有它所拥有的一切,并将始终指向 C
,而提交 E
将始终拥有 它 拥有的一切,并将总是指向 D
。但是...如果您提取提交 C
、 修复问题 并进行 新的和改进的 提交会怎么样。我们将此称为 new-and-improved 提交 C'
,并将其绘制在:
...--o--● <-- main
\
A--B--C--D--E <-- MyBranch, origin/MyBranch
\
C' <-- improved-branch
现在我们想采用现有提交 D
并非常相似地改进它:新提交 D'
,我们现有 D
的副本应该对 C'
做任何事情D
对 C
做了,应该向后指向 C'
:
...--o--● <-- main
\
A--B--C--D--E <-- MyBranch, origin/MyBranch
\
C'-D' <-- improved-branch
我们再次重复提交 E
得到:
...--o--● <-- main
\
A--B--C--D--E <-- MyBranch, origin/MyBranch
\
C'-D'-E' <-- improved-branch
然后我们去掉名称 improved-branch
并使名称 MyBranch
找到提交 E'
而不是查找提交 E
:
...--o--● <-- main
\
A--B--C--D--E <-- origin/MyBranch
\
C'-D'-E' <-- MyBranch
1每次提交:
- 是编号的,有一个又大又丑的random-looking(但完全不是运行dom),cryptographic-checksum哈希ID;
- 是不可变的;
- 包含两件事:每个文件的完整快照和一些元数据。
元数据提供诸如提交作者的姓名和电子邮件地址以及提交时间的 date-and-time 之类的信息。它包括他们(您)当时写的日志消息。而且,为了制作这些“箭头”,每个提交都有一个 先前提交哈希 ID 的列表 。大多数提交在此列表中只有一个条目,这是我们从提交中出来的“箭头”:哈希 ID 允许 Git 使用 this 提交来查找 上一个提交。
由于每次提交都包含每个文件的完整快照——文件内容de-duplicated在提交内和提交之间——Git可以简单地比较 A
和B
中的快照,例如,查看哪些文件相同,哪些不同。 Git 然后仅向您显示 不同的 文件,并通过计算 git diff
来向您显示,而不是向您显示每个提交中每个文件的全部内容.但是那个差异不是提交 存储 的内容。它实际上有每个文件的完整副本(de-duplication 处理明显的反对意见,这会太快填满你的磁盘)。
用 git rebase
走到这一步
实际复制提交的命令(例如,将 E
变成 E'
)是 git cherry-pick
,但我们必须多次使用它——在本例中是三次。我们在这里想要 Git 的强大工具,那就是 Git 的 interactive rebase。我们运行:
git switch MyBranch # or git checkout MyBranch, if/as needed
然后:
git rebase -i HEAD~3 # 3 here means "count back 3 times from `E`
这会调出一条指令 sheet,其中包含三个 pick
命令。这些对应于 运行ning git cherry-pick
,这是 Git 的 built-in 命令,用于制作一些提交的副本。我们不想要 C
的 完整 副本,所以我们必须将第一个 pick
命令更改为 edit
,然后写出这个指令集并退出编辑器,2 这使得 git rebase
开始整个过程并执行第一个 cherry-pick,但随后停止修改。我们现在可以 运行:
git rm file2 file3 file4
然后:
git commit --amend
(这有点谎言,2 但让我们 C'
)然后:
git rebase --continue
这完成了工作——剩下的两个提交仍然被标记为 pick
——并得到了我们想要的结果:
...--o--● <-- main
\
A--B--C--D--E <-- origin/MyBranch
\
C'-D'-E' <-- MyBranch
2对于某些编辑器,您不最后 退出 编辑器,您只需让编辑器向 Git 发回信号,表明指令 sheet 已完成。详细信息取决于您的编辑器。 git commit
命令会打开一个编辑器供您编写提交日志消息,其工作方式相同,因此无论您为此使用什么——只要它不是 -m
或其他东西——都会也在这里工作。
3Git 无法更改任何现有提交,git commit --amend
也不例外。这就是为什么 --amend
是一个谎言。 --amend
所做的是:
- 无论我们现在在哪里,都进行新的提交,但是
- 不是让新提交指向 当前 提交,而是让它向后指向当前提交的 parent(s) .
此外,git rebase -i
如果可能的话,它会“作弊”,而不是实际上 复制 一个提交,如果可能的话。所以当我们把pick
改成edit
写出指令退出的时候,Git其实也懒得去copyC
.它只是让我们进入“分离的 HEAD”模式,C
是 当前提交 ,像这样:
...--o--● <-- main
\
A--B D--E <-- MyBranch, origin/MyBranch
\ /
C <-- HEAD
我们的 git commit --amend
使用 Git 的 索引 又名 暂存区 中的任何内容,所以 git rm file2 file3 file4
更新它,然后我们 运行 git commit --amend
命令。这使得新的 C'
与 C
具有相同的父级 - 即 B
- 并将 HEAD
指向 C'
:
...--o--● <-- main
\
A--B--C--D--E <-- MyBranch, origin/MyBranch
\
C' <-- HEAD
当我们 运行 git rebase --continue
时,Git 从指令中停止的地方继续。这还有两个 pick
命令,用于 D
和 E
,因此 rebase 现在对这些执行正常的 cherry-picks:此处不允许使用快捷方式。所以在 cherry-picking 序列的末尾,rebase 有:
...--o--● <-- main
\
A--B--C--D--E <-- MyBranch, origin/MyBranch
\
C'-D'-E' <-- HEAD
现在 rebase 已成功到达指令末尾,它将 b运行ch 名称 MyBranch
从它之前所在的位置(提交 E
)中拉出并粘贴它在这里(在 E'
),然后是“re-attaches” Git 的 HEAD
:
...--o--● <-- main
\
A--B--C--D--E <-- origin/MyBranch
\
C'-D'-E' <-- MyBranch (HEAD)
这就是我通常画这些东西的方式。
您自己的版本库现已修复; GitHub 还没有
您现在在您的 存储库中拥有您想要的提交(加上一些您不想要的,但您对此无能为力)。您现在需要将 new 提交发送到 GitHub,以便他们可以将它们放入 他们的 存储库(您控制的那个在那边)。它们还不存在。
通常你会 运行:
git push origin MyBranch
这会让你的 Git 调用他们的 Git,枚举你有但他们没有的任何提交——在本例中是 C'
、D'
、和 E'
——然后发送这些提交并要求他们设置他们的名字 MyBranch
,你的 Git 记得是 origin/MyBranch
.
如果你现在这样做,你会看到提交 do 被发送,但是 GitHub 拒绝请求到更新名称 MyBranch
:
! [rejected] MyBranch -> MyBranch (non-fast-forward)
这是 Git 的说法,“他们抱怨说,如果他们遵从您的礼貌请求更新他们的 MyBranch
,他们最终会丢失一些提交”。他们将丢失的提交当然是提交C-D-E
:正是您希望他们丢失的提交。
为了 使 他们放弃那些提交,您需要使用 git push
的“强制”变体之一,而不是发送 请,如果可以,请更新您的姓名MyBranch
请求,您发送更新您的姓名MyBranch
! 立即,该死! 命令。那是 git push --force
.
为了更小心——在这种情况下你不需要,但通常明智的做法是小心使用像 --force
这样的锋利锯子——你可以使用 --force-with-lease
。这发送,而不是礼貌的请求 或 一个压倒一切的命令,一个妥协: I think your b运行ch name MyBranch
identifies commit _______(用 E
的散列 ID 填空)。如果我是对的,将其更改为 _______(用另一个哈希 ID 填充空白,这次是 E'
),即使在 b运行 的末尾丢失了提交通道让我知道您是否这样做了。 他们现在将进行此项检查。请注意,您的 Git 根据您的 origin/MyBranch
名称为 E
提供散列 ID,并根据您 运行 的事实为 E'
提供散列 ID:
git push --force-with-lease origin MyBranch
也就是说,此处的名称 MyBranch
提供了两个哈希 ID:一个是直接提供的,另一个是通过您 Git 查找该名称的 origin/
变体。
使用 --force-with-lease
解决 shared GitHub(或其他站点)存储库出现的问题,多个人可能会向其推送提交.如果 其他人 在您修复 C-D-E
成为 C'-D'-E'
时添加了提交 F
,您的 git push --force-with-lease origin MyBranch
将会失败,因为您的Git 将发送 E
的哈希 ID,而他们现在实际上持有 F
的哈希 ID。然后你可以 运行 git fetch
到获取新的提交并将它们 git cherry-pick
到您更新的 b运行ch 并再次尝试 --force-with-lease
。
由于没有其他人写入此 GitHub 存储库,因此您不需要 --force-with-lease
,但了解一下也很好。