Collapsing commits in Git

There are times with any source control system that changes are committed after which you realize that you made a mistake and need to make a quick change.  In those cases, it seems pretty silly to keep the history for both those commits.

Git supports collapsing commits natively with the git rebase command. ¬†This¬†command provides more than simply collapse functionality, and the collapse functionality appears to be provided only in an interactive mode. ¬†Paralyzed Egg has a good article on how to do this, but there was one pitfall I fell into that wasn’t described there so I’ll provide the detailed instructions here.

The first step is to look at the log to determine which commits you want to collapse:

$ git log -3
commit d43ace68df8a298d205fb422b3261d69bc5d19e4
Author: David Potter <davidp@example.com>
Date:   Sat Mar 26 15:21:29 2011 -0700

    Move README back to the root.

commit 553561e3330ea60db8a3f7baadd08ef116d22129
Author: David Potter <davidp@example.com>
Date:   Sat Mar 26 15:20:46 2011 -0700

    Move project files to the src directory.

commit 41a6af9de1f0e3fb84ae46c301a0252d0f861f02
Author: David Potter <davidp@example.com>
Date:   Tue Mar 22 11:35:14 2011 -0700

    Updated API layer

In this case I had moved one too many files into the src directory and needed to move it back. I wanted to collapse the last two commits into a single commit. To do this, I executed the following command:

$ git rebase -i HEAD~2

This will bring up your editor.

Note that git expects the editor to return when it is done. I had my EDITOR environment variable defined to launch TextMate which returns immediately, which prevents this feature from working.

The contents of the editor will look something like this:

pick 553561e Move project files to the src directory.
pick d43ace6 Move README back to the root.

# Rebase 41a6af9..d43ace6 onto 41a6af9
#
# 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 <cmd>, exec <cmd> = Run a shell command <cmd>, and stop if it fails
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

To merge those two into a single commit, change the second pick command to squash. Here is the result of that edit:

pick 553561e Move project files to the src directory.
squash d43ace6 Move README back to the root.

# Rebase 41a6af9..d43ace6 onto 41a6af9
#
# 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 <cmd>, exec <cmd> = Run a shell command <cmd>, and stop if it fails
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

Save the file and exit. Git will bring up the editor with content that looks like this:

# This is a combination of 2 commits.
# The first commit's message is:

Move project files to the src directory.

# This is the 2nd commit message:

Move README back to the root.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# Not currently on any branch.
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       renamed:    74-location.png -> src/74-location.png
#       renamed:    ACS-Info.plist -> src/ACS-Info.plist
#       renamed:    ACS.xcodeproj/project.pbxproj -> src/ACS.xcodeproj/project.pbxproj
#       renamed:    ACS.xcodeproj/project.xcworkspace/contents.xcworkspacedata -> src/ACS.xcodeproj/project.xcworkspace/contents.xcworkspacedata
#       renamed:    ACS_Prefix.pch -> src/ACS_Prefix.pch
#       renamed:    Classes/ACSAppDelegate.h -> src/Classes/ACSAppDelegate.h
#       renamed:    Classes/ACSAppDelegate.m -> src/Classes/ACSAppDelegate.m
#       renamed:    Classes/BaseUrlConnection.h -> src/Classes/BaseUrlConnection.h
#       renamed:    Classes/BaseUrlConnection.m -> src/Classes/BaseUrlConnection.m
#       renamed:    Classes/InrixApi.h -> src/Classes/InrixApi.h
#       renamed:    Classes/InrixApi.m -> src/Classes/InrixApi.m
#       renamed:    Classes/MKMapView+ZoomLevel.h -> src/Classes/MKMapView+ZoomLevel.h
#       renamed:    Classes/MKMapView+ZoomLevel.m -> src/Classes/MKMapView+ZoomLevel.m
#       renamed:    Classes/Settings.h -> src/Classes/Settings.h
#       renamed:    Classes/TileOverlay.h -> src/Classes/TileOverlay.h
#       renamed:    Classes/TileOverlay.m -> src/Classes/TileOverlay.m
#       renamed:    Classes/TileOverlayView.h -> src/Classes/TileOverlayView.h
#       renamed:    Classes/TileOverlayView.m -> src/Classes/TileOverlayView.m
#       renamed:    Classes/TrafficViewController.h -> src/Classes/TrafficViewController.h
#       renamed:    Classes/TrafficViewController.m -> src/Classes/TrafficViewController.m
#       renamed:    Classes/TrafficViewController.xib -> src/Classes/TrafficViewController.xib
#       renamed:    GDataXMLNode.h -> src/GDataXMLNode.h
#       renamed:    GDataXMLNode.m -> src/GDataXMLNode.m

The instructions say that all lines beginning with a # will be ignored. That means all lines NOT beginning with a # will be included in the resulting commit message. That wasn’t clear to me when I first read that, maybe because that message is in the middle of the buffer. I changed this buffer to look like this:

# This is a combination of 2 commits.
# The first commit's message is:

Move project files to the src directory.

# This is the 2nd commit message:

#Move README back to the root.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# Not currently on any branch.
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       renamed:    74-location.png -> src/74-location.png
#       renamed:    ACS-Info.plist -> src/ACS-Info.plist
#       renamed:    ACS.xcodeproj/project.pbxproj -> src/ACS.xcodeproj/project.pbxproj
#       renamed:    ACS.xcodeproj/project.xcworkspace/contents.xcworkspacedata -> src/ACS.xcodeproj/project.xcworkspace/contents.xcworkspacedata
#       renamed:    ACS_Prefix.pch -> src/ACS_Prefix.pch
#       renamed:    Classes/ACSAppDelegate.h -> src/Classes/ACSAppDelegate.h
#       renamed:    Classes/ACSAppDelegate.m -> src/Classes/ACSAppDelegate.m
#       renamed:    Classes/BaseUrlConnection.h -> src/Classes/BaseUrlConnection.h
#       renamed:    Classes/BaseUrlConnection.m -> src/Classes/BaseUrlConnection.m
#       renamed:    Classes/InrixApi.h -> src/Classes/InrixApi.h
#       renamed:    Classes/InrixApi.m -> src/Classes/InrixApi.m
#       renamed:    Classes/MKMapView+ZoomLevel.h -> src/Classes/MKMapView+ZoomLevel.h
#       renamed:    Classes/MKMapView+ZoomLevel.m -> src/Classes/MKMapView+ZoomLevel.m
#       renamed:    Classes/Settings.h -> src/Classes/Settings.h
#       renamed:    Classes/TileOverlay.h -> src/Classes/TileOverlay.h
#       renamed:    Classes/TileOverlay.m -> src/Classes/TileOverlay.m
#       renamed:    Classes/TileOverlayView.h -> src/Classes/TileOverlayView.h
#       renamed:    Classes/TileOverlayView.m -> src/Classes/TileOverlayView.m
#       renamed:    Classes/TrafficViewController.h -> src/Classes/TrafficViewController.h
#       renamed:    Classes/TrafficViewController.m -> src/Classes/TrafficViewController.m
#       renamed:    Classes/TrafficViewController.xib -> src/Classes/TrafficViewController.xib
#       renamed:    GDataXMLNode.h -> src/GDataXMLNode.h
#       renamed:    GDataXMLNode.m -> src/GDataXMLNode.m

After saving and quitting the editor, Git prints out what it did:

[detached HEAD 419d987] Move project files to the src directory.
 26 files changed, 0 insertions(+), 0 deletions(-)
 rename 74-location.png => src/74-location.png (100%)
 rename ACS-Info.plist => src/ACS-Info.plist (100%)
 rename {ACS.xcodeproj => src/ACS.xcodeproj}/project.pbxproj (100%)
 rename {ACS.xcodeproj => src/ACS.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%)
 rename ACS_Prefix.pch => src/ACS_Prefix.pch (100%)
 rename {Classes => src/Classes}/ACSAppDelegate.h (100%)
 rename {Classes => src/Classes}/ACSAppDelegate.m (100%)
 rename {Classes => src/Classes}/BaseUrlConnection.h (100%)
 rename {Classes => src/Classes}/BaseUrlConnection.m (100%)
 rename {Classes => src/Classes}/InrixApi.h (100%)
 rename {Classes => src/Classes}/InrixApi.m (100%)
 rename {Classes => src/Classes}/MKMapView+ZoomLevel.h (100%)
 rename {Classes => src/Classes}/MKMapView+ZoomLevel.m (100%)
 rename {Classes => src/Classes}/Settings.h (100%)
 rename {Classes => src/Classes}/TileOverlay.h (100%)
 rename {Classes => src/Classes}/TileOverlay.m (100%)
 rename {Classes => src/Classes}/TileOverlayView.h (100%)
 rename {Classes => src/Classes}/TileOverlayView.m (100%)
 rename {Classes => src/Classes}/TrafficViewController.h (100%)
 rename {Classes => src/Classes}/TrafficViewController.m (100%)
 rename {Classes => src/Classes}/TrafficViewController.xib (100%)
 rename GDataXMLNode.h => src/GDataXMLNode.h (100%)
 rename GDataXMLNode.m => src/GDataXMLNode.m (100%)
 rename MainWindow.xib => src/MainWindow.xib (100%)
 rename car_256.png => src/car_256.png (100%)
 rename main.m => src/main.m (100%)
Successfully rebased and updated refs/heads/master.

Here is the result:

$ git log -2
commit 419d987ffbf2c70b1cad664aa2a56281e681268d
Author: David Potter <davidp@example.com>
Date:   Sat Mar 26 15:20:46 2011 -0700

    Move project files to the src directory.

commit 41a6af9de1f0e3fb84ae46c301a0252d0f861f02
Author: David Potter <david@example.com>
Date:   Tue Mar 22 11:35:14 2011 -0700

    Updated API layer

If conflicts occur during this process, you can resolve them and then continue the rebase, like so:

$ git rebase --continue

If you decide you don’t want to continue, you can abort the operation instead:

$ git rebase --abort

You can see the operations that have been performed on the repository using the following command:

$ git reflog --oneline

419d987 HEAD@{0}: rebase -i (squash): Move project files to the src directory.
41a6af9 HEAD@{1}: rebase -i (squash): updating HEAD
553561e HEAD@{2}: checkout: moving from master to 553561e
d43ace6 HEAD@{3}: commit: Move README back to the root.
553561e HEAD@{4}: commit: Move project files to the src directory.
41a6af9 HEAD@{5}: pull origin master: Fast-forward
ce65952 HEAD@{6}: pull origin master: Fast-forward
876b07b HEAD@{7}: pull origin master: Fast-forward
28c415e HEAD@{8}: clone: from git://server/project

2 Responses to “Collapsing commits in Git on “Collapsing commits in Git”

Leave your comment...

Name (required)
Email (required)
Website