有什么方法可以在分支上以增量方式使用分支过滤器吗?

大致这样说(但这实际上不起作用):

git checkout -b branchA origin/branchA
git branch headBranchA
# inital rewrite
git filter-branch ... -- branchA
git fetch origin
# incremental rewrite
git filter-branch ... -- headBranchA..origin/branchA
git merge origin/branchA

最佳答案

我不确定您真正要实现的目标,因此我在这里要说的是“是的,有点,但可能不是您的想法,无论那是什么,它都可能无法帮助您实现目标”。

重要的是,不仅要了解filter-branch的功能,而且还要在某种程度上了解它的功能。

背景(使此答案对其他人有用)

git存储库包含一些提交图。这些是通过以下方式找到的:通过外部引用找到了一些起始提交节点(主要是分支和标签名称,还有带注释的标签,在这种情况下,我认为这并不是特别重要),然后使用这些起始节点来查找更多的节点,直到找到所有“可达”节点。

每个提交都有零个或多个“父提交”。大多数普通的提交都有一个父母。合并有两个或多个父母。根提交(例如存储库中的初始提交)没有父级。

分支名称指向一个特定的提交,它指向其父级,依此类推。

  B-C-D
 /     \
A---E---F   <-- master
 \
  G     J   <-- branch1
   \   /
    H-I-K   <-- branch2

分支名称master指向提交F(这是一个合并提交)。名称branch1branch2分别指向commit JK

我们还要注意,由于提交指向其父母,因此名称master中的“可访问集”为A B C D E Fbranch1的集为A G H I J,而branch2的集为A G H I K

每个提交节点的“真实名称”是其SHA-1,它是提交内容的加密校验和。内容包括相应工作树内容的SHA-1校验和以及父提交的SHA-1。因此,如果您去复制一个提交但不做任何更改(不是一个位),您将获得相同的SHA-1,因此最终将获得相同的提交;但是,即使您只更改了一点(例如,更改提交者名称的拼写,任何时间戳或相关工作树的任何部分),您都会得到一个新的,不同的提交。
git rev-parsegit rev-list
这两个命令对于大多数git操作来说都是非常重要的。
rev-parse命令将any valid git revision specifier转换为提交ID。 (它也有很多我们可以称为“辅助模式”的功能,它们允许将大多数git命令作为shell脚本编写,而git filter-branch实际上是一个shell脚本。)
rev-list命令将修订范围(也在gitrevisions中)转换为提交ID列表。仅给出一个分支名称,它就会找到该分支可到达的所有修订的集合,因此,在上面的示例提交图中,给出branch2,它列出了AGHIK的SHA-1值。 (默认情况下按相反的时间顺序列出它们,但是可以告诉他们按“地形顺序”列出它们,这对于filter-branch很重要,而不是我打算在这里深入了解细节。)

但是,在这种情况下,您将需要使用“提交限制”:给定修订范围(如A..B语法)或给定诸如B ^A的内容,git rev-list将其输出修订集限制为B可以访问但不可访问的提交。来自A。因此,给定branch2~3..branch2或等效地称为branch2 ^branch2~3,它列出了HIK的SHA-1值。这是因为branch2~3名称落实G,因此将AG的提交从可达集中删除。
git filter-branch
filter-branch脚本相当复杂,但是总结其对“命令行上给定的引用名称”的操作并不难。

首先,它使用git rev-parse查找要过滤的一个或多个分支的实际头部修订。实际上,它使用了两次:一次获取SHA-1值,一次获取名称。给定例如headBranchA..origin/branchA,它需要获取“真实全名” refs/remotes/origin/branchA:
git rev-parse --revs-only --symbolic-full-name headBranchA..origin/branchA

将打印:
refs/remotes/origin/branchA
^refs/heads/headBranchA

filter-branch脚本丢弃任何以^前缀的结果,以获取“正引用名称”列表;这些都是最终打算重写的内容。

这些是git-filter-branch manual中描述的“正引用”。

然后,它使用git rev-list获取要在其上应用过滤器的提交SHA-1的完整列表。这就是headBranchA..origin/branchA限制语法出现的地方:该脚本现在知道仅适用于origin/branchA可以到达的提交,而不适用于headBranchA可以到达的提交。

一旦有了提交ID列表,git filter-branch实际上就会应用过滤器。这些做出新的 promise 。

与往常一样,如果新提交与原始提交完全相同,则提交ID不变。但是,如果想使用filter-branch,可能会在某些时候更改某些提交,从而为它们提供新的SHA-1。这些提交的任何直接子代都必须获取新的父ID,因此这些提交也将更改,并且这些更改会传播到最终分支提示。

最后,在将过滤器应用于所有列出的提交之后,filter-branch脚本更新了“正引用”。

下一部分取决于您的实际过滤器。为了说明起见,我们假设您的过滤器在每次提交时都会更改作者姓名的拼写,或者在每次提交时(或类似情况)更改时间戳,以便每次提交都被重写,但出于某种原因它会使根提交保持不变,以便新分支和旧分支具有共同的祖先。

我们从这个开始:
git checkout -b branchA origin/branchA

(您现在位于branchA上,即HEAD包含ref: refs/heads/branchA)
git branch headBranchA

(这会使另一个分支标签指向当前的HEAD提交,但不会更改HEAD)
# inital rewrite
git filter-branch ... -- branchA

在这种情况下,“正参考”是branchA。除了根提交branchA之外,其余的提交都是可以从o到达的每个提交,即下面的所有R节点(开始提交图组成的图示)。
R-o-o-x-x-x   <-- master
     \
      o-o-o   <-- headBranchA, HEAD=branchA, origin/branchA

复制每个o提交,并移动branchA指向最后一个新提交:
R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- headBranchA, origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA

以后,您可以从远程origin拾取新内容:
git fetch origin

假设这添加了标记为n的提交(我将仅添加一个):
R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- headBranchA
|          \
|           n <-- origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA

这是出问题的地方:
git filter-branch ... -- headBranchA..origin/branchA

这里的“positive ref”是origin/branchA,因此将被移动。 rev-list选择的提交只是标记为n的那些,这就是您想要的。这次让我们拼写重写的提交N(大写):
R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- headBranchA
|         |\
|         | n [semi-abandoned - filter-branch writes refs/original/...]
|          \
|           N <-- origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA

现在,您尝试git merge origin/branchA,这意味着git merge提交N,这需要找到*链与commit N ...之间的合并基础,这就是R的提交。

我认为,这根本不是您的意思。

我怀疑您要执行的操作是将N落实到*链上。让我们画一下:
R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- headBranchA
|         |\
|         | n [semi-abandoned - filter-branch writes refs/original/...]
|          \
|           N <-- origin/branchA
 \
  *-*-*-*-*-N'<-- HEAD=branchA

这部分还可以,但将来会一团糟。事实证明,您实际上根本不需要提交N,并且也不想移动origin/branchA,因为(我假设)您希望以后可以重复git fetch origin步骤。因此,让我们“撤消”此操作并尝试其他操作。让我们完全删除headBranchA标签,并从此开始:
R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA

让我们为origin/branchA指向的提交添加一个临时标记,然后运行git fetch origin,以便获得commit n:
R-o-o-x-x-x     <-- master
|    \     .--------temp
|     o-o-o-n   <-- origin/branchA
 \
  *-*-*-*-*     <-- HEAD=branchA

现在,让我们将commit n复制到branchA,并在我们对其进行复制的同时,对其进行修改(执行对git filter-branch的任何修改)以获得一次提交,我们将只调用N:
R-o-o-x-x-x     <-- master
|    \     .--------temp
|     o-o-o-n   <-- origin/branchA
 \
  *-*-*-*-*-N    <-- HEAD=branchA

完成此操作后,我们将删除temp并准备重复该循环。

使它工作

这就留下了几个问题。最明显的是:我们如何复制n(或几个/很多n)然后进行修改?好吧,假设您已经使用了filter-branch,最简单的方法是使用git cherry-pick复制它们,然后使用git filter-branch过滤它们。

这仅在cherry-pick步骤不会遇到树差异问题的情况下才有效,因此这取决于您的过滤器执行的操作:
# all of this to be done while on branchA
git tag temp origin/branchA
git fetch origin # pick up `n` commit(s)

git tag temp2    # mark the point for filtering
git cherry-pick temp..origin/branchA
git filter-branch ... -- temp2..branchA

# remove temporary markers
git tag -d temp temp2

如果您的筛选器分支更改了树,该方法将永远无法使用怎么办?好吧,我们可以求助于直接将过滤器应用于n提交,给出n'提交,然后复制n'提交。这些(n'')提交将保留在本地(已过滤的)branchA上。 n'提交一旦被复制就不再需要,因此我们将其丢弃。
# lay down temporary marker as before, and fetch
git tag temp origin/branchA
git fetch origin

# now make a new branch, just for filtering
git checkout -b temp2 origin/branchA
git filter-branch ... -- temp..temp2
# the now-altered new branch, temp..temp2, has filtered commits n'

# copy n' commits to n'' commits on branchA
git checkout branchA
git cherry-pick temp..temp2

# and finally, delete the temporary marker and the temporary branch
git tag -d temp
git branch -D temp2 # temp2 requires a force-delete

其他问题

我们已经(在图形图中)介绍了如何将新提交复制并修改到“增量过滤”的branchA中。但是,如果当您查阅origin时发现提交已被删除,会发生什么情况?

也就是说,我们从以下开始:
R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA

我们像往常一样放下临时标记,然后执行git fetch origin。但是他们所做的是删除最后的o提交,并在其末端强制执行。现在我们有:
R-o-o-x-x-x   <-- master
|    \
|     o-o     <-- origin/branchA
|        `o.......temp
 \
  *-*-*-*-*   <-- HEAD=branchA

此处的含义是,我们可能也应该将branchA备份为一个修订版本。

是否要处理此完全取决于您。我会在这里注意到git rev-list temp..origin/branchA的结果在这种特殊情况下将为空(修订后的origin/branchA上没有从temp无法到达的提交),但是origin/branchA..temp不会为空:它将列出一个“已删除”的提交。如果删除了两个提交,它将列出两个提交,依此类推。

控制origin的任何人都有可能删除了多个提交并添加了一些其他新的提交(实际上,这正是“上游重新设置”所发生的)。在这种情况下,两个git rev-list命令都是非空的:origin/branchA..temp将显示删除的内容,temp..origin/branchA将显示添加的内容。

最后,控制origin的任何人都有可能为您彻底破坏一切。他们能:
  • 完全删除其branchA
  • 将其标签branchA指向一个不相关的分支。

  • 同样,由您自己决定是否以及如何处理这些情况。

    07-24 13:23