以增量方式使用filter-branch的任何方法

时间:2014-02-25 12:21:56

标签: git git-rewrite-history

有没有办法在分支上以增量方式使用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 个答案:

答案 0 :(得分:11)

我不确定你真正想要实现的目标,所以我在这里说的是#34;是的,但可能不是你在想什么,它可能会不能帮助你实现目标,不管是什么,#34;。

在此理解这不仅仅是 filter-branch的作用,而且在某种程度上 它是如何做到的。


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

git存储库包含一些提交图。这些是通过一些起始提交节点找到的,这些节点是通过外部引用找到的 - 主要是分支和标签名称,还有注释标签,我只是对这种情况不太重视,然后使用那些起始节点找到更多节点,直到所有节点都可以到达"已找到节点。

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

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

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

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

我们还要注意,因为提交指向他们的父母,所以&#34;可达到的设置&#34;名称master的{​​{1}}为A B C D E Fbranch1的设置为A G H I Jbranch2的设置为A G H I K

&#34;真实姓名&#34;每个提交节点的一个是SHA-1,它是提交内容的加密校验和。内容包括相应工作树内容的SHA-1校验和以及父提交的SHA-1。因此,如果您要复制提交并更改 nothing (而不是单个位),则会获得相同的SHA-1,因此最终会使用相同的提交;但是如果你改变一个比特(包括,例如,改变提交者姓名的拼写,任何时间戳或相关工作树的任何部分),你会得到一个新的,不同的提交。

git rev-parsegit rev-list

这两个命令对大多数git操作都非常重要。

rev-parse命令将any valid git revision specifier转换为提交ID。 (它还有很多我们可以称之为&#34;辅助模式&#34;,允许将大多数git命令编写为shell脚本 - 而git filter-branch实际上是一个shell脚本。)

rev-list命令将修订版范围(也在gitrevisions中)转换为提交ID列表。鉴于只是一个分支名称,它会找到可从该分支到达的所有修订版本的集合,因此在上面的示例提交图中,给定branch2,它列出了提交{1}},AGHI的SHA-1值。 (它默认以反向时间顺序列出它们,但可以告诉它们在地形顺序&#34;中列出它们,这对K很重要,而不是我打算深入了解细节此处。)

但是,在这种情况下,您将需要使用&#34;提交限制&#34;:给定修订范围,如filter-branch语法,或给出{ {1}},A..B将其输出转速限制为可从B ^A到达的提交,但 可从git rev-list到达。因此,给定B - 或者euivalently A - 它会列出branch2~3..branch2branch2 ^branch2~3H的SHA-1值。这是因为I名称提交K,因此提交branch2~3G会从可到达的集合中删除。


A

filter-branch脚本相当复杂,但总结了它对命令行中给出的&#34; ref名称的操作&#34;并不太难。

首先,它使用G来查找要过滤的分支或分支的实际头部修订。它使用它两次,实际上:一次获得SHA-1值,一次获取名称。例如,git filter-branch,它需要获得&#34;真正的全名&#34; git rev-parse

headBranchA..origin/branchA

将打印:

refs/remotes/origin/branchA

filter-branch脚本会丢弃所有git rev-parse --revs-only --symbolic-full-name headBranchA..origin/branchA - 前缀结果,以获取&#34;肯定引用名称列表&#34 ;;最后,这些是它打算重写的内容。

这些是&#34;肯定参考&#34; git-filter-branch manual

中描述的内容

然后使用refs/remotes/origin/branchA ^refs/heads/headBranchA 获取要应用过滤器的提交SHA-1的完整列表。这就是^限制语法的用武之地:现在脚本只知道可以从git rev-list到达的提交,但不能从headBranchA..origin/branchA到达。

一旦有了提交ID列表,origin/branchA实际应用了过滤器。这些提交了新的提交。

与往常一样,如果新提交与原始提交完全相同,则commit-ID保持不变。但是,如果filter-branch是有用的,可能在某些时候,某些提交会被更改,从而为它们提供新的SHA-1。这些提交的任何直接子项都必须获取新的父ID,因此这些提交也会更改,并且这些更改会传播到最终的分支提示。

最后,将过滤器应用于所有列出的提交后,headBranchA脚本会更新&#34;肯定参考&#34;。


下一部分取决于您的实际过滤器。让我们假设您的过滤器在每次提交时更改了作者姓名的拼写,或者更改了每次提交时的时间戳,或者更改了每次提交的时间戳,以便每次提交都会被重写,除非出于某种原因而离开根提交不变,以便新分支和旧分支确实有共同的祖先。

我们从这开始:

git filter-branch

(您现在在filter-branch,即git checkout -b branchA origin/branchA 包含branchA

HEAD

(这会使另一个分支标签指向当前的ref: refs/heads/branchA提交,但不会改变git branch headBranchA

HEAD

&#34;积极参考&#34;在这种情况下是HEAD。要重写的提交是每个可以从# inital rewrite git filter-branch ... -- branchA 到达的提交,即下面的所有branchA节点(这里开始提交图表用于说明),除了根提交branchA:< / p>

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

&#34;积极参考&#34;这里是R-o-o-x-x-x <-- master | \ | o-o-o <-- headBranchA | \ | n <-- origin/branchA \ *-*-*-*-* <-- HEAD=branchA ,因此将会移动rev-list选择的提交只是标记为git filter-branch ... -- headBranchA..origin/branchA 的提交,这是您想要的。让我们这次拼写重写的提交origin/branchA(大写):

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步骤。所以,让我们撤消&#34;这和尝试不同的东西。让我们完全删除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 s)然后修改它们?好吧,假设您的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

其他问题

我们已经(在图形图纸中)涵盖了提交如何被复制和修改到您的&#34;逐步过滤&#34; 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无法提交),但{ {1}} 为空:它会列出一个&#34;已删除&#34;承诺。如果删除了两个提交,它将列出两个提交,依此类推。

控制origin/branchA的任何人都可以移除多个提交并且添加了一些其他新提交(事实上,这正是&#34;上游变种&#34;)。在这种情况下,两个temp命令都将为非空:origin/branchA..temp将显示已删除的内容,origin将显示已添加的内容。

最后,控制git rev-list的任何人都可以完全破坏你的一切。他们可以:

  • 完全删除origin/branchA..temp
  • 使其标签temp..origin/branchA指向无关的分支。

同样,由您来决定是否以及如何处理这些案件。

答案 1 :(得分:1)

Git 2.18(2018年第二季度)确实提出了增量过滤。

&#34; git filter-branch&#34;学会了使用不同的退出代码来允许 调用者告诉没有新提交的情况 从其他错误案例中重写。

commit 0a0eb2eMichele Locati (mlocati)(2018年3月15日) Junio C Hamano -- gitster --合并于commit cb3e97d,2018年4月9日)

  

filter-branch:当没有要重写的时候返回2

     

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