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

时间:2014-03-08 15:02:48

标签: git git-filter-branch

假设我有一个分支dev,我希望丢弃在{{1}的提交范围内对一组文件所做的所有更改因为它与dev不同而分支。如果此范围内的提交仅触及那些文件,我希望它被修剪。我得到的最接近的是:

master

但这有这些缺点

  1. 如果该文件已在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.,但
  2. 此文件可能不存在于master-dev-older-ancestor中,并且稍后从主服务器合并到git checkout master-dev-older-ancestor
  3. 毕竟我可能想要放弃对主文件中无处可见的某些文件的更改
  4. 从根本上说,我不希望告诉git签出文件的特定版本 - 我想告诉git过滤 dev范围内的所有提交以获取所有任意文件集中的更改(存在于主上的任何位置<或> 已弃用

    那我怎么告诉git?

1 个答案:

答案 0 :(得分:10)

从根本上说,filter-branch的作用是什么 - 其他一切都是优化和/或边缘情况: 1

  • 对于列出的修订中的每个提交:
    1. 查看提交;
    2. 应用过滤器;
    3. 根据步骤2创建一个新提交,它可能与旧提交相同或不同(即,这个新副本是旧版本的修改版本,除非它是一点一滴的相同的,在这种情况下,&#34;创建的新&#34;提交实际上只是旧的提交。)
  • 每个&#34;肯定&#34; ref在命令行上,重写它以指向步骤3中的新提交,无论它指向在步骤1中检出的旧提交。

现在让我们考虑你想要的行动,但我要强调一个不同的词:

  

过滤[a]范围内的所有提交...让所有更改在任意文件集中...丢弃

我强调&#34;变化&#34;这里因为每个提交都是一个完整的,独立的实体。提交 &#34;更改&#34;,他们只有文件。查看更改的唯一方法是将一个特定提交与另一个特定提交进行比较:例如git diff commitA commitB

因此,当你说&#34;对某些文件&#34;进行更改时,显而易见的问题应该是:关于什么的改变?

在大多数情况下,谈论&#34;提交中的变化的人&#34;意味着&#34;此提交相对于其直接祖先的变化&#34;:对于简单(非合并)提交,您使用git showgit 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)提交系列C1C2,...,Cn。你希望他们“不改变内容甚至存在”。某些路径的一部分,相对于某些父级的C1提交。您必须在$WITH_RESPECT_TO中设置适当的说明符。 (这可能来自环境,或者只是硬编码到实际脚本中。请注意,对于--index-filter--tree-filter,您可以让shell运行脚本,而不是尝试执行全部排成一行。)

例如,如果您正在过滤X..Y,则表示&#34;所有可从标签Y到达的提交,但不包括可从标签X&#34;到达的所有提交。 ,$WITH_RESPECT_TO的适当值可能只是X,但更有可能是XY的合并基础。如果XY是看起来像这样的分支:

...-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的情况。)

其他差异状态字母为MCRTUX,但有些不会发生(我们通过指定适当的B选项排除CRB; 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}时退出}}编