在 Git 中的根提交之前插入提交?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/645450/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Insert a commit before the root commit in Git?
提问by kch
I've asked before about how to squash the first two commitsin a git repository.
While the solutions are rather interesting and not really as mind-warping as some other things in git, they're still a bit of the proverbial bag of hurt if you need to repeat the procedure many times along the development of your project.
虽然这些解决方案相当有趣并且不像 git 中的其他一些东西那样令人费解,但如果您需要在项目的开发过程中多次重复该过程,它们仍然是众所周知的伤害。
So, I'd rather go through pain only once, and then be able to forever use the standard interactive rebase.
所以,我宁愿只经历一次痛苦,然后就可以永远使用标准的交互式 rebase。
What I want to do, then, is to have an empty initial commit that exists solely for the purpose of being the first. No code, no nothing. Just taking up space so it can be the base for rebase.
那么,我想要做的是有一个空的初始提交,它的存在只是为了成为第一个。没有代码,什么都没有。只是占用空间,所以它可以成为 rebase 的基础。
My question then is, having an existing repository, how do I go about inserting a new, empty commit before the first one, and shifting everyone else forward?
那么我的问题是,拥有一个现有的存储库,我如何在第一个之前插入一个新的空提交,并将其他人转移到前面?
回答by Aristotle Pagaltzis
There are 2 steps to achieving this:
有两个步骤可以实现这一点:
- Create a new empty commit
- Rewrite history to start from this empty commit
- 创建一个新的空提交
- 重写历史以从此空提交开始
We'll put the new empty commit on a temporary branch newroot
for convenience.
newroot
为方便起见,我们会将新的空提交放在临时分支上。
1. Create a new empty commit
1. 创建一个新的空提交
There is a number of ways you can do this.
有多种方法可以做到这一点。
Using just plumbing
仅使用管道
The cleanest approach is to use Git's plumbing to just create a commit directly, which avoids touching the working copy or the index or which branch is checked out, etc.
最干净的方法是使用 Git 的管道直接创建一个提交,这样可以避免接触工作副本或索引或检出哪个分支等。
Create a tree object for an empty directory:
tree=`git hash-object -wt tree --stdin < /dev/null`
Wrap a commit around it:
commit=`git commit-tree -m 'root commit' $tree`
Create a reference to it:
git branch newroot $commit
为空目录创建一个树对象:
tree=`git hash-object -wt tree --stdin < /dev/null`
围绕它进行一次提交:
commit=`git commit-tree -m 'root commit' $tree`
创建对它的引用:
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:
使用普通的瓷器命令,你不能在没有检查newroot
分支并重复更新索引和工作副本的情况下创建空提交,没有充分的理由。但有些人可能会觉得这更容易理解:
git checkout --orphan newroot
git rm -rf .
git clean -fd
git commit --allow-empty -m 'root commit'
Note that on very old versions of Git that lack the --orphan
switch to checkout
, you have to replace the first line with this:
请注意,在缺少--orphan
切换到 的非常旧版本的 Git上checkout
,您必须将第一行替换为:
git symbolic-ref HEAD refs/heads/newroot
2. Rewrite history to start from this empty commit
2. 重写历史以从这个空提交开始
You have two options here: rebasing, or a clean history rewrite.
您在这里有两个选择:变基或干净的历史重写。
Rebasing
变基
git rebase --onto newroot --root master
This has the virtue of simplicity. However, it will also update the committer name and date on every last commit on the branch.
这具有简单的优点。但是,它还会在分支上的每次最后一次提交时更新提交者名称和日期。
Also, with some edge case histories, it may even fail due to merge conflicts – despite the fact that you are rebasing onto a commit that contains nothing.
此外,对于一些边缘案例历史,它甚至可能由于合并冲突而失败——尽管您正在重新建立一个不包含任何内容的提交。
History rewrite
历史改写
The cleaner approach is to rewrite the branch. Unlike with git rebase
, you will need to look up which commit your branch starts from:
更简洁的方法是重写分支。与 with 不同git rebase
,您需要查找您的分支从哪个提交开始:
git replace <currentroot> --graft newroot
git filter-branch master
The rewriting happens in the second step, obviously; it's the first step that needs explanation. What git replace
does is it tells Git that whenever it sees a reference to an object you want replaced, Git should instead look at the replacement of that object.
显然,重写发生在第二步;这是需要解释的第一步。什么git replace
做的是它告诉Git的,每当它看到您要更换的对象的引用,Git的应改为看替换该对象的。
With the --graft
switch, you are telling it something slightly different than normally. You are saying don't have a replacement object yet, but you want to replace the <currentroot>
commit object with an exact copy of itself exceptthe parent commit(s) of the replacement should be the one(s) that you listed (i.e. the newroot
commit). Then git replace
goes ahead and creates this commit for you, and then declares that commit as the replacement for your original commit.
使用--graft
开关,你告诉它一些与平常略有不同的东西。您是说还没有替换对象,但是您想<currentroot>
用自身的精确副本替换提交对象,除了替换的父提交应该是您列出的那个(即newroot
提交) )。然后git replace
继续为您创建此提交,然后将该提交声明为原始提交的替换。
Now if you do a git log
, you will see that things already look as you want them to: the branch starts from newroot
.
现在,如果您执行 a git log
,您将看到事情已经如您所愿:分支从newroot
.
However, note that git replace
does not actually modify history– nor does it propagate out of your repository. It merely adds a local redirect to your repository from one object to another. What this means is that nobody else sees the effect of this replacement – only you.
但是,请注意,git replace
它实际上并没有修改历史记录,也不会从您的存储库中传播出去。它只是向您的存储库添加从一个对象到另一个对象的本地重定向。这意味着没有其他人看到这种替换的效果——只有你。
That's why the filter-branch
step is necessary. With git replace
you create an exact copy with adjusted parent commits for the root commit; git filter-branch
then repeats this process for all the following commits as well. That is where history actually gets rewritten so that you can share it.
这就是为什么这filter-branch
一步是必要的。随着git replace
你的根调整父提交创建一个完全相同的副本提交; git filter-branch
然后对所有以下提交重复此过程。这就是历史实际上被重写的地方,以便您可以共享它。
回答by Antony Hatchkins
Merge of Aristotle Pagaltzis's and Uwe Kleine-K?nig's answers and Richard Bronosky's comment.
合并亚里士多德 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
(just to put everything in one place)
(只是为了把所有东西放在一个地方)
回答by Kent
I like Aristotle's answer. But found that for a large repository (>5000 commits) filter-branch works better than rebase for several reasons 1) it's faster 2) it doesn't require human intervention when there's a merge conflict. 3) it can rewrite the tags -- preserving them. Note that filter-branch works because there is no question about the contents of each commit -- it is exactly the same as before this 'rebase'.
我喜欢亚里士多德的回答。但是发现对于大型存储库(> 5000 次提交),filter-branch 比 rebase 效果更好,原因有以下几个:1)它更快 2)当出现合并冲突时不需要人工干预。3)它可以重写标签——保留它们。请注意,filter-branch 有效,因为对每个提交的内容没有任何疑问——它与此“rebase”之前的完全相同。
My steps are:
我的步骤是:
# 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
Note that the '--tag-name-filter cat' options means that tags will be rewritten to point to the newly created commits.
请注意,'--tag-name-filter cat' 选项意味着标签将被重写以指向新创建的提交。
回答by ldav1s
I used pieces of Aristotle's and Kent's answer successfully:
我成功地使用了亚里士多德和肯特的答案:
# 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
This will also rewrite all branches (not just master
) in addition to tags.
master
除了标签之外,这还将重写所有分支(不仅仅是)。
回答by Uwe Kleine-K?nig
git rebase --root --onto $emptyrootcommit
git rebase --root --onto $emptyrootcommit
should do the trick easily
应该很容易做到这一点
回答by Philippe
I think that using git replace
and git filter-branch
is a better solution than using a git rebase
:
我认为使用git replace
andgit filter-branch
是比使用 a 更好的解决方案git rebase
:
- better performance
- easier and less risky (you could verify your result at each step and undo what you did...)
- work well with multiple branches with guaranteed results
- 更好的性能
- 更容易且风险更小(您可以在每一步验证您的结果并撤消您所做的......)
- 与保证结果的多个分支配合良好
The idea behind it is to:
其背后的想法是:
- Create a new empty commit far in the past
- Replace the old root commit by a commit exactly similar except that the new root commit is added as a parent
- Verify that all is as expected and run
git filter-branch
- Once again, verify that all is OK and clean the no more needed git files
- 在很久以前创建一个新的空提交
- 用完全相似的提交替换旧的根提交,除了新的根提交被添加为父提交
- 验证一切是否符合预期并运行
git filter-branch
- 再次验证一切正常并清理不再需要的 git 文件
Here is a script for the 2 first steps:
这是前两个步骤的脚本:
#!/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"
You could run this script without risk (even if doing a backup before doing action you never did before is a good idea ;) ), and if the result is not the one expected, just delete the files created in the folder .git/refs/replace
and try again ;)
您可以毫无风险地运行此脚本(即使在执行您以前从未做过的操作之前先进行备份是个好主意;)),如果结果不是预期的结果,只需删除文件夹中创建的文件,.git/refs/replace
然后重试; )
Once you have verified that the state of the repository is what you expect, run the following command to update the history of all branches:
确认存储库的状态符合您的预期后,运行以下命令以更新所有分支的历史记录:
git filter-branch -- --all
Now, you must see 2 histories, the old one and the new one (see help on filter-branch
for more information). You could compare the 2 and check again if all is OK. If you are satisfied, delete the no more needed files:
现在,您必须看到 2 个历史记录,旧的和新的(有关filter-branch
更多信息,请参阅帮助)。您可以比较 2 并再次检查是否一切正常。如果满意,请删除不再需要的文件:
rm -rf ./.git/refs/original
rm -rf ./.git/refs/replace
You could return to your master
branch and delete the temporary branch:
您可以返回您的master
分支并删除临时分支:
git checkout master
git branch -D new-root
Now, all should be done ;)
现在,一切都应该完成;)
回答by Rory
To add an empty commit at the start of a repository, if you forgot to create an empty commit immediately after "git init":
如果您忘记在“git init”之后立即创建空提交,则在存储库开头添加空提交:
git rebase --root --onto $(git commit-tree -m 'Initial commit (empty)' 4b825dc642cb6eb9a060e54bf8d69288fbee4904)
回答by Sam Watkins
I got excited and wrote an 'idempotent' version of this nice script ... it will always insert the same empty commit, and if you run it twice, it doesn't change your commit hashes each time. So, here's my take on git-insert-empty-root:
我很兴奋并编写了这个漂亮脚本的“幂等”版本......它总是会插入相同的空提交,如果你运行两次,它不会每次都改变你的提交哈希。所以,这是我对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
Is it worth the extra complexity? maybe not, but I will be using this one.
是否值得额外的复杂性?也许不是,但我会用这个。
This SHOULD also allow to perform this operation on several cloned copies of the repo, and end up with the same results, so they are still compatible ... testing ... yes it does, work, but need also to delete and add your remotes again, e.g.:
这也应该允许对 repo 的多个克隆副本执行此操作,并最终得到相同的结果,因此它们仍然兼容......测试......是的,它可以工作,但还需要删除并添加您的再次远程,例如:
git remote rm origin
git remote add --track master user@host:path/to/repo
回答by luxagen
Here's my bash
script based on Kent's answer with improvements:
这是我的bash
脚本,基于Kent的改进答案:
- it checks out the original branch, not just
master
, when done; - I tried to avoid the temporary branch, but
git checkout --orphan
only works with a branch, not detached-head state, so it's checked out long enough to make the new root commit and then deleted; - it uses the hash of the new root commit during the
filter-branch
(Kent left a placeholder in there for manual replacement); - the
filter-branch
operation rewrites only the local branches, not remotes too - the author and committer metadata is standardised so that the root commit is identical across repositories.
- 完成后,它会检查原始分支,而不仅仅是
master
; - 我试图避免临时分支,但
git checkout --orphan
只适用于一个分支,而不是 detached-head 状态,所以它被检出足够长的时间来使新的根提交然后删除; - 它在
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='[email protected]'
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"
回答by tigre200
To switch the root commit:
切换根提交:
First, create the commit you want as the first.
首先,创建您想要的提交作为第一个。
Second, switch the order of the commits using:
其次,使用以下命令切换提交的顺序:
git rebase -i --root
git rebase -i --root
An editor will appear with the commits until the root commit, like:
一个编辑器将与提交一起出现,直到根提交,例如:
pick 1234 old root message
选择 1234 旧根消息
pick 0294 A commit in the middle
选择 0294 中间的提交
pick 5678 commit you want to put at the root
选择要放在根目录的 5678 次提交
You can then put the commit you want first, by placing it in the first line. In the example:
然后你可以把你想要的提交放在第一行,把它放在第一行。在示例中:
pick 5678 commit you want to put at the root
选择要放在根目录的 5678 次提交
pick 1234 old root message
选择 1234 旧根消息
pick 0294 A commit in the middle
选择 0294 中间的提交
Exit the editor the commit order will have changed.
退出编辑器,提交顺序将发生变化。
PS: To change the editor git uses, run:
PS:要更改 git 使用的编辑器,请运行:
git config --global core.editor name_of_the_editor_program_you_want_to_use
git config --global core.editor name_of_the_editor_program_you_want_to_use