【问题标题】:Can I rewrite an entire git repository's history to include something we forgot?我可以重写整个 git 存储库的历史以包含我们忘记的内容吗?
【发布时间】:2015-01-13 17:26:18
【问题描述】:

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

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

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

我找到了关于将 master 重新定位到新的根提交的信息,这适用于 master,问题是 它使我们的功能分支在旧历史树上分离,它还重播了整个历史具有新的提交日期/时间。

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

【问题讨论】:

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


    【解决方案1】:

    您想要做的将涉及两个阶段:追溯添加具有合适.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 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,除非已经存在。

    在命令行上,被咒语为

    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 引用和解释的。
      • 使用git ls-files 检查现有的.gitignore,如果不存在,请指向新根目录中的那个。
    • 如果您有任何标签,它们将通过身份操作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
    

    新根引用不再有用,因此将其丢弃

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

    【讨论】:

    • 你是我的英雄!
    【解决方案2】:

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

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

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

    因为提交的内容是一样的,所以补丁 ID 应该保持不变,rebase 只会应用必要的。

    您需要一个一个地重新设置每个分支,但这很容易编写脚本。

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

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

    【讨论】:

    • 这是我首先尝试的方法,我遇到的问题是,一旦你将 master 重新定位到新的历史,特性分支就没有分歧点,所以你不能有效地重新定位特性分支因为您必须一一挑选旧历史的正确部分。
    • 您可以将任何分支重新定位到与旧历史中分支开始的提交对应的新提交上,前提是新历史中的所有提交具有相同的内容(也就是说除了在开头添加 .gitignore 之外,master 分支的 rebase 基本上是无操作的)
    • 无论如何,接受的答案是更好的方法。
    • @FélixCantournet 你所说的一切都不是空话。如果添加.gitignore,则内容不同;无论如何,提交时间戳不同;因此,所有提交 ID 不同。
    • @Jubobs 提交 ID != 补丁 ID。准备好精美的手册。我输入了参考。我还测试了我在上面的编辑和评论中所说的内容,它适用于非平凡的 200Mb 7 分支存储库。
    猜你喜欢
    • 2021-09-21
    • 1970-01-01
    • 2022-03-18
    • 2013-07-02
    • 2023-03-17
    • 2015-07-31
    • 2014-04-13
    • 2013-05-02
    • 1970-01-01
    相关资源
    最近更新 更多