【问题标题】:git filter-branch - discard the changes to a set of files in a range of commitsgit filter-branch - 在一系列提交中丢弃对一组文件的更改
【发布时间】:2014-03-08 15:02:48
【问题描述】:

假设我有一个分支 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 呢?

【问题讨论】:

    标签: git git-filter-branch


    【解决方案1】:

    从根本上说,filter-branch 所做的是——其他一切都是优化和/或边缘情况:1

    • 对于列出的修订版中的每个提交:
      1. 检查该提交;
      2. 应用过滤器;
      3. 创建一个新提交,它可能与旧提交相同也可能不同,具体取决于第 2 步(即,此新副本是旧提交的修改版本,除非它逐位相同,其中万一“创建的新”提交实际上只是旧的提交)。
    • 对于命令行上的每个“肯定”引用,重写它以指向在步骤 3 中所做的新提交,只要它指向在步骤 1 中签出的旧提交。

    现在让我们考虑一下您希望采取的行动,但我要强调一个不同的词:

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

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

    因此,当您说“对某些文件进行更改”时,最直接的问题应该是:针对什么进行更改?

    在大多数情况下,谈论“提交中的更改”的人的意思是“此提交中相对于其直接祖先的更改”:对于简单(非合并)提交,您可以使用 git show 获得的补丁或git log -p。 (通常他们没有考虑过提交是合并的含义,因此有多个父级。对于这些,git show 通常显示合并提交与其所有父级的组合差异,但这可能与用户的不匹配此处的意图;有关详细信息,请参阅the git-show documentation。)

    使用git filter-branch 时,您必须自己定义这个(关于什么的变化)。 filter-branch 命令在环境变量 $GIT_COMMIT 中为您提供签出提交的 SHA-1 ID——即使它只是在步骤 1 中“虚拟”签出,而不是实际填充到磁盘树中.因此,如果您对“关于什么”的定义是“关于第一父”,则可以使用 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(我们称之为 C6' 以区分它们)删除了 foo,因为它不在 C5 中,那么您的 C7' 可能也应该省略文件 foo

    另一种选择是将每个提交与(单个)提交就在 整个范围之前进行比较。如果您的范围涵盖提交 C1、C2、C3、...、C9,我们可以将单个先前提交称为 C0。然后,我们可以将 C1 与 C0、C2 与 C0、C3 与 C0 等进行比较,而不是将 C1 与 C1^、C2 与 C2^ 等进行比较。根据您对“更改”的定义,这可能正是您想要的,因为“撤消更改”可能是传递性的:我们在新 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,这意味着“所有可从标签 Y 访问的提交,不包括可从标签 X 访问的所有提交”,$WITH_RESPECT_TO 的适当值可能只是 @ 987654355@,但它更可能是XY 的合并基础。如果XY 是看起来像这样的分支:

    ...-o-o-o-o-o-o   <-- master
         \
          *-o-o       <-- X
           \
            o-o-o-o   <-- Y
    

    然后您正在过滤底行的提交,并且要过滤的第一个提交可能应该“相对于提交* 中看到的某些路径保持不变”(我用星号标记的提交)。这就是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
    

    现在我们发现了一个添加的文件,但没有关于不存在的文件的投诉。所以给出“不存在”的路径是可以的;它们根本不会出现在输出中。

    如果将提交 $WITH_RESPECT_TO 与稍后的提交进行比较 C 表示路径 p 已添加到提交 C ,我们知道它在$WITH_RESPECT_TO 中不存在,而在C 中存在,因此我们想将其删除,使其“保持不变”。 (状态信A就是这种情况。)

    如果差异表明路径 pC 中被删除,我们知道它确实首先存在,并且必须恢复以保持“不变”。 (状态信D就是这种情况。)

    如果差异表明路径 p 存在于两者中,但文件内容在 C 中不同,则必须恢复内容以保持"不变”。 (状态信M就是这种情况。)

    其他 diff 状态字母为 CRTUXB,但有些不会出现(我们排除了 CR 和 @ 987654392@ 通过指定适当的git diff 选项;U 仅在不完整的合并期间出现;并且X 永远不会出现:请参阅What do the Git “pairing broken” and “unknown” statuses mean, and when do they occur?)。 T 的情况可能会导致过滤中止(例如,常规文件更改为符号链接,反之亦然;或替换为子模块)。


    如果在考虑了一段时间后,您决定“相对于”应该使用父提交,您可以使用git diff-tree,它——给定一个提交— 将提交的树与其父级的树进行比较。 (但同样,请注意它在合并提交时的行为,并确保这是您想要的。)


    1 当使用--tree-filter 时,它实际上完成了全面检查所有内容的部分。使用--index-filter,它将提交写入索引,但实际上并未写入文件系统,并允许您在索引中进行所有更改。使用--env-filter--msg-filter--parent-filter--commit-filter,您可以更改每个提交的文本、作者和/或父项。 --tag-name-filter 允许您在需要时更改标签名称,并导致新名称指向新提交而不是旧提交(因此 --tag-name-filter cat 保持名称不变并使那些指向旧提交的名称,现在指向给新的)。

    --prune-empty 涵盖了一个极端情况:如果您有一个提交链C1 &lt;- C2 &lt;- C3,并且您的C2'(您的C2 副本)与您的C1' 具有相同的底层树,比较这些树C2'C1' 产生一个空差异。 filter-branch 操作通常会保留这些,但如果您使用--prune-empty,则忽略它们:您的新链将是C1' &lt;- C3'。但请注意,原始链可能有“空”提交;在这种情况下,filter-branch 将修剪这些副本,即使副本实际上与原件相同。

    2 这些脚本就像在脚本文件中一样编写。如果你把它们变成单行,你需要根据需要添加分号,也许还把exit变成return,因为你不希望整个事情在evaled时退出。

    【讨论】:

    • 非常感谢-虽然我消化了两个观察结果:1.确实我想到了简单的情况(不合并只是线性提交流)2.“如果文件 foo 不存在于提交 C5,确实存在于 C6 中,并且在 C7 中保持不变,C7 和 C6 之间的比较说“文件未更改”,而 C5 到 C6 的早期比较说“文件添加”。如果您的新(更改)C6,让我们称它为 C6' 以区分它们 - 删除 foo 因为它不在 C5 中,大概你的 C7' 也应该省略文件 foo" 它应该由我的(省略的)更改定义 - 所以必须将 C7 与 C6' 进行比较而不是C6 - 怎么样?
    • new 提交进行比较并不明显(在过滤器中)——它可以完成,只需查看 filter-branch shell 脚本以了解如何——但是你如果您将所有内容与我称为 C-zero 的提交进行比较,则不需要 需要,因为“未更改”的传递属性:通过使 C6'“相对于 C0 保持不变”,将 C7 与C6' 并将 C7 与 C0 进行比较将(必然)给出相同的结果。
    • 所以最后 - 没有提交“接触”一组文件的没有概念?我知道 git 提交是整个存储库的快照,但是这种认为更改的概念仅针对不同的快照定义的想法相当令人惊讶。我猜每个提交都知道哪些文件已被触及(相对于其父级 - 它知道除非 root 提交)但我显然错了? (并且将一个函数从文件 A 移动到文件 B 应该算作一个操作,如果文件 A 被忽略,那么移动 operation 应该被丢弃 - 所以不仅 A 的变化,而且B)
    • 这更像是一个推算的概念:您将提交与其父级进行比较以查看发生了什么变化。许多 git 命令执行此操作:git showgit cherry-pickgit rebase,甚至 git merge 将提交与某些父级进行比较。合并与合并基础进行比较,而不是直接父级,并且至少进行两次这样的比较:一次用于 HEAD 提交/分支,将添加一个新的提交,一次用于每个被合并的提交-在。 (两者都与 merge-base 进行比较,然后将两个 diff 的结果组合起来产生合并。)
    • 是的 - 所以人们会期望在 :git filter-branch --tree-filter 'git --discard-changes -- a/b/c.png' --prune-empty -- master..dev 中有一个默认的推算概念 - 如果该范围内的提交不知道路径,则默认情况下将是 noop。仍在努力吸收它
    猜你喜欢
    • 1970-01-01
    • 2011-07-16
    • 2013-11-07
    • 1970-01-01
    • 2015-09-24
    • 1970-01-01
    • 2013-03-29
    • 1970-01-01
    • 2011-07-03
    相关资源
    最近更新 更多