从旧 git 提交中删除一些文件

Remove few files from old git commit

我正在尝试删除我不小心包含在我的旧提交(不是以前或最后一次提交)中但 运行 与我想保留的其他文件发生大冲突的文件。我只想删除不需要的文件并在特定提交中保留我想要的文件。我正在使用 VS2022。

假设我的本地功能分支 MyBranch 有提交:A -> B -> C -> D -> E。所有提交也被推送到远程 MyBranch 分支。

提交 Cfile1, file2, file3 and file4。我只想删除不需要的文件 2、3、4,并将 file1 保留在 C 本地和远程分支中。 MyBranch 是我的私人功能分支,除了我没有其他人在使用它。如果我恢复提交 Cfile1 有很多合并冲突。我想知道是否有办法在本地重写历史记录并更新远程,就好像 MyBranch 从未包含不需要的文件 2、3、4。谢谢

TL;DR

使用 git rebase -igit 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 中添加的您不想要的文件也存在于提交 DE.

无论如何,这里的最终问题是您确实不能更改提交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' 做任何事情DC 做了,应该向后指向 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可以简单地比较 AB中的快照,例如,查看哪些文件相同,哪些不同。 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 命令,用于 DE,因此 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,但了解一下也很好。