【问题标题】:Insert a commit before the root commit in Git?在 Git 的根提交之前插入一个提交?
【发布时间】:2010-10-13 07:50:03
【问题描述】:

我之前问过如何在 git 存储库中squash the first two commits

虽然这些解决方案相当有趣,并且不像 git 中的其他一些东西那样令人费解,但如果您需要在项目开发过程中多次重复该过程,它们仍然会有点伤脑筋.

所以,我宁愿只经历一次痛苦,然后就可以永远使用标准的交互式变基。

然后,我想要做的是有一个空的初始提交,它的存在只是为了成为第一个。没有代码,什么都没有。只是占用空间,所以它可以成为 rebase 的基础。

那么我的问题是,如果有一个现有的存储库,我该如何在第一个提交之前插入一个新的空提交,然后让其他所有人都前进?

【问题讨论】:

  • ;) 我想无论如何它都值得回答。我正在探索通过痴迷于编辑历史而发疯的多种方式。别担心,不是共享存储库。
  • 从一位痴迷、疯狂的历史编辑到另一位,感谢您发布问题! ;D
  • 在@kch 的辩护中,我发现自己处于一个完全合理的原因:添加一个从未在回购中捕获的历史版本的快照。
  • 我还有另一个正当理由!在第一个提交之前添加一个空提交,以便能够重新定位到第一个提交并删除在存储库的初始提交中添加的二进制膨胀(:

标签: git version-control rebase git-rebase


【解决方案1】:

有两个步骤可以实现:

  1. 创建一个新的空提交
  2. 重写历史以从这个空提交开始

为方便起见,我们会将新的空提交放在临时分支 newroot

1。创建一个新的空提交

有很多方法可以做到这一点。

仅使用管道

最干净的方法是使用 Git 的管道直接创建一个提交,这样可以避免接触工作副本或索引或签出哪个分支等。

  1. 为一个空目录创建一个树对象:

    tree=`git hash-object -wt tree --stdin < /dev/null`
    
  2. 围绕它进行提交:

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. 创建对它的引用:

    git branch newroot $commit
    

如果你对你的 shell 足够了解,你当然可以将整个过程重新安排成一行。

没有管道

使用常规瓷器命令,如果没有检查newroot 分支并重复更新索引和工作副本,您将无法创建空提交,没有充分的理由。但有些人可能会觉得这更容易理解:

git checkout --orphan newroot
git rm -rf .
git clean -fd
git commit --allow-empty -m 'root commit'

请注意,在缺少 --orphan 切换到 checkout 的非常旧版本的 Git 上,您必须将第一行替换为:

git symbolic-ref HEAD refs/heads/newroot

2。重写历史以从这个空提交开始

你有两个选择:变基,或干净的历史重写。

变基

git rebase --onto newroot --root master

这具有简单的优点。但是,它还会在分支上的每个最后一次提交时更新提交者名称和日期。

此外,对于一些极端情况历史,它甚至可能由于合并冲突而失败 - 尽管事实上您正在基于一个不包含任何内容的提交。

历史改写

更简洁的方法是重写分支。与git rebase 不同,您需要查找您的分支从哪个提交开始:

git replace <currentroot> --graft newroot
git filter-branch master

显然,重写发生在第二步;这是需要解释的第一步。 git replace 的作用是告诉 Git,每当它看到对要替换的对象的引用时,Git 应该查看该对象的替换。

使用--graft 开关,您告诉它的内容与正常情况略有不同。您是说还没有替换对象,但是您想用自己的精确副本替换&lt;currentroot&gt; 提交对象除了替换的父提交应该是那个(s) 您列出的(即newroot 提交)。然后git replace 继续为您创建此提交,然后声明该提交作为您原始提交的替换。

现在,如果您执行git log,您会看到事情已经如您所愿:分支从newroot 开始。

但是,请注意git replace 实际上并没有修改历史记录——它也不会传播到您的存储库之外。它只是将本地重定向添加到您的存储库,从一个对象到另一个对象。这意味着没有其他人看到这种替换的效果——只有你。

这就是为什么filter-branch 步骤是必要的。使用git replace,您可以创建一个精确的副本,其中包含为根提交调整过的父提交; git filter-branch 然后对所有后续提交也重复此过程。这就是历史真正被重写的地方,以便您可以分享它。

【讨论】:

  • --onto newroot 选项是多余的;你可以不用它,因为你传递的参数newroot与上游参数相同——newroot
  • 为什么不使用瓷器代替管道命令?我会用 git checkout --orphan newroot 替换 git symbolic-ref HEAD refs/heads/newroot
  • @nenopera:因为这个答案是在git-checkout 有那个开关之前写的。我已经更新它以首先提到该方法,感谢您的指点。
  • 如果您的新根不为空,请使用git rebase --merge -s recursive -X theirs --onto newroot --root master 自动解决所有冲突(请参阅this 答案)。 @AlexanderKuzin
  • @Geremia 您只能修改最后一次提交,因此如果您的存储库仅包含根提交,它可能会起作用,否则您将不得不在修改后的根之上重新设置 repo 中的所有其他提交无论如何都要提交。但即便如此,该主题也暗示您不想更改根提交,而是想在现有根之前插入另一个。
【解决方案2】:

合并亚里士多德 Pagaltzis 和 Uwe Kleine-König 的答案和 Richard Bronosky 的评论。

git symbolic-ref HEAD refs/heads/newroot
git rm --cached -r .
git clean -f -d
# touch .gitignore && git add .gitignore # if necessary
git commit --allow-empty -m 'initial'
git rebase --onto newroot --root master
git branch -d newroot

(只是把所有东西放在一个地方)

【讨论】:

  • 这太棒了。如果这可以是 git rebase -i --root 在内部所做的,那就太好了。
  • 是的,我很惊讶地发现它没有。
  • 由于错误,我不得不将 rebase 命令更改为 git rebase newroot master
  • @antony-hatchkins 对此表示感谢。我有一个现有的 git repo 并且(出于各种原因,我不会在这里讨论)我正在尝试附加一个 NON-EMPTY git commit 作为我的第一个提交。所以我用 git add .; 替换了 git commit --allow-empty -m 'initial' git commit -m "初始 laravel 提交";混帐推;然后这个 rebase 步骤: git rebase --onto newroot --root master 因大量合并冲突而失败。有什么建议吗? :((
  • @kp123 尝试空提交 :)
【解决方案3】:

我喜欢亚里士多德的回答。但发现对于大型存储库(> 5000 次提交),filter-branch 比 rebase 效果更好,原因有几个 1)它更快 2)发生合并冲突时不需要人工干预。 3)它可以重写标签——保留它们。 请注意,filter-branch 之所以有效,是因为每次提交的内容都没有问题——它与此 'rebase' 之前完全相同。

我的步骤是:

# first you need a new empty branch; let's call it `newroot`
git symbolic-ref HEAD refs/heads/newroot
git rm --cached -r .
git clean -f -d

# then you apply the same steps
git commit --allow-empty -m 'root commit'

# then use filter-branch to rebase everything on newroot
git filter-branch --parent-filter 'sed "s/^\$/-p <sha of newroot>/"' --tag-name-filter cat master

请注意,“--tag-name-filter cat”选项意味着将重写标签以指向新创建的提交。

【讨论】:

  • 这无助于创建非空提交,这也是一个有趣的用例。
  • 与其他解决方案相比,您的解决方案只有一个微不足道的副作用:它改变了哈希值,但整个历史保持不变。谢谢!
【解决方案4】:

我认为使用git replacegit filter-branch 是比使用git rebase 更好的解决方案:

  • 更好的性能
  • 更简单,风险更小(您可以在每个步骤中验证您的结果并撤消您所做的...)
  • 与多个分支一起工作并保证结果

其背后的想法是:

  1. 在很久以前创建一个新的空提交
  2. 用一个完全相似的提交替换旧的根提交,除了新的根提交被添加为父提交
  3. 验证一切都按预期运行并运行git filter-branch
  4. 再次确认一切正常并清理不再需要的 git 文件

这是前两个步骤的脚本:

#!/bin/bash
root_commit_sha=$(git rev-list --max-parents=0 HEAD)
git checkout --force --orphan new-root
find . -path ./.git -prune -o -exec rm -rf {} \; 2> /dev/null
git add -A
GIT_COMMITTER_DATE="2000-01-01T12:00:00" git commit --date==2000-01-01T12:00:00 --allow-empty -m "empty root commit"
new_root_commit_sha=$(git rev-parse HEAD)

echo "The commit '$new_root_commit_sha' will be added before existing root commit '$root_commit_sha'..."

parent="parent $new_root_commit_sha"
replacement_commit=$(
 git cat-file commit $root_commit_sha | sed "s/author/$parent\nauthor/" |
 git hash-object -t commit -w --stdin
) || return 3
git replace "$root_commit_sha" "$replacement_commit"

您可以毫无风险地运行此脚本(即使在执行您以前从未做过的操作之前进行备份也是一个好主意;)),如果结果不是预期的,只需删除在文件夹 @ 中创建的文件987654326@ 再试一次 ;)

一旦您确认存储库的状态符合您的预期,请运行以下命令来更新所有分支的历史记录:

git filter-branch -- --all

现在,您必须查看 2 个历史记录,旧的和新的(有关更多信息,请参阅 filter-branch 上的帮助)。您可以比较 2 并再次检查是否一切正常。如果您满意,请删除不再需要的文件:

rm -rf ./.git/refs/original
rm -rf ./.git/refs/replace

您可以返回您的master 分支并删除临时分支:

git checkout master
git branch -D new-root

现在,一切都应该完成;)

【讨论】:

    【解决方案5】:

    如果您忘记在“git init”之后立即创建一个空提交,则在存储库的开头添加一个空提交:

    git rebase --root --onto $(git commit-tree -m 'Initial commit (empty)' 4b825dc642cb6eb9a060e54bf8d69288fbee4904)
    

    【讨论】:

    【解决方案6】:

    我成功地使用了亚里士多德和肯特的答案:

    # first you need a new empty branch; let's call it `newroot`
    git checkout --orphan newroot
    git rm -rf .
    git commit --allow-empty -m 'root commit'
    git filter-branch --parent-filter \
    'sed "s/^\$/-p <sha of newroot>/"' --tag-name-filter cat -- --all
    # clean up
    git checkout master
    git branch -D newroot
    # make sure your branches are OK first before this...
    git for-each-ref --format="%(refname)" refs/original/ | \
    xargs -n 1 git update-ref -d
    

    除了标签之外,这还将重写所有分支(不仅仅是master)。

    【讨论】:

    • 最后一行是做什么的?
    • 它搜索refs/original/ 并删除每个引用。它删除的 refs 应该已经被其他分支引用了,所以它们并没有真正消失,只是 refs/original/ 被删除了。
    • 这对我有用。此外,我使用timedatectl set-time '2017-01-01 00:00:00'newroot 一个旧时间戳。
    【解决方案7】:

    git rebase --root --onto $emptyrootcommit

    应该很容易做到这一点

    【讨论】:

    • $emptyrootcommit 是一个扩展为空的 shell 变量,确定吗?
    • @Flimm:$emptyrootcommit 是原始发布者似乎已经拥有的空提交的 sha1。
    【解决方案8】:

    我很兴奋并为这个漂亮的脚本编写了一个“幂等”版本......它总是会插入相同的空提交,如果你运行它两次,它不会每次都改变你的提交哈希。所以,这是我对 git-insert-empty-root 的看法:

    #!/bin/sh -ev
    # idempotence achieved!
    tmp_branch=__tmp_empty_root
    git symbolic-ref HEAD refs/heads/$tmp_branch
    git rm --cached -r . || true
    git clean -f -d
    touch -d '1970-01-01 UTC' .
    GIT_COMMITTER_DATE='1970-01-01T00:00:00 +0000' git commit \
      --date='1970-01-01T00:00:00 +0000' --allow-empty -m 'initial'
    git rebase --committer-date-is-author-date --onto $tmp_branch --root master
    git branch -d $tmp_branch
    

    额外的复杂性值得吗?也许不是,但我会用这个。

    这也应该允许对 repo 的多个克隆副本执行此操作,并最终得到相同的结果,因此它们仍然兼容...测试...是的,可以,但还需要删除并再次添加您的遥控器,例如:

    git remote rm origin
    git remote add --track master user@host:path/to/repo
    

    【讨论】:

      【解决方案9】:

      切换根提交:

      首先,创建您想要的提交作为第一个。

      其次,使用以下命令切换提交的顺序:

      git rebase -i --root

      一个编辑器将与提交一起出现,直到根提交,例如:

      挑选 1234 条旧根消息

      pick 0294 中间的提交

      选择要放在根目录的 5678 提交

      然后你可以把你想要的提交放在第一行,把它放在第一行。在示例中:

      选择要放在根目录的 5678 提交

      挑选 1234 条旧根消息

      pick 0294 中间的提交

      退出编辑器,提交顺序将发生变化。

      PS:要更改 git 使用的编辑器,请运行:

      git config --global core.editor name_of_the_editor_program_you_want_to_use

      【讨论】:

      • 现在 rebase 有了 --root,这是迄今为止最简洁的解决方案。
      • 希望我能在页面下方阅读更多内容以第一次看到这一点。很好的答案!
      【解决方案10】:

      好吧,这就是我想出的:

      # Just setting variables on top for clarity.
      # Set this to the path to your original repository.
      ORIGINAL_REPO=/path/to/original/repository
      
      # Create a new repository…
      mkdir fun
      cd fun
      git init
      # …and add an initial empty commit to it
      git commit --allow-empty -m "The first evil."
      
      # Add the original repository as a remote
      git remote add previous $ORIGINAL_REPO
      git fetch previous
      
      # Get the hash for the first commit in the original repository
      FIRST=`git log previous/master --pretty=format:%H  --reverse | head -1`
      # Cherry-pick it
      git cherry-pick $FIRST
      # Then rebase the remainder of the original branch on top of the newly 
      # cherry-picked, previously first commit, which is happily the second 
      # on this branch, right after the empty one.
      git rebase --onto master master previous/master
      
      # rebase --onto leaves your head detached, I don't really know why)
      # So now you overwrite your master branch with the newly rebased tree.
      # You're now kinda done.
      git branch -f master
      git checkout master
      # But do clean up: remove the remote, you don't need it anymore
      git remote rm previous
      

      【讨论】:

        【解决方案11】:

        这是我的bash 脚本,基于Kent 的改进答案:

        • 完成后,它会检查原始分支,而不仅仅是 master
        • 我试图避免临时分支,但git checkout --orphan 仅适用于分支,而不是 detached-head 状态,因此它已签出足够长的时间以进行新的根提交,然后将其删除;
        • 它在filter-branch 期间使用新根提交的哈希(Kent 在其中留了一个占位符用于手动替换);
        • filter-branch 操作只重写本地分支,而不是远程分支
        • 作者和提交者元数据已标准化,因此根提交在各个存储库中是相同的。

        #!/bin/bash
        
        # Save the current branch so we can check it out again later
        INITIAL_BRANCH=`git symbolic-ref --short HEAD`
        TEMP_BRANCH='newroot'
        
        # Create a new temporary branch at a new root, and remove everything from the tree
        git checkout --orphan "$TEMP_BRANCH"
        git rm -rf .
        
        # Commit this empty state with generic metadata that will not change - this should result in the same commit hash every time
        export GIT_AUTHOR_NAME='nobody'
        export GIT_AUTHOR_EMAIL='nobody@example.org'
        export GIT_AUTHOR_DATE='2000-01-01T00:00:00+0000'
        export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
        export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
        export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
        git commit --allow-empty -m 'empty root'
        NEWROOT=`git rev-parse HEAD`
        
        # Check out the commit we just made and delete the temporary branch
        git checkout --detach "$NEWROOT"
        git branch -D "$TEMP_BRANCH"
        
        # Rewrite all the local branches to insert the new root commit, delete the 
        # original/* branches left behind, and check out the rewritten initial branch
        git filter-branch --parent-filter "sed \"s/^\$/-p $NEWROOT/\"" --tag-name-filter cat -- --branches
        git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
        git checkout "$INITIAL_BRANCH"
        

        【讨论】:

          【解决方案12】:

          结合最新和最伟大的。无副作用,无冲突,保留标签。

          git log --reverse
          
          tree=`git hash-object -wt tree --stdin < /dev/null`
          commit=`git commit-tree -m 'Initialize empty repository' $tree`
          echo $commit # copy below, interpolation didn't work for me
          
          git filter-branch --parent-filter 'sed "s/^\$/-p <commit>/"' --tag-name-filter cat master
          
          git log --reverse
          

          请注意,在 GitHub 上,您将丢失 CI 运行数据,并且 PR 可能会搞砸,除非其他分支也得到修复。

          【讨论】:

            【解决方案13】:

            按照 Aristotle Pagaltzis 和其他人的回答,但使用更简单的命令

            zsh% git checkout --orphan empty     
            Switched to a new branch 'empty'
            zsh% git rm --cached -r .
            zsh% git clean -fdx
            zsh% git commit --allow-empty -m 'initial empty commit'
            [empty (root-commit) 64ea894] initial empty commit
            zsh% git checkout master
            Switched to branch 'master'
            zsh% git rebase empty
            First, rewinding head to replay your work on top of it...
            zsh% git branch -d empty 
            Deleted branch empty (was 64ea894).
            

            请注意,您的存储库不应包含任何等待提交的本地修改。
            注意 git checkout --orphan 将适用于新版本的 git,我猜。
            请注意,git status 大部分时间都会提供有用的提示。

            【讨论】:

              【解决方案14】:

              启动一个新的存储库。

              将您的日期设置回您想要的开始日期。

              按照您希望的方式做所有事情,调整系统时间以反映您希望以这种方式完成的时间。根据需要从现有存储库中提取文件以避免大量不必要的输入。

              到了今天,交换存储库就大功告成了。

              如果你只是疯狂(成熟)但相当聪明(可能是因为你必须有一定的聪明才智才能想出像这样的疯狂想法),你会编写这个过程。

              当您决定希望过去一周后以其他方式发生时,这也会变得更好。

              【讨论】:

              • 我对一个需要你搞乱系统日期的解决方案有不好的感觉,但你确实给了我一个想法,我开发了一点,唉,它奏效了。所以,谢谢。
              【解决方案15】:

              我知道这篇文章很旧,但是当谷歌搜索“inserting commit git”时,这个页面是第一个。

              为什么要把简单的事情复杂化?

              你有 A-B-C,你想要 A-B-Z-C。

              1. git rebase -i trunk(或 B 之前的任何内容)
              2. 在 B 行更改选择以编辑
              3. 进行更改:git add ..
              4. git commitgit commit --amend 将编辑 B 而不是创建 Z)

              [您可以在此处创建任意数量的 git commit 以插入更多提交。当然,第 5 步你可能会遇到麻烦,但是用 git 解决合并冲突是你应该具备的技能。如果没有,请练习!]

              1. git rebase --continue

              很简单,不是吗?

              如果您理解git rebase,添加“根”提交应该不是问题。

              玩得开心!

              【讨论】:

              • 问题要求插入一个 first 提交:从 A-B-C 你想要 Z-A-B-C。直截了当的git rebase 无法做到这一点。
              猜你喜欢
              • 2017-09-09
              • 1970-01-01
              • 2013-05-21
              • 2019-10-27
              • 2013-12-24
              • 1970-01-01
              • 2014-10-29
              • 2023-02-20
              • 2011-07-03
              相关资源
              最近更新 更多