git - git filter-branch-放弃一系列提交中对一组文件的更改

标签 git git-filter-branch

假设我有一个分支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过滤范围内的所有提交,以使所有更改都出现在任意文件集中(存在于master的任何位置)或不)丢弃。

那我怎么告诉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}可以简单地提取每个此类文件的父版本,如下所示: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检索文件的父提交版本,而丢弃由于该文件在父版本中不存在而出现的任何错误消息。但这也可能与您的意图不符:如果文件不在父目录中,则是否要删除该文件尚不清楚。此外,如果在您范围内的提交序列中的某个位置添加或删除文件,则仅将每个原始提交与其(单个)原始父提交进行比较可能会触发错误。例如,如果文件--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-filterC1,...,C2。对于某些Cn提交父项,您希望它们“不更改某些路径集的内容,甚至不存在”。您必须在C1中设置适当的说明符。 (这可以来自环境,也可以硬编码到实际脚本中。请注意,对于您的$WITH_RESPECT_TO--index-filter,您可以让Shell运行脚本,而不是尝试全部按顺序执行。 )

例如,如果您要过滤--tree-filter,这意味着“从标签X..Y可以访问的所有提交,但不包括从标签Y可以访问的所有提交”,则X的适当值很可能就是$WITH_RESPECT_TO,但是更有可能XX的合并基础。如果YX是看起来像这样的分支:

...-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就是这种情况。)

其他差异状态字母是MCRTUX,但是有些不能出现(我们将BCR排除在外指定适当的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/

相关文章:

git - 在 ubuntu 20.04 上安装 git 时发生错误

git 过滤器分支错误 : "fatal: Not a valid object name HEAD"

git - `git filter-branch --subdirectory-filter` 的反义词是什么?

regex - 如何替换 Git 历史中的单词并正确调试相关问题?

git - 从历史记录中删除不需要的文件,包括所有带有过滤器分支的引用

python - 使用 openshift 部署本地 django 应用程序

git - SourceTree 终端无法推送,GUI 可以推送

git - 在气隙环境中使用 docker 的最佳实践

git - stash 命令的真正好处是什么?

git - 从 git 历史记录中删除已删除的文件