如果较早的提交中包含对某个文件不需要的更改,你可以针对当前分支撤销该文件的更改,并将结果保存到新的提交中。只要反向补丁仍然适用,这样既能保留后续提交,又能保留它们所做的修改。在 macOS 上,请从代码仓库打开终端;先审查补丁,尤其是在后续工作中修改了相同行的情况下。
选择正确的历史更改方式
此过程会添加一个新的提交,用以抵消目标提交对某个路径所做的更改。它不会从旧提交中抹去该文件,因此旧版本仍会保留在历史记录中。这种方法避免了重写分支历史,并且在提交已经共享或后续提交依赖于它的情况下通常是最合适的。Git 将 git revert 描述为记录一个用于撤销早期提交的新提交,但它并没有提供内置选项来仅撤销现有提交中的某一个路径。相反,下面的步骤将生成该路径的补丁并反向应用它。Git: git-revert 文档
如果必须从历史提交本身中删除文件更改,则属于重写历史任务,通常使用交互式 rebase(变基)来处理。重写会更改提交标识符(commit ID),并可能影响那些基于旧提交展开工作的协作者。本指南介绍的是更安全的附加性方案。
撤销目标提交对单个文件的更改
打开终端,进入代码仓库,在更改任何内容前先检查工作区。将 TARGET 替换为提交 ID,将 path/to/file 替换为相对于仓库根目录的路径。本示例使用 macOS /tmp 目录下的临时补丁文件;如果该文件已存在,请选择其他临时名称。
git status --short
git show --stat --oneline TARGET
git diff TARGET^ TARGET -- "path/to/file" > /tmp/remove-file-change.patch
该 diff 将目标提交与其第一个父提交进行比较,并将结果限定在指定的路径内。对于常规的非合并(non-merge)提交,这表示该提交对该文件所做的修改。请确认摘要信息标识了预期的提交,并且补丁不为空。你可以使用 less /tmp/remove-file-change.patch 查看它;按 q 退出。Git 文档记录了带有可选路径限制的提交间 diff。Git: git-diff 文档
在应用补丁之前,请确保目标文件中没有与之无关的暂存或未暂存修改。反向补丁会修改工作副本,因此干净的工作区是最稳妥的起点。如果你有有用的本地工作,请先提交或将其安全暂存;不要使用 restore 命令覆盖它。
检查反向补丁是否适用,然后应用它:
git apply --reverse --check /tmp/remove-file-change.patch
git apply --reverse /tmp/remove-file-change.patch
--reverse 用于反向应用 diff,而 --check 则检查是否可以在不更改文件的情况下成功应用。默认情况下,如果某些代码块(hunk)无法适配,git apply 会使整个补丁失败,而不是仅应用能够适配的部分。如果检查失败,请停下来检查文件和补丁。后续的修改可能与目标更改存在重叠,导致 Git 无法安全确定应该移除什么内容。此时可以手动解决目标内容,或者如果必须保留精确编辑的历史序列,则使用重写历史的工作流。切勿随意添加 --reject:它可能会只应用匹配的片段,而留下被拒绝的代码块。Git: git-apply 文档
审查并提交结果
在暂存之前,请仔细检查究竟更改了什么。git diff 会显示工作区的修改;再次使用路径来聚焦审查。如果结果正确,请暂存该文件,并在创建新提交之前检查已暂存的 diff。
git diff -- "path/to/file"
git add -- "path/to/file"
git diff --cached -- "path/to/file"
git status --short
git commit -m "Remove file change from earlier commit"
引号可防止路径被 Shell 解析,而 -- 标明选项的结束。对于包含空格或 Shell 元字符的名称,它们尤其有用。提交记录的是已暂存的索引;审查 git diff --cached 可以核实即将提交的准确内容。Git: git-commit 文档
提交后,检查 git status --short,并使用 git show --stat --oneline HEAD 审查新的提交。如果原始提交添加了该文件,反向应用其补丁可能会将其删除——如果没有后续内容需要保留,这是符合预期的。如果后续提交向该文件添加了有价值的内容,那么在反向补丁干净应用后,这些添加的内容应当会被保留;请审查最终文件和 diff 进行确认。
需要格外注意的情况
合并提交具有多个父提交,其更改取决于你与哪一个父提交进行对比。TARGET^ 表示第一个父提交;它可能并不代表你想要移除的更改。在应用任何更改之前,请明确找出正确的父提交并与之对比。重命名、二进制文件以及后来移动过的文件也可能导致限制路径的补丁不再适用;请检查 diff,如果它没有描述预期的文件更改,请中止操作。对于针对合并的撤销操作,Git 的 revert 文档解释了主线父提交的选择及其后果。Git: git-revert 文档
可靠的检查基准是经过审查的补丁:它应仅显示目标提交对预期路径所做的更改。如果它无法干净地应用到当前文件,请不要强行应用。仔细重新构建所需的当前文件,仅在完整审查结果后再进行暂存和提交。
来源
Git: git-revert 文档 — Git
Git: git-diff 文档 — Git
Git: git-apply 文档 — Git
Git: git-commit 文档 — Git