[英]git filter-branch - discard the changes to a set of files in a range of commits
假设我有一个分支dev
,我想放弃在dev
分支的提交范围内对一组文件所做的所有更改,因为它与master
分开。 如果此范围内的提交仅触及我喜欢的那些文件,则会将其修剪掉。 我得到的最接近的是:
git checkout dev
git filter-branch --force --tree-filter 'git checkout master -- \
a/b/c.png \
...
' --prune-empty -- master-dev-older-ancestor..HEAD
但这有这些缺点
error: pathspec 'a/b/c.png' did not match any file(s) known to git.
我可能决定git checkout master-dev-older-ancestor
但是接着, dev
从根本上说,我不想告诉git签出文件的特定版本 - 我想告诉git过滤范围 master-dev-older-ancestor..HEAD
中的所有提交,以便在任意集合中进行所有更改文件(呈现上的任何地方主与否 ) 丢弃 。
那我怎么告诉git?
从根本上说,filter-branch的作用是什么 - 其他一切都是优化和/或边缘情况: 1
现在让我们考虑你想要的行动,但我要强调一个不同的词:
过滤[a]范围内的所有提交...以使任意文件集中的所有更改 ...被丢弃
我在此强调“更改”,因为每次提交都是一个完整的,独立的实体。 承诺没有 “改变”,他们只是有文件 。 查看更改的唯一方法是将一个特定提交与另一个特定提交进行比较:例如git diff commitA commitB
。
因此,当你说“改变某些文件”时,显而易见的问题应该是:关于什么的改变?
在大多数情况下,谈论“提交中的更改”的人意味着“此提交相对于其直接祖先的更改”:对于简单(非合并)提交,您使用git show
或git log -p
获得的补丁git log -p
。 (通常他们没有考虑如果提交是一个合并它们意味着什么,因此有多个父母。对于这些, git show
通常显示合并提交与其所有父项的组合差异,但这可能与用户的意图不匹配这里;有关详细信息,请参阅git-show文档 。)
使用git filter-branch
,您必须自己定义(更改相关内容)。 filter-branch
命令为您提供签出提交的SHA-1 ID - 即使它仅在步骤1中“虚拟”检出,而不是实际填充到磁盘树中 - 在环境变量$GIT_COMMIT
。 因此,如果您对“关于什么”的定义是“关于第一个父母”,您可以使用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-let叫它C6'告诉他们分开 - 删除foo
因为它不在C5中,大概你的C7'也应该省略文件foo
。
另一种方法是将每个提交与整个范围之前的(单个)提交进行比较。 如果您的范围涵盖提交C1,C2,C3,...,C9,我们可以调用单个先前的提交C0。 然后,不是将C1与C1 ^,C2与C2 ^进行比较,而是将C1与C0,C2与C0,C3与C0进行比较,依此类推。 根据您的“变化”的定义,这可能正是你想要的,因为“撤消变更”可能是传递的:除去foo
在我们的新C6,因此,我们必须消除foo
在我们新的C7为好; 我们在新的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
说明:上面假设您正在过滤一个(可能是线性的,可能是branch-y)系列的提交C1
, C2
,..., Cn
。 对于某些父级的C1
提交,您希望它们“不改变某些路径的内容甚至存在”。 您必须在$WITH_RESPECT_TO
设置适当的说明$WITH_RESPECT_TO
。 (这可能来自环境,或者只是硬编码到实际的脚本中。请注意,对于--index-filter
或--tree-filter
,您可以让shell运行脚本,而不是尝试执行它一切都好。)
例如,如果您正在过滤X..Y
,这意味着“所有可从标签Y
到达的提交(不包括从标签X
可到达的所有提交”), $WITH_RESPECT_TO
的适当值可能只是X
,但更可能是X
和Y
合并基础。 如果X
和Y
是看起来像这样的分支:
...-o-o-o-o-o-o <-- master
\
*-o-o <-- X
\
o-o-o-o <-- Y
然后你要过滤底行的提交,并且第一个要过滤的提交可能应该“相对于commit *
某些路径不变”(我用星号标记的提交)。 这就是git merge-base XY
提出的提交。
如果您正在使用原始SHA-1 ID,则可以使用以下内容:
WITH_RESPECT_TO=676699a0e0cdfd97521f3524c763222f1c30a094 \
git filter-branch ... (filter-branch arguments go here) ... --
676699a0e0cdfd97521f3524c763222f1c30a094..branch
其中原始SHA-1是commit *
的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
本身的源代码树上git diff
实际输出)。 在这两个版本之间,修改了一个发布说明文本文件,添加了一个,修改了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
现在我们找到一个添加的文件,但没有关于不存在的文件的抱怨。 所以给“不存在”的路径是可以的; 它们根本不会出现在输出中。
如果针对某些后来的提交C
$WITH_RESPECT_TO
提交$WITH_RESPECT_TO
表示路径p
在提交C
添加,我们知道它在$WITH_RESPECT_TO
中不存在并且在C
,因此我们想要删除它以使其“未更改”。 (这是状态字母A
。)
如果差异表示路径p
在C
被删除,我们知道它确实存在于第一个中,并且必须恢复以保持“不变”。 (这是状态字母D
。)
如果diff表示路径p
存在,但文件内容在C
不同,则必须恢复内容以保持“不变”。 (这是状态字母M
。)
其他差异状态字母是C
, R
, T
, U
, X
和B
,但有些不能发生(我们通过指定适当的git diff
选项排除C
, R
和B
; U
仅在不完全合并期间发生;并且X
应该永远不会发生:看看Git“配对破坏”和“未知”状态意味着什么,它们何时发生? )。 T
情况可能会导致中止过滤(例如,常规文件更改为符号链接,反之亦然;或者替换为子模块)。
如果在考虑了问题一段时间之后,你决定“关于” 应该使用父提交,你可以使用git diff-tree
,它给定一个提交 - 比较提交树和那些提交树它的父母。 (但请再次注意它在合并提交时的行为,并确保这是你想要的。)
1当使用--tree-filter
,它实际上会执行完整的检查 - 所有内容部分。 使用--index-filter
它将提交写入索引,但实际上不会写入文件系统,并允许您在索引中进行所有更改。 使用--env-filter
, - --msg-filter
, --parent-filter
--commit-filter
和--commit-filter
,它允许您更改每个提交的文本,作者和/或父级。 --tag-name-filter
允许您根据需要更改标记名称,并使新名称指向新提交而不是旧提交(因此--tag-name-filter cat
名称不变并使这些名称保持不变指向旧的提交,现在指向新的提交)。
--prune-empty
覆盖了一个边缘情况:如果你有--prune-empty
提交C1 <- C2 <- C3
,你的C2'
(你的C2
副本)与你的C1'
具有相同的底层树,比较树木C2'
和C1'
产生空差异。 filter-branch操作通常会保留这些,但如果你使用--prune-empty
则省略它们:你的新链将是C1' <- C3'
。 但请注意,原始链可能有“空”提交; 在这种情况下,即使副本实际上与原始副本相同, filter-branch
也会修剪它们。
2这些脚本就像在脚本文件中一样编写。 如果你将它们变成单行,你需要根据需要添加分号,也可以将exit
转换为return
,因为你不希望在eval
ed时退出整个东西。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.