添加原始哈希以在 git rebase 上提交(使用新根)

Add original hash to commit on git rebase (with new root)

我有一个代码库,以前用 SVN 管理,但现在用 git 管理。当代码迁移到 git 时,历史记录丢失了。

我已经设法 recover the SVN-history,现在正在尝试 git-rebase 最近的提交。

我有两个分支,git-commits,其中包含自迁移到 git 以来的提交,以及 svn-commits,其中包含较旧的历史记录。每个分支包含 3000 多个提交。

我发现以下命令在旧历史之上构建新历史(尽管有一些手动合并冲突处理):

git rebase git-commits --root --onto svn-commits --preserve-merges

一些提交引用了提交哈希,我知道这些会在 rebase 完成后发生变化。为了这些信息不会永远丢失,我想将每个提交的原始提交哈希添加到新提交的提交消息中。

这意味着像这样的原始提交:

commit aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Author: Boaty McBoatface <boaty@example.com>
AuthorDate: Wed Jul 27 00:00:00 1938 +0000
Commit: Boaty McBoatface <boaty@example.com>
CommitDate: Wed Jul 27 00:00:00 1938 +0000

Reticulate splines

The splines had been derezzed, and needed to be reticulated.

会变成

commit bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
Author: Boaty McBoatface <boaty@example.com>
AuthorDate: Wed Jul 27 00:00:00 1938 +0000
Commit:     Meshy <meshy@example.com>
CommitDate: Wed Nov 16 10:23:31 2016 +0000

Reticulate splines

The splines had been derezzed, and needed to be reticulated.

Original hash: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

这可能吗?也许 git-filter-branch?

答案可能取决于您要变基的提交数。
如果您要变基的分支包含的提交数量相当少,您可以手动编辑以下提示可能会起作用:
https://help.github.com/articles/changing-a-commit-message/
一般来说,交互式变基应该对你有所帮助,但我希望你不一定应该进行分支过滤。

r, reword = use commit, but edit the commit message

使用交互式变基尝试通过在提交消息中插入原始散列来改写每个提交。

对于更大数量的提交,在这种情况下 3000 左右让我们尝试使用 filter-branch:

git filter-branch --msg-filter 'cat && echo "Original hash $GIT_COMMIT"' HEAD~3000..HEAD

它将为您所在分支的过去 3000 次提交中的每一次生成带有重写提交消息的新提交。新的提交消息将具有类似于此的格式(请注意底部的提交哈希):

commit 08ac9b84d820ec7b70fa53075adc06f0a8185ab4
Author:
Date:   Mon Nov 14 13:14:30 2016 +0100

 Adds javadoc

Auto inserted text: ....
Change-Id: ...dbf9497387a3c271ae0349822cb4b8...
Original hash 9d01f3e5b39b15c9dbe923916b6c25019b5b9796

之后你就可以安全地做你的变基了。应保留旧提交哈希。

BR 马切

首先,请注意:请确保您确实想要这样做,因为 git replace(在下面简要提及)可用于以保留 ID 的方式将历史拼接在一起。当然,它也有自己的缺点;搜索使用过它的人的报告。


是的,您可以使用 git filter-branch

不过,您可能想要 将 "rebase new commits atop new conversion" 步骤与“......然后编辑所有新提交以也包含它们的旧 ID”步骤,因为 rebase 通过 copying 提交工作,而 filter-branch 通过 ... copying 提交工作。 :-)

所有执行此类操作的 Git 命令 必须 复制,因为每个提交的哈希 ID 是提交内容的函数。如果新提交与原始提交有任何不同,它会获得一个新的不同 ID。

git rebasegit filter-branch 之间的区别在于复制哪些提交以及如何执行复制。

Rebase,在没有 --preserve-merges 的情况下完成时,通过 selecting 列表 non-merge 提交,将每个这样的提交变成一个变更集(通过减法,或多或少:child 减去 parent = 从 parent 到 child) 的增量,然后将此增量添加到 --onto 点或 commits-added-so-far.

当您使用 --preserve-merges 时,变基 仍然是 select 一个 non-merge 提交的列表。然后,在有合并提交的地方,对合并进行变基 re-performs(这就是为什么你必须重新解决合并冲突)。它必须 re-merge,因为新的基础可能会导致不同的合并,并且因为合并不能变成一个单一的变更集("child - parent" 给你一个增量,但至少有两个 parents,因此至少有两个增量,在一般情况下我们不能同时保留两者)。

Filter-branch 使用完全不同的方法。要过滤的提交是 selected,不管它们是否合并。 (实际的 selection 是由 运行ning git rev-list 完成的,它相当于 git log 的 "plumbing"。)这个完整的提交 ID 列表被放入一堆:排序后的 topological-order 堆存储在普通文件中,因此 parent 提交总是在 children.

之前处理

然后,对于列表中的每个 ID:

  • 将原始提交拉 git checkout 提取到没有基础 Git 存储库的临时树中。

  • 应用树过滤器修改树。 (这个修改 运行s 在保存临时树的临时目录中。这部分让很多人在第一次 tree-filter 尝试访问像 ../../fixed-version 这样的文件时绊倒了。相对路径失败,因为临时树根本不在存储库中。)

  • 重建一组新的Gittree-and-blob-objects代表新的树,即新的提交快照。

  • 对消息应用提交消息过滤器。

  • 将提交环境过滤器应用于剩余的提交元数据(作者和提交者的东西)。

  • 使用新消息和新树进行新提交。或者,如果您提供提交过滤器,请将其用于 make-or-don't-make 提交;您还可以在此时使用 parent 过滤器修改新提交的 parent(s)。

  • 最后,记录一个配对:"old commit <oldhash> became new commit <newhash>."(如果您使用提交过滤器跳过提交,则旧哈希映射到其对应的新祖先,即 parent 你 没有 跳过。)这个配对是 map.

由于提取 + tree-filter + 重建部分,此过程非常缓慢。因此,如果您 使用树过滤器,git filter-branch 会跳过这部分:无论如何它都会恢复原来的树。为了让您无论如何修改新提交的内容,filter-branch 还允许您指定一个 索引过滤器 (提交总是从索引开始工作,所以提取+修改+重建只是更新索引;如果我们可以就地更新,那会快得多)。但是——这是关键点——为了您的目的,您根本不需要对每棵树做任何事情。您只想修改 parentage! 这将使您保留原始合并 及其源树 ,而没有 re-merging。

请注意,--commit-filter 描述谈到了 map 便利函数(shell 函数)。这个 "map" 函数使用了我上面提到的地图。默认是自动映射到新复制的提交的新parent。

最后,在复制所有提交后——并且,如果您提供 --tag-name-filter,还复制带注释的标签并映射副本(因此,如果您确实有带注释的标签,您 want a --tag-name-filter cat here)—filter-branch 命令重写了一些引用,即分支和标签名称。仍然指向原始提交(和带注释的标记 objects)的原始引用被转储到 refs/original/name-space。 (除非您使用 --force,否则该过程开始时必须为空。)重写的引用指向新副本。重写使用相同的映射技术,因此如果有跳过的提交,名称现在指向保留的祖先提交。

("Some" 引用?等等,哪个 引用?答案在文档中,但有点神秘:它谈论 positive references。参数被传递给 git rev-list,以便您可以过滤特定范围的提交,例如,branch~30..branchbranch ^otherbranch。"positive" 引用是主动 select 提交的那些,而 "negative" 引用是限制提交的引用,因此对于 branch ^otherbranch 我们有一个正引用 branch 和一个负引用, not-otherbranch 部分。所以这只重写 refs/heads/branch 而不是 refs/heads/otherbranch。)

废话太多了,但是……怎么办?

之所以解释以上所有内容,是为了指出使用git filter-branch时移植过程是多么简单,然后显示如何访问地图。

首先,我们只需要显式替换一个parent ID。具体来说,我们希望 git-commitsroot commit 的 parent 成为 svn-commits:

的现有 tip commit
$ git rev-parse svn-commits
9999999999999...

(这是所需的新 parent),并且:

$ git rev-list --max-parents=0 git-commits
11111111111111...

(这是根提交——幸运的是只有一个,否则,现在怎么办?)。

因此,我们需要一个 parent 过滤器 表示:"if this is commit 1111111... then echo 9999999..., else just echo the arguments back"。默认的 parent 参数在 stdin 上,作为一系列 -p <id>s,ID 已经映射。当然,一个现有的根有 no parents,所以标准输入将没有我们想要在这里更改的一次提交的内容。因此:

--parent-filter 'if [ $GIT_COMMIT = 11111... ]; then
  echo -p 999999...; else cat; fi'

这部分filter-branch将完成我们的re-parenting。请注意,与 git rebase 不同的是,所有的树都完好无损地保留下来。我们从不在这里将快照转换为增量,我们只是将其 as-is。 这意味着不需要re-resolve合并冲突。

(旁注:您实际上可以在此处使用名称 svn-commits 代替 hard-coded 99999...。您可以使用名称代替 hard-coded 11111... 但我们 没有 名称。此外,每次查找名称都会给过滤增加一点延迟。对于 re-parenting 到 svn-commits,这是一个微小的延迟;不过,为了测试这是否是旧根,这将是一个微小的延迟乘以 3000 次提交。)

(第二个旁注:您也可以通过 "grafts" 或其更现代的版本 git replace 重新 parent 执行此操作。如果移植或替换生效,当您运行 filter-branch,移植或替换成为 永久性 ,因为 Git 只是按照指示复制提交,替换后也有说明。)

这仍然存在过滤提交消息的问题,以添加:

Original hash: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

如上图,原始hash在$GIT_COMMIT,所以我们只需要这样:

--msg-filter 'cat; echo; echo "Original hash: $GIT_COMMIT"'

如果我们想花哨一点,我们甚至可以使用 map 便利函数:

--msg-filter 'cat; echo; echo "new commit $(map $GIT_COMMIT) \
filtered to reparent original commit $GIT_COMMIT"'

或者像那样愚蠢的东西,但没有充分的理由去打扰...... 除非你想要真的花哨,并且看看您是否可以在提交消息中检测到旧的哈希 ID 并在适当的位置重写它们。我不确定这是否是个好主意,并且不会尝试为其提供一些 shell 脚本,但请注意所有 1 这些过滤器被 "eval"-ed 为 shell 片段。您可以从 这些 eval-ed 片段调用其他 shell 脚本 ,请记住所有过滤都在临时目录中进行。

运行 过滤参考 git-commits。过滤完成后,refs/heads/git-commits 将指向最后复制的提交,而 refs/original/refs/heads/git-commits 将指向原始链(上面示例中以 11111... 为根的链)。


1嗯,几乎全部。正如文档所说,"with the notable exception of the commit filter, for technical reasons".


总结

我们需要两个过滤器,--parent-filter(或有效的移植或替换)和 --msg-filter。 parent 过滤器说 "replace the root of the transplanted copy with the tip of the place we're transplanting onto",这完成了我们的 rebase-without-changing-snapshots。消息过滤器说 "this new commit replaces the commit whose ID we expanded at filtering-time from the variable $GIT_COMMIT".