可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I\'ve asked before about how to squash the first two commits in 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.
So, I\'d rather go through pain only once, and then be able to forever use the standard interactive 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.
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?
回答1:
Mid-2017 answer
Creating a new completely empty commit with no side effects is probably best done by using Git’s plumbing directly. Doing it that way avoids any side effects: no touching the working copy or the index, no temporary branches to clean up, etc. So:
To create a commit, we need a directory tree for it, so we create an empty one first:
tree=`git hash-object -wt tree --stdin < /dev/null`
Now we can wrap a commit around it:
commit=`git commit-tree -m \'root commit\' $tree`
And now we can rebase onto that:
git rebase --onto $commit --root master
And that’s it. You can rearrange that whole thing into a one-liner if you know your shell well enough.
(N.B.: in practice I’d now use filter-branch
. Will edit that in later.)
Historical answer (referenced by other answers)
Here’s a cleaner implementation of the same solution, in that it works without the need to create an extra repository, futz around with remotes, and correct a detached head:
# first you need a new empty branch; let\'s call it `newroot`
git checkout --orphan newroot
git rm -rf .
# then you apply the same steps
git commit --allow-empty -m \'root commit\'
git rebase --onto newroot --root master
git branch -d newroot
Voila, you’ve ended up on master
with its history rewritten to include an empty root commit.
NB.: on old versions of Git that lack the --orphan
switch to checkout
, you need the plumbing to create an empty branch:
git symbolic-ref HEAD refs/heads/newroot
git rm --cached -r .
git clean -f -d
回答2:
Merge of Aristotle Pagaltzis\'s and Uwe Kleine-König\'s answers and Richard Bronosky\'s comment.
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)
回答3:
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\'.
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.
回答4:
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.
回答5:
git rebase --root --onto $emptyrootcommit
should do the trick easily
回答6:
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:
#!/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.:
git remote rm origin
git remote add --track master user@host:path/to/repo
回答7:
Well, here\'s what I came up with:
# 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
回答8:
I think that using git replace
and git filter-branch
is a better solution than using a git rebase
:
- better preformance
- 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
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 ;)
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:
rm -rf ./.git/refs/original
rm -rf ./.git/refs/replace
You could return to your master
branch and delete the temporary branch:
git checkout master
git branch -D new-root
Now, all should be done ;)
回答9:
Here\'s my bash
script based on Kent\'s answer with improvements:
- 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.
#!/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\"
回答10:
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
An editor will appear with the commits until the root commit, like:
pick 1234 old root message
pick 0294 A commit in the middle
pick 5678 commit you want to put at the root
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
pick 1234 old root message
pick 0294 A commit in the middle
Exit the editor the commit order will have changed.
PS: To change the editor git uses, run:
git config --global core.editor name_of_the_editor_program_you_want_to_use
回答11:
Here\'s a simple one-liner which can be used to add an empty commit at the start of a repository, if you forgot to create an empty commit immediately after \"git init\":
git rebase --root --onto $(git commit-tree -m \'Initial commit (empty)\' 4b825dc642cb6eb9a060e54bf8d69288fbee4904)
回答12:
Following answer Aristotle Pagaltzis and others but using more simple commands
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).
Note your repo shouldn\'t contain no local modifications waiting to be commited.
Note git checkout --orphan
will work at new versions of git, I guess.
Note most of the time git status
gives useful hints.
回答13:
Start a new repository.
Set your date back to the start date you want.
Do everything the way you wish you\'d done it, adjusting the system time to reflect when you\'d wished you\'d done it that way. Pull files from the existing repository as needed to avoid a lot of needless typing.
When you get to today, swap the repositories and you\'re done.
If you\'re just crazy (established) but reasonably intelligent (likely, because you have to have a certain amount of smarts to think up crazy ideas like this) you will script the process.
That will also make it nicer when you decide you want the past to have happened some other way a week from now.
回答14:
I know this post is old but this page is the first one when Googling \"inserting commit git\".
Why make simple things complicated?
You have A-B-C and you want A-B-Z-C.
git rebase -i trunk
(or anything before B)
- change pick to edit on the B line
- make your changes:
git add ..
git commit
(git commit --amend
which will edit B and not create Z)
[You can make as many git commit
as you want here to insert more commits. Of course, you may have troubles with step 5, but resolving merging conflict with git is a skill you should have. If not, practice!]
git rebase --continue
Simple, isn\'t it?
If you understand git rebase
, adding a \'root\' commit should not be a problem.
Have fun with git!