我不确定你真正想要实现什么,所以我在这里要说的是“是的,有点,但可能不是你想的那样,它可能无法帮助你实现你的目标,不管是什么”。
重要的是不仅要了解它是做什么 filter-branch
的,而且在某种程度上还要了解它是如何做到的。
背景(使此答案对其他人有用)
一个 git 存储库包含一些提交图。这些是通过获取一些起始提交节点来找到的,这些节点是通过外部引用找到的——主要是分支和标签名称,还有带注释的标签,我只是在某种程度上掩盖了它们对这种情况并不特别重要——然后使用这些起始节点来查找更多节点,直到找到所有“可达”节点。
每个提交都有零个或多个“父提交”。大多数普通提交都有一个父级;合并有两个或多个父级。根提交(例如存储库中的初始提交)没有父提交。
分支名称指向一个特定的提交,该提交又指向其父级,依此类推。
B-C-D
/ \
A---E---F <-- master
\
G J <-- branch1
\ /
H-I-K <-- branch2
分支名称master
指向提交F
(这是一个合并提交)。名称branch1
和branch2
指向提交J
和K
分别。
我们还要注意,因为提交指向它们的父节点,所以 name 的“可达集”master
是A B C D E F
,for 的集合branch1
是A G H I J
,for 的集合branch2
是A G H I K
。
每个提交节点的“真实名称”是它的 SHA-1,它是提交内容的加密校验和。内容包括相应工作树内容的 SHA-1 校验和以及父提交的 SHA-1。因此,如果你去复制一个提交并且什么都不改变(不是一个单一的位),你会得到相同的 SHA-1 并因此以相同的提交结束;但是,即使您更改了一点点(包括,例如,更改提交者姓名的拼写、任何时间戳或相关工作树的任何部分),您都会得到一个新的、不同的提交。
git rev-parse
和git rev-list
这两个命令是大多数 git 操作的核心。
该rev-parse
命令将任何有效的 git 修订说明符转换为提交 ID。(它还有很多我们可以称之为“辅助模式”的东西,允许将大多数 git 命令编写为 shell 脚本——git filter-branch
实际上是一个 shell 脚本。)
该rev-list
命令将修订范围(也在gitrevisions中)转换为提交 ID 列表。仅给定一个分支名称,它会找到可从该分支访问的所有修订的集合,因此对于上面的示例提交图, given branch2
,它列出了提交A
、G
、H
、I
和的 SHA-1 值K
。(它默认按时间倒序列出它们,但可以被告知按“地形顺序”列出它们,这对 很重要filter-branch
,而不是我打算在这里深入了解细节。)
但是,在这种情况下,您将需要使用“提交限制”:给定修订范围,如A..B
语法,或给定诸如 之类的东西B ^A
,将其输出 rev 集限制为可从 访问但不可从git rev-list
访问的提交。因此,给定- 或等效地- 它列出了、和的 SHA-1 值。这是因为名称 commit ,所以提交并从可达集合中删除。B
A
branch2~3..branch2
branch2 ^branch2~3
H
I
K
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 手册中描述的“正引用” 。
然后它用于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
每个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
这里的“正面参考”是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
之间的合并基础......这就是提交。*
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
现在让我们将提交复制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
指向一个不相关的分支。
同样,是否以及如何处理这些情况取决于您。