164

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?

Lii
  • 11,553
  • 8
  • 64
  • 88
Tyr1on
  • 1,999
  • 2
  • 13
  • 20

7 Answers7

176

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
Aurélien Gâteau
  • 4,010
  • 3
  • 25
  • 30
  • 25
    this is genius, thanks a lot! – PinguinoSod Dec 07 '21 at 14:20
  • 17
    ...and `git push --force` in the end – pmishev Jun 30 '22 at 16:10
  • 1
    This is perfect, I'd like to know why you don't have to solve conflicts this way tho – andrestascon Oct 03 '22 at 13:59
  • 1
    @andrestascon where do you think conflicts would occur ? When overwriting the feature branch with the temp one, or during the squash ? – Valentin Trinqué Oct 03 '22 at 18:12
  • I assumed since in most of the other workarounds (at least the ones that I tried) you have to re-resolve conflicts. You would need to resolve them as well when squashing – andrestascon Oct 04 '22 at 09:12
  • I do not know about your tries, but when you are squashing a git history against itself, you don't have conflict because the newest commit takes precedence. – Valentin Trinqué Oct 04 '22 at 10:31
  • 1
    You get conflicts when you add things in between the squashed commits, because, the commit you try to squash doesn't recognize its parent anymore. But if you do not temper with the history, there is no reason to have a conflict. – Valentin Trinqué Oct 04 '22 at 10:33
  • Thanks, saved, just had to do something like this `git push --force github-desktop-rwp0 HEAD:patch-28` to finalize it since branch names might differ. https://gist.github.com/rwp0/3972fbdc90ac936035a1088d1d0f005f – Elvin Apr 19 '23 at 13:29
  • This one is perfect. You might still get conflicts but way less than just trying to rebase to the latest. – Ali May 18 '23 at 01:59
  • amazing - not one single conflict - 50+ commits merged into 1 - awesome – danday74 May 31 '23 at 04:44
  • Just for completion, `git push --force-with-lease` should always be preferred over `git push --force`. [Here's why](https://stackoverflow.com/a/52823955/1014782). – René Schubert Jul 24 '23 at 14:25
  • wow amazing, thanks a lot – Binh Le Aug 29 '23 at 07:32
57

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.

Kristján
  • 18,165
  • 5
  • 50
  • 62
  • It worked. But Can you point me to the "theory" why it worked? – Tyr1on May 10 '15 at 04:48
  • 6
    Absolutely, check out [this article](https://www.atlassian.com/git/tutorials/merging-vs-rebasing/workflow-walkthrough) and comment again if you've got some specific questions. – Kristján May 10 '15 at 14:58
  • 2
    Looking again (and playing with a toy repo), it looks like `git rebase -i master` should also have worked just fine, and it wouldn't have included the `master` commit in your `squash`. Is that what you were trying before? What was going wrong? – Kristján May 10 '15 at 15:00
  • 1
    One more thing - mixing merges and rebases is generally a good way to get confused in your Git tree. I'd recommend sticking to just one or the other, and since you want to squash, that means avoiding merge in favor of rebase. In this case, you would have `git rebase master`d instead of `git merge master`, which would hoist each branch commit up as if you'd started from `commit 2`. Then when you finally squash, there's no merge to tangle you up. However, if you've pushed the branch to other people, rebase requires a force push and breaks their history, so tradeoffs. – Kristján May 10 '15 at 15:10
  • 6
    Happened upon yet another way that might interest you: `git rebase --preserve-merges origin/master` from https://stackoverflow.com/questions/4783599/rebasing-a-git-merge-commit – Kristján May 12 '15 at 15:46
18

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
sheerun
  • 1,728
  • 14
  • 27
  • 3
    This is a winner in my books. However, it would be neat if you could also include the commands/procedure you do here and explain what it does. It's really quite simple when you look at the code, but would be nice to have it right here :) – fgblomqvist Jan 05 '21 at 16:08
6

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.

Nimantha
  • 6,405
  • 6
  • 28
  • 69
4

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.

user6096790
  • 380
  • 4
  • 8
3

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

Cesar Celis
  • 166
  • 1
  • 4
  • 8
2

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.

Saurav Sahu
  • 13,038
  • 6
  • 64
  • 79