我可以重写整个 git 存储库的历史以包含我们忘记的内容吗?

Can I rewrite an entire git repository's history to include something we forgot?

我们最近完成了从 Mercurial 到 Git 的转换,一切进展顺利,我们甚至能够获得所需的转换,使存储库中的所有内容看起来/工作相对正确。我们添加了一个 .gitignore 并开始了。

然而,一旦我们 encorporate/work 使用我们的任何旧功能分支,我们就会遇到一些极端的减速。稍微探索一下,我们发现由于 .gitignore 仅在我们查看其他提交时才添加到 develop 分支,而没有合并 develop up 到它们 git chuggs 因为它正在尝试分析所有内容我们的构建工件(二进制文件)等...因为这些旧分支没有 .gitignore 文件。

我们想要做的是有效地插入一个带有 .gitignore 的新根提交,这样它将追溯填充所有 heads/tags. 我们很乐意重写历史记录,我们的团队相对较小,因此让每个人都停止此操作并在历史重写完成后重新拉取他们的存储库是没有问题的。

我找到了有关将 master 变基到新根提交的信息,这对 master 有效,问题是 它使我们的功能分支在旧历史树上分离 ,它还使用新提交 date/time.

重播整个历史记录

有什么想法或者我们在这方面运气不好吗?

你想做的事情将涉及两个阶段:追溯添加一个具有合适 .gitignore 的新根并清理你的历史记录以删除不应该添加的文件。 git filter-branch 命令可以同时执行这两项操作。

设置

考虑一个代表你的历史。

$ git lola --name-status
* f1af2bf (HEAD, bar-feature) Add bar
| A     .gitignore
| A     bar.c
| D     main.o
| D     module.o
| * 71f711a (master) Add foo
|/
|   A   foo.c
|   A   foo.o
* 7f1a361 Commit 2
| A     module.c
| A     module.o
* eb21590 Commit 1
  A     main.c
  A     main.o

为清楚起见,*.c 文件代表 C 源文件,*.o 是本应忽略的已编译目标文件。

在 bar-feature 分支上,您添加了一个合适的 .gitignore 并删除了不应被跟踪的对象文件,但您希望该策略在您的导入中随处可见。

请注意 git lola 是一个 non-standard 但很有用的别名。

git config --global alias.lola \
  'log --graph --decorate --pretty=oneline --abbrev-commit --all'

新根提交

按如下方式创建新的根提交。

$ git checkout --orphan new-root
Switched to a new branch 'new-root'

git checkout 文档记录了新孤立分支可能出现的意外状态。

If you want to start a disconnected history that records a set of paths that is totally different from the one of start_point, then you should clear the index and the working tree right after creating the orphan branch by running git rm -rf . from the top level of the working tree. Afterwards you will be ready to prepare your new files, repopulating the working tree, by copying them from elsewhere, extracting a tarball, etc.

继续我们的例子:

$ git rm -rf .
rm 'foo.c'
rm 'foo.o'
rm 'main.c'
rm 'main.o'
rm 'module.c'
rm 'module.o'

$ echo '*.o' >.gitignore

$ git add .gitignore

$ git commit -m 'Create .gitignore'
[new-root (root-commit) 00c7780] Create .gitignore
 1 file changed, 1 insertion(+)
 create mode 100644 .gitignore

现在的历史是这样的

$ git lola
* 00c7780 (HEAD, new-root) Create .gitignore
* f1af2bf(bar-feature) Add bar
| * 71f711a (master) Add foo
|/
* 7f1a361 Commit 2
* eb21590 Commit 1

这有点误导,因为它使 new-root 看起来像是 bar-feature 的后代,但实际上它没有父代。

$ git rev-parse HEAD^
HEAD^
fatal: ambiguous argument 'HEAD^': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

记下孤儿的 SHA,因为您稍后需要它。本例中为

$ git rev-parse HEAD
00c778087723ae890e803043493214fb09706ec7

改写历史

我们希望 git filter-branch 进行三项广泛的更改。

  1. 拼接新的根提交。
  2. 删除所有临时文件。
  3. 使用新根中的 .gitignore 除非已经存在。

在命令行中,如

git filter-branch \
  --parent-filter '
    test $GIT_COMMIT = eb215900cd15ca2cf9ded74f1a0d9d25f65eb2bf && \
              echo "-p 00c778087723ae890e803043493214fb09706ec7" \
      || cat' \
  --index-filter '
    git rm --cached --ignore-unmatch "*.o"; \
    git ls-files --cached --error-unmatch .gitignore >/dev/null 2>&1 ||
      git update-index --add --cacheinfo \
        100644,$(git rev-parse new-root:.gitignore),.gitignore' \
  --tag-name-filter cat \
  -- --all

解释:

  • --parent-filter 选项挂钩在您的新根提交中。
    • eb215... 是旧根提交的完整 SHA,cf. git rev-parse eb215
  • --index-filter选项有两部分:
    • 运行 git rm 从整个树中删除任何匹配 *.o 的内容,因为 glob 模式由 git 而不是 [=127= 引用和解释].
    • 检查现有的 .gitignoregit ls-files,如果不存在,指向 new-root 中的那个。
  • 如果您有任何标签,它们将通过恒等操作进行映射,cat
  • 单独的--终止选项,--all是shorthand所有参考。

您看到的输出类似于

Rewrite eb215900cd15ca2cf9ded74f1a0d9d25f65eb2bf (1/5)rm 'main.o'
Rewrite 7f1a361ee918f7062f686e26b57788dd65bb5fe1 (2/5)rm 'main.o'
rm 'module.o'
Rewrite 71f711a15fa1fc60542cc71c9ff4c66b4303e603 (3/5)rm 'foo.o'
rm 'main.o'
rm 'module.o'
Rewrite f1af2bf89ed2236fdaf2a1a75a34c911efbd5982 (5/5)
Ref 'refs/heads/bar-feature' was rewritten
Ref 'refs/heads/master' was rewritten
WARNING: Ref 'refs/heads/new-root' is unchanged

您的原件仍然安全。例如,master 分支现在位于 refs/original/refs/heads/master 下。查看新重写的分支中的更改。当您准备好删除备份时,运行

git update-ref -d refs/original/refs/heads/master

您可以编写一个命令以在一条命令中覆盖所有备份引用,但我建议仔细检查每一个。

结论

终于,新的历史是

$ git lola --name-status
* ab8cb1c (bar-feature) Add bar
| M     .gitignore
| A     bar.c
| * 43e5658 (master) Add foo
|/
|   A   foo.c
* 6469dab Commit 2
| A     module.c
* 47f9f73 Commit 1
| A     main.c
* 00c7780 (HEAD, new-root) Create .gitignore
  A     .gitignore

观察到所有目标文件都消失了。 bar-feature中对.gitignore的修改是因为我使用了不同的内容来确保它会被保留。为了完整性:

$ git diff new-root:.gitignore bar-feature:.gitignore
diff --git a/new-root:.gitignore b/bar-feature:.gitignore
index 5761abc..c395c62 100644
--- a/new-root:.gitignore
+++ b/bar-feature:.gitignore
@@ -1 +1,2 @@
 *.o
+*.obj

新根 ref 不再有用,所以用

处理掉它
$ git checkout master
$ git branch -d new-root

免责声明:这是理论上的(基于文档),我没有这样做。 克隆并尝试。

据我了解,您从未提交过现在会被您要添加到历史根目录的 .gitignore 过滤的文件。

因此,如果您将主分支变基到仅包含 .gitignore 的新根提交,您实际上不会修改提交的内容,之后您应该能够变基任何和所有其他分支您已经进行了新的提交,rebase 将为您完成工作。

因为commit的内容是一样的,patch ID应该是一样的,rebase只会应用必要的。

尽管您需要一个接一个地变基每个分支,但这很容易编写脚本。

可以在 in the git rebase documentation 部分找到更多信息: 正在从页面末尾的 UPSTREAM REBASE 中恢复。

编辑:好的,没关系,经过测试但不能完全以这种方式工作。您必须为新历史中的每个分支提供变基点 "manually",这很痛苦。 仍然可以工作,但它显然是一个比接受的答案更糟糕的解决方案。