繁体   English   中英

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

[英]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

但这有这些缺点

  1. 如果文件在master中被删除,它将失败并显示error: pathspec 'a/b/c.png' did not match any file(s) known to git. 我可能决定git checkout master-dev-older-ancestor但是接着,
  2. 这个文件可能不存在于master-dev-older-ancestor中,并且在以后的某个时候从master返回到dev
  3. 毕竟我可能想要放弃一些在master中无处可见的文件的更改

从根本上说,我不想告诉git签出文件的特定版本 - 我想告诉git过滤范围 master-dev-older-ancestor..HEAD 中的所有提交以便在任意集合中进行所有更改文件(呈现上的任何地方主与否丢弃

那我怎么告诉git?

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

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

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

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

我在此强调“更改”,因为每次提交都是一个完整的,独立的实体。 承诺没有 “改变”,他们只是有文件 查看更改的唯一方法是将一个特定提交与另一个特定提交进行比较:例如git diff commitA commitB

因此,当你说“改变某些文件”时,显而易见的问题应该是:关于什么的改变?

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

例如,如果您正在过滤X..Y ,这意味着“所有可从标签Y到达的提交(不包括从标签X可到达的所有提交”), $WITH_RESPECT_TO的适当值可能只是X ,但更可能是XY合并基础。 如果XY是看起来像这样的分支:

...-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 。)

如果差异表示路径pC被删除,我们知道它确实存在于第一个中,并且必须恢复以保持“不变”。 (这是状态字母D 。)

如果diff表示路径p存在,但文件内容在C不同,则必须恢复内容以保持“不变”。 (这是状态字母M 。)

其他差异状态字母是CRTUXB ,但有些不能发生(我们通过指定适当的git diff选项排除CRB ; 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM