在Git中的根提交之前插入提交?


231

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

尽管解决方案相当有趣,并且不像git中的其他某些东西那样令人费解,但如果您需要在项目开发过程中多次重复该过程,那么它们仍然会给我们带来很多伤害。

因此,我宁愿只经历一次痛苦,然后才能永远使用标准的交互式基础。

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

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


3
;)我想无论如何它还是值得一个答案。我正在探索一种通过痴迷于编辑历史来疯狂的多种方式。不用担心,不是共享存储库。
2009年

11
从一位痴迷,疯狂的历史编辑者到另一位,感谢您发布问题!; D
Marco Marco

10
在@kch的辩护中,一个完全合理的理由是我发现自己的原因:添加从未在存储库中捕获的历史版本的快照。
Old McStopher

4
我还有另一个正当理由!在第一个提交之前添加一个空提交,以便能够重新定位到第一个提交并删除在存储库的初始提交中添加的二进制膨胀(:
pospi 2012年

Answers:


314

有两个步骤可以实现此目的:

  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'

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

git symbolic-ref HEAD refs/heads/newroot

2.重写历史记录以从此空提交开始

您在此处有两个选择:重新定基础或重写干净的历史记录。

变基

git rebase --onto newroot --root master

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

此外,对于某些极端情况,即使合并到一个不包含任何内容的提交,它甚至可能由于合并冲突而失败。

历史重写

较干净的方法是重写分支。与with不同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然后对以下所有提交重复此过程。这实际上是历史记录被重写的地方,以便您可以共享它。


1
--onto newroot选项是多余的;您可以不用它,因为您传递的参数newroot与上游参数-相同newroot
威廉·泰勒,2010年

7
为什么不使用瓷器代替管道命令?我用git checkout替换了git symbolic-ref HEAD refs / heads / newroot --orphan newroot
albfan 2012年

4
@nenopera:因为此答案是在进行git-checkout此切换之前编写的。我已经对其进行了更新,以首先提到该方法,谢谢您的指导。
亚里斯多德·帕加尔兹

1
如果您的newroot不为空,请使用git rebase --merge -s recursive -X theirs --onto newroot --root master来自动解决所有冲突(请参阅答案)。@AlexanderKuzin
用户

1
@Geremia您只能修改最后的提交,因此,如果您的存储库仅包含根提交,则它可以工作,否则无论如何您都必须将所有其他提交重新存储在存储库中,以修改后的根提交为基础。但是即使如此,该主题仍暗示您不想更改根落实,而是想在现有根之前插入另一个。
用户

30

亚里士多德·帕加尔齐斯(Aristotle Pagaltzis)和UweKleine-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内部执行的操作,那就太好了。
aredridel

是的,我很惊讶地发现事实并非如此。
安东尼·哈奇金斯

git rebase newroot master由于错误,我不得不将rebase命令更改为。
marbel82 '19

@ antony-hatchkins对此表示感谢。我有一个现有的git repo,并且(出于各种原因(我不会在这里讨论)),我试图将NON-EMPTY git提交追加为我的第一次提交。所以我用git add替换了git commit --allow-empty -m'initial'; git commit -m“初始laravel提交”; git push; 然后执行此重新设置步骤:git rebase --onto newroot --root master失败,并出现大量合并冲突。有什么建议吗?:((
kp123

@ kp123尝试一个空的提交:)
安东尼·哈奇金斯

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


这无助于创建非空的提交,这也是一个有趣的用例。
ceztko 2015年

与其他解决方案相比,您的解决方案只有一个微不足道的副作用:它可以改变哈希值,但整个历史保持不变。谢谢!
弗拉迪斯拉夫·萨夫琴科

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


最后一行是做什么的?
Diederick C. Niehorster '16

它搜索refs/original/并删除每个引用。它删除的引用应该已经被其他分支引用,因此它们并没有真正消失,只是refs/original/被删除了。
ldav1s 2016年

这对我有用。另外,我曾经timedatectl set-time '2017-01-01 00:00:00'给过newroot一个旧的时间戳。
chrm


4

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

  • 更好的性能
  • 更轻松,风险更低(您可以在每个步骤中验证结果,然后撤消所做的操作...)
  • 与多个分支机构一起很好地工作并保证结果

其背后的想法是:

  • 过去创建一个新的空提交
  • 用完全相似的提交替换旧的根提交,除了将新的根提交作为父添加
  • 验证所有操作均符合预期并运行 git filter-branch
  • 再次确认一切正常,并清除不再需要的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"

您可以毫无风险地运行该脚本(即使在执行之前从未做过的操作之前进行备份是个好主意;)),如果结果不是预期的,则只需删除在文件夹中创建的文件,.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

现在,所有步骤都应该完成;)


3

我很兴奋,并为这个漂亮的脚本编写了一个“幂等”版本……它将始终插入相同的空提交,并且如果您运行两次,它不会每次都更改您的提交哈希。所以,这是我对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

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

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

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

3

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

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

1
4b825dc ......是空树的哈希:stackoverflow.com/questions/9765453/...
mrks

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

2

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

  • 它签出原始分支,而不仅仅是 master
  • 我试图避开临时分支,但是 git checkout --orphan仅适用于分支,而不适用于分离头状态,因此已签出足够长的时间以进行新的根提交,然后将其删除。
  • 它使用新根提交的哈希值 filter-branch(肯特在此处保留了一个占位符以进行手动替换);
  • 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"

2

要切换根提交:

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

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

git rebase -i --root

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

选择1234旧根消息

选择0294在中间提交

选择5678提交您想放在根

然后,可以将所需的提交放在第一行,将其放在第一位。在示例中:

选择5678提交您想放在根

选择1234旧根消息

选择0294在中间提交

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

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

git config --global core.editor name_of_the_editor_program_you_want_to_use


1
现在,该基础已经有了--root,这是迄今为止最简洁的解决方案。
Ross Burton

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可能会混乱,除非还修复了其他分支。


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

请注意,您的存储库不应包含任何等待提交的本地修改。我想
注意git checkout --orphan将在新版本的git上工作。
注意大多数时候都git status给出有用的提示。


-6

启动一个新的存储库。

将日期设置回所需的开始日期。

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

到今天为止,交换存储库就可以了。

如果您只是疯狂(已确立)但相当聪明(可能是因为您必须具有一定数量的聪明才智来思考这样的疯狂想法),则可以编写该过程的脚本。

当您决定希望过去的某个星期从现在开始发生时,这也将变得更好。


我对需要您弄乱系统日期的解决方案有不好的感觉,但是您确实给了我一个主意,我开发了一个主意,and,它确实有效。那谢谢啦。
kch

-7

我知道这篇文章很旧,但是在谷歌搜索“插入commit 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,添加“ root”提交应该不是问题。

玩得开心!


5
该问题要求插入第一次提交:从ABC您要ZABC。一个简单的人git rebase不能做到这一点。
Petr Viktorin
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.