Rewriting History
这是指修改提交历史。Git 的优点是可以让你尽量迟的做出一些决定。比如,你可以在提交时才决定stage中的哪些文件要进入提交,可以用stash命令保存当前工作(不必立即提交当前工作),也可以重写已有的提交。重写提交历史包括:改变提交的顺序,改变提交中的信息或修改文件,将提交压缩或是拆分,或完全地移除提交。
Many times, when working with Git, you may want to revise your commit history for some reason. One of the great things about Git is that it allows you to make decisions at the last possible moment. You can decide what files go into which commits right before you commit with the staging area, you can decide that you didn’t mean to be working on something yet with git stash, and you can rewrite commits that already happened so they look like they happened in a different way. This can involve changing the order of the commits, changing messages or modifying files in a commit, squashing together or splitting apart commits, or removing commits entirely – all before you share your work with others.
本节学习如何修改你自己的提交历史。
In this section, you’ll cover how to accomplish these very useful tasks so that you can make your commit history look the way you want before you share it with others.
注意:推送给别人后最好不要修改。
Note
One of the cardinal rules of Git is that, since so much work is local within your clone, you have a great deal of freedom to rewrite your history locally. However, once you push your work, it is a different story entirely, and you should consider pushed work as final unless you have good reason to change it. In short, you should avoid pushing your work until you’re happy with it and ready to share it with the rest of the world.
Changing the Last Commit
修改最近一次提交比较常见,其内容通常是修改“提交信息”和修改提交快照。
Changing your last commit is probably the most common rewriting of history that you’ll do. You’ll often want to do two basic things to your last commit: change the commit message, or change the snapshot you just recorded by adding, changing and removing files.
$ git commit --amend
这会打开文本编辑器,里面包含最近一次的“提交信息”。 保存、关闭后,你输入的内容就会替换该条提交信息。
如果提交后才发现忘了添加一个新创建的文件,你可以:先补充忘记了的操作,缓存到stage,然后 git commit --amend
就会替换先前的提交。
If you’ve committed and then you want to change the snapshot you committed by adding or changing files, possibly because you forgot to add a newly created file when you originally committed, the process works basically the same way. Make the changes you think you forgot, stage those changes, and the subsequent git commit --amend
replaces the previous commit with your new, improved commit.
使用这个技巧的时候需要小心:修正会改变提交的 SHA-1 校验和,它类似于一个小的变基。如果已经推送,就不要修正。
Changing Multiple Commit Messages
修改多个提交,则需要更复杂的工具。Git 本身并没有改变历史的工具,但可以变基,把一列提交变基到它们的HEAD上(注意,HEAD是当前分支-指针,所以也指向最近的一次提交)。命令 git rebase
后接 -i
选项,再加一个参数,就可以运行一个交互式的变基工具。这个参数告诉命令要变基到哪个提交。
To modify a commit that is farther back in your history, you must move to more complex tools. Git doesn’t have a modify-history tool, but you can use the rebase tool to rebase a series of commits onto the HEAD they were originally based on instead of moving them to another one. With the interactive rebase tool, you can then stop after each commit you want to modify and change the message, add files, or do whatever you wish. You can run rebase interactively by adding the -i
option to git rebase
. You must indicate how far back you want to rewrite commits by telling the command which commit to rebase onto.
例如,想要修改最近三次(或者其中某次)的提交信息,可以将 HEAD~2^
或
HEAD~3
作为参数传递给 git rebase -i
命令。注意,你要修改的是最近三次提交,参数则是它们的父提交:
For example, if you want to change the last three commit messages, or any of the commit messages in that group, you supply 【as an argument to git rebase -i
】 the parent of the last commit (you want to edit), which is HEAD~2^
or HEAD~3
. It may be easier to remember the ~3
because you’re trying to edit the last three commits, but keep in mind that you’re actually designating four commits ago, the parent(父对象) of the last commit you want to edit:
$ git rebase -i HEAD~3
再次强调:这是一个变基命令,在 HEAD~3..HEAD
范围内的所有提交都会被重写,而不管你是否修改提交中的信息。 不要涉及任何已经推送到中央服务器的提交——这样做会产生一次变更的两个版本,因而使他人困惑。
运行这个命令会给你一个脚本(在文本编辑器中),看起来像下面这样:
pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file
# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending 修订
# s, squash = use commit, but meld into previous commit 压扁=融合
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
注意,其中各提交的顺序,与 log
命令是相反的:
$ git log --pretty=format:"%h %s" HEAD~3..HEAD
a5f4a0d added cat-file
310154e updated README formatting and added blame
f7f3f6d changed my name a bit
脚本将会从 HEAD~3
开始,从上到下的依次重演每一个提交引入的修改。 它将最旧的而不是最新的列在上面,因为那会是第一个将要重演的。
Notice the reverse order. The interactive rebase gives you a script that it’s going to run. It will start at the commit you specify on the command line (HEAD~3) and replay the changes introduced in each of these commits from top to bottom. It lists the oldest at the top, rather than the newest, because that’s the first one it will replay.
你可以按需修改脚本:
You need to edit the script so that it stops at the commit you want to edit. To do so, change the word ‘pick’ to the word ‘edit’ for each of the commits you want the script to stop after. For example, to modify only the third commit message, you change the file to look like this:
edit f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file
当保存并退出编辑器时,Git 回到命令行并提示以下信息:
When you save and exit the editor, Git rewinds you back to the last(是否有误?) commit in that list and drops you on the command line with the following message:
$ git rebase -i HEAD~3
Stopped at f7f3f6d... changed my name a bit
You can amend the commit now, with
git commit --amend
Once you’re satisfied with your changes, run
git rebase --continue
读提示就知道怎么操作了:先用$ git commit --amend
修改提交信息,退出编辑器后,运行$ git rebase --continue
,就会继续执行脚本。
This command will apply the other two commits automatically, and then you’re done. If you change pick to edit on more lines, you can repeat these steps for each commit you change to edit. Each time, Git will stop, let you amend the commit, and continue when you’re finished.
Reordering Commits 重新排序
对提交进行重新排序、完全移除某些提交,也可以使用上述的交互式变基工具。如果想要移除“a5f4a0d added cat-file”提交,然后修改另外两个提交引入的顺序,可以将变基脚本从这样:
pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file
改为这样:
pick 310154e updated README formatting and added blame
pick f7f3f6d changed my name a bit
当保存并退出编辑器时,Git 会回到父提交,应用 310154e
然后应用 f7f3f6d
。这就实现了我们的目标。
When you save and exit the editor, Git rewinds your branch to the parent of these commits, applies 310154e and then f7f3f6d, and then stops. You effectively change the order of those commits and remove the “added cat-file” commit completely.
Squashing Commits 压缩提交
还可以将一连串提交压缩成一个单独的提交。例如,按照说明s, squash (压扁) = use commit, but meld (融合) into previous commit
将脚本改为:
pick f7f3f6d changed my name a bit
squash 310154e updated README formatting and added blame
squash a5f4a0d added cat-file
当保存并退出编辑器时,Git 会应用所有的三次修改,然后回到编辑器,以便合并提交信息:
When you save and exit the editor, Git applies all three changes and then puts you back into the editor to merge the three commit messages:
# This is a combination of 3 commits.
# The first commit's message is:
changed my name a bit
# This is the 2nd commit message:
updated README formatting and added blame
# This is the 3rd commit message:
added cat-file
保存即可。
Splitting a Commit (我不用)
The Nuclear Option: filter-branch 核武器
如果要进行大规模的改写,可以使用filter-branch。例如,全局修改你的邮箱地址,从每一个提交中移除某个文件。谨慎使用,最好只在以下场景中使用:
1、从每一个提交中移除一个文件
例如,从整个提交历史中移除passwords.txt文件,可以使用 --tree-filter
选项:
$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten
这条命令是针对整个分支的。可以使用通配符,如 'rm -f *~'
表示移除波浪符(~)结尾的文件(许多文本编辑软件,比如 Emacs,都用这样的文件名保存副本)。
The --tree-filter option runs the specified command after each checkout of the project and then recommits the results. In this case, you remove a file called passwords.txt from every snapshot, whether it exists or not. If you want to remove all accidentally committed editor backup files, you can run something like git filter-branch --tree-filter 'rm -f *~' HEAD.
可以看到Git 重写了树与提交,然后移动分支指针。最好在一个测试分支中这么做,确定没问题了再去硬重置 master 分支。要让 filter-branch
在所有分支上运行,可以用 --all
选项。
You’ll be able to watch Git rewriting trees and commits and then move the branch pointer at the end. It’s generally a good idea to do this in a testing branch and then hard-reset your master branch after you’ve determined the outcome is what you really want. To run filter-branch on all your branches, you can pass --all to the command.
2、将一个子目录设为新项目的根目录
从其他源码控制系统(Git能识别吗?)导入项目时:
Suppose you’ve done an import from another source control system and have subdirectories that make no sense (trunk, tags, and so on). If you want to make the trunk subdirectory be the new project root for every commit, filter-branch can help you do that, too:
$ git filter-branch --subdirectory-filter trunk HEAD
Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)
Ref 'refs/heads/master' was rewritten
现在新项目根目录是 trunk 子目录了。 Git 会自动移除所有不影响子目录的提交。
Now your new project root is what was in the trunk subdirectory each time. Git will also automatically remove commits that did not affect the subdirectory.
3、全局修改邮箱地址
只修改你自己的邮箱地址,使用 --commit-filter 选项:
$ git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
then
GIT_AUTHOR_NAME="Scott Chacon";
GIT_AUTHOR_EMAIL="schacon@example.com";
git commit-tree "$@";
else
git commit-tree "$@";
fi' HEAD
这会遍历每一个提交。因为提交总是包含其父提交的 SHA-1 校验和,所以这个命令会修改所有提交的校验和,而不限于匹配的提交。