263

我之前问过如何压缩 git 存储库中的前两个提交

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

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

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

那么我的问题是,拥有一个现有的存储库,我该如何在第一个提交之前插入一个新的空提交,并将其他所有人向前推进?

4

15 回答 15

371

有两个步骤来实现这一点:

  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
    

如果您足够了解您的外壳,您当然可以将整个过程重新安排成一个单行程序。

没有管道

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

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

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

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

现在,如果您执行 a git log,您将看到事情已经如您所愿:分支从newroot.

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

这就是为什么该filter-branch步骤是必要的。随着git replace您为根提交创建具有调整的父提交的精确副本;git filter-branch然后对所有以下提交也重复此过程。那是历史实际上被重写的地方,以便您可以分享它。

于 2009-03-15T07:45:24.160 回答
34

合并亚里士多德 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

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

于 2012-03-16T11:02:53.263 回答
12

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

我的步骤是:

# 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”选项意味着将重写标签以指向新创建的提交。

于 2013-03-29T15:55:35.817 回答
7

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

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

其背后的想法是:

  1. 在过去创建一个新的空提交
  2. 用一个完全相似的提交替换旧的根提交,除了新的根提交被添加为父提交
  3. 验证一切是否符合预期并运行git filter-branch
  4. 再次验证一切正常并清理不再需要的 git 文件

这是前 2 个步骤的脚本:

#!/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"

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

一旦您验证了存储库的状态是您所期望的,运行以下命令来更新所有分支的历史记录:

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

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

于 2015-05-31T13:57:10.720 回答
7

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

git rebase --root --onto $(git commit-tree -m 'Initial commit (empty)' 4b825dc642cb6eb9a060e54bf8d69288fbee4904)
于 2017-12-09T21:00:41.557 回答
5

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

# 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除了标签之外,这还将重写所有分支(不仅仅是)。

于 2014-04-17T22:44:10.883 回答
4

git rebase --root --onto $emptyrootcommit

应该很容易做到这一点

于 2011-01-17T19:27:47.877 回答
4

我很兴奋并为这个漂亮的脚本写了一个“幂等”版本……它总是会插入相同的空提交,如果你运行它两次,它不会每次都改变你的提交哈希。所以,这是我对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
于 2013-02-06T12:28:56.370 回答
3

要切换根提交:

首先,首先创建您想要的提交。

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

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

于 2017-02-18T16:17:06.093 回答
2

好吧,这就是我想出的:

# 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
于 2009-03-14T08:08:44.007 回答
2

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

  • 完成后,它会检查原始分支,而不仅仅是master
  • 我试图避免临时分支,但git checkout --orphan仅适用于分支,而不是分离头状态,因此它已被签出足够长的时间以进行新的根提交,然后将其删除;
  • 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"
于 2015-05-07T17:29:29.667 回答
1

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

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 可能会搞砸,除非其他分支也得到修复。

于 2019-09-12T09:24:30.083 回答
0

遵循 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).

请注意,您的 repo 不应包含等待提交的本地修改。
我猜, Notegit checkout --orphan将适用于新版本的 git。
请注意,大多数情况下git status都会提供有用的提示。

于 2013-02-18T08:28:04.107 回答
-6

启动一个新的存储库。

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

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

到了今天,交换存储库就完成了。

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

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

于 2009-03-14T05:49:02.907 回答
-7

我知道这篇文章很旧,但这个页面是谷歌搜索“插入提交 git”时的第一个页面。

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

你有ABC,你想要ABZC。

  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,添加“根”提交应该不是问题。

玩得开心!

于 2011-06-27T09:40:24.530 回答