Combining Git commits with squash
Imagine you have done lots of commits, i.e. several commits for fixing a bug, but you don’t need all of them, and somehow you want to meld them and squash them into a single commit. You can use rebase. Git always squashes a newer commit into an older commit or “upward”. Let’s create some files:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
mkdir remote repos1 repos2 cd remote/ git init --bare cd ../repos1/ git clone ../remote/ . touch a git add . git commit -m "a added" touch b-1 git add . git commit -m "b-1 added" touch b-2 git add . git commit -m "b-2 added" touch b-3 git add . git commit -m "b-3 added" touch c git add . git commit -m "c added" touch d git add . git commit -m "d added" |
Your tree would be something like this:
But now you would like to squash b-1, b2 and b-3 into one commit. So copy the hash ID of b-3 and execute rebase:
1 2 |
git rebase -i <b-3 HASH ID>~3 git rebase -i 02f4da994c622f7db23a051b3cad3d92385b21c2~3 |
This will open your editor and the first few lines are:
1 2 3 4 5 |
pick 5833ab5 b-1 added pick b7d1e42 b-2 added pick 8d7dae1 b-3 added pick b2fa00f c added pick f1d7439 d added |
Git will melt new commits into older ones, and we want to squash b-3 and b-2 into b-1 so change it into:
1 2 3 4 5 |
pick 5833ab5 b-1 added squash b7d1e42 b-2 added squash 8d7dae1 b-3 added pick b2fa00f c added pick f1d7439 d added |
Close all instances of your editor and save them. This will open another editor to write a new commit message.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# This is a combination of 3 commits. # This is the 1st commit message: b-1 added # This is the commit message #2: b-2 added # This is the commit message #3: b-3 added # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # # Date: Thu Nov 26 11:04:26 2020 +0100 # # interactive rebase in progress; onto ef211ef # Last commands done (3 commands done): # squash b54c2ca b-2 added # squash 30e735c b-3 added # Next commands to do (2 remaining commands): # pick 591529e c added # pick 7960530 d added # You are currently rebasing branch 'master' on 'ef211ef'. # # Changes to be committed: # new file: b-1 # new file: b-2 # new file: b-3 # |
Write your new commit message and save it. Your new tree should look like this:
Squashing commits in git after push
Squash the commits locally (for the last N commits) :
1 |
git rebase -i origin/<branch-name>~<number-of-commits-from-last> <branch-name> |
force push them with +
:
1 |
git push origin +master |
Refs: 1
Difference between --force
and +
:
Note that --force
applies to all the refs that are pushed, hence using it with push.default
set to matching
or with multiple push destinations configured with remote.*.push
may overwrite refs other than the current branch (including local refs that are strictly behind their remote counterparts). To force a push to only one branch, use a +
in front of the refspec to push (e.g git push origin +master
to force a push to the master
branch).
Refs: 1
Rebasing branches onto the base
When you create a feature branch and start working on that, your master might grow and also your branch. Let’s create a repository and add “a” and b:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
mkdir remote repos1 repos2 cd remote/ git init --bare cd ../repos1/ git clone ../remote/ . touch a git add . git commit -m "a added" touch b git add . git commit -m "b added" |
Then let’s create a feature branch for “c”
1 2 3 4 5 6 7 8 9 10 |
git checkout -b feature-c touch c-1 git add . git commit -m "c-1 added" touch c-2 git add . git commit -m "c-2 added" touch c-3 git add . git commit -m "c-3 added" |
Now let’s get back to master and “d” and “e”:
1 2 3 4 5 6 7 |
git checkout master touch d git add . git commit -m "d added" touch e git add . git commit -m "e added" |
At a certain point, you will decide to merge with the master branch, and your tree looks like this:
1 |
git merge feature-c -m "feature-c merged into master" |
By doing this you will the following tree, which in a big project might become very complicated and difficult to track.
As you can see, the branch feature-c is 2 commits behind of the master branch. You can rebase your feature branch onto the master branch, so you can make it as your feature branch has just branched from your master.
1 2 |
git checkout feature-c git rebase master |
1 2 |
git checkout master git merge feature-c |
By doing so, you finally end up with a “linear history”. You can also give tags to your commits so you make your tree more readable:
1 2 |
git tag <tage name> SHA1 ID git tag feature-c 392111d90a31a6bbbd0dc1c8ea15bd8dec7dd0d3 |
If you want git to do a rebase instead of a merge when pulling:
1 |
git pull --rebase |
or you can set it as the default action in your gitconfig file:
1 |
git config --global pull.rebase true |
Rebase with a merge conflict
Let’s create a repository:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
mkdir remote repos1 cd remote/ git init --bare cd ../repos1/ git clone ../remote/ . echo "from master branch " $RANDOM >>a { echo "from master branch " ; date ; } >> a echo "from master branch " $RANDOM >>a git add . git commit -m "a added" echo "from master branch " $RANDOM >>b { echo "from master branch " ; date ; } >> b git add . git commit -m "b added" #git push --set-upstream origin master |
And create a feature branch and modify some files and add some files:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
git checkout -b feature-c touch c-1 git add . git commit -m "c-1 added" touch c-2 git add . git commit -m "c-2 added" touch c-3 git add . git commit -m "c-3 added" echo "from feature branch " $RANDOM >>a sed -i "1imore changes from feature branch $(echo $RANDOM)" a sed -i "6imore changes from feature branch $(echo $RANDOM)" a date >> a git add . git commit -m "a modified in feature branch" #git push --set-upstream origin feature-c sed -i "2imore changes from feature branch $(echo $RANDOM)" a sed -i "3imore changes from feature branch $(echo $RANDOM)" a git add . git commit -m "a modified again in feature branch" #git push echo "from master branch " $RANDOM >>d git add . git commit -m "d added" #git push sed -i "4imore changes from feature branch $(echo $RANDOM)" a ls >> a git add . git commit -m "a modified one more time again in feature branch" #git push |
Now let’s switch back to master and update some files that have been updated in the feature branch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
git checkout master sed -i "4ichanges from master branch $(echo $RANDOM)" a date >> a git add . git commit -m "a modified in main" sed -i "41changes from master branch $(echo $RANDOM)" b git add . git commit -m "b modified" sed -i "3ichanges from master branch $(echo $RANDOM)" a sed -i "6ichanges from master branch $(echo $RANDOM)" a git add . git commit -m "a modified again in main" touch e git add . git commit -m "e added" sed -i "3imore changes from master branch $(echo $RANDOM)" a git add . git commit -m "a modified one more time in main" #git push |
Now let’s switch back to the feature branch and rebase it on master:
1 2 |
git checkout feature-c git rebase -i master |
Now except for the first one, choose edit or reword for the rest, keep another terminal open terminal, and run
gitk --all
and save and close
1 2 3 4 5 6 7 |
pick 432bb96 c-1 added edit 5b1f58d c-2 added edit 043fc1a c-3 added edit 7300582 a modified in feature branch edit 0c0591a a modified again in feature branch edit 832e0f3 d added edit 58e78d0 a modified one more time again in feature branch |
Now
1 |
git rebase --continue |
again:
1 |
git rebase --continue |
Here we can merge conflict, so run:
1 |
git mergetool |
Now in the above, since we are applying commits from the feature branch on master, B the local is master and C is the feature branch that we are applying changes from
1 |
git rebase --continue |
1 |
git rebase --continue |
1 |
git rebase --continue |
undo a completed rebase
If you’ve successfully completed a rebase and later decide you want to undo it, there are a few strategies you can use to reset or revert your branch to its state before the rebase. The method you choose depends on whether you’ve pushed the rebased commits to a remote repository and how you’ve managed your commits.
1. Using git reflog
(Local Repository)
If you haven’t pushed your changes to a remote repository, you can use the git reflog
command to find the commit your branch was at before the rebase and reset to it:
– Use git reflog
to list the recent changes to your repository’s HEAD and branches, look for “rebase (start)” and choose the HEAD before that one
– Once you’ve identified the right entry, note the commit hash or the HEAD@{n} reference that points to your branch’s state before the rebase.
– Reset your branch to that state using:
1 |
git reset --hard HEAD@{n} |
2. Resetting After Pushing (Remote Repository)
If you know the exact commit your branch was at before the rebase, you can force reset your branch to that commit and force push it:
1 2 |
git reset --hard <commit-hash> git push --force-with-lease |
– <commit-hash>
is the commit ID of the branch before you started the rebase.
Reverting Using a New Commit:
If force pushing is not an option (e.g., due to team policies), you can revert the changes introduced by the rebase using git revert
in a way that creates a new commit that undoes the changes:
1 2 3 |
git revert --no-commit <newest-commit-hash>^..<oldest-commit-hash> git commit -m "Revert rebase" git push |
This method creates a new commit on top of the current branch that undoes the changes brought by the rebase, without rewriting the branch’s history.
At any time, you abort and clean up the workspace operations by:
1 2 |
git rebase --abort git clean -xfd |
Rebase remote branch onto master
What if we have had pushed our changes into remote? On that occasion, you should “Rebase remote branch onto master”
This will remove your local feature-c repository, update references, and change to feature-c branch, but because you deleted it, this command will also download the origin/feature-c
1 2 3 4 5 |
git branch -D feature-c git pull --all git checkout feature-c git rebase -p origin/master git push -f origin feature-c |
Refs: [1]
How to determine if and when a branch has been rebased on the main
1 |
git reflog <branch-name> |