Skip to content
Created: November 18, 2017

Git Tools - Rewriting History ​

Many times when working with Git, you may want to revise your commit history.The great thing about Git is that it allows you to make last-minute decisions.You can decide which files go into the commit before committing the contents of the staging area, you can decide not to work with certain contents through git stash, and you can override commits that have already occurred as if they had occurred in another way.This may involve changing the order of commits, changing information or files in commits, compressing or splitting commits, or removing commits entirely before sharing your work with others.

In this section, you can learn how to do this so that when you share your work with others, your commit history will appear the way you want.

NoteDon't push your work until you're satisfied. One of the basic principles of Git is that since a lot of the work in the clone is local, you can rewrite the history locally at will.Once you push your work, however, it's a different story entirely, and unless you have a good reason to make the change, pushed work should be considered final.In short, avoid pushing your work until you are happy with it and ready to share it with others.

Modify the last commit ​

Modifying your most recent commit is probably the most common of all modifications to past commits.With your latest commit, you often want to do two things: simply modify the commit message, or change the actual contents of the commit by adding, removing, or modifying files.

Modify submission information ​

If you just want to modify the commit information of the most recent commit, it's very simple:

sh
$ git commit --amend

The above command will load the last commit information into the editor for you to modify.When you save and close the editor, the editor writes the updated commit information into a new commit, which becomes the new last commit.

Modify actual content ​

If, on the other hand, you want to modify the actual content of the last commit, the process is very similar: first make the changes you want to make up, stage them, and then use git commit --amend to replace the old last commit with the new and improved commit,

Be careful when using this trick, as the fix will change the SHA-1 checksum of the commit.It's like a small rebase - Don't fix the last commit if it has already been pushed 。

TipThe patched commit may require patching the commit message. When you patch a commit, you can modify both the commit message and the commit content at the same time.If you patch a commit, you'll almost certainly need to update the commit message to reflect the changes.If, on the other hand, your fix is trivial (such as fixing a typo or adding a file you forgot to stage), then the previous commit information does not have to be modified, you can just make the changes, stage them, and then avoid unnecessary editor sessions with: $ git commit --amend --no-edit

Modify multiple submissions ​

In order to modify commits that are farther in the commit history, more sophisticated tools must be used.Git does not have a change history tool, but you can use the rebase tool to rebase a series of commits based on their original HEAD instead of moving it to a new one.With the interactive rebase tool, you can stop after any commit you want to modify, and then modify the information, add files, or do whatever you want.Rebase can be run interactively by adding the -i option to git rebase.You must specify how far back in history you want to rewrite, which you can do by telling the command the commit to which you want to rebase.

For example, if you want to modify the last three commit information, or any commit information in that group of commits, pass the parent commit of the latest commit you want to modify as a parameter to the git rebase -i command, that is, HEAD~2^ or HEAD~3.It may be easier to remember ~3 since you are trying to modify the last three commits; but note that you actually specify the previous four commits, the parent commits of the commits you want to modify:

sh
$ git rebase -i HEAD~3

Remember again that this is a rebase command - every commit in the range HEAD~3..HEAD that changes the commit information and all descendants will be rewritten.Don't involve any commits that have already been pushed to the central server - doing so will produce two versions of one change, thus confusing others.

Running this command will give you a list of commits in a text editor that looks like this:

sh
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

It is important to note that these commits are displayed in the reverse order relative to normal use of the log command.Run the log command once and you'll see something like this:

sh
$ 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

Note the reverse order.Interactive rebasing gives you a script that it will run.It will start from the commit you specified on the command line (HEAD~3) and replay the changes introduced by each commit in order from top to bottom.It lists the oldest instead of the newest because that will be the first one that will be repeated.

You need to modify the script so that it stays on the changes you want to make.To do this, just change 'pick' to 'edit' before each commit you want to modify.For example, if you only want to modify the third commit information, you can modify the file as follows:

sh
edit f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

When you save and exit the editor, Git takes you back to the last commit in the list, sending you back to the command line with the following message:

sh
$ 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

These instructions tell you exactly what to do.input

sh
$ git commit --amend

Modify the commit information and exit the editor.Then, run

sh
$ git rebase --continue

This command will automatically apply the other two commits and then you're done.If you need to change pick to edit in more than one place, you need to repeat these steps for each commit that is modified to edit.Each time, Git will stop, let you fix the commit, and then continue until it's complete.

Reorder commits ​

It is also possible to use interactive rebasing to reorder or remove commits entirely.If you want to remove the "added cat-file" commit and change the order in which the other two commits are introduced, you can change the rebase script to this:

sh
pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

Change it to this:

sh
pick 310154e updated README formatting and added blame
pick f7f3f6d changed my name a bit

When you save and exit the editor, Git takes your branch back to the parent of those commits, applies 310154e then f7f3f6d, and finally stops.This actually changes the order of those commits and completely removes the "added cat-file" commit.

Compressed commits ​

It is also possible to condense a series of commits into a single commit using interactive rebasing tools.The script gives useful instructions in the rebase message:

sh
#
# 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 you specify "squash" instead of "pick" or "edit", Git will apply the changes from both and merge the commit information together.So, if you want these three submissions to be turned into one submission, you can modify the script like this:

sh
pick f7f3f6d changed my name a bit
squash 310154e updated README formatting and added blame
squash a5f4a0d added cat-file

When you save and exit the editor, Git applies all three changes and puts you in the editor to merge the three commits:

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

When you save, you have one commit that contains all the changes from the previous three commits.

split commit ​

Splitting a commit undoes the commit, then stages and commits parts of it as many times as you want until you have completed as many commits as you need.For example, suppose you want to split the middle of three commits.Want to split it into two commits: the first "updated README formatting" and the second "added blame" instead of the original "updated README formatting and added blame".You can do this by modifying the rebase -i script, changing the command for the commit to be split to "edit":

sh
pick f7f3f6d changed my name a bit
edit 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

Then, when the script takes you to the command line, reset that commit, get the reset changes, and create a few commits from them.When you save and exit the editor, Git takes you to the parent of the first commit in the list, applies the first commit (f7f3f6d), applies the second commit (310154e), and then drops you into the command line.There, you can do a hybrid reset of that commit with git reset HEAD^, which will actually undo that commit and unstage the modified files.You can now stage and commit the file until there are a few commits, and then run git rebase --continue when complete:

sh
$ 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 applies the last commit (a5f4a0d) in the script and the history looks like this:

sh
$ git log -4 --pretty=format:"%h %s"
1c002dd added cat-file
9b29157 added blame
35cfb2b updated README formatting
f3cc40e changed my name a bit

Again, these change the SHA-1 checksums of all commits in the list, so make sure no commits in the list have been pushed to the shared repository.

Nuclear weapons grade option: filter-branch ​

There is another history rewriting option, which can be used if you want to rewrite a large number of commits via a script - for example, changing your email address globally or removing a file from each commit.This command is filter-branch, which can rewrite a large number of commits in history, and you should not use it unless your project is not yet public and no one else has done work based on the commits of the work you want to rewrite.However, it can be useful.You'll learn a few common uses so you can get an idea of ​​where it's suitable.

Cautiongit filter-branch There are many pitfalls and using it to rewrite history is no longer recommended.Please consider using git-filter-repo , which is a Python script, compared to most using filter-branch For applications, it does better.Its documentation and source code are accessible https://github.com/newren/git-filter-repo Get.

Remove a file from each commit ​

This happens often.Someone carelessly committed a huge binary via git add . that you want to delete from everywhere.You may accidentally commit a file that includes a password, but you want to open source the project.filter-branch is a tool that may be used to scrub the entire commit history.To remove a file called passwords.txt from the entire commit history, use the --tree-filter option to filter-branch:

sh
$ 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 commit of the checkout project and then resubmits the results.In this example, you removed a file called passwords.txt from every snapshot, whether it existed or not.If you want to remove all accidentally committed editor backup files, you can run a command like git filter-branch --tree-filter 'rm -f *~' HEAD.

Finally you will see Git rewriting the tree with commits and then moving the branch pointers.Usually a good idea is to do this in a test branch, and then when you decide the end result is what you really want, you can hard reset the master branch.To make filter-branch run on all branches, pass the --all option to the command.

Make a subdirectory the new root directory ​

Suppose you've imported from another source control system and have several subdirectories that don't make sense (trunk, tags, etc.).If you want the trunk subdirectory to be the new project root directory for every commit, filter-branch can also help you do that:

sh
$ git filter-branch --subdirectory-filter trunk HEAD
Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)
Ref 'refs/heads/master' was rewritten

The new project root directory is now the trunk subdirectory.Git will automatically remove all commits that do not affect subdirectories.

Globally modify email address ​

Another common situation is when you start working and forget to run git config to set your name and email address, or you want to open source a project and change all your work email addresses to your personal email addresses.In any case, you can also use filter-branch to change email addresses in multiple commits at once.Be careful to only change your own email address, so you use --commit-filter:

sh
$ 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 will go through and rewrite every commit to include your new email address.Because commits contain the SHA-1 checksum of their parent commit, this command will modify the SHA-1 checksum of every commit in your history, not just those that match the email address.