69

I check my code into a Git branch every few minutes or so, and the comments end up being things like "Everything broken starting again" and other absurdities.

Then every few minutes/hours/days I do a serious commit with a real comment like, "Fixed bug #22.55, 3rd time." How can I separate these two concepts? I would like to be able to remove all my frequent-commits and just leave the serious ones.

Dan Rosenstark
  • 68,471
  • 58
  • 283
  • 421

5 Answers5

105

Edited answer with now (in the second half of this entry) the new Git1.7 fixup! action and --autosquash option for quick commit reordering and message editing.


First, the classic squashing process, as done before Git1.7.
(Git1.7 has the same process, only made faster by the possibility of automatic commit reordering as opposed to manual reordering, and by cleaner squashing messages)

I would like to be able to remove all my frequent-checkins and just leave the serious ones.

This is called squashing commits.
You have some good example of "comit cleaning" in this Git ready article:
(Note: the rebase interactive feature came along since September 2007, and allows for squashing or splitting or removing or reordering commits: see also the GitPro page)

A word of caution: Only do this on commits that haven’t been pushed an external repository. If others have based work off of the commits that you’re going to delete, plenty of conflicts can occur. Just don’t rewrite your history if it’s been shared with others.

alt text

The last 4 commits would be much happier if they were wrapped up together

$ git rebase -i HEAD~4

pick 01d1124 Adding license
pick 6340aaa Moving license into its own file
pick ebfd367 Jekyll has become self-aware.
pick 30e0ccb Changed the tagline in the binary, too.

# Rebase 60709da..30e0ccb onto 60709da
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

rebase using the last four commits from where the HEAD is with HEAD~4.
We’re just going to squash everything into one commit.
So, changing the first four lines of the file to this will do the trick:

pick 01d1124 Adding license
squash 6340aaa Moving license into its own file
squash ebfd367 Jekyll has become self-aware.
squash 30e0ccb Changed the tagline in the binary, too.

Basically this tells Git to combine all four commits into the the first commit in the list. Once this is done and saved, another editor pops up with the following:

# This is a combination of 4 commits.
# The first commit's message is:
Adding license

# This is the 2nd commit message:

Moving license into its own file

# This is the 3rd commit message:

Jekyll has become self-aware.

# This is the 4th commit message:

Changed the tagline in the binary, too.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# Explicit paths specified without -i nor -o; assuming --only paths...
# Not currently on any branch.
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   new file:   LICENSE
#   modified:   README.textile
#   modified:   Rakefile
#   modified:   bin/jekyll
#

Since we’re combining so many commits, Git allows you to modify the new commit’s message based on the rest of the commits involved in the process. Edit the message as you see fit, then save and quit.
Once that’s done, your commits have been successfully squashed!

Created commit 0fc4eea: Creating license file, and making jekyll self-aware.
 4 files changed, 27 insertions(+), 30 deletions(-)
  create mode 100644 LICENSE
  Successfully rebased and updated refs/heads/master.

And if we look at the history again…

alt text


Note: for "commit squashing" purposes, Git1.7 (February 2010) has introduced 2 new elements (as mentioned by Dustin in the comment):

  • "git rebase -i" learned new action "fixup" that squashes the change but does not affect existing log message.
  • "git rebase -i" also learned --autosquash option that is useful together with the new "fixup" action.

Both (fixup action and --autosquash option) are illustrated in this Thechnosorcery Networks blog entry. Those features have been cooking since last June 2009 and debated further last December.

The fixup action or directive is for squashing a commit you would have manually reordered in the commit edit list of a rebase --interactive, while ignoring the second commit message, which will make the message edition step faster (you can just save it: the squashed commit will have the first commit message only)
The resulting commit message will only be the first commit one.

  # s, squash = use commit, but meld into previous commit
  # f, fixup = like "squash", but discard this commit's log message

The --autosquash option is about making the commit reordering process automatically for you:

If you know what commit you want to squash something in to you can commit it with a message of “squash! $other_commit_subject”. Then if you run @git rebase --interactive --autosquash commitish@, the line will automatically be set as squash, and placed below the commit with the subject of $other_commit_subject.

(Actually, the squash! can only use the beginning of another commit message)

$ vim Foo.txt
$ git commit -am "Change all the 'Bar's to 'Foo's"
[topic 8374d8e] Change all the 'Bar's to 'Foo's
 1 files changed, 2 insertions(+), 2 deletions(-)
$ vim Bar.txt
$ git commit -am "Change all the 'Foo's to 'Bar's"
[topic 2d12ce8] Change all the 'Foo's to 'Bar's
 1 files changed, 1 insertions(+), 1 deletions(-)

$ vim Foo.txt
$ git commit -am "squash! Change all the 'Bar's"
[topic 259a7e6] squash! Change all the 'Bar's
 1 files changed, 2 insertions(+), 1 deletions(-)

See? Here the third commit uses only the beginning of the first commit message.
A rebase --interactive --autosquash will move the squashed commit below the relevant one:

pick 8374d8e Change all the 'Bar's to 'Foo's
squash 259a7e6 squash! Change all the 'Bar's
pick 2d12ce8 Change all the 'Foo's to 'Bar's

The message edition would be:

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

Change all the 'Bar's to 'Foo's

# This is the 2nd commit message:

squash! Change all the 'Bar's

Meaning by default you would keep the squashing operation recorded in the commit message.
But with the fixup! directive, you could keep that squashing "invisible" in the commit message, while still benefiting from the automatic commit reordering with the --autosquash option (and the fact that your second commit message is based on the first commit you want to be squashed with).

pick 8374d8e Change all the 'Bar's to 'Foo's
fixup cfc6e54 fixup! Change all the 'Bar's
pick 2d12ce8 Change all the 'Foo's to 'Bar's

The message by default will be:

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

Change all the 'Bar's to 'Foo's

# The 2nd commit message will be skipped:

#    fixup! Change all the 'Bar's

Notice that the fixup! commit’s message is already commented out.
You can just save out the message as-is, and your original commit message will be kept.
Very handy for including changes when you realize that you forgot to add part of an earlier commit.

Now if you want to fixup or squash based on the previous commit you just did, Jacob Helwig (the author of the Technosorcery Networks blog entry) recommends the following aliases:

[alias]
    fixup = !sh -c 'git commit -m \"fixup! $(git log -1 --format='\\''%s'\\'' $@)\"' -
    squash = !sh -c 'git commit -m \"squash! $(git log -1 --format='\\''%s'\\'' $@)\"' -

And for doing a rebase interactive which will always benefit from the automatic reordering of commits meant to be squashed:

[alias]
    ri = rebase --interactive --autosquash

Update for Git 2.18 (Q2 2018): "git rebase -i" sometimes left intermediate "# This is a combination of N commits" message meant for the human consumption inside an editor in the final result in certain corner cases, which has been fixed.

See commit 15ef693, commit dc4b5bc, commit e12a7ef, commit d5bc6f2 (27 Apr 2018) by Johannes Schindelin (dscho).
(Merged by Junio C Hamano -- gitster -- in commit 4a3bf32, 23 May 2018)

rebase --skip: clean up commit message after a failed fixup/squash

During a series of fixup/squash commands, the interactive rebase builds up a commit message with comments. This will be presented to the user in the editor if at least one of those commands was a squash.

In any case, the commit message will be cleaned up eventually, removing all those intermediate comments, in the final step of such a fixup/squash chain.

However, if the last fixup/squash command in such a chain fails with merge conflicts, and if the user then decides to skip it (or resolve it to a clean worktree and then continue the rebase), the current code fails to clean up the commit message.

This commit fixes that behavior.

The fix is quite a bit more involved than meets the eye because it is not only about the question whether we are git rebase --skiping a fixup or squash. It is also about removing the skipped fixup/squash's commit message from the accumulated commit message. And it is also about the question whether we should let the user edit the final commit message or not ("Was there a squash in the chain that was not skipped?").

For example, in this case we will want to fix the commit message, but not open it in an editor:

pick  <- succeeds
fixup <- succeeds
squash    <- fails, will be skipped

This is where the newly-introduced current-fixups file comes in real handy. A quick look and we can determine whether there was a non-skipped squash. We only need to make sure to keep it up to date with respect to skipped fixup/squash commands. As a bonus, we can even avoid committing unnecessarily, e.g. when there was only one fixup, and it failed, and was skipped.

To fix only the bug where the final commit message was not cleaned up properly, but without fixing the rest, would have been more complicated than fixing it all in one go, hence this commit lumps together more than a single concern.


Git 2.19 (Q3 2018) fixes a bug: When "git rebase -i" is told to squash two or more commits into one, it labeled the log message for each commit with its number.
It correctly called the first one "1st commit", but the next one was "commit #1", which was off-by-one(!).

See commit dd2e36e (15 Aug 2018) by Phillip Wood (phillipwood).
(Merged by Junio C Hamano -- gitster -- in commit 36fd1e8, 20 Aug 2018)

rebase -i: fix numbering in squash message

Commit e12a7ef ("rebase -i: Handle "combination of <n> commits" with GETTEXT_POISON", 2018-04-27, Git 2.18) changed the way that individual commit messages are labelled when squashing commits together.
In doing so a regression was introduced where the numbering of the messages is off by one. This commit fixes that and adds a test for the numbering.


Note: "git rebase -i"(man) with a series of squash/fixup, when one of the steps stopped in conflicts and ended up getting skipped, did not handle the accumulated commit log messages, which has been corrected with Git 2.42 (Q3 2023).

See commit 6ce7afe (03 Aug 2023) by Phillip Wood (phillipwood).
(Merged by Junio C Hamano -- gitster -- in commit e8c53ff, 09 Aug 2023)

rebase --skip: fix commit message clean up when skipping squash

Signed-off-by: Phillip Wood

During a series of "fixup" and/or "squash" commands, the interactive rebase accumulates a commit message from all the commits that are being squashed together.
If one of the commits has conflicts when it is picked and the user chooses to skip that commit then we need to remove that commit's message from accumulated messages.

To do this 15ef693 ("rebase --skip: clean up commit message after a failed fixup/squash", 2018-04-27, Git v2.18.0-rc0 -- merge listed in batch #6) updated commit_staged_changes() to reset the accumulated message to the commit message of HEAD (which does not contain the message from the skipped commit) when the last command was "fixup" or "squash" and there are no staged changes.
Unfortunately the code to do this contains two bugs.

  1. If parse_head() fails we pass an invalid pointer to unuse_commit_buffer().

  2. The reconstructed message uses the entire commit buffer from HEAD including the headers, rather than just the commit message.

The first issue is fixed by splitting up the "if" condition into several statements each with its own error handling.
The second issue is fixed by finding the start of the commit message within the commit buffer using find_commit_subject().

The existing test added by 15ef693 is modified to show the effect of this bug.
The bug is triggered when skipping the first command in the chain (as the test does before this commit) but the effect is hidden because opts->current_fixup_count is set to zero which leads update_squash_messages() to recreate the squash message file from scratch overwriting the bad message created by commit_staged_changes().
The test is also updated to explicitly check the commit messages rather than relying on grep to ensure they do not contain any stray commit headers.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • note that git 1.7 adds "fixup" which does squashing without having you edit all of the commit messages as you smash them together. – Dustin Feb 21 '10 at 02:07
  • @Dustin: good point. I have edited my answer to reflect and explain those 2 new elements: `fixup!` action and `--autosquash` option. – VonC Feb 21 '10 at 11:58
  • @VonC thanks for working on this. The answer is now too big, IMO. Also I tried it out this weekend (first part only) and it didn't work out for me. The reason is that I run into 'Automatic cherry-pick failed.' Is there any easy way around this without revisiting my code? – Dan Rosenstark Feb 21 '10 at 23:29
  • @yar: there was a bug with squashing (http://www.mail-archive.com/debian-bugs-dist@lists.debian.org/msg436310.html) but fixed if you are using a recent version of Git. Otherwise, it seems this is about merge conflict. See for instance http://blog.robseaman.com/2009/1/15/upgrading-to-mephisto-0-8-1 which illustrates some merge scenario where that message pops up. – VonC Feb 21 '10 at 23:41
  • @VonC, I put in an answer of my own which takes a different, not-GIT approach. Thanks! – Dan Rosenstark Feb 22 '10 at 14:36
  • I was a young man when I started reading this answer. Now I'm old. – Seun Osewa Mar 04 '10 at 13:43
  • @Seun: ... but wiser, I presume? ;) – VonC Mar 04 '10 at 13:55
  • Note that the emphasis on "previous commit" in the paragraph about the `squash` and `fixup` aliases is a bit confusing IMO; these aliases aren't about squashing the "previous commit you just did" (you would presumably use `commit --amend` for that), they actually allow you to create a commit to be squashed to an *arbitrary* commit later (a bit like a deferred `commit --amend` on steroid). – tne Nov 05 '14 at 15:04
27

Using Soft Reset Instead of Rebase to Squash GIT History

I think the length of VonC's answers speaks volumes -- literally -- about how complicated git rebase is. This is my extension of another answer to a question of mine.

  1. You have a branch ticket-201 that you branched from master. You want to pretend that all the commits from ticket-201 never happened, but that you did all the work in one shot.
  2. Soft reset to the branch point using git reset --soft hash where hash should be a commit hash that is in ticket-201's log.
  3. Commit your changes using add then commit. Now the branch history will only have the first commit and the new one with the new stuff.

Making Up Histories From Arbitrary Commits in Different Branches

Using resets you can rewrite the history as you see fit, though your edits will lose the charm of having the right timestamp. Assuming you don't care about that (the times/dates on your files will be enough, perhaps?), or if you want to fiddle with the commits as you go, you can follow these steps:

  1. Checkout a new branch at commit0 (pretend that's a hash): git checkout -b new-history commit0
  2. Now you can get the files from commit5: git reset --hard commit5
  3. Switch back to your index point: git reset --soft commit0
  4. Commit and this will be the second commit in the branch.

This idea is simple, effective and flexible.

Community
  • 1
  • 1
Dan Rosenstark
  • 68,471
  • 58
  • 283
  • 421
  • 1
    @yar: interesting update: `soft reset`: I should have thought about it. – VonC Feb 24 '10 at 15:21
  • @yar: so that will leave you with dangling commits which will be purged during a future `git gc`. – VonC Feb 24 '10 at 15:22
  • @VonC, so before I execute `git gc` will `git reflog` still show me those commits. I hope so, otherwise this is too dangerous. – Dan Rosenstark Feb 24 '10 at 18:02
  • If there is a chance that *master* has advanced since your branch forked from it (e.g. it took several days to finish *ticket-201* and work had to continue on *master* in the interim), you should use `git reset --soft "$(git merge-base master HEAD)"` to avoid inadvertently reverting the commits made on *master* after your ‘ticket’ branch was forked. – Chris Johnsen Mar 30 '10 at 01:53
  • Also, the disclaimer you quoted that made you think you should avoid `git rebase` applies to all forms of history rewriting. Rolling back the tip of a branch with `git reset --soft` is a form of history rewriting, so the disclaimer still applies to the procedure you devised. – Chris Johnsen Mar 30 '10 at 02:00
  • @yar: see this old (2005!) explanation of `git rebase` (http://149.20.20.133/pub/software/scm/git-core/docs/howto/rebase-from-internal-branch.txt) by J.C. Hamano. – VonC Mar 30 '10 at 07:18
  • @yar: or (still for `git rebase`) this visual reference: http://marklodato.github.com/visual-git-guide/#rebase – VonC Mar 30 '10 at 08:31
  • @Chris Johnsen, good point about the disclaimer not really being related to my choice of solution. Regarding the forking problem (master continues to advance), I've changed the first part of my solution a bit to accomodate. – Dan Rosenstark Mar 30 '10 at 09:40
  • @VonC, checking out the links you mentioned. I still find that, for this particular case, using a combination of `git reset --hard` and `git reset --soft` is easier to understand. Note that your answer to this question is a case in point. Even commenters point out how long it is :) – Dan Rosenstark Mar 30 '10 at 09:44
  • @Chris Johnsen, I redid the entire answer in part due to your note about my the disclaimer. @VonC, I respectfully critique your answer here, and I think you'd enjoy knowing. – Dan Rosenstark Apr 02 '10 at 22:54
  • @Yar - your comment about `git gc` and `git reflog` working otherwise too dangerous, based on you later commenting that you redid answer appropriately, does that mean that you no longer have those concerns? Also curious, do I need to manually run a `git gc` or does Git automatically run this periodically for me? (probably a setting I need to config, I'll google this while waiting for your first answer) – Terry May 04 '11 at 04:51
  • @Terry 1) git reflog is not dangerous. It does not write anything. 2) git gc is not run automatically, but it WILL clean up any commits that are not in a branch (which is dangerous if you wanted those). When you find out that I am wrong about #2 (who knows?) please comment back here again. Thanks. – Dan Rosenstark May 04 '11 at 12:13
  • @Yar - I assumed git reflog wasn't dangerous in itself. You just mentioned that if git reflog didn't list the 'dangling commits' after the soft reset and 'new bulk commit' then you thought this 'whole process was dangerous'?? That is how I read it from your comment on 2/24/10. But I was just asking if that comment is now moot and you fully believe/promote this type of activity for 'rebasing' commits? – Terry May 04 '11 at 15:01
  • @Terry, I see. I still promote this activity for rebasing commits, but please note that you are making new commits (and abandoning the old ones, though you will see them with reflog). This is good unless your commit history is very valuable. But anyway, yes, I use this method all the time. `git reset --soft` works like a charm. You do end up doing `git push --force` quite a bit though ;) – Dan Rosenstark May 05 '11 at 06:29
  • 1
    @Yar - when is the `git push --force` needed? My proposed work flow is always create new branch for 'issue'. Work, commit, commit, commit. When done, `git pull --rebase master` then `git reset --soft` then `git commit`, then checkout master and `git merge branch`. Hopefully with this flow, I'll keep history clean and never harm any 'public commits'. Any thoughts or warnings? – Terry May 06 '11 at 05:50
  • 1
    @Terry, if you're in a branch that is on the server and you rebase (the way I suggest, or the other way) you will have to do --force. However, you're right: if you are not in a branch that anybody else is using, you will not have any problems. If your branch is not on the server and you just reset --soft etc. and then merge to a branch that is on the server, you will have no problems. In summary, you understand this pretty well :) – Dan Rosenstark May 06 '11 at 17:43
9

Using Squash Instead

Recently, I've been working in another branch and using squash. The other branch is called temp, and then I use git merge temp --squash to bring it into the real branch that gets pushed to the server.

Workflow is something like this, assuming I'm working in Ticket65252:

git branch -d temp #remove old temp bbranch
git checkout -b temp
# work work work, committing all the way
git checkout Ticket65252
git merge temp --squash
git commit -m "Some message here"

Advantages over using rebase? Way less complicated.

Advantages over using reset --hard and then reset --soft? Less confusing and slightly less error prone.

Dan Rosenstark
  • 68,471
  • 58
  • 283
  • 421
  • True (+1). I explained in another question the [difference between `merge --squash` and `rebase`](http://stackoverflow.com/a/2427520/6309). `git reset` can be a complement to a `merge --squash`, as illustrated in [`git merge testing` branch (final commit) to `master` branch](http://stackoverflow.com/a/13470530/6309). – VonC Aug 08 '13 at 21:18
  • For my ego I take your +1 and multiply it by your rep, so it feels awesome! Thanks @VonC! – Dan Rosenstark Aug 12 '13 at 20:35
0

Use git rebase -i to pick and squash your commits together.

Restore the Data Dumps
  • 38,967
  • 12
  • 96
  • 122
  • 2
    So if I have two commits, `42636015569e` and `f315059d52df87740` how can I eliminate those two? `git rebase -i` just spits the help. Thanks for your answer. – Dan Rosenstark Feb 20 '10 at 16:12
0

With git version 2.xx and newer, the easy method is to clone the repo to a new location using --depth NN, use a number higher than the last commit your users may have, and then just, commit a change and push it back.

The just saved repo will have "Initial Commit" with NN commits in place.

At this point you can delete your old repo

fcm
  • 1,247
  • 15
  • 28