从根本上说,filter-branch 所做的是——其他一切都是优化和/或边缘情况:1
- 对于列出的修订版中的每个提交:
- 检查该提交;
- 应用过滤器;
- 创建一个新提交,它可能与旧提交相同也可能不同,具体取决于第 2 步(即,此新副本是旧提交的修改版本,除非它逐位相同,其中万一“创建的新”提交实际上只是旧的提交)。
- 对于命令行上的每个“肯定”引用,重写它以指向在步骤 3 中所做的新提交,只要它指向在步骤 1 中签出的旧提交。
现在让我们考虑一下您希望采取的行动,但我要强调一个不同的词:
过滤 [a] 范围内的所有提交 ... 以使任意文件集中的所有 更改 ... 丢弃
我在这里强调“更改”,因为每个提交都是一个完整的、独立的实体。提交没有“更改”,它们只有文件。查看更改的唯一方法是将一个特定提交与另一个特定提交进行比较:例如git diff commitA commitB。
因此,当您说“对某些文件进行更改”时,最直接的问题应该是:针对什么进行更改?
在大多数情况下,谈论“提交中的更改”的人的意思是“此提交中相对于其直接祖先的更改”:对于简单(非合并)提交,您可以使用 git show 获得的补丁或git log -p。 (通常他们没有考虑过提交是合并的含义,因此有多个父级。对于这些,git show 通常显示合并提交与其所有父级的组合差异,但这可能与用户的不匹配此处的意图;有关详细信息,请参阅the git-show documentation。)
使用git filter-branch 时,您必须自己定义这个(关于什么的变化)。 filter-branch 命令在环境变量 $GIT_COMMIT 中为您提供签出提交的 SHA-1 ID——即使它只是在步骤 1 中“虚拟”签出,而不是实际填充到磁盘树中.因此,如果您对“关于什么”的定义是“关于第一父”,则可以使用 gitrevisions 语法来指代父:${GIT_COMMIT}^ 是第一父,即使 ${GIT_COMMIT} 是原始 SHA-1。
一个非常粗略且未经优化的--tree-filter,它简单地提取每个此类文件的父版本,如下所示:2
for path in ...list-of-paths...; do
git checkout -q ${GIT_COMMIT}^ -- $path 2>/dev/null
done
exit 0 # in case the last "git checkout" failed, override its status
它只是要求 git 检索文件的父提交版本,丢弃由于父版本中不存在文件而发生的任何错误消息。但这也可能不符合您的意图:如果文件不在父级中,您是否要删除该文件尚不清楚。此外,如果在您的范围内的提交序列中的某处添加或删除文件,则仅将每个原始提交与其(单个)原始父提交进行比较可能会出错。例如,如果文件foo 在提交 C5 中不存在,在 C6 中确实存在,并且在 C7 中保持不变,则 C7 和 C6 之间的比较显示“文件未更改”,而之前 C5 与 C6 的比较显示“文件添加”。如果您的新(更改的)C6(我们称之为 C6' 以区分它们)删除了 foo,因为它不在 C5 中,那么您的 C7' 可能也应该省略文件 foo。
另一种选择是将每个提交与(单个)提交就在 整个范围之前进行比较。如果您的范围涵盖提交 C1、C2、C3、...、C9,我们可以将单个先前提交称为 C0。然后,我们可以将 C1 与 C0、C2 与 C0、C3 与 C0 等进行比较,而不是将 C1 与 C1^、C2 与 C2^ 等进行比较。根据您对“更改”的定义,这可能正是您想要的,因为“撤消更改”可能是传递性的:我们在新 C6 中删除 foo,因此我们也必须在新 C7 中删除 foo ;我们在新的 C7 中添加回bar,因此我们必须在新的 C8 中也添加回它,依此类推。
比较脚本的一个不太粗略的版本如下所示(这也可以针对--index-filter 进行优化,尽管我会将工作留给其他人,因为这是为了说明):
# Note: I haven't tested this either, not sure how it behaves if
# used inside git filter-branch. As a --tree-filter you would not
# really want to "git rm" anything, just to "rm" it. As an
# --index-filter you would want to "git rm --cached". For
# checkout, as a tree filter you want to extract the file into
# the working tree, and as an index filter you want to extract
# the file into the index.
git diff --name-status --no-renames $WITH_RESPECT_TO $GIT_COMMIT \
-- ...paths... |
while read status path; do
# note: $path may have embedded white space, so we
# quote it below to protect it from breaking into words
case $status in
A) git rm -- "$path";; # file was added, rm it to undo
D|M) git checkout $WITH_RESPECT_TO -- "$path";; # deleted or modified
*) echo "file $path has strange status $status, help!" 1>&2; exit 1;;
esac
done
说明:以上假设您正在过滤(可能是线性的,可能是分支-y)一系列提交C1、C2、...、Cn。对于某些父级C1 提交,您希望他们“不更改某些路径的内容甚至存在”。您必须在$WITH_RESPECT_TO 中设置适当的说明符。 (这可以来自环境,或者只是硬编码到实际脚本中。请注意,对于您的 --index-filter 或 --tree-filter,您可以让 shell 运行脚本,而不是尝试全部执行。 )
例如,如果您要过滤 X..Y,这意味着“所有可从标签 Y 访问的提交,不包括可从标签 X 访问的所有提交”,$WITH_RESPECT_TO 的适当值可能只是 @ 987654355@,但它更可能是X 和Y 的合并基础。如果X 和Y 是看起来像这样的分支:
...-o-o-o-o-o-o <-- master
\
*-o-o <-- X
\
o-o-o-o <-- Y
然后您正在过滤底行的提交,并且要过滤的第一个提交可能应该“相对于提交* 中看到的某些路径保持不变”(我用星号标记的提交)。这就是git merge-base X Y 提出的提交。
如果您使用的是原始 SHA-1 ID,则可能可以使用以下内容:
WITH_RESPECT_TO=676699a0e0cdfd97521f3524c763222f1c30a094 \
git filter-branch ... (filter-branch arguments go here) ... --
676699a0e0cdfd97521f3524c763222f1c30a094..branch
原始的 SHA-1 是提交 * 的 ID。
至于git diff 本身,让我们看看它产生的输出类型:
$ git diff --name-status --no-renames \
> 2cd861672e1021012f40597b9b68cc3a9af62e10 \
> 7bbc4e8fdb33e0a8e42e77cc05460d4c4f615f4d
M Documentation/RelNotes/1.8.5.4.txt
A Documentation/RelNotes/1.8.5.5.txt
M Documentation/git.txt
M GIT-VERSION-GEN
M RelNotes
(这是git diff 在源树上git 本身的实际输出)。在这两次修订之间,修改了一个发行说明文本文件,添加了一个,修改了Documentation/git.txt,等等。现在让我们再试一次,但将其限制为一个真实路径名和一个假路径名:
$ git diff --name-status --no-renames \
> 2cd861672e1021012f40597b9b68cc3a9af62e10 \
> 7bbc4e8fdb33e0a8e42e77cc05460d4c4f615f4d \
> -- Documentation/RelNotes/1.8.5.5.txt NoSuchFile
A Documentation/RelNotes/1.8.5.5.txt
现在我们发现了一个添加的文件,但没有关于不存在的文件的投诉。所以给出“不存在”的路径是可以的;它们根本不会出现在输出中。
如果将提交 $WITH_RESPECT_TO 与稍后的提交进行比较 C 表示路径 p 已添加到提交 C ,我们知道它在$WITH_RESPECT_TO 中不存在,而在C 中存在,因此我们想将其删除,使其“保持不变”。 (状态信A就是这种情况。)
如果差异表明路径 p 在 C 中被删除,我们知道它确实首先存在,并且必须恢复以保持“不变”。 (状态信D就是这种情况。)
如果差异表明路径 p 存在于两者中,但文件内容在 C 中不同,则必须恢复内容以保持"不变”。 (状态信M就是这种情况。)
其他 diff 状态字母为 C、R、T、U、X 和 B,但有些不会出现(我们排除了 C、R 和 @ 987654392@ 通过指定适当的git diff 选项;U 仅在不完整的合并期间出现;并且X 永远不会出现:请参阅What do the Git “pairing broken” and “unknown” statuses mean, and when do they occur?)。 T 的情况可能会导致过滤中止(例如,常规文件更改为符号链接,反之亦然;或替换为子模块)。
如果在考虑了一段时间后,您决定“相对于”应该使用父提交,您可以使用git diff-tree,它——给定一个提交— 将提交的树与其父级的树进行比较。 (但同样,请注意它在合并提交时的行为,并确保这是您想要的。)
1
当使用--tree-filter 时,它实际上完成了全面检查所有内容的部分。使用--index-filter,它将提交写入索引,但实际上并未写入文件系统,并允许您在索引中进行所有更改。使用--env-filter、--msg-filter、--parent-filter 和--commit-filter,您可以更改每个提交的文本、作者和/或父项。 --tag-name-filter 允许您在需要时更改标签名称,并导致新名称指向新提交而不是旧提交(因此 --tag-name-filter cat 保持名称不变并使那些指向旧提交的名称,现在指向给新的)。
--prune-empty 涵盖了一个极端情况:如果您有一个提交链C1 <- C2 <- C3,并且您的C2'(您的C2 副本)与您的C1' 具有相同的底层树,比较这些树C2' 和 C1' 产生一个空差异。 filter-branch 操作通常会保留这些,但如果您使用--prune-empty,则忽略它们:您的新链将是C1' <- C3'。但请注意,原始链可能有“空”提交;在这种情况下,filter-branch 将修剪这些副本,即使副本实际上与原件相同。
2
这些脚本就像在脚本文件中一样编写。如果你把它们变成单行,你需要根据需要添加分号,也许还把exit变成return,因为你不希望整个事情在evaled时退出。