重写历史 Rewriting History 修改提交信息

实践:

需求:
cid0
cid1
cid2
cid3
将cid1、cid2、cid3压缩到cid0
即压缩最近4次提交
操作:

git rebase -i cid0~4


修改提交信息  

 

git commit --amend

 

https://git-scm.com/book/zh/v2/Git-工具-重写历史

Git - Rewriting History https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History

 

7.6 Git 工具 - 重写历史

重写历史

许多时候,在使用 Git 时,你可能想要修订提交历史。 Git 很棒的一点是它允许你在最后时刻做决定。 你可以在将暂存区内容提交前决定哪些文件进入提交,可以通过 git stash 来决定不与某些内容工作, 也可以重写已经发生的提交就像它们以另一种方式发生的一样。 这可能涉及改变提交的顺序,改变提交中的信息或修改文件,将提交压缩或是拆分, 或完全地移除提交——在将你的工作成果与他人共享之前。

在本节中,你可以学到如何完成这些工作,这样在与他人分享你的工作成果时你的提交历史将如你所愿地展示出来。

Note
在满意之前不要推送你的工作

Git 的基本原则之一是,由于克隆中有很多工作是本地的,因此你可以 在本地 随便重写历史记录。 然而一旦推送了你的工作,那就完全是另一回事了,除非你有充分的理由进行更改,否则应该将推送的工作视为最终结果。 简而言之,在对它感到满意并准备与他人分享之前,应当避免推送你的工作。

修改最后一次提交

修改你最近一次提交可能是所有修改历史提交的操作中最常见的一个。 对于你的最近一次提交,你往往想做两件事情:简单地修改提交信息, 或者通过添加、移除或修改文件来更改提交实际的内容。

如果,你只是想修改最近一次提交的提交信息,那么很简单:

$ git commit --amend

上面这条命令会将最后一次的提交信息载入到编辑器中供你修改。 当保存并关闭编辑器后,编辑器会将更新后的提交信息写入新提交中,它会成为新的最后一次提交。

另一方面,如果你想要修改最后一次提交的实际内容,那么流程很相似:首先作出你想要补上的修改, 暂存它们,然后用 git commit --amend 以新的改进后的提交来 替换 掉旧有的最后一次提交,

使用这个技巧的时候需要小心,因为修正会改变提交的 SHA-1 校验和。 它类似于一个小的变基——如果已经推送了最后一次提交就不要修正它。

Tip
修补后的提交可能需要修补提交信息

当你在修补一次提交时,可以同时修改提交信息和提交内容。 如果你修补了提交的内容,那么几乎肯定要更新提交消息以反映修改后的内容。

另一方面,如果你的修补是琐碎的(如修改了一个笔误或添加了一个忘记暂存的文件), 那么之前的提交信息不必修改,你只需作出更改,暂存它们,然后通过以下命令避免不必要的编辑器环节即可:

$ 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,它可以改写历史中大量的提交,除非你的项目还没有公开并且其他人没有基于要改写的工作的提交做的工作,否则你不应当使用它。 然而,它可以很有用。 你将会学习到几个常用的用途,这样就得到了它适合使用地方的想法。

Caution

git filter-branch 有很多陷阱,不再推荐使用它来重写历史。 请考虑使用 git-filter-repo,它是一个 Python 脚本,相比大多数使用 filter-branch 的应用来说,它做得要更好。它的文档和源码可访问 https://github.com/newren/git-filter-repo 获取。

从每一个提交中移除一个文件

这经常发生。 有人粗心地通过 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 选项。

使一个子目录做为新的根目录

假设已经从另一个源代码控制系统中导入,并且有几个没意义的子目录(trunktags 等等)。 如果想要让 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 校验和, 而不仅仅只是那些匹配邮箱地址的提交。

 

7.6 Git Tools - Rewriting History

Rewriting History

Many times, when working with Git, you may want to revise your local commit history. 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 see how to accomplish these tasks so that you can make your commit history look the way you want before you share it with others.

Note
Don’t push your work until you’re happy with it

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 most recent 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: simply change the commit message, or change the actual content of the commit by adding, removing and modifying files.

If you simply want to modify your last commit message, that’s easy:

$ git commit --amend

The command above loads the previous commit message into an editor session, where you can make changes to the message, save those changes and exit. When you save and close the editor, the editor writes a new commit containing that updated commit message and makes it your new last commit.

If, on the other hand, you want to change the actual content of your last commit, the process works basically the same way — first make the changes you think you forgot, stage those changes, and the subsequent git commit --amend replaces that last commit with your new, improved commit.

You need to be careful with this technique because amending changes the SHA-1 of the commit. It’s like a very small rebase — don’t amend your last commit if you’ve already pushed it.

Tip
An amended commit may (or may not) need an amended commit message

When you amend a commit, you have the opportunity to change both the commit message and the content of the commit. If you amend the content of the commit substantially, you should almost certainly update the commit message to reflect that amended content.

On the other hand, if your amendments are suitably trivial (fixing a silly typo or adding a file you forgot to stage) such that the earlier commit message is just fine, you can simply make the changes, stage them, and avoid the unnecessary editor session entirely with:

$ git commit --amend --no-edit

Changing Multiple Commit Messages

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

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 ~3because 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

Remember again that this is a rebasing command — every commit in the range HEAD~3..HEAD with a changed message and all of its descendants will be rewritten. Don’t include any commit you’ve already pushed to a central server — doing so will confuse other developers by providing an alternate version of the same change.

Running this command gives you a list of commits in your text editor that looks something like this:

pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add 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

It’s important to note that these commits are listed in the opposite order than you normally see them using the log command. If you run a log, you see something like this:

$ git log --pretty=format:"%h %s" HEAD~3..HEAD
a5f4a0d Add cat-file
310154e Update README formatting and add blame
f7f3f6d Change my name a bit

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 Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file

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

These instructions tell you exactly what to do. Type:

$ git commit --amend

Change the commit message, and exit the editor. Then, run:

$ 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

You can also use interactive rebases to reorder or remove commits entirely. If you want to remove the “Add cat-file” commit and change the order in which the other two commits are introduced, you can change the rebase script from this:

pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file

to this:

pick 310154e Update README formatting and add blame
pick f7f3f6d Change my name a bit

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 “Add cat-file” commit completely.

Squashing Commits

It’s also possible to take a series of commits and squash them down into a single commit with the interactive rebasing tool. The script puts helpful instructions in the rebase message:

#
# 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

If, instead of “pick” or “edit”, you specify “squash”, Git applies both that change and the change directly before it and makes you merge the commit messages together. So, if you want to make a single commit from these three commits, you make the script look like this:

pick f7f3f6d Change my name a bit
squash 310154e Update README formatting and add blame
squash a5f4a0d Add cat-file

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:
Change my name a bit

# This is the 2nd commit message:

Update README formatting and add blame

# This is the 3rd commit message:

Add cat-file

When you save that, you have a single commit that introduces the changes of all three previous commits.

Splitting a Commit

Splitting a commit undoes a commit and then partially stages and commits as many times as commits you want to end up with. For example, suppose you want to split the middle commit of your three commits. Instead of “Update README formatting and add blame”, you want to split it into two commits: “Update README formatting” for the first, and “Add blame” for the second. You can do that in the rebase -i script by changing the instruction on the commit you want to split to “edit”:

pick f7f3f6d Change my name a bit
edit 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file

Then, when the script drops you to the command line, you reset that commit, take the changes that have been reset, and create multiple commits out of them. When you save and exit the editor, Git rewinds to the parent of the first commit in your list, applies the first commit (f7f3f6d), applies the second (310154e), and drops you to the console. There, you can do a mixed reset of that commit with git reset HEAD^, which effectively undoes that commit and leaves the modified files unstaged. Now you can stage and commit files until you have several commits, and run git rebase --continue when you’re done:

$ git reset HEAD^
$ git add README
$ git commit -m 'Update README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'Add blame'
$ git rebase --continue

Git applies the last commit (a5f4a0d) in the script, and your history looks like this:

$ git log -4 --pretty=format:"%h %s"
1c002dd Add cat-file
9b29157 Add blame
35cfb2b Update README formatting
f7f3f6d Change my name a bit

This changes the SHA-1s of the three most recent commits in your list, so make sure no changed commit shows up in that list that you’ve already pushed to a shared repository. Notice that the last commit (f7f3f6d) in the list is unchanged. Despite this commit being shown in the script, because it was marked as “pick” and was applied prior to any rebase changes, Git leaves the commit unmodified.

Deleting a commit

If you want to get rid of a commit, you can delete it using the rebase -i script. In the list of commits, put the word “drop” before the commit you want to delete (or just delete that line from the rebase script):

pick 461cb2a This commit is OK
drop 5aecc10 This commit is broken

Because of the way Git builds commit objects, deleting or altering a commit will cause the rewriting of all the commits that follow it. The further back in your repo’s history you go, the more commits will need to be recreated. This can cause lots of merge conflicts if you have many commits later in the sequence that depend on the one you just deleted.

If you get partway through a rebase like this and decide it’s not a good idea, you can always stop. Type git rebase --abort, and your repo will be returned to the state it was in before you started the rebase.

If you finish a rebase and decide it’s not what you want, you can use git reflog to recover an earlier version of your branch. See Data Recovery for more information on the reflog command.

Note

Drew DeVault made a practical hands-on guide with exercises to learn how to use git rebase. You can find it at: https://git-rebase.io/

The Nuclear Option: filter-branch

There is another history-rewriting option that you can use if you need to rewrite a larger number of commits in some scriptable way — for instance, changing your email address globally or removing a file from every commit. The command is filter-branch, and it can rewrite huge swaths of your history, so you probably shouldn’t use it unless your project isn’t yet public and other people haven’t based work off the commits you’re about to rewrite. However, it can be very useful. You’ll learn a few of the common uses so you can get an idea of some of the things it’s capable of.

Caution

git filter-branch has many pitfalls, and is no longer the recommended way to rewrite history. Instead, consider using git-filter-repo, which is a Python script that does a better job for most applications where you would normally turn to filter-branch. Its documentation and source code can be found at https://github.com/newren/git-filter-repo.

Removing a File from Every Commit

This occurs fairly commonly. Someone accidentally commits a huge binary file with a thoughtless git add ., and you want to remove it everywhere. Perhaps you accidentally committed a file that contained a password, and you want to make your project open source. filter-branch is the tool you probably want to use to scrub your entire history. To remove a file named passwords.txtfrom your entire history, you can use the --tree-filter option to filter-branch:

$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten

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.

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.

Making a Subdirectory the New Root

Suppose you’ve done an import from another source control system and have subdirectories that make no sense (trunktags, 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

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.

Changing Email Addresses Globally

Another common case is that you forgot to run git config to set your name and email address before you started working, or perhaps you want to open-source a project at work and change all your work email addresses to your personal address. In any case, you can change email addresses in multiple commits in a batch with filter-branch as well. You need to be careful to change only the email addresses that are yours, so you use --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

This goes through and rewrites every commit to have your new address. Because commits contain the SHA-1 values of their parents, this command changes every commit SHA-1 in your history, not just those that have the matching email address.

 

posted @ 2022-03-25 17:15  papering  阅读(138)  评论(0编辑  收藏  举报