Overview
Git merges in three conceptually different ways: it records a merge commit (merge), replays commits on a new base (rebase), or collapses a branch into one commit (squash). Choose the strategy before merging; changing it after rewrites shared history. This card covers the invocations, the resulting graph shape, and the tradeoffs. For day-to-day command reference see git-commands; for interactive rebase recipes see git-rebase.
Fast-forward and no-ff
Fast-forward (default when possible) moves the branch pointer without creating a merge commit. --no-ff always creates a merge commit even when a fast-forward is possible.
| Command | Graph result | Use when |
|---|---|---|
git merge feature | Linear; no merge commit if possible | CI/CD pipelines where linear history is required |
git merge --ff-only feature | Fails if FF not possible; no commit | Enforcing “rebase before merge” policy |
git merge --no-ff feature | Always creates a merge commit | Preserving the fact that a feature branch existed |
git merge --no-ff -m "msg" feature | Merge commit with custom message | When the merge commit message should document intent |
Fast-forward loses the branch topology: once merged, there is no marker that those commits came from a feature branch. Use --no-ff when the branch boundary matters for audit or rollback.
Squash merge
Squash collapses all commits from the feature branch into a single staged diff, which you commit manually. The source branch is not recorded as a parent.
| Command | Effect |
|---|---|
git merge --squash feature | Stages all changes; no commit yet |
git commit -m "feat: add login" | Creates one commit on current branch |
git branch -D feature | Delete the branch; squash does not track it |
Squash produces clean, bisectable history on main at the cost of losing per-commit granularity from the branch. GitHub’s “Squash and merge” button does this automatically.
After a squash merge, git branch -d feature fails because git does not know the branch was merged. Use -D to force-delete.
Rebase
Rebase replays commits from the current branch on top of a target, rewriting their hashes. The result is a linear history as if the branch was started from the current tip of the target.
| Command | Effect |
|---|---|
git rebase main | Replay current branch on top of main |
git rebase --onto new-base old-base | Transplant a range of commits to a new base |
git pull --rebase | Fetch then replay local commits on remote tip |
git rebase -i HEAD~n | Interactive: squash, reorder, drop, edit commits |
git rebase --continue | After resolving a conflict, continue |
git rebase --abort | Abandon and restore original state |
Never rebase commits already pushed to a shared branch. Every commit hash changes; teammates diverge.
Conflict resolution: ours and theirs
During a conflicted merge or rebase, ours and theirs refer to opposite sides depending on the command.
| Context | ours | theirs |
|---|---|---|
git merge feature | Current branch (HEAD) | feature branch |
git rebase main | main (the new base) | current branch being replayed |
git cherry-pick <ref> | Current branch (HEAD) | the cherry-picked commit |
| Command | Effect |
|---|---|
git checkout --ours <file> | Take the “ours” version of a file |
git checkout --theirs <file> | Take the “theirs” version of a file |
git merge -X ours | Auto-resolve all conflicts in favor of current branch |
git merge -X theirs | Auto-resolve all conflicts in favor of incoming branch |
git merge -s ours | Ignore all changes from the other branch entirely |
-X ours and -s ours look similar but differ: -X is a conflict resolution hint; -s ours discards the other branch completely.
Recursive and octopus strategies
These are the -s (strategy) options used by git under the hood.
| Strategy | Invocation | Use case |
|---|---|---|
recursive | Default for two-way merge | Standard branch merges; handles renames |
resolve | git merge -s resolve | Older two-way; less rename-aware |
octopus | Default for 3+ branches | Merging many branches at once; no conflicts allowed |
ours | git merge -s ours | Keep current tree entirely; record the merge fact |
subtree | git merge -s subtree | Merging into a subdirectory |
recursive is the default since git 0.99.9 for two-branch merges. You rarely need to specify it explicitly; it is documented here because the option appears in error messages and config.
Common gotchas
- Rebase inside a rebase produces confusing conflicts.
git rebase --abortand start over from a clean merge-base. - Squash merges leave the original branch undeleted; always delete with
-Dafter squash. --ff-onlyfails loudly rather than silently creating a merge commit; add it to your branch protection or global config.git merge -s oursdiscards all incoming changes silently. Use it only to close a branch without taking its content.- After a rebase,
git push --force-with-leaseis safer than--force; it aborts if the remote moved since your fetch. - Merge commits in
git bisectare skipped automatically; squash commits are bisectable, which is whybisectworks better on squash-merged history.