TechEarl

How to Squash Git Commits with Rebase or Merge

Squash selected Git commits, preserve or discard messages, recover from conflicts, and choose safe whole-branch and publishing workflows.

Ishan Karunaratne⏱️ 6 min readUpdated
Share thisCopied
Squash Git commits into one with interactive rebase: change pick to squash or fixup, fold commits into the line above, or flatten a branch with git merge --squash.
bash
git status --short
git branch backup/before-squash
git rebase -i HEAD~3

Start with a clean working tree and a private feature branch. The backup branch preserves the starting commit. In the editor, leave the oldest of the three commits as pick and change the other two to squash or fixup. Save the plan, resolve any conflicts and review the result before publishing it.

Use a fresh backup-branch name if backup/before-squash already exists. These examples assume a simple linear history; merge commits need a deliberate plan rather than blindly counting HEAD~N.

Choose the range before rewriting

Inspect the branch first:

bash
git log --oneline --graph --decorate -10

HEAD~3 names the first-parent ancestor three commits back. In a linear history, rebasing onto it selects the last three commits. It requires that ancestor to exist, so it cannot include the repository's root commit. To edit a history including the root, use:

bash
git rebase -i --root

To select commits unique to your branch and replay them onto the current local main:

bash
git rebase -i main

That can also incorporate new work from main; it is not merely a different spelling for squashing at the original branch point. Check which base you intend. By default, rebase does not preserve the original merge topology. See merge versus rebase when the branch has merges.

Squash into the preceding commit

The plan lists commits oldest first:

text
pick   a1b2c3d Add login form
squash d4e5f6a Add validation
fixup  7a8b9c0 Fix label typo

The result is one commit containing all three changes. squash and fixup fold their commit into the preceding group, which is why the first selected commit normally remains pick.

text
Before: base -- A -- B -- C  (feature)
After:  base -- ABC          (feature)

The combined commit gets a new object ID; the original objects remain reachable through the backup branch until you remove that reference.

pick, squash, fixup and reword

ActionWhat happens
pickReplay the commit as a separate commit
squashFold it into the preceding commit and edit the combined message
fixupFold it into the preceding commit and discard this commit's message
rewordKeep the change but edit its message

Plain fixup is useful for a typo correction whose message adds no information. Its -c and -C variants have different message behavior; the table describes the plain action.

For a fix intended for a specific earlier commit:

bash
git commit --fixup=a1b2c3d
git rebase -i --autosquash HEAD~5

The target must be included in the selected range. Autosquash moves the marked fix next to its target and sets the appropriate action. Review the plan instead of accepting a range that misses the commit you meant to amend.

For one message change, use reword in the plan. If it is only the most recent commit, git commit --amend is simpler, subject to the same shared-history warning.

Conflicts, abort and recovery

When rebase stops on a conflict, edit the files, stage the resolved version and continue:

bash
git add path/to/resolved-file
git rebase --continue

To stop the rebase and return to its starting branch state:

bash
git rebase --abort

Abort discards conflict-resolution edits made during the rebase. Save any resolution work you want to keep separately before aborting. git rebase --skip discards the commit currently being replayed; it is not a generic conflict fix.

After completion, compare the final tree to the backup when the only intended change was commit grouping:

bash
git diff backup/before-squash HEAD
git log --oneline --graph --decorate -10

For a pure squash, the diff should be empty. If you also rebased onto newer upstream work, inspect those expected changes separately. Run the project's relevant tests.

If the result is wrong, keep the current state on a new branch and inspect the backup or reflog. Do not jump straight to reset --hard with uncommitted work: it can discard that work.

Choose the interactive editor

bash
GIT_SEQUENCE_EDITOR=nano git rebase -i HEAD~3

GIT_SEQUENCE_EDITOR chooses the editor for the instruction list. sequence.editor is its configuration equivalent. GIT_EDITOR or core.editor controls ordinary commit-message editing and is the fallback for the plan when no sequence editor is configured. Set GIT_EDITOR=nano as well if you want Nano for the combined commit message.

Squash a branch into the target branch

For “put this feature into one new commit on the target,” merge-squash leaves the original feature branch alone:

bash
git switch main
git merge --squash my-feature
git diff --cached
git commit -m "Add the feature"

Start with a clean working tree and an up-to-date target. Resolve conflicts if the merge reports them. --squash prepares the merge result in the working tree and index without making a merge commit or recording the feature as a merged parent. The final commit is an ordinary single-parent commit on main.

On a protected repository, do this on an integration branch and use the required pull-request flow, or use the host's squash-merge option. This operation does not rewrite my-feature, so it does not inherently need a force push.

Reset to a verified base and recommit

For a private, simple feature branch, a soft reset is another way to flatten its changes. Find the base rather than assuming that main still points at the old branch point:

bash
# Run while on the feature branch, with a clean index and working tree.
base=$(git merge-base main HEAD)
git show --no-patch --oneline "$base"
git diff --stat "$base" HEAD
# After checking the base and diff:
git reset --soft "$base"
git diff --cached
git commit -m "Combine the feature changes"

This moves the current feature branch to the base and preserves its previous tree in the index. The new commit contains the net difference from that base. It does not bring in later main changes; integrate those separately.

Do not replace the base with main without checking ancestry. If main advanced independently, reset --soft main can stage removal of changes that exist only on main. For a complex history with multiple merge bases, use a reviewed rebase or merge strategy instead of this shortcut.

Publish a rewritten private branch carefully

A local-only branch can normally be pushed without force. Rewriting a branch that already exists remotely can require a non-fast-forward update. Coordinate with anyone using it, respect repository rules and avoid rewriting shared main.

Before rewriting, fetch and inspect the remote branch, then save its exact commit ID in the same shell:

bash
git fetch origin
git log --oneline HEAD..origin/my-feature
expected=$(git rev-parse refs/remotes/origin/my-feature)

If the log reveals remote work you have not included, stop and reconcile it. After the rewrite and review:

bash
git push --force-with-lease="refs/heads/my-feature:$expected" origin HEAD:refs/heads/my-feature

This explicit lease requires the remote to still equal the reviewed commit. A teammate's later push causes rejection. The shorter --force-with-lease uses remote-tracking information, which a background fetch can update without your review. Neither form proves that your rewritten content is correct. See force-pushing safely for the full tradeoffs.

FAQ

Keep the first commit in the group as pick. Mark the later commits squash or fixup so they fold into that preceding group.

Yes. From the target branch, git merge --squash my-feature prepares one combined change to commit. The original feature branch is left untouched.

No. A local-only branch or a squash merge onto a target can use a normal push. Publishing rewritten commits over an already-pushed branch may require a permitted, reviewed force update.

See also

Sources

Authoritative references this article was fact-checked against.

Tagsgitsquash commitsinteractive rebasegit rebasefixupgit merge --squashDevOpsCLI

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Systems and Network Architect · Chief Technology Officer

Systems and network architect and Chief Technology Officer with more than two decades designing, building, and running production software, cloud and network architecture, Linux systems, and the bare metal underneath them, and lately working AI into the stack. A US Army veteran who served in Operation Iraqi Freedom. What I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

Using Git with a WordPress project: what to track, what to ignore, and how to deploy.

How to Use Git with WordPress

How to put a WordPress project under Git: what to track vs ignore, a ready .gitignore, version-controlling just your theme or plugin, and deploy options.

How to use a .gitignore file to stop Git from tracking build output, dependencies, and secrets

How to Use .gitignore (with Examples)

A practical guide to .gitignore: pattern syntax, per-repo vs global ignore, ready-made templates, and the gotcha that trips everyone up - already-tracked files keep showing up.