Git之深入解析如何重写提交历史
Posted Forever_wj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Git之深入解析如何重写提交历史相关的知识,希望对你有一定的参考价值。
一、Git 的提交原则
- 了解了管理或者维护 Git 仓库、实现代码控制所需的大多数日常命令和工作流程,尝试跟了踪和提交文件的基本操作,并且掌握了暂存区和轻量级地分支及合并的威力。如果想进一步对 Git 深入学习,可以学习一些 Git 更加强大的功能,这些功能可能并不会在日常操作中使用,但在某些时候可能还是会起到一定的关键性作用。
- 如果还不清楚 Git 的基础使用流程、分支的管理、托管服务器的技术以及分布式工作流程等相关的技术和能力,请参考博客:
- 许多时候,在使用 Git 时,可能想要修订提交历史,Git 允许我们在最后时刻做决定,可以在将暂存区内容提交前决定哪些文件进入提交,可以通过 git stash 来决定不与某些内容工作, 也可以重写已经发生的提交就像它们以另一种方式发生的一样。这可能涉及改变提交的顺序,改变提交中的信息或修改文件,将提交压缩或是拆分,或完全地移除提交,再将我们的工作成果与他人共享之前。
- Git 的基本原则之一是,由于克隆中有很多工作是本地的,因此可以在本地随便重写历史记录,然而一旦推送了工作,那就完全是另一回事了,除非有充分的理由进行更改,否则应该将推送的工作视为最终结果。简而言之,在我们对它感到满意并准备与他人分享之前,应当避免推送工作。
二、修改最后一次提交
- 修改最近一次提交可能是所有修改历史提交的操作中最常见的一个,对于最近一次提交,我们往往想做两件事情:简单地修改提交信息, 或者通过添加、移除或修改文件来更改提交实际的内容。
- 如果,只是想修改最近一次提交的提交信息,那么很简单:
$ git commit --amend
- 这条命令会将最后一次的提交信息载入到编辑器中供我们修改,当保存并关闭编辑器后,编辑器会将更新后的提交信息写入新提交中,它会成为新的最后一次提交。
- 另一方面,如果想要修改最后一次提交的实际内容,那么流程很相似:首先作出想要补上的修改,暂存它们,然后用 git commit --amend 以新的改进后的提交来替换掉旧有的最后一次提交。使用这个技巧的时候需要小心,因为修正会改变提交的 SHA-1 校验和,它类似于一个小的变基,如果已经推送了最后一次提交就不要修正它。
- 当在修补一次提交时,可以同时修改提交信息和提交内容,如果修补了提交的内容,那么几乎肯定要更新提交消息以反映修改后的内容。另一方面,如果我们的修补是琐碎的(如修改了一个笔误或添加了一个忘记暂存的文件),那么之前的提交信息不必修改,只需作出更改,暂存它们,然后通过以下命令避免不必要的编辑器环节即可:
$ git commit --amend --no-edit
三、修改多个提交信息
- 为了修改在提交历史中较远的提交,必须使用更复杂的工具。Git 没有一个改变历史工具,但是可以使用变基工具来变基一系列提交,基于它们原来的 HEAD 而不是将其移动到另一个新的上面,通过交互式变基工具,可以在任何想要修改的提交后停止,然后修改信息、添加文件或做任何想做的事情;可以通过给 git rebase 增加 -i 选项来交互式地运行变基,必须指定想要重写多久远的历史,这可以通过告诉命令将要变基到的提交来做到。
- 例如,如果想要修改最近三次提交信息,或者那组提交中的任意一个提交信息,将想要修改的最近一次提交的父提交作为参数传递给 git rebase -i 命令,即 HEAD~2^ 或 HEAD~3。记住 ~3 可能比较容易,因为正尝试修改最后三次提交;但是注意实际上指定了以前的四次提交,即想要修改提交的父提交:
$ 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 <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# 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 命令,这些提交显示的顺序是相反的。运行一次 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)开始,从上到下的依次重演每一个提交引入的修改。它将最旧的而不是最新的列在上面,因为那会是第一个将要重演的。
- 我们需要修改脚本来让它停留在想修改的变更上,要达到这个目的,只要将你想修改的每一次提交前面的 ‘pick’ 改为 ‘edit’。 例如,只想修改第三次提交信息,可以像下面这样修改文件:
edit f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file
- 当保存并退出编辑器时,Git 将我们带回到列表中的最后一次提交,送回命令行并提示以下信息:
$ 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
- 这个命令将会自动地应用另外两个提交,然后就完成。如果需要将不止一处的 pick 改为 edit,需要在每一个修改为 edit 的提交上重复这些步骤。每一次,Git 将会停止,让我们修正提交,然后继续直到完成。
四、重新排序提交
- 也可以使用交互式变基来重新排序或完全移除提交,如果想要移除 “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,最后停止。事实修改了那些提交的顺序并完全地移除了 “added cat-file” 提交。
五、压缩提交
- 通过交互式变基工具,也可以将一连串提交压缩成一个单独的提交。在变基信息中脚本给出了有用的指令:
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# 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
- 如果,指定 “squash” 而不是 “pick” 或 “edit”,Git 将应用两者的修改并合并提交信息在一起。所以,如果想要这三次提交变为一个提交,可以这样修改脚本:
pick f7f3f6d changed my name a bit
squash 310154e updated README formatting and added blame
squash a5f4a0d added cat-file
- 当保存并退出编辑器时,Git 应用所有的三次修改,然后将我们放到编辑器中来合并三次提交信息:
# 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
- 当保存之后就拥有了一个包含前三次提交的全部变更的提交。
六、拆分提交
- 拆分一个提交会撤消这个提交,然后多次地部分地暂存与提交直到完成所需次数的提交。例如,假设想要拆分三次提交的中间那次提交,想要将它拆分为两次提交:第一个 “updated README formatting”,第二个 “added blame” 来代替原来的 “updated README formatting and added blame”。可以通过修改 rebase -i 的脚本来做到这点,将要拆分的提交的指令修改为 “edit”:
pick f7f3f6d changed my name a bit
edit 310154e updated README formatting and added blame
pick a5f4a0d added cat-file
- 然后,当脚本带我们进入到命令行时,重置那个提交,拿到被重置的修改,从中创建几次提交。当保存并退出编辑器时,Git 带我们到列表中第一个提交的父提交,应用第一个提交(f7f3f6d),应用第二个提交(310154e),然后进入命令行。 那里,可以通过 git reset HEAD^ 做一次针对那个提交的混合重置,实际上将会撤消那次提交并将修改的文件取消暂存。现在可以暂存并提交文件直到有几个提交,然后当完成时运行 git rebase --continue:
$ git reset HEAD^
$ git add README
$ git commit -m 'updated README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'added blame'
$ git rebase --continue
- Git 在脚本中应用最后一次提交(a5f4a0d),历史记录看起来像这样:
$ git log -4 --pretty=format:"%h %s"
1c002dd added cat-file
9b29157 added blame
35cfb2b updated README formatting
f3cc40e changed my name a bit
- 再次强调,这些改动了所有在列表中的提交的 SHA-1 校验和,所以要确保列表中的提交还没有推送到共享仓库中。
七、filter-branch
- 有另一个历史改写的选项,如果想要通过脚本的方式改写大量提交的话可以使用它。例如,全局修改我们的邮箱地址或从每一个提交中移除一个文件,这个命令是 filter-branch,它可以改写历史中大量的提交,除非项目还没有公开并且其他人没有基于要改写的工作的提交做的工作,否则不应当使用它。
① 从每一个提交中移除一个文件
- 有人粗心地通过 git add . 提交了一个巨大的二进制文件,我们想要从所有地方删除,可能偶然地提交了一个包括一个密码的文件,然而想要开源项目。filter-branch 是一个可能会用来擦洗整个提交历史的工具,为了从整个提交历史中移除一个叫做 passwords.txt 的文件,可以使用 --tree-filter 选项给 filter-branch:
$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten
- –tree-filter 选项在检出项目的每一个提交后运行指定的命令然后重新提交结果。例子中,从每一个快照中移除了一个叫作 passwords.txt 的文件,无论它是否存在。如果想要移除所有偶然提交的编辑器备份文件,可以运行类似 git filter-branch --tree-filter ‘rm -f *~’ HEAD 的命令。
- 最后将可以看到 Git 重写树与提交然后移动分支指针,通常一个好的想法是在一个测试分支中做这件事,然后当你决定最终结果是真正想要的,可以硬重置 master 分支。为了让 filter-branch 在所有分支上运行,可以给命令传递 --all 选项。
② 使一个子目录做为新的根目录
- 假设已经从另一个源代码控制系统中导入,并且有几个没意义的子目录(trunk、tags 等等)。如果想要让 trunk 子目录作为每一个提交的新的项目根目录,filter-branch 也可以那么做:
$ git filter-branch --subdirectory-filter trunk HEAD
Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)
Ref 'refs/heads/master' was rewritten
- 现在新项目根目录是 trunk 子目录了,Git 会自动移除所有不影响子目录的提交。
③ 全局修改邮箱地址
- 另一个常见的情形是在开始工作时忘记运行 git config 来设置名字与邮箱地址, 或者想要开源一个项目并且修改所有工作邮箱地址为个人邮箱地址。任何情形下,也可以通过 filter-branch 来一次性修改多个提交中的邮箱地址。 需要小心的是只修改我们自己的邮箱地址,所以使用 --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 校验和,这个命令会修改你的历史中的每一个提交的 SHA-1 校验和,而不仅仅只是那些匹配邮箱地址的提交。
以上是关于Git之深入解析如何重写提交历史的主要内容,如果未能解决你的问题,请参考以下文章