在Git的根提交之前插入一个提交?

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

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

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

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

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

65439 次浏览

启动一个新的存储库。

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

按照你希望的方式做每件事,调整系统时间以反映你希望那样做的时间。根据需要从现有存储库中提取文件,以避免大量不必要的输入。

当您到达今天时,交换存储库,您就完成了。

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

当你决定让过去的事情从现在开始以另一种方式发生时,这也会让事情变得更好。

好吧,这是我想到的:

# 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

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

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

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

1. 创建一个新的空提交

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

只使用管道

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

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

    tree=`git hash-object -wt tree --stdin < /dev/null`
    
  2. Wrap a commit around it:

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. Create a reference to it:

    git branch newroot $commit
    

You can of course rearrange the whole procedure into a one-liner if you know your shell well enough.

Without plumbing

With regular porcelain commands, you cannot create an empty commit without checking out the newroot branch and updating the index and working copy repeatedly, for no good reason. But some may find this easier to understand:

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

注意,在非常旧的Git版本中,缺少--orphancheckout的开关,你必须将第一行替换为:

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继续为你创建这个提交,然后声明这个提交作为你原始提交的替换。

现在如果你执行git log,你会看到事情看起来已经像你想要的那样:分支从newroot开始。

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

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

git rebase --root --onto $emptyrootcommit

应该很容易做到吗

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

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

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

  1. git rebase -i trunk(或B之前的任何值)
  2. 更改pick在B行上编辑
  3. 进行更改:git add ..
  4. git commit(将编辑B而不创建Z的git commit --amend)

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

  1. git rebase --continue

很简单,不是吗?

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

玩得开心!

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

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

我很兴奋,写了一个“幂等”版本的这个不错的脚本…它总是插入相同的空提交,如果你运行它两次,它不会每次都改变你的提交哈希值。所以,下面是我对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

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

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给出了有用的提示

我喜欢亚里士多德的答案。但我发现对于大型存储库(>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'选项意味着标签将被重写以指向新创建的提交。

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

# 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

这也将重写标签之外的所有分支(不仅仅是masterinit.defaultBranch)。

下面是我的bash脚本,基于肯特的答案进行了改进:

  • 当完成时,它会检出原来的分支,而不仅仅是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"

我认为使用git replacegit filter-branch是一个比使用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

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

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

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

git checkout master
git branch -D new-root

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

切换根提交。

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

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

Git rebase -i—root

在根节点提交之前,会出现一个编辑器,比如:

选择1234旧根消息

在中间提交

选择你想放在根目录下的5678提交

然后,您可以将您想要的提交放在第一行中。在这个例子中:

选择你想放在根目录下的5678提交

选择1234旧根消息

在中间提交

退出编辑器,提交顺序将会改变。

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

Git配置——全局核心。编辑< em > name_of_the_editor_program_you_want_to_use < / em >

如果你忘记在"git init"之后立即创建一个空提交,那么在存储库的开始添加一个空提交:

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

结合最新和最好的。没有副作用,没有冲突,保持标签。

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