我曾经问过如何压缩git存储库中的前两次提交。

虽然这些解决方案相当有趣,而且不像git中的其他一些东西那样令人费解,但如果您需要在项目开发过程中多次重复这个过程,那么它们仍然是一个众所周知的伤害。

所以,我宁愿只经历一次痛苦,然后能够永远使用标准的交互式rebase。

那么,我想做的是有一个空的初始提交,它的存在仅仅是为了成为第一个。没有代码,什么都没有。只是占地方,这样就可以做地基了。

我的问题是,有了一个现有的存储库,我如何在第一个提交之前插入一个新的空提交,并将其他所有人向前移动?


当前回答

实现这一目标有2个步骤:

创建一个新的空提交 重写历史,从这个空提交开始

为了方便起见,我们将把新的空提交放在一个临时分支newroot上。

1. 创建一个新的空提交

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

只使用管道

最简洁的方法是使用Git的管道直接创建一个提交,这避免了触及工作副本、索引或检出哪个分支等。

为空目录创建一个树对象: Tree = ' git hash-object -wt Tree——stdin < /dev/null ' 在它周围包装一个提交: 提交= ' git Commit -tree -m '根提交' $tree ' 创建对它的引用: Git分支newroot $commit

当然,如果您足够了解shell,您可以将整个过程重新排列为一行程序。

没有管道

使用常规的瓷器命令,如果不签出新根分支并重复更新索引和工作副本,就不能创建空提交。但有些人可能会觉得这更容易理解:

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

注意,在非常旧的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都应该查看该对象的替换。

通过这个-嫁接开关,你会告诉它一些与正常情况略有不同的东西。你说的是还没有替换对象,但你想要替换<currentroot>提交对象与自己的精确副本,除了替换的父提交应该是你列出的(即newroot提交)。然后git replace继续为你创建这个commit,然后声明这个commit替换了你原来的commit。

现在如果你做一个git日志,你会看到事情已经像你想要的那样:分支从newroot开始。

但是,请注意,git replace实际上并不修改历史记录——它也不会传播到存储库之外。它只是在存储库中添加一个从一个对象到另一个对象的本地重定向。这意味着没有人能看到这种替代的影响——只有你自己。

这就是为什么过滤器分支步骤是必要的。使用git replace,你可以创建一个精确的副本,调整了根提交的父提交;然后,Git过滤器分支对接下来的所有提交也重复这个过程。这就是历史被改写的地方,这样你就可以分享它。

其他回答

合并亚里士多德Pagaltzis和Uwe Kleine-König的答案和理查德布罗诺斯基的评论。

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

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

遵循亚里士多德和其他人的回答,但使用更简单的命令

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检出——孤儿将在新版本的git中工作,我猜。 注意,大多数时候git状态给出了有用的提示。

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

# 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
# pre- git 2.28...
git checkout master
# or git 2.28 and later...
git checkout $(git config --get init.defaultBranch)
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或init.defaultBranch)。

实现这一目标有2个步骤:

创建一个新的空提交 重写历史,从这个空提交开始

为了方便起见,我们将把新的空提交放在一个临时分支newroot上。

1. 创建一个新的空提交

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

只使用管道

最简洁的方法是使用Git的管道直接创建一个提交,这避免了触及工作副本、索引或检出哪个分支等。

为空目录创建一个树对象: Tree = ' git hash-object -wt Tree——stdin < /dev/null ' 在它周围包装一个提交: 提交= ' git Commit -tree -m '根提交' $tree ' 创建对它的引用: Git分支newroot $commit

当然,如果您足够了解shell,您可以将整个过程重新排列为一行程序。

没有管道

使用常规的瓷器命令,如果不签出新根分支并重复更新索引和工作副本,就不能创建空提交。但有些人可能会觉得这更容易理解:

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

注意,在非常旧的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都应该查看该对象的替换。

通过这个-嫁接开关,你会告诉它一些与正常情况略有不同的东西。你说的是还没有替换对象,但你想要替换<currentroot>提交对象与自己的精确副本,除了替换的父提交应该是你列出的(即newroot提交)。然后git replace继续为你创建这个commit,然后声明这个commit替换了你原来的commit。

现在如果你做一个git日志,你会看到事情已经像你想要的那样:分支从newroot开始。

但是,请注意,git replace实际上并不修改历史记录——它也不会传播到存储库之外。它只是在存储库中添加一个从一个对象到另一个对象的本地重定向。这意味着没有人能看到这种替代的影响——只有你自己。

这就是为什么过滤器分支步骤是必要的。使用git replace,你可以创建一个精确的副本,调整了根提交的父提交;然后,Git过滤器分支对接下来的所有提交也重复这个过程。这就是历史被改写的地方,这样你就可以分享它。

好吧,这是我想到的:

# 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