有什么方法可以在分支上以增量方式使用分支过滤器吗?
大致这样说(但这实际上不起作用):
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
(这是一个合并提交)。名称branch1
和branch2
分别指向commit J
和K
。我们还要注意,由于提交指向其父母,因此名称
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-parse
和git rev-list
这两个命令对于大多数git操作来说都是非常重要的。
rev-parse
命令将any valid git revision specifier转换为提交ID。 (它也有很多我们可以称为“辅助模式”的功能,它们允许将大多数git命令作为shell脚本编写,而git filter-branch
实际上是一个shell脚本。)rev-list
命令将修订范围(也在gitrevisions中)转换为提交ID列表。仅给出一个分支名称,它就会找到该分支可到达的所有修订的集合,因此,在上面的示例提交图中,给出branch2
,它列出了A
,G
,H
,I
和K
的SHA-1值。 (默认情况下按相反的时间顺序列出它们,但是可以告诉他们按“地形顺序”列出它们,这对于filter-branch
很重要,而不是我打算在这里深入了解细节。)但是,在这种情况下,您将需要使用“提交限制”:给定修订范围(如
A..B
语法)或给定诸如B ^A
的内容,git rev-list
将其输出修订集限制为B
可以访问但不可访问的提交。来自A
。因此,给定branch2~3..branch2
或等效地称为branch2 ^branch2~3
,它列出了H
,I
和K
的SHA-1值。这是因为branch2~3
名称落实G
,因此将A
和G
的提交从可达集中删除。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
指向一个不相关的分支。 同样,由您自己决定是否以及如何处理这些情况。