假设我有一个分支dev
,并且由于dev
分支与master
分叉,所以我想放弃对error: pathspec 'a/b/c.png' did not match any file(s) known to git.
分支中大量提交所做的所有更改。如果该范围内的提交仅涉及那些我希望将其修剪的文件。我最接近的是:
git checkout dev
git filter-branch --force --tree-filter 'git checkout master -- \
a/b/c.png \
...
' --prune-empty -- master-dev-older-ancestor..HEAD
但这有这些缺点
如果文件是从母版中删除的,它将以
git checkout master-dev-older-ancestor
失败,我可能决定使用dev
,但是,该文件可能在master-dev-older-ancestor中不存在,并在以后从master合并回
master-dev-older-ancestor..HEAD
毕竟,我可能想放弃对某些文件的更改,这些更改在master中无处可见
从根本上讲,要点是我不想告诉git检出文件的特定版本-我想告诉git过滤
那我怎么告诉git?
最佳答案
从根本上讲,分支过滤器是这样做的-其他一切都是优化和/或边缘情况:1
对于列出的修订中的每个提交:
查看该提交;
应用过滤器;
创建一个新提交,它可能与步骤2相同,也可能与旧提交不同(即,此新副本是旧副本的修改版本,除非它一点一点相同,在这种情况下,“创建新的”提交实际上实际上只是旧的提交)。
对于命令行上的每个“正”引用,无论它指向在步骤1中检出的旧提交,都将其重写以指向在步骤3中进行的新提交。
现在,让我们考虑一下您想要的操作,但是我要强调一个不同的词:
过滤[a]范围内的所有提交...以使任意文件集中的所有更改...丢弃
我在这里强调“更改”,因为每个提交都是一个完整的独立实体。提交没有“更改”,它们只有文件。查看更改的唯一方法是将一个特定的提交与另一个特定的提交进行比较:例如,git diff commitA commitB
。
因此,当您说“更改某些文件”时,最明显的问题应该是:针对哪些内容进行了更改?
在大多数情况下,谈论“提交更改”的人表示“此提交相对于其直接祖先的更改”:对于简单(非合并)提交,您使用git show
或git log -p
通常显示合并提交与其所有父级的合并差异,但可能与用户的父级不匹配。目的在这里;有关详细信息,请参见the git-show documentation。)
使用git show
时,您将必须自己定义(相对于内容的更改)。 git filter-branch
命令为您提供已签出提交的SHA-1 ID(即使它只是在步骤1中“虚拟”签出,而不是实际上被塞入磁盘树中)在环境变量filter-branch
中。因此,如果您对“相对于什么”的定义是“相对于第一个父项”,则可以使用$GIT_COMMIT
语法引用父项:gitrevisions
是第一个父项,即使${GIT_COMMIT}^
是原始SHA-1。
一个非常粗糙且未经优化的${GIT_COMMIT}
可以简单地提取每个此类文件的父版本,如下所示:2for 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检索文件的父提交版本,而丢弃由于该文件在父版本中不存在而出现的任何错误消息。但这也可能与您的意图不符:如果文件不在父目录中,则是否要删除该文件尚不清楚。此外,如果在您范围内的提交序列中的某个位置添加或删除文件,则仅将每个原始提交与其(单个)原始父提交进行比较可能会触发错误。例如,如果文件--tree-filter
在提交C5中不存在,在C6中确实存在,并且在C7中保持不变,则C7和C6之间的比较表示“文件不变”,而C5-to-C6的早期比较则表示“文件”添加”。如果新的(更改的)C6(将其区分为C6')将其删除,因为它不在C5中,因此删除了foo
,大概您的C7'也应该省略文件foo
。
另一种选择是将每个提交与(单个)提交进行比较,就在整个范围之前。如果您的范围涵盖提交C1,C2,C3,...,C9,我们可以调用单个先前的提交C0。然后,代替将C1与C1 ^,C2与C2 ^等进行比较,我们可以将C1与C0,C2与C0,C3与C0等进行比较。根据您对“更改”的定义,这可能正是您想要的,因为“撤消更改”可能是传递性的:我们在新C6中删除了foo
,因此我们也必须在新C7中也删除了foo
;我们在新C7中重新添加foo
,因此我们也必须在新C8中将其重新添加。
比较脚本的粗略版本是这样的(也可以针对bar
进行优化,尽管我会把工作留给其他人,因为这是为了说明):# 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)一系列提交--index-filter
,C1
,...,C2
。对于某些Cn
提交父项,您希望它们“不更改某些路径集的内容,甚至不存在”。您必须在C1
中设置适当的说明符。 (这可以来自环境,也可以硬编码到实际脚本中。请注意,对于您的$WITH_RESPECT_TO
或--index-filter
,您可以让Shell运行脚本,而不是尝试全部按顺序执行。 )
例如,如果您要过滤--tree-filter
,这意味着“从标签X..Y
可以访问的所有提交,但不包括从标签Y
可以访问的所有提交”,则X
的适当值很可能就是$WITH_RESPECT_TO
,但是更有可能X
和X
的合并基础。如果Y
和X
是看起来像这样的分支:...-o-o-o-o-o-o <-- master
\
*-o-o <-- X
\
o-o-o-o <-- Y
那么您要过滤底部行上的提交,并且要过滤的第一个提交可能应该“相对于某些路径保持不变,如在Y
中看到的一样”(我用星号标记的提交)。这就是*
的承诺。
如果您使用原始SHA-1 ID,则可以使用类似以下的方法:WITH_RESPECT_TO=676699a0e0cdfd97521f3524c763222f1c30a094 \
git filter-branch ... (filter-branch arguments go here) ... --
676699a0e0cdfd97521f3524c763222f1c30a094..branch
原始SHA-1是提交git merge-base X Y
的ID。
至于*
本身,让我们看一下它产生的输出类型:$ 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 diff
的实际输出)。在这两个修订版之间,修改了一个发行说明文本文件,添加了一个,修改了git
,依此类推。现在让我们再试一次,但将其限制为一个真实的路径名和一个伪造的路径名:$ git diff --name-status --no-renames \
> 2cd861672e1021012f40597b9b68cc3a9af62e10 \
> 7bbc4e8fdb33e0a8e42e77cc05460d4c4f615f4d \
> -- Documentation/RelNotes/1.8.5.5.txt NoSuchFile
A Documentation/RelNotes/1.8.5.5.txt
现在,我们找到了一个添加的文件,但是没有关于不存在的文件的投诉。因此可以给出“不存在”的路径。它们根本不会出现在输出中。
如果将提交Documentation/git.txt
与以后的提交$WITH_RESPECT_TO
进行比较,则说在提交C
中添加了路径p
,我们知道该路径在C
中不存在,而在$WITH_RESPECT_TO
中存在,因此我们想删除因此它是“不变的”。 (状态字母C
就是这种情况。)
如果diff指出A
中的路径p
已删除,则我们知道它确实存在于第一个路径中,必须将其还原以保持“不变”。 (状态字母C
就是这种情况。)
如果差异说明两个路径中都存在路径D
,但是文件的内容在p
中不同,则必须还原内容以保持“不变”。 (状态字母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
情况可能导致中止过滤(例如,将常规文件更改为symlink,反之亦然;或者用子模块替换了某些内容)。
如果在考虑了一段时间之后,您决定“相对于”应使用父提交,则可以使用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
。但是请注意,原始链可能具有“空”提交;在这种情况下,即使副本实际上与原始副本相同,C1' <- C3'
也会删减这些副本。
2
这些脚本的编写就像在脚本文件中一样。如果将它们变成单行,则需要根据需要添加分号,也许还需要将filter-branch
转换为exit
,因为您不希望在return
完成后退出整个过程。
关于git - git filter-branch-放弃一系列提交中对一组文件的更改,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22270938/