我不确定你真正想要实现什么,所以我要说的是:“是的,有点类似,但可能不是你想要的,而且可能无法帮助你实现任何目标”。
重要的是要理解 filter-branch
做了什么,以及在某种程度上它是如何做到的。
背景(为了让这个答案对他人有用)
一个 Git 仓库包含一些提交记录图表。这些通过外部引用找到一些起始提交节点,主要是分支和标签名称,还有一些注释标签,我将忽略它们,因为在这种情况下它们不是特别重要,并使用这些起始节点来查找更多节点,直到找到所有“可达”节点。
每个提交记录都有零个或多个“父提交记录”。大多数普通提交都有一个父提交记录;合并提交有两个或多个父提交记录。根提交记录(例如存储库中的初始提交记录)没有父提交记录。
分支名称指向一个特定的提交记录,该提交记录指向其父级,依此类推。
B-C-D
/ \
A---E---F <-- master
\
G J <-- branch1
\ /
H-I-K <-- branch2
分支名称为master
,指向提交F
(这是一个合并提交)。名称branch1
和branch2
分别指向提交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
命令将任何有效的git修订说明符转换为提交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手册中描述的“正引用”。
然后它使用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
)
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
步骤不会遇到树差异问题的情况,因此这取决于您的过滤器执行什么操作:
git tag temp origin/branchA
git fetch origin
git tag temp2
git cherry-pick temp..origin/branchA
git filter-branch ... -- temp2..branchA
git tag -d temp temp2
如果您的filter-branch更改了树,那么这种方法将无法始终起作用。那么我们可以直接将过滤器应用于n个提交,得到n'个提交,然后复制n'个提交。这n''个提交是将保留在本地(过滤后的)branchA上的提交。一旦复制了这些n'个提交,就不再需要它们,所以我们将它们丢弃。
git tag temp origin/branchA
git fetch origin
git checkout -b temp2 origin/branchA
git filter-branch ... -- temp..temp2
git checkout branchA
git cherry-pick temp..temp2
git tag -d temp
git branch -D temp2
其他问题
在图形绘制中,我们已经介绍了如何将新提交复制并修改到您的“增量过滤”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
指向一个无关的分支。
同样,如何处理这些情况完全取决于您。
filter-branch
构建的映射进行重写。 - torekgit rev-list A..B
限制其输出修订集为从 A 可达但从 B 不可达的提交。" 这不应该是相反的(从 B 可达但从 A 不可达)吗? - Fernando Costa Bertoldi