有没有一种增量方式使用filter-branch?

5

有没有办法在分支上逐步使用filter-branch?

大致来说,就像这样(但这实际上是不起作用的):

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  
2个回答

13

我不确定你真正想要实现什么,所以我要说的是:“是的,有点类似,但可能不是你想要的,而且可能无法帮助你实现任何目标”。

重要的是要理解 filter-branch 做了什么,以及在某种程度上它是如何做到的。


背景(为了让这个答案对他人有用)

一个 Git 仓库包含一些提交记录图表。这些通过外部引用找到一些起始提交节点,主要是分支和标签名称,还有一些注释标签,我将忽略它们,因为在这种情况下它们不是特别重要,并使用这些起始节点来查找更多节点,直到找到所有“可达”节点。

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

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

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

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

需要注意的是,由于提交指向它们的父项,“可到达集合”从名称master开始是A B C D E F,从branch1开始是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命令将任何有效的git修订说明符转换为提交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手册中描述的“正引用”。

然后它使用git rev-list获取要应用过滤器的完整提交SHA-1列表。这就是headBranchA..origin/branchA限制语法发挥作用的地方:脚本现在知道只在从origin/branchA可达的提交上工作,但不在headBranchA可达的提交上工作。

一旦有了提交ID列表,git filter-branch实际上就应用了过滤器。这将产生新的提交。

和往常一样,如果新提交和原提交完全相同,则提交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

每次提交都会被复制,然后 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,这意味着要合并提交N,需要找到*链和提交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,这样我们就可以获取提交n了。

R-o-o-x-x-x     <-- master
|    \     .--------temp
|     o-o-o-n   <-- origin/branchA
 \
  *-*-*-*-*     <-- HEAD=branchA

现在让我们将提交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
如果您的filter-branch更改了树,那么这种方法将无法始终起作用。那么我们可以直接将过滤器应用于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指向一个无关的分支。

同样,如何处理这些情况完全取决于您。


1
感谢您提供这么详细的解释,非常有帮助! - Gert
如何处理合并? 步骤1)A和E存在并被过滤为A'和E'。 步骤2)添加B、C、D和F,需要进行过滤。 - René Scheibe
@RenéScheibe:这总是涉及到选择要复制的提交,然后应用您的过滤器。如果您选择的提交太少,您永远没有机会将它们复制。如果您选择的太多,您必须安排一些新副本与原始副本完全相同(通过设计您的过滤器)。最后,您在命令行上给出的正面引用将根据filter-branch构建的映射进行重写。 - torek
"git rev-list A..B 限制其输出修订集为从 A 可达但从 B 不可达的提交。" 这不应该是相反的(从 B 可达但从 A 不可达)吗? - Fernando Costa Bertoldi
@FernandoBertoldi:嗯,是的。A..B = B ^ A。这很容易搞反...(现在已经修复了,谢谢) - torek

1

Git 2.18 (Q2 2018) 现在提供了增量过滤功能。

"git filter-branch" 学会了使用不同的退出代码,以便让调用者告知没有新提交可重写的情况和其他错误情况。

参见 提交 0a0eb2e (2018年3月15日) 来自 Michele Locati (mlocati)
(由 Junio C Hamano -- gitster -- 合并于 提交 cb3e97d,2018年4月9日)

filter-branch:没有需要重写的内容时返回2

使用--state-branch选项可以执行增量过滤。这可能会导致在随后的过滤中没有任何需要重写的内容,因此我们需要一种方法来识别这种情况。
所以,当出现这种“错误”时,让我们退出并返回2而不是1。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接