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

标签 git git-rebase git-filter-branch git-rewrite-history

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

但是,一旦我们 merge/使用我们的任何旧功能分支,我们就会遇到一些极端的减速。稍微探索一下,我们发现由于 .gitignore 仅在我们查看其他提交时添加到 develop 分支,而没有将 develop merge 到它们中 git chuggs 因为它令人窒息分析我们所有的构建工件(二进制文件)等...因为这些旧分支没有 .gitignore 文件。

我们想要做的是使用 .gitignore 有效地插入一个新的根提交,这样它就会追溯填充到所有的头部/标签中。我们很乐意重写历史,我们的团队相对较小,因此让每个人都停止此操作并在历史重写完成后重新 pull 他们的存储库是没有问题的。

我找到了关于将 master rebase 到一个新的根提交上的信息,这对 master 有效,问题是它让我们的特性分支在旧的历史树上分离,它还会重播整个历史具有新的提交日期/时间。

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

最佳答案

你想要做的事情将涉及两个阶段:追溯添加一个带有合适的 .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 选项 Hook 在您的新根提交中。
    • eb215... 是旧根提交的完整 SHA,cf. git rev-parse eb215
  • --index-filter 选项有两部分:
    • 如上所述运行 git rm 会从整个树中删除任何匹配 *.o 的内容,因为 glob 模式是由 git 而不是 shell 引用和解释的。
    • 使用 git ls-files 检查现有的 .gitignore,如果不存在,指向 new-root 中的那个。
  • 如果您有任何标签,它们将通过标识操作 cat 进行映射。
  • 单独的 -- 终止选项,而 --all 是所有引用的简写。

您看到的输出类似于

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

new-root ref 不再有用,所以处理它

$ git checkout master
$ git branch -d new-root

关于git - 我可以重写整个 git 存储库的历史以包含我们忘记的东西吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27927933/

相关文章:

linux - 依次传递两个变量

git - 如何在 OpenShift 部署中指定除 master 之外的特定分支?

git rebase -i 提交标志?

git - git实现并发的加锁策略?

git - 单个存储库 (monorepo) 中多个项目的结构化发布(Git 流程)

visual-studio-code - 当 VSCode 设置为 Git 编辑器时中止交互式 rebase

git - 在不改变内容的情况下 rebase

git - 如何使用 index-filter & co 从 Git 仓库中提取一个具有提交历史的文件?

git - 提取 git 子目录,同时通过重命名保留历史记录

Git 无法推送一些引用?