假设我有一个分支dev
,我希望丢弃在{{1}的提交范围内对一组文件所做的所有更改因为它与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
从根本上说,我不希望告诉git签出文件的特定版本 - 我想告诉git过滤 dev
范围内的所有提交以获取所有任意文件集中的更改(存在于主上的任何位置<或> ) 已弃用 。
那我怎么告诉git?
答案 0 :(得分:10)
从根本上说,filter-branch的作用是什么 - 其他一切都是优化和/或边缘情况: 1
现在让我们考虑你想要的行动,但我要强调一个不同的词:
过滤[a]范围内的所有提交...让所有更改在任意文件集中...丢弃
我强调&#34;变化&#34;这里因为每个提交都是一个完整的,独立的实体。提交 &#34;更改&#34;,他们只有文件。查看更改的唯一方法是将一个特定提交与另一个特定提交进行比较:例如git diff commitA commitB
。
因此,当你说&#34;对某些文件&#34;进行更改时,显而易见的问题应该是:关于什么的改变?
在大多数情况下,谈论&#34;提交中的变化的人&#34;意味着&#34;此提交相对于其直接祖先的变化&#34;:对于简单(非合并)提交,您使用git show
或git log -p
获得的补丁。 (通常他们没有考虑如果提交是合并它们意味着什么,因此有多个父母。对于这些,git show
通常显示合并提交与其所有父项的组合差异,但这可能不匹配用户的意图;有关详细信息,请参阅the git-show documentation。)
使用git filter-branch
时,您必须自己定义(更改相关内容)。 filter-branch
命令为您提供签出提交的SHA-1 ID - 即使它只是&#34;虚拟&#34;在步骤1中检出,而不是实际填充到环境变量$GIT_COMMIT
中的磁盘树上。那么,如果你对&#34;的定义是关于什么&#34;是&#34;对于第一个父级&#34;,您可以使用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检索父提交的文件版本,丢弃由于该文件在父版本中不存在而发生的任何错误消息。但这可能与您的意图不符:如果不在父文件中,您是否要删除该文件尚不清楚。此外,如果在您的范围内的提交序列中的某处添加或删除文件,则仅将每个原始提交与其(单个)原始父提交进行比较可能会错误触发。例如,如果提交C5中不存在文件foo
,确实存在于C6中,并且在C7中保持不变,则C7和C6之间的比较表示&#34;文件未更改&#34;而早期的C5-C6比较说&#34;文件已添加&#34;。如果你的新的(改变的)C6-let称它为C6&#39;告诉他们除了foo
,因为它不在C5中,大概是你的C7&#39;还应该省略文件foo
。
另一种方法是在整个范围之前将每个提交与(单个)提交进行比较。如果您的范围涵盖提交C1,C2,C3,...,C9,我们可以调用单个先前的提交C0。然后,不是将C1与C1 ^,C2与C2 ^进行比较,而是将C1与C0,C2与C0,C3与C0进行比较,依此类推。根据您对&#34;更改&#34;的定义,这可能正是您想要的,因为&#34;撤消更改&#34;可以传递:我们删除新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
,则表示&#34;所有可从标签Y
到达的提交,但不包括可从标签X
&#34;到达的所有提交。 ,$WITH_RESPECT_TO
的适当值可能只是X
,但更有可能是X
和Y
的合并基础。如果X
和Y
是看起来像这样的分支:
...-o-o-o-o-o-o <-- master
\
*-o-o <-- X
\
o-o-o-o <-- Y
然后你要对底行的提交进行过滤,第一个要过滤的提交可能应该在某些路径上保持不变,如commit *
&#34; (我用星号标记的提交)。这是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
现在我们找到一个添加的文件,但没有关于不存在的文件的抱怨。所以可以给予&#34;不存在&#34;路径;他们根本不会在输出中出现。
如果对某些后来的提交 $WITH_RESPECT_TO
进行差异提交C
,则说明 p
在commit {中添加{1}} ,我们知道它在C
中不存在,并且在 $WITH_RESPECT_TO
中存在,因此我们要删除它以便它s&#34;未改变&#34;。 (这是状态字母C
的情况。)
如果差异显示在 A
中删除了路径 p
,我们知道它 存在在第一个,并且必须恢复以保持&#34;不变&#34;。 (这是状态字母C
的情况。)
如果差异显示路径 D
同时存在,但文件内容在 p
中有所不同,则内容必须为恢复保持&#34;不变&#34;。 (这是状态字母C
的情况。)
其他差异状态字母为M
,C
,R
,T
,U
和X
,但有些不会发生(我们通过指定适当的B
选项排除C
,R
和B
; git diff
仅在不完整合并期间发生;并且U
不应发生:见What do the Git “pairing broken” and “unknown” statuses mean, and when do they occur?)。 X
案例可能会导致中止过滤(例如,常规文件更改为符号链接,反之亦然;或者替换为子模块)。
如果在考虑了这个问题一段时间之后,你决定&#34;关于&#34; 应该使用父提交,你可以使用T
,它给定一个提交 - 将提交树与其父提交的树进行比较。 (但请再次注意它在合并提交时的行为,并确保它是你想要的。)
1
当使用git diff-tree
时,它实际上完成了全面检查 - 一切都出来的部分。使用--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'
的树会产生空差异。 filter-branch操作通常会保留这些操作,但如果您使用C1'
则会省略它们:您的新链将是--prune-empty
。但请注意,原始链条可能有空白&#34;提交;在这种情况下,即使副本实际上与原件相同,C1' <- C3'
也会修剪它们。
2
这些脚本就像在脚本文件中一样编写。如果你把它们变成单行,你需要根据需要添加分号,也可以将filter-branch
变成exit
,因为你不希望整个事物在{{1}时退出}}编