How to squash commits which have merge-commit in between?

GitMergeGit Squash

Git Problem Overview


I am working on a feature branch.

  1. Made several commits. Squashed commits.
  2. Pushed changes to remote branch. Got conflicts.
  3. Merged changes from master, resolved conflicts on feature branch.
    • git fetch origin master
    • git merge FETCH_HEAD
    • Resolved conflicts manually.
    • git commit
    • git push
  4. I made one more commit.

So, current commit history looks like this. From current to old:

  1. commit 3
  2. commit M yyy (Merged)
  3. commit 2

How do I squash above 3 commits into 1 before I merge my feature branch to master?

Git Solutions


Solution 1 - Git

You can rebase -i starting with commit 2's parent (that is, the commit on master that you branched from. You'll likely have to re-resolve conflicts when you get to the merge commit.

So if your history looks like

  * D commit 3 (HEAD)
  * M merge
 /|
| * C commit 2
* | B commit on master
|/
* A (master)

Start with git rebase -i A. You'll see a list of commits including both master and your_branch, but not the merge commit. pick the first one (B or C, depending on timing) and squash the rest.

Solution 2 - Git

Assuming the feature branch is called feature and the main branch main:

Create a temporary branch from main:

git checkout -b temp main

Squash the feature branch in:

git merge --squash feature

Commit the changes (the commit message contains all squashed commit messages):

git commit

Go back to the feature branch and point it to the temp branch:

git checkout feature
git reset --hard temp

Delete the temporary branch:

git branch -d temp

Solution 3 - Git

You can use the tool I've created specifically for this task:

https://github.com/sheerun/git-squash

It's only necessary to merge master branch, and then run squashing command:

git merge master
git squash master

Solution 4 - Git

The only way I have found to not have to re-resolve conflicts is this:

Given branch main and branch work, perform the following steps:

git checkout -b work-squashed `git merge-base main work`

This creates a new branch from the last main commit you merged into the work branch.

git diff work-squashed...work | patch -p1

This grabs and applies to the working directory all the changes between the last commit on main that was merged into work and the tip of the work branch. In other words, all the work, including the resolved conflicts.

At this point you need to take care of files added/removed on the work branch, because patch is not git. It doesn't know what files are being tracked by git. So, you need to git add/git rm until all the files are accounted for. Then, you simply commit the changes as a single commit.

Solution 5 - Git

In my case, I started working with a branch that had several commits, then a merge with the main/source branch, then more commits and I wanted to squash all commits, but kept running into an error because of the merge commit:

error: commit is a merge but no -m option was given.

->C1->C2->M(merge with source branch)->C3->C4

There's probably a better way (and I look forward to learning), but what I ended up doing after much reading and trial and error was creating a copy branch for reference, then reverting the current branch to C1,

reset --hard (C1 hash)

then cherry-picking C2, C3, C4, then squashing, then rebasing ... resulting in:

M->C

(just one commit that has been rebased with source!)

I hope this helps someone else with the same problem.

Solution 6 - Git

I came across this issue when I committed a code change using Github UI, and then committed a change locally(without updating the local clone), and then tried to push the local commit to origin. A merge-commit was created as a result.

master
       \ 
         remote-branch --- commit #1 ---------------MergeCommit
                                                    ^
             |                                      |    
           local-branch -------------- commit #2 --- 

Checkout a new branch from the same origin branch, cherry-pick all commits except merge-commit, and then push the branch

master
       \ 
         new-branch --------------------------------------- Squash-commits
                                   ^                     ^
             |                     |   < Cherry-pick >   |                      
         previous-branch -----commit #1 --------- commit #2 

Note that the origin-branch from which we are forking is the same i.e. master.

The process of cherry-picking is straightforward for IntelliJ users. The new-branch will not have the merge-commit as a result.

Solution 7 - Git

enter image description here

  1. Get the PR locally:
gh pr checkout 1938
  1. Add upstream to tell from where you are going to obtain the changes
git remote add upstream git@github.com:minio/console.git
  1. fetch from upstream to get all new changes
git fetch upstream
  1. rebase in a special way, look:
git rebase -i upstream/master

> You will see

pick 18eea859 add csr under tenant details

# Rebase 3bfdbb5e..01964c30 onto 3bfdbb5e (1 command)
#
# 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 [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# 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.
#
  1. Just don't change the pick let it be, if you select squash or reword for this particular case, it won't work. All you really want is to rebase your change and remove the merge commit from your PR, so just save the file as is and get below:
$ git rebase -i upstream/master
Successfully rebased and updated refs/heads/add-csr-under-tenant-details.
  1. Now you can push it:
git push -f
  1. Squashed, or actually rebased, no more merged commit in between:

enter image description here

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionTyr1onView Question on Stackoverflow
Solution 1 - GitKristjánView Answer on Stackoverflow
Solution 2 - GitAurélien GâteauView Answer on Stackoverflow
Solution 3 - GitsheerunView Answer on Stackoverflow
Solution 4 - GitGabriel SchulhofView Answer on Stackoverflow
Solution 5 - Gituser6096790View Answer on Stackoverflow
Solution 6 - GitSaurav SahuView Answer on Stackoverflow
Solution 7 - GitCesar CelisView Answer on Stackoverflow