你确实不能那样做,因为这会涉及到更改一些现有的提交。在这种情况下,现有提交 F 将 B 的哈希 ID 存储为其父级。任何现有提交都无法更改:提交完全是只读的(事实上,Git 的所有内部对象都是只读的)。
我假设您想假装 F 将 E 的哈希 ID 存储为其父级,而不更改与任何现有提交关联的快照。你可以做这个,你可以做很多类似的事情。不过,在我们看这些之前,让我们先看看git rebase。
在这里使用git rebase 的问题是通过复制 提交的rebase 函数(到目前为止一切都很好),每个副本都像1通过使用git cherry-pick(这是事情开始出错的地方)。为了挑选一个提交,Git 将:
与往常一样,新提交进入当前分支,使分支一次提交更长,并更改 HEAD 提交以命名新提交。
合并步骤,加上 Git 将快照(提交)转换为变更集(差异)的事实,意味着与原始快照相比,最终精心挑选的结果可能是一个非常不同的快照。这一切都取决于您启动进程时<hash1> 与HEAD 的不同之处。
git rebase 命令本质上自动化了一次挑选整个系列提交的过程。在所有复制结束时,git rebase 强制分支标签指向最终复制的提交。这将允许您将F 复制到父级为E 的新F',然后将G 复制到父级为F' 的新G',依此类推:
A--B--F--G--H--I--J <-- (original)
\
C--D--E <-- (target of rebase)
\
F'-G'-H'-I'-J' <-- branch
1有时git rebase 会运行git cherry-pick,有时它使用的方法通常会产生相同的结果,但在某些奇怪的极端情况下却不会。
现在,假设我们创建了一个 Git 对象,而不是所有这些,Git 可以在它即将使用F 时“看向一边”。这个替换-for-F 像这样进入图表:
A--B--F--G--H--I--J <-- branch
\
C--D--E <-- some_name
\
Frepl <-- refs/replace/<hash>
我们将 F 的提交消息复制到 Frepl 的提交消息,并复制大部分其他字段,包括提交的树对象的内部 Git 哈希 ID。但不是指向提交B,而是提交Frepl 指向提交E。
现在我们只需要 Git 将其从 F “转向”到 Frepl 每次它即将与提交 F 一起工作时,无论出于何种原因。还有一种方法可以做到这一点:我们给Frepl 一个特殊的名称refs/replace/<em>big-ugly-hash-id</em>,其中big-ugly-hash-id 是提交F 的实际哈希ID。
进行替换的 Git 命令是 git replace。旁观是自动的:Git 总是这样做,除非我们运行 git --no-replace-objects。这一切都是在不改变任何现有 Git 对象的情况下完成的,所以它只是添加到存储库中。
替换对象的最大缺点是git clone 默认情况下不复制替换对象(它不获取它们,也不获取它们的名称)。这意味着克隆没有看到替代品,也从不旁观它。您可以将替换显式添加到您的 fetch refspecs 以获取它们,但这有点麻烦。 git push 操作默认也不传输它们。
最大的优势是他们不需要每个人都停止使用原始提交来支持新的和改进的提交。但是,如果您可以让每个人都切换,则可以使用git rebase 复制许多提交并移动分支标签。或者,您可以先使用git replace 来创建替换对象,然后不使用过滤器运行git filter-branch,但告诉它过滤发生替换的分支。
git filter-branch 所做的是复制 每个 提交(在它被告知要过滤的一个或多个分支上)。它——至少在逻辑上;有很多优化——提取每个提交,从最旧的提交到最新的提交,到一个临时目录,以某种顺序应用每个过滤器,然后使用过滤器所做的任何事情进行新的提交。如果新提交与原始提交逐位相同,则两次提交实际上只是一次提交,否则副本是新的不同提交。每个新副本的默认父级是在父级的早期副本中所做的提交(尽管有一个--parent-filter 可以让您更改它!)。它通过替换后备噱头完成所有这些操作,2 因此,当 Git 复制 F 时,它实际上复制了 Frepl,然后跟随 Frepl 回到 E。如果我们将这个过滤器命名为branch,结果是Git 复制A,然后是B,然后是C,然后是D,然后是E,然后是Frepl,然后是G ,然后是H,然后是I,然后是J,给出:
A--B--F--G--H--I--J <-- refs/original/refs/heads/branch
\
C--D--E <-- some_name
\
Frepl <-- refs/replace/<hash>
\
G'-H'-I'-J' <-- branch
注意branch 指向提交J',其父级为I',但其树(快照)与J 相同。提交I' 指向H',它又指向G',又指向Frepl(我们在运行git replace 时创建的副本:它在过滤期间保持不变)。这指向E(这是它自己未更改的副本),它又指向D,依此类推回到A。
那么,如果我们扔掉所有的 refs/original/ 名称,比如 refs/original/refs/heads/branch,那么最终的效果就是“巩固”任何替换提交。然后我们可以删除Frepl 的refs/replace/ 名称,看起来好像我们只在存储库中提交过A-B-C-D-E-Frepl-G'-H'-I'-J'。许多提交的 hash IDs 已更改,因此此存储库不再与原始存储库兼容;但如果我们从名称 branch 开始,我们只会在所有正确的地方看到闪亮的新复制提交。
2当然,如果您运行git --no-replace-objects filter-branch,则会禁用替换后备。可能永远没有任何理由这样做。