Merge vs Rebase Decision Matrix Jump to heading
Selecting the wrong integration strategy for a branch is one of the most disruptive mistakes in collaborative Git workflows. As part of Git Workflow Architecture & Branching Strategies, this page gives you a concrete decision framework — not a stylistic preference — grounded in branch characteristics, team size, CI/CD topology, and compliance requirements.
Prerequisites Jump to heading
Before applying this decision matrix, confirm:
Step 1 — Evaluate Branch Characteristics Jump to heading
Intent: Identify the structural facts about the branch before choosing a strategy. Opinions about “clean history” come after the facts.
# How old is the branch relative to main?
git log --oneline main..HEAD | wc -l # commit count
git log --format="%ar" main..HEAD | tail -1 # age of the oldest commit
# How many distinct authors contributed?
git shortlog -sn main..HEAD
# Has the branch already been pushed to the remote?
git fetch origin
git log --oneline origin/$(git branch --show-current)..HEAD
# Empty output = remote is up to date; non-empty = local-only commits Verification: You have three numbers — commit count, author count, and whether any commits exist only locally. These drive every decision below.
Step 2 — Apply the Decision Rules Jump to heading
Intent: Map the branch facts to a concrete strategy using the four rules below. The rules are ordered by severity; the first one that matches wins.
| Rule | Condition | Required strategy | Reason |
|---|---|---|---|
| 1 | More than one author on the branch | Merge (--no-ff) | Rebase rewrites SHAs and destroys per-author commit attribution |
| 2 | Branch already pushed and fetched by teammates | Merge (--no-ff) | Rebase forces a re-sync on every engineer who fetched the old SHAs |
| 3 | Compliance, audit, or signed-commit requirement | Merge with -S | Merge commits preserve a signed, immutable record of integration |
| 4 | Single author, not yet shared, no compliance constraint | Rebase | Linear history, no merge noise, easy git bisect traversal |
Decision logic you can encode in a pre-merge automation script:
#!/usr/bin/env bash
# Usage: ./pick-strategy.sh <branch>
# Requires Git v2.30+ and POSIX tools
BRANCH="${1:-$(git branch --show-current)}"
BASE="main"
author_count=$(git shortlog -sn "${BASE}..${BRANCH}" | wc -l)
local_only=$(git log --oneline "origin/${BRANCH}..${BRANCH}" 2>/dev/null | wc -l)
compliance="${REQUIRE_SIGNED_COMMITS:-false}" # set externally
if [[ "$author_count" -gt 1 ]]; then
echo "merge # multi-author: preserve attribution"
elif [[ "$local_only" -eq 0 ]]; then
echo "merge # already shared: preserve SHAs for teammates"
elif [[ "$compliance" == "true" ]]; then
echo "merge-signed # compliance: signed merge commit required"
else
echo "rebase # single-author, local-only, no compliance constraint"
fi Verification:
chmod +x pick-strategy.sh
./pick-strategy.sh feat/my-feature
# Expected: one of merge / merge-signed / rebase Step 3 — Execute the Chosen Strategy Safely Jump to heading
Intent: Run the integration operation with the flags that match the decision above.
Merge (--no-ff) Jump to heading
The --no-ff flag forces an explicit merge commit even when a fast-forward is possible. This preserves the branch topology and creates an unambiguous integration record.
# Ensure the local mainline is current
git fetch origin
git switch main
git merge --ff-only origin/main # fast-forward main to remote, fail if not possible
# Integrate the feature branch
git merge --no-ff feat/my-feature -m "Merge feat/my-feature: add payment gateway"
# Push the result
git push origin main Verification:
git log --oneline --graph main | head -10
# You should see a merge commit with two parent pointers (the asterisk + backslash shape) Merge with signed commit (-S) Jump to heading
# Configure signing key once (SSH key shown; GPG key also supported)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
# Signed merge
git merge --no-ff -S feat/my-feature -m "Merge feat/my-feature: add payment gateway"
# Verify the signature on the resulting merge commit
git log --show-signature -1 Verification:
git log --show-signature -1 | grep -E "Good|Valid"
# Expected: "Good signature" or "Valid signature" line SAFETY WARNING: Never use
git push --forceonmainor any protected branch. If the push is rejected, resolve the divergence withgit pull --rebaseon your personal branch — never force-push shared mainlines. Doing so rewrites shared history, invalidates other engineers’ local refs, and breaks artifact traceability in downstream pipelines.
Rebase (single-author, local-only branches) Jump to heading
Interactive rebase gives you fine-grained control over the commit sequence before integration.
# Update main, then rebase the feature branch onto it
git fetch origin
git switch feat/my-feature
git rebase origin/main
# Resolve any conflicts, then continue
# git rebase --continue (after staging resolved files)
# Push the rewritten branch — use --force-with-lease, never bare --force
git push --force-with-lease origin feat/my-feature Verification:
git log --oneline origin/main..feat/my-feature
# Confirms the branch contains only your commits, rebased onto the latest main SAFETY WARNING:
git push --forcebypasses the remote-state check. Always usegit push --force-with-leaseinstead: it rejects the push if another engineer pushed to the remote branch after your last fetch, preventing silent data loss.
Step 4 — Enforce the Strategy via Branch Protection Jump to heading
Intent: Make the decision automatic and auditable rather than relying on individual discipline.
GitHub — require merge strategy via repository settings Jump to heading
# GitHub CLI: enforce squash or rebase on pull requests
gh api repos/{owner}/{repo} \
--method PATCH \
--field allow_merge_commit=true \
--field allow_squash_merge=true \
--field allow_rebase_merge=false # disable rebase-on-merge at platform level CI policy gate (GitHub Actions) Jump to heading
The workflow below blocks any PR whose branch is not rebased onto the current main, enforcing a linear history policy before merge is permitted:
# .github/workflows/topology-gate.yml
name: History topology gate
on:
pull_request:
branches: [main, "release/*"]
jobs:
check-linear:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Reject non-rebased branches
run: |
BASE_SHA=$(git merge-base HEAD origin/main)
MAIN_SHA=$(git rev-parse origin/main)
if [[ "$BASE_SHA" != "$MAIN_SHA" ]]; then
echo "Branch is not rebased onto current main."
echo "Run: git fetch origin && git rebase origin/main"
exit 1
fi
echo "Branch is current with main — topology check passed." Verification: Open a PR whose branch is one commit behind main. The check-linear job should fail with the rebase instruction.
Integration with Adjacent Workflows Jump to heading
Feature Branch Isolation Jump to heading
Feature branch isolation defines the lifecycle rules — naming conventions, TTL limits, and permission scoping — that govern a branch before it reaches this decision point. The merge vs rebase choice is the last step in that lifecycle. Branches under the feat/ namespace with a single author and short TTL are strong candidates for rebase; hotfix/ branches touching shared infrastructure almost always warrant a signed merge commit.
Trunk-Based Development Jump to heading
In trunk-based development setup, branches live less than 24 hours and belong to a single engineer. This matches the rebase conditions almost automatically. The merge vs rebase decision simplifies to: rebase short-lived branches before opening the PR; the merge queue handles the final merge commit onto main.
CI/CD Pipeline Trigger Mapping Jump to heading
Rebase operations change commit SHAs and invalidate pipeline caches pinned to the old SHAs. After a rebase and force-push, configure your CI/CD pipeline trigger mapping to invalidate artifact caches keyed on the branch’s previous SHA. Path-aware triggers help re-run only the affected jobs rather than the full pipeline.
Troubleshooting Jump to heading
| Symptom | Likely cause | Fix |
|---|---|---|
git push --force-with-lease rejected even though you just fetched | Another engineer pushed to the branch between your fetch and push | Fetch again (git fetch origin), inspect their commits, rebase on top, then retry |
| Merge commit missing from audit log | git merge was run with --ff (fast-forward) and created no merge commit | Re-merge with --no-ff to force a merge commit, or check platform squash-merge settings |
| GPG signature missing after rebase | Rebase rewrites SHAs, invalidating old signatures | Set commit.gpgSign=true globally or pass --gpg-sign to git rebase |
| CI artifact lookup fails after rebase | Pipeline cached artifact by pre-rebase SHA | Clear the SHA-keyed cache and re-run the pipeline on the new tip SHA |
Teammate reports diverged branch after your rebase | They fetched before your force-push; their local branch still tracks old SHAs | They should run git fetch origin then git reset --hard origin/<branch> after confirming they have no local-only commits |
git bisect skips a merge commit | --no-ff merge commits have two parents; bisect can traverse both | Run git bisect start --first-parent to restrict traversal to the mainline path |
Frequently Asked Questions Jump to heading
Does rebase or merge produce a cleaner Git log? Jump to heading
Rebase produces a linear log that is easier to read with git log --oneline. Merge preserves the parallel-development context with explicit join commits, which matters when you need to reconstruct exactly when parallel work converged and who was responsible for each side. “Cleaner” depends on what question you are trying to answer with the history.
Can I use --force-with-lease after rebasing a shared branch? Jump to heading
Yes — --force-with-lease is always safer than bare --force: it verifies the remote ref matches your last fetch before overwriting. But the underlying coordination issue remains: teammates who fetched the branch before your rebase must run git fetch and rebase their own work on the new SHAs. Communicate before force-pushing any branch that others have checked out.
How do merge queues affect the merge vs rebase choice? Jump to heading
Most merge queue implementations (GitHub Merge Queue, GitLab merge trains) rebase pull requests onto the latest mainline before running CI, then merge with --no-ff. This means you get linear speculative history during validation and an explicit merge commit on mainline — combining both strategies automatically. When using a merge queue, you typically rebase locally to reduce conflicts, and the queue handles the final merge commit.
When should I squash instead of merge or rebase? Jump to heading
Squash is appropriate when the individual commits on a feature branch are noisy WIP snapshots and the branch represents a single logical unit of work. The result is one clean commit on mainline, preserving readability without rewriting individual commit SHAs across the branch. For the full trade-off analysis, see the squash and fixup strategies guidance.
Does rebasing break GPG-signed commits? Jump to heading
Yes. Rebase rewrites commit SHAs, which invalidates any existing GPG or SSH signatures. Pass --gpg-sign to the git rebase command, or set commit.gpgSign=true globally, to re-sign each replayed commit. Verify the result with git log --show-signature.
Related Jump to heading
- Feature Branch Isolation — lifecycle rules, naming conventions, and permission scoping that apply before the merge vs rebase decision
- Trunk-Based Development Setup — short-lived branch discipline that makes rebase the natural default for most integrations
- Interactive Rebase Workflows — surgical commit history cleanup to run immediately before choosing your integration strategy
- Squash and Fixup Strategies — when collapsing a branch to a single commit is preferable to both merge and rebase
- CI/CD Pipeline Trigger Mapping — how SHA changes from rebase affect pipeline cache keys and artifact references