Reputation: 148744
How do I squash my last N commits together into one commit?
Upvotes: 5670
Views: 4310663
Reputation: 4704
Yes, you could, with a little hack of the environment variable GIT_SEQUENCE_EDITOR
to write a script and let it act as an "editor" for git todo files.
Put this on your PATH
(I have put that on my dotfiles) and name it git-squash
:
#!/usr/bin/env bash
set -e
set -o pipefail
if [ "$1" == "-i" ]; then
shift
SEQUENCE_FILE="$(mktemp)"
SEQUENCE_DEBUG=/dev/null
if [ "$1" == "-d" ]; then
SEQUENCE_DEBUG=/dev/stderr
shift
set -x
fi
awk '(NR>1&&$1=="pick") {$1="squash"} {print}' "$1" \
| tee "${SEQUENCE_FILE}" > "${SEQUENCE_DEBUG}"
exec mv "${SEQUENCE_FILE}" "$1"
else
GIT_SEQUENCE_EDITOR="$0 -i"
GIT_EDITOR=cat
if [ "$1" == "-d" ]; then
shift
GIT_SEQUENCE_EDITOR="${GIT_SEQUENCE_EDITOR} -d"
set -x
fi
export GIT_SEQUENCE_EDITOR
export GIT_EDITOR
exec git rebase -i --keep-base --autosquash "$@"
fi
Usage:
git checkout your-work-branch
git fetch origin
git autosquash origin/master
Then all the commits newer than origin/master
will be squashed.
Upvotes: 2
Reputation: 94794
Use git rebase -i <after-this-commit>
and replace "pick" on the second and subsequent commits with "squash" or "fixup", as described in the manual.
In this example, <after-this-commit>
is either the SHA1 hash or the relative location from the HEAD of the current branch from which commits are analyzed for the rebase command. For example, if the user wishes to view 5 commits from the current HEAD in the past, the command is git rebase -i HEAD~5
.
Upvotes: 3057
Reputation: 9586
Thanks to this handy blog post I found that you can use this command to squash the last 3 commits:
git rebase -i HEAD~3
This is handy as it works even when you are on a local branch with no tracking information/remote repo.
The command will open the interactive rebase editor which then allows you to reorder, squash, reword, etc as per normal.
Using the interactive rebase editor:
See the Git docs on using the interactive rebase. A summary follows:
From the example above, the interactive rebase editor shows the last three commits. This constraint was determined by HEAD~3
when running the command git rebase -i HEAD~3
.
The commits are listed in reverse order to what you may expect. The oldest commit is displayed on line 1, and the newest commit on the last line. The lines starting with a #
are comments/documentation.
The documentation displayed is pretty clear. On any given line you can change the command from pick
to a command of your choice.
I prefer to use the command fixup
as this "squashes" the commit's changes into the commit on the line above and discards the commit's message.
In most cases you would leave line 1 as pick
. You cannot use squash
or fixup
as there is no earlier commit to squash the commit into. It will give an error similar to the following: error: cannot 'fixup' without a previous commit
.
You may also change the order of the commits. This allows you to squash or fixup commits that are not adjacent chronologically.
A practical everyday example
I've recently committed a new feature. Since then, I have committed two bug fixes. But now I have discovered a bug (or maybe just a spelling error) in the new feature I committed. How annoying! I don't want a new commit polluting my commit history!
The first thing I do is fix the mistake and make a new commit with the comment squash this into my new feature!
.
I then run git log
or gitk
and get the commit SHA of the new feature (in this case 1ff9460
).
Next, I bring up the interactive rebase editor with git rebase -i 1ff9460~
. The ~
after the commit SHA tells the editor to include that commit in the editor.
Next, I move the commit containing the fix (fe7f1e0
) to underneath the feature commit, and change pick
to fixup
.
When closing the editor, the fix will get squashed into the feature commit and my commit history will look nice and clean!
This works well when all the commits are local, but if you try to change any commits already pushed to the remote you can really cause problems for other devs that have checked out the same branch!
Upvotes: 349
Reputation: 224691
You can do this fairly easily without git rebase
or git merge --squash
. In this example, we'll squash the last 3 commits.
If you want to write the new commit message from scratch, this suffices:
git reset --soft HEAD~3
git commit
If you want to start editing the new commit message with a concatenation of the existing commit messages (i.e. similar to what a pick/squash/squash/…/squash git rebase -i
instruction list would start you with), then you need to extract those messages and pass them to git commit
:
git reset --soft HEAD~3 &&
git commit --edit -m"$(git log --format=%B --reverse HEAD..HEAD@{1})"
Both of those methods squash the last three commits into a single new commit in the same way. The soft reset just re-points HEAD to the last commit that you do not want to squash. Neither the index nor the working tree are touched by the soft reset, leaving the index in the desired state for your new commit (i.e. it already has all the changes from the commits that you are about to “throw away”).
You have rewritten that history you must than use the --force flag to push this branch back to remote. This is what the force flag is meant for, but you can be extra careful, and always fully define your target.
git push --force-with-lease origin <branch-name>
Upvotes: 5896
Reputation: 599
If you don't care about the commit messages of the in-between commits, you can use
git reset --soft <commit-hash-into-which-you-want-to-squash>
git commit -a --amend
Upvotes: 14
Reputation: 179
I was inspired by Chris Johnsen's answer and find a better solution.
If we wanna squash the last 3 commits, and write the new commit message from scratch, this suffices:
git reset --soft HEAD~3 && git commit
If we wanna squash the last many commits, lets say 162 commits, it's very hard to count the rank of the 162th commit, but we can use the commit ID, this suffices:
git reset --soft ceb5ab28ca && git commit
ceb5ab28ca is the commit ID of the 162th commit.
Upvotes: 9
Reputation: 31686
Variation of existing answers for the specific case when the remote is configured to disallow --force
; assuming the commits are in a branch that you have permissions to delete remotely:
# 'rewind' to last commit before your commit (use `git log` to identify)
git reset <hash-BEFORE-first-commit-in-feature>
# re-add files (new and modified)
git add <modified-and-new-files>
# make new single commit with all changes
git commit -m <new-single-commit-message>
# delete remote branch if you already pushed a PR
git push origin --delete <feature-branch-name>
git push -u origin <feature-branch-name>
Upvotes: 0
Reputation: 8447
Let's say n
is really large. You do not want to deal with each commit. You just want a new branch called new_feature_branch
that has 1 commit and all your changes of old_feature_branch
. You can do the following
git checkout main
# will contain changes all changes from old_feature_branch
git checkout -b new_feature_branch
# get all changes from old_feature_branch and stage them
for f in $(git --no-pager diff --name-only old_feature_branch ) ; do git checkout old_feature_branch -- $f ; done
git commit -m "one commit message"
Upvotes: 3
Reputation: 37100
Just add this bash function to your bash of .zshrc file.
# Squash last X commits with a Commit message.
# Usage: squash X 'COMMIT_MSG'
# where X= Number of last commits.
# where COMMIT_MSG= New commit msg.
function squash() {
if [ -z "${1}" -o -z "${2}" ]; then
echo "Usage: \`squash X COMMIT_MSG\`"
echo "X= Number of last commits."
echo "COMMIT_MSG= New commit msg."
return 1
fi
git reset --soft HEAD~"$1"
git add . && git ci -m "$2" # With 100 emoji
git push --force
}
Then just run
squash X 'New Commit Message'
And you're done.
Upvotes: 3
Reputation: 371
In question it could be ambiguous what is meant by "last".
for example git log --graph
outputs the following (simplified):
* commit H0
|
* merge
|\
| * commit B0
| |
| * commit B1
| |
* | commit H1
| |
* | commit H2
|/
|
Then last commits by time are H0, merge, B0. To squash them you will have to rebase your merged branch on commit H1.
The problem is that H0 contains H1 and H2 (and generally more commits before merge and after branching) while B0 don't. So you have to manage changes from H0, merge, H1, H2, B0 at least.
It's possible to use rebase but in different manner then in others mentioned answers:
rebase -i HEAD~2
This will show you choice options (as mentioned in other answers):
pick B1
pick B0
pick H0
Put squash instead of pick to H0:
pick B1
pick B0
s H0
After save and exit rebase will apply commits in turn after H1. That means that it will ask you to resolve conflicts again (where HEAD will be H1 at first and then accumulating commits as they are applied).
After rebase will finish you can choose message for squashed H0 and B0:
* commit squashed H0 and B0
|
* commit B1
|
* commit H1
|
* commit H2
|
P.S. If you just do some reset to BO:
(for example, using reset --mixed
that is explained in more detail here https://stackoverflow.com/a/18690845/2405850):
git reset --mixed hash_of_commit_B0
git add .
git commit -m 'some commit message'
then you squash into B0 changes of H0, H1, H2 (losing completely commits for changes after branching and before merge.
Upvotes: 6
Reputation: 546
I find a more generic solution is not to specify 'N' commits, but rather the branch/commit-id you want to squash on top of. This is less error-prone than counting the commits up to a specific commit—just specify the tag directly, or if you really want to count you can specify HEAD~N.
In my workflow, I start a branch, and my first commit on that branch summarizes the goal (i.e. it's usually what I will push as the 'final' message for the feature to the public repository.) So when I'm done, all I want to do is git squash master
back to the first message and then I'm ready to push.
I use the alias:
squash = !EDITOR="\"_() { sed -n 's/^pick //p' \"\\$1\"; sed -i .tmp '2,\\$s/^pick/f/' \"\\$1\"; }; _\"" git rebase -i
This will dump the history being squashed before it does so—this gives you a chance to recover by grabbing an old commit ID off the console if you want to revert. (Solaris users note it uses the GNU sed -i
option, Mac and Linux users should be fine with this.)
Upvotes: 7
Reputation: 496902
This is super-duper kludgy, but in a kind of cool way, so I'll just toss it into the ring:
GIT_EDITOR='f() { if [ "$(basename $1)" = "git-rebase-todo" ]; then sed -i "2,\$s/pick/squash/" $1; else vim $1; fi }; f' git rebase -i foo~5 foo
Translation: provide a new "editor" for git which, if the filename to be edited is git-rebase-todo
(the interactive rebase prompt) changes all but the first "pick" to "squash", and otherwise spawns vim - so that when you're prompted to edit the squashed commit message, you get vim. (And obviously I was squashing the last five commits on branch foo, but you could change that however you like.)
I'd probably do what Mark Longair suggested, though.
Upvotes: 21
Reputation: 3281
What can be really convenient:
Find the commit hash you want to squash on top of, say d43e15
.
Now use
git reset d43e15
git commit -am 'new commit name'
Upvotes: 33
Reputation: 8886
To squash the last 10 commits into 1 single commit:
git reset --soft HEAD~10 && git commit -m "squashed commit"
If you also want to update the remote branch with the squashed commit:
git push -f
Upvotes: 47
Reputation: 2111
Based on this article I found this method easier for my usecase.
My 'dev' branch was ahead of 'origin/dev' by 96 commits (so these commits were not pushed to the remote yet).
I wanted to squash these commits into one before pushing the change. I prefere to reset the branch to the state of 'origin/dev' (this will leave all changes from the 96 commits unstaged) and then commit the changes at once:
git reset origin/dev
git add --all
git commit -m 'my commit message'
Upvotes: 69
Reputation: 4289
Based on Chris Johnsen's answer,
Add a global "squash" alias from bash: (or Git Bash on Windows)
git config --global alias.squash '!f(){ git reset --soft HEAD~${1} && git commit --edit -m"$(git log --format=%B --reverse HEAD..HEAD@{1})"; };f'
... or using Windows' Command Prompt:
git config --global alias.squash "!f(){ git reset --soft HEAD~${1} && git commit --edit -m\"$(git log --format=%B --reverse HEAD..HEAD@{1})\"; };f"
[alias]
squash = "!f(){ git reset --soft HEAD~${1} && git commit --edit -m\"$(git log --format=%B --reverse HEAD..HEAD@{1})\"; };f"
git squash N
... Which automatically squashes together the last N
commits, inclusive.
Note: The resultant commit message is a combination of all the squashed commits, in order. If you are unhappy with that, you can always git commit --amend
to modify it manually. (Or, edit the alias to match your tastes.)
Upvotes: 164
Reputation: 10809
2020 Simple solution without rebase :
git reset --soft HEAD~2
git commit -m "new commit message"
git push -f
2 means the last two commits will be squashed. You can replace it by any number
Upvotes: 408
Reputation: 467191
You can use git merge --squash
for this, which is slightly more elegant than git rebase -i
. Suppose you're on master and you want to squash the last 12 commits into one.
WARNING: First make sure you commit your work—check that git status
is clean (since git reset --hard
will throw away staged and unstaged changes)
Then:
# Reset the current branch to the commit just before the last 12:
git reset --hard HEAD~12
# HEAD@{1} is where the branch was just before the previous command.
# This command sets the state of the index to be as it would just
# after a merge from that commit:
git merge --squash HEAD@{1}
# Commit those squashed changes. The commit message will be helpfully
# prepopulated with the commit messages of all the squashed commits:
git commit
The documentation for git merge
describes the --squash
option in more detail.
Update: the only real advantage of this method over the simpler git reset --soft HEAD~12 && git commit
suggested by Chris Johnsen in his answer is that you get the commit message prepopulated with every commit message that you're squashing.
Upvotes: 1049
Reputation: 53
If you are on windows, you might want to use this powershell code.
function SquashCommits([int]$count) {
$commitHashes = git log --pretty=format:%h -n $count
$commands= ( 0..$($count-2) ) | %{ "sed -i 's/^pick $($commitHashes[$_])/squash $($commitHashes[$_])/' `$file" }
$st= $commands -join "`n"
$st="func() {
local file=`$1
$st
}; func"
$env:GIT_SEQUENCE_EDITOR=$st
try{
git rebase -i HEAD~$count
}finally
{
Remove-Item Env:\GIT_SEQUENCE_EDITOR
}
}
Usage Example:
SquashCommits 3
Upvotes: 0
Reputation: 1660
method 1 if you have many commits
git rebase -i master
then press keyboard 'i' to edit
you will see like this:
pick etc1
pick etc2
pick etc2
replace the word pick with 'f'
and press esc y :wq
pick etc1 //this commit will the one commit
f etc2
f etc2
and press this command
git push origin +head
method 2 if you have few commits you can do this to delete a commit, you need to do same for delete your second commit and so on
git reset --soft HEAD^1 // or git reset --soft head~1
git commit --amend //then press `:wq`
git push -f
method 3 if you already have one commit and you dont want submit another commit more
git add files...
git commit --amend //then press `:wq`
git push origin +head
Upvotes: 28
Reputation: 450
We were really happy with this answer, it means you don't need to count through commits.
If you have loads of commits on master branch and want to combine them all into one:
[ You'll notice that all your changes at this point will be shown in the source control section of your IDE - good to review them to check it's all there]
That's it!
Upvotes: 10
Reputation: 2073
Many answers are based on git rebase
command, but in my experience it is somewhat complex and advanced for git-beginners.
Let's say you want to squash last 3 commits. Then following are the steps:
git log -1 --oneline
and note the commit-id of the present state (just in case you do something wrong with git reset)git reset --soft HEAD~3
you'll go back 3 commits (and sort of forget that you've had made these three commits earlier)git commit -m <NEW_SINGLE_MESSAGE>
which will automatically combine the three commits under your messageIn case something goes wrong with git reset, you can again return to the original state by git reset --soft <ORIGINAL_COMMIT>
Upvotes: 66
Reputation: 7736
Easiest way to do this is using GitHub Desktop. Just select all the commits in your History, right-click and select "Squash x commits":
Upvotes: 20
Reputation: 4238
In the branch you would like to combine the commits on, run:
git rebase -i HEAD~(n number of commits back to review)
example:
git rebase -i HEAD~2
This will open the text editor and you must switch the 'pick' in front of each commit with 'squash' if you would like these commits to be merged together. From documentation:
For example, if you are looking to merge all the commits into one, the 'pick' is the first commit you made and all future ones (placed below the first) should be set to 'squash'. If using vim, use :x in insert mode to save and exit the editor.
Then to continue the rebase:
git add .
git rebase --continue
For more on this and other ways to rewrite your commit history see this helpful post
Upvotes: 95
Reputation: 13934
Did anyone mention how easy it is to do on IntelliJ IDEA UI:
git
windowSquash Commits
> Edit the squashed commit messageForce Push
Upvotes: 30
Reputation: 1111
Here is another visual example of what would follow after executing:
git rebase -i HEAD~3
Source: https://www.git-tower.com/learn/git/faq/git-squash/
Upvotes: 84
Reputation: 1980
simple solution:
git reset --soft HEAD~5
git commit -m "commit message"
git push origin branch --force-with-lease
Upvotes: 25
Reputation: 895
If for example, you want to squash the last 3 commits to a single commit in a branch (remote repository) in for example: https://bitbucket.org
What I did is
git reset --soft HEAD~3
git commit
git push origin <branch_name> --force
Upvotes: 39
Reputation: 1262
Tried all approaches mention here. But finally my issue resolved by following this link. https://gist.github.com/longtimeago/f7055aa4c3bba8a62197
$ git fetch upstream
$ git checkout omgpull
$ git rebase -i upstream/master
< choose squash for all of your commits, except the first one >
< Edit the commit message to make sense, and describe all your changes >
$ git push origin omgpull -f
Upvotes: 3
Reputation: 563
I think the easiest way to do this is by making a new branch based on master and doing a merge --squash
of the feature branch.
git checkout master
git checkout -b feature_branch_squashed
git merge --squash feature_branch
Then you have all of the changes ready to commit.
Upvotes: 46