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

Ger*_*ert 4 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  
Run Code Online (Sandbox Code Playgroud)

tor*_*rek 13

我不确定你到底想要实现什么,所以我在这里要说的是"是的,有点,但可能不是你在想什么,它可能无助于你实现你的目标,无论那是什么".

重要的是要在这里理解不仅仅是什么 filter-branch,而且在某种程度上,它是如何做到的.


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

git存储库包含一些提交图.这些是通过一些起始提交节点找到的,通过外部引用找到 - 主要是分支和标记名称,还有注释标记,我只是掩盖其中对这种情况不是特别重要 - 然后使用这些起始节点查找更多节点,直到找到所有"可到达"节点.

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

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

  B-C-D
 /     \
A---E---F   <-- master
 \
  G     J   <-- branch1
   \   /
    H-I-K   <-- branch2
Run Code Online (Sandbox Code Playgroud)

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

我们还要注意,因为提交指向他们的父母,所以名称中的"可达集合" masterA B C D E F,设置为branch1is A G H I J,而设置为branch2is A G H I K.

每个提交节点的"真实名称"是其SHA-1,它是提交内容的加密校验和.内容包括相应工作树内容的SHA-1校验和以及父提交的SHA-1.因此,如果你去复制一个提交并且什么都不改变(不是一个单独的位),你就得到了相同的SHA-1,因此结束了相同的提交; 但是如果你改变一个位(包括,例如,更改提交者名称的拼写,任何时间戳或相关工作树的任何部分),你会得到一个新的,不同的提交.

git rev-parsegit rev-list

这两个命令对于大多数git操作来说都是非常重要的.

rev-parse命令将任何有效的git修订说明符转换为commit-ID.(它还有许多我们称之为"辅助模式"的东西,允许将大多数git命令编写为shell脚本 - git filter-branch实际上是一个shell脚本.)

rev-list命令将修订范围(也在gitrevisions中)转换为commit-ID列表.由于只是一个分支的名字,它找到了一套全部由该分支到达的修订,使之与例如提交如上图所示,给出branch2,它列出了SHA-1的值提交A,G,H,I,和K.(它默认以反向时间顺序列出它们,但可以告诉它们以"地形顺序"列出它们,这很重要filter-branch,而不是我打算深入了解这里的细节.)

但是,在这种情况下,您将需要使用"提交限制":给定修订范围(如A..B语法)或给定的内容B ^A,git rev-list将其输出转速限制为可从中访问B无法访问的提交A.因此,给定的branch2~3..branch2-or branch2 ^branch2~3euivalently,-它列出了SHA-1值H,IK.这是因为branch2~3名称提交G,所以提交AG从可到达的集合中删除.


git filter-branch

filter-branch脚本相当复杂,但总结其对"在命令行上给出的ref名称"的操作并不太难.

首先,它用于git rev-parse查找要过滤的分支或分支的实际头部修订.它使用它两次,实际上:一次获得SHA-1值,一次获取名称.例如,headBranchA..origin/branchA它需要获得"真正的全名" refs/remotes/origin/branchA:

git rev-parse --revs-only --symbolic-full-name headBranchA..origin/branchA
Run Code Online (Sandbox Code Playgroud)

将打印:

refs/remotes/origin/branchA
^refs/heads/headBranchA
Run Code Online (Sandbox Code Playgroud)

filter-branch脚本丢弃任何^预先定义的结果以获取"正ref名称"列表; 最后,这些是它打算重写的内容.

这些是git-filter-branch手册中描述的"正面参考" .

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

一旦它具有提交ID列表,git filter-branch实际应用过滤器.这些提交了新的提交.

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

最后,将过滤器应用于所有列出的提交,filter-branch脚本更新"正面参考".


下一部分取决于您的实际过滤器.让我们假设您的过滤器在每次提交时更改作者姓名的拼写,或者更改每次提交时的时间戳,或者更改每次提交的时间戳,以便重写每个提交,除非由于某种原因它保持根提交不变,以便新分支和旧分支确实有共同的祖先.

我们从这开始:

git checkout -b branchA origin/branchA
Run Code Online (Sandbox Code Playgroud)

(你现在在branchA,即HEAD包含ref: refs/heads/branchA)

git branch headBranchA
Run Code Online (Sandbox Code Playgroud)

(这使得另一个分支标签指向当前HEAD提交但不会改变HEAD)

# inital rewrite
git filter-branch ... -- branchA
Run Code Online (Sandbox Code Playgroud)

在这种情况下,"正面参考"是branchA.要重写的提交是每个提交都可以从branchA,即o下面的所有节点(开始提交图在这里用于说明),除了根提交R:

R-o-o-x-x-x   <-- master
     \
      o-o-o   <-- headBranchA, HEAD=branchA, origin/branchA
Run Code Online (Sandbox Code Playgroud)

每个o提交都被复制,并被branchA移动到指向最后一个新提交:

R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- headBranchA, origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA
Run Code Online (Sandbox Code Playgroud)

之后,你去从远程拿起新东西origin:

git fetch origin
Run Code Online (Sandbox Code Playgroud)

让我们说这会添加标记的提交n(我只需添加一个):

R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- headBranchA
|          \
|           n <-- origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA
Run Code Online (Sandbox Code Playgroud)

这里出了问题:

git filter-branch ... -- headBranchA..origin/branchA
Run Code Online (Sandbox Code Playgroud)

这里的"正面参考"是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
Run Code Online (Sandbox Code Playgroud)

现在你尝试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
Run Code Online (Sandbox Code Playgroud)

这部分还可以,但是未来一团糟.事实证明你实际上并不想要提交N,而且你不想移动origin/branchA,因为(我假设)你希望git fetch origin以后能够重复这个步骤.所以让我们"撤消"这个并尝试不同的东西.让我们headBranchA完全放弃标签,从这开始:

R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA
Run Code Online (Sandbox Code Playgroud)

让我们为提交origin/branchA点添加一个临时标记,并运行git fetch origin,以便我们得到提交n:

R-o-o-x-x-x     <-- master
|    \     .--------temp
|     o-o-o-n   <-- origin/branchA
 \
  *-*-*-*-*     <-- HEAD=branchA
Run Code Online (Sandbox Code Playgroud)

现在,让我们拷贝提交nbranchA,虽然我们复制它,修改它太(做什么MODS的你会用做git filter-branch)得到一个承诺,我们将只需要调用N:

R-o-o-x-x-x     <-- master
|    \     .--------temp
|     o-o-o-n   <-- origin/branchA
 \
  *-*-*-*-*-N    <-- HEAD=branchA
Run Code Online (Sandbox Code Playgroud)

完成后,我们擦除temp,我们准备重复循环.


让它工作

这留下了几个问题.最明显的是:我们如何复制n(或几个/多个ns)然后修改它们?好吧,假设你已经在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
Run Code Online (Sandbox Code Playgroud)

如果你的filter-branch改变了树,那么这个方法并不总是有用呢?好吧,我们可以直接将过滤器应用于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
Run Code Online (Sandbox Code Playgroud)

其他问题

我们已经(在图形图中)介绍了如何将提交复制并修改为"逐步过滤" branchA.但是,当你去咨询时origin,你会发现提交被删除后会发生什么?

也就是说,我们从这开始:

R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA
Run Code Online (Sandbox Code Playgroud)

我们像往常一样放下我们的临时标记git fetch origin.但是他们所做的就是删除最后一次o提交,并强行推动他们.现在我们有:

R-o-o-x-x-x   <-- master
|    \
|     o-o     <-- origin/branchA
|        `o.......temp
 \
  *-*-*-*-*   <-- HEAD=branchA
Run Code Online (Sandbox Code Playgroud)

这里的含义是我们可能也应该备份branchA一个修订版.

您是否想要处理此问题取决于您自己.我会在这里指出的结果git rev-list temp..origin/branchA将是在这个特殊的情况下,空(有关于修订没有提交origin/branchA不是来自到达temp),但origin/branchA..temp不会是空的:它会列出一个"删除"的承诺.如果删除了两个提交,它将列出两个提交,依此类推.

控件的任何人origin都可以删除多个提交添加一些其他新提交(实际上,这正是"上游rebase"所发生的情况).在这种情况下,两个git rev-list命令都将是非空的:origin/branchA..temp将显示已删除的内容,temp..origin/branchA并将显示添加的内容.

最后,无论谁控制origin完全破坏你的一切都是可能的.他们能:

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

同样,由您决定是否以及如何处理这些案例.