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

时间:2015-01-13 17:26:19

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

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

但是,只要我们加入/合作任何旧的功能分支,我们就会遇到一些极端的减速。稍微探索一下,我们发现,因为.gitignore只是在我们查看其他提交而没有合并时才添加到develop分支中,因此它会窒息,试图分析我们所有的构建工件(二进制文件)等...因为这些旧分支没有.gitignore文件。

我们想要做的是有效地插入一个带有.gitignore的新root提交,以便它可以追溯填充所有头文件/标签。我们很乐意重写历史记录,我们的团队规模相对较小,所以当历史重写完成后,让每个人都停止这个操作并重新拉出他们的存储库是没有问题的。

我发现有关将master重新设置为新的root提交的信息,这适用于master,问题是它使我们的功能分支在旧历史树上分离,它还会重播整个历史记录使用新的提交日期/时间。

任何想法或者我们在这个想法上运气不好吗?

2 个答案:

答案 0 :(得分:8)

您要执行的操作将涉及两个阶段:追溯添加具有合适.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是应该被忽略的编译目标文件。

在条形图功能分支上,您添加了一个合适的.gitignore和已删除的目标文件,这些文件不应该被跟踪,但您希望该政策反映在导入的任何位置。

请注意,git lolanon-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文档指出了新的孤儿分支可能出现意料之外的状态。

  

如果要启动记录一组与 start_point 完全不同的路径的断开连接的历史记录,则应在创建孤立分支后立即清除索引和工作树通过从工作树的顶层运行git rm -rf .。之后,您将准备好准备新文件,重新填充工作树,从其他地方复制它们,提取tarball等。

继续我们的例子:

$ 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,除非已存在。
  4. 在命令行上,显示为

    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而不是shell引用和解释的。
      • 检查现有.gitignoregit ls-files,如果不存在,请指向新根目录。
    • 如果您有任何标记,它们将使用身份操作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
    

    您的原件仍然安全。例如,主分支现在位于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
    

    新根参考不再有用,所以用

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

答案 1 :(得分:-1)

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

根据我的理解,您从未提交过的文件,而这些文件将被您想要添加到历史记录根目录的.gitignore过滤。

因此,如果您将主分支重新绑定到仅包含.gitignore的newroot提交,您实际上不会修改提交的内容,之后您应该能够重新设置任何和所有其他分支。在新的提交中,rebase将为你完成工作。

由于提交的内容相同,修补程序ID应保持不变,并且rebase仅应用必要的内容。

你需要逐个重新定义每个分支,但这很容易编写脚本。

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

编辑:好的,没关系,经过测试,并且不能完全按照这种方式工作。您必须“手动”为新历史记录中的每个分支指定rebase点,这很痛苦。 仍然可以使用,但它显然是一个比接受的答案更糟糕的解决方案。