4

有没有办法在分支上以增量方式使用过滤器分支?

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

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  
4

2 回答 2

12

我不确定你真正想要实现什么,所以我在这里要说的是“是的,有点,但可能不是你想的那样,它可能无法帮助你实现你的目标,不管是什么”。

重要的是不仅要了解它是做什么 filter-branch的,而且在某种程度上还要了解它是如何做到的。


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

一个 git 存储库包含一些提交图。这些是通过获取一些起始提交节点来找到的,这些节点是通过外部引用找到的——主要是分支和标签名称,还有带注释的标签,我只是在某种程度上掩盖了它们对这种情况并不特别重要——然后使用这些起始节点来查找更多节点,直到找到所有“可达”节点。

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

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

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

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

我们还要注意,因为提交指向它们的父节点,所以 name 的“可达集”masterA B C D E F,for 的集合branch1A G H I J,for 的集合branch2A 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 列表。给定一个分支名称,它会找到可从该分支访问的所有修订的集合,因此对于上面的示例提交图, given branch2,它列出了提交AGHI和的 SHA-1 值K。(它默认按时间倒序列出它们,但可以被告知按“地形顺序”列出它们,这对 很重要filter-branch,而不是我打算在这里深入了解细节。)

但是,在这种情况下,您将需要使用“提交限制”:给定修订范围,如A..B语法,或给定诸如 之类的东西B ^A,将其输出 rev 集限制为可从 访问但不可git rev-list访问的提交。因此,给定- 或等效地- 它列出了、和的 SHA-1 值。这是因为名称 commit ,所以提交并从可达集合中删除。BAbranch2~3..branch2branch2 ^branch2~3HIKbranch2~3GAG


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之间的合并基础......这就是提交。*NR

我想,这根本不是你想要做的。

我怀疑你想要做的是,相反,选择提交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

现在让我们将提交复制nbranchA,在我们复制它的同时,也对其进行修改(做任何你想做的模组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/branchAtemporigin/branchA..temp

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

最后,任何控制origin者都有可能为你彻底破坏一切。他们能:

  • 完全删除它们branchA,或
  • 让他们的标签branchA指向一个不相关的分支。

同样,是否以及如何处理这些情况取决于您。

于 2014-02-25T19:54:50.377 回答
1

Git 2.18 (Q2 2018) 现在确实提出了增量过滤。

" git filter-branch" 学会了使用不同的退出代码来允许调用者从其他错误情况中分辨出没有新提交要重写的情况。

请参阅Michele Locati ( ) 的提交 0a0eb2e(2018 年 3 月 15 日(由Junio C Hamano 合并 -- --提交 cb3e97d中,2018 年 4 月 9 日)mlocati
gitster

filter-branch: 当没有东西可以重写时返回 2

使用该--state-branch选项可以让我们执行增量过滤。这可能导致在后续过滤中没有什么可重写的,因此我们需要一种方法来识别这种情况。
所以,当这个“错误”发生时,让我们退出 2 而不是 1。

于 2018-04-14T00:49:23.570 回答