Most Git tutorials explain rebase, merge, and fast-forward as if they're three equal options. They're not. They're three different answers to a question most developers never consciously ask: what should the commit history look like after this branch is integrated?
That question shapes everything. Fast-forward is the simplest—no extra commits, linear history, done. Merge preserves the full branch structure with a merge commit. Rebase rewrites your commits onto a new base, producing a linear history that looks like you never branched. Each produces a meaningfully different git log, and each implies a different workflow.
The general git rebase vs merge comparison covers the philosophical differences. This post is the next level: how fast-forward fits in, what real teams do day to day, and how to configure the platforms you use. If you're learning rebase for the first time, our git rebase tutorial is the hands-on companion. If you're on Bitbucket or GitLab, our Bitbucket/GitLab rebase vs merge guide covers the platform-specific options.
What Fast-Forward Actually Means
Fast-forward is the simplest integration. When the target branch hasn't changed since your feature branch was created, Git can just move the target branch's pointer forward to your latest commit. No new commit is created. No branch structure is recorded. The commits just... appear on the target.
git checkout main
git merge feature
# Fast-forward
# Updating a1b2c3d..e5f6g7h
If main and feature diverged—if someone else merged something to main after you branched—fast-forward isn't possible. Git falls back to creating a merge commit (with default --ff behavior) or refuses the merge entirely (with --ff-only).
The result of a fast-forward merge is a linear history. git log --oneline shows a straight line. There's no indication that a feature branch ever existed. The commits look as if they were made directly on main.
The appeal: Clean, readable history. No merge commits cluttering the log. git bisect works predictably because there are no divergent paths.
The catch: Fast-forward only works when the target hasn't moved. In active teams where main changes frequently, you'll rarely get a clean fast-forward. You have to rebase first (to catch up with main), then fast-forward. That combination is what most "rebase workflow" teams actually use.
How Merge and Rebase Differ (In History, Not Opinion)
The mechanics are covered in our rebase vs merge post, so I'll be brief here. What matters for this comparison is the shape of history each one produces.
Merge creates a merge commit with two parents. The branch structure is preserved. You can see exactly when the feature was integrated, and git log --graph shows the fork and the merge.
Rebase rewrites your commits on top of the target branch. The original commits are abandoned, and new commits with new hashes appear. The history is linear—no branch structure is preserved.
Here's the same scenario visualized. Suppose main has commits A, B, C, and your feature branch has D, E, F on top of B:
After merge:
A --- B --- C --- M (main)
\ /
D --- E --- F (feature)
After rebase + fast-forward:
A --- B --- C --- D' --- E' --- F' (main)
The ' marks indicate new commit hashes. The commits are the same changes, but they're different commits because their parent is different.
The difference is not just cosmetic. It affects:
git bisectbehavior. Linear history makes bisecting predictable. Merge history requires choosing a parent at each merge commit.git logreadability. Linear history reads like a narrative. Merge history shows structure but is noisier.- Revertability. Reverting a merge commit is a single operation. Reverting a rebased set of commits requires reversing each one or squashing them first.
- Blame attribution. Rebase preserves the author of each commit. Merge preserves the committer of the merge commit, but the underlying commits keep their original authorship in both cases.
The Real-World Workflows
Most teams settle into one of three workflows. Each one uses these three commands differently.
Workflow 1: The Merge Commit Team
This is the default in most organizations. Developers create feature branches, push commits freely (including "WIP" and "fix typo" commits), and merge into main with a merge commit.
The flow:
- Create a feature branch off
main. - Commit freely—no discipline required at this stage.
- Open a pull request.
- Reviewers see individual commits, but the noise is tolerated because the merge commit will be the permanent record.
- Merge via the platform's "Create a merge commit" option.
What the history looks like: main is a series of merge commits, each marking the integration of one feature branch. The commit graph shows the branch structure.
Why teams choose it: It's non-destructive, easy to understand, and never rewrites shared history. New developers don't have to learn rebase. Reviewers see exactly what was on the branch.
The tradeoff: The main history becomes noisy over time. Half the commits are merge commits. git log --oneline on main shows the pattern "Merge branch 'feature/x' into main" repeated. git bisect requires choosing a parent at each merge commit. And you can't easily answer "what changed in this release?" without filtering out merge commits.
Workflow 2: The Rebase-and-Fast-Forward Team
This is the workflow for teams that want a clean, linear history. Developers rebase their branches against main before merging, and the merge itself is a fast-forward.
The flow:
- Create a feature branch off
main. - Commit freely during development.
- Before opening a PR, run an interactive rebase to clean up commits (squash WIPs, reword messages, reorder).
- Rebase against
mainto catch up with any new commits. - Push (with
--force-with-leasesince the history was rewritten). - Open the PR.
- Merge via fast-forward.
What the history looks like: main is a straight line of well-named commits. No merge commits. Each commit represents a logical unit of work.
Why teams choose it: The history reads like a story. git log --oneline is genuinely useful. git bisect is trivial. Release notes can be generated from the commit log.
The tradeoff: Every developer has to learn rebase. Rebasing shared branches is forbidden. Force-pushing is part of the workflow (with --force-with-lease for safety). And the "when do I rebase?" question never fully goes away—you rebase against main when you're behind, and you rebase interactively when you're cleaning up, and these are different rebases.
The full mechanics of the cleanup rebase are covered in our git rebase tutorial.
Workflow 3: The Squash-and-Merge Team
This is a middle ground. Developers commit freely on their branches, and the platform squashes all commits into a single commit on main at merge time.
The flow:
- Create a feature branch.
- Commit freely—as messy as you want.
- Open a PR.
- Reviewers see all the individual commits.
- Merge via the platform's "Squash and merge" option.
What the history looks like: main is a linear sequence of single commits, each representing one PR. The individual commits from the branch are gone—the squashed commit is the only record.
Why teams choose it: The developer experience is simple—no rebase required. The history on main is clean—one commit per feature. And each commit represents a logical unit (the PR), which makes reverting easy.
The tradeoff: You lose the granular commit history of the feature branch. If the branch had 30 commits, main gets one. For a bug that spans multiple logical changes within a single PR, this makes git bisect less precise. But for most teams, this is an acceptable tradeoff for the simplicity.
Squash-and-merge is the default in many organizations because it's the easiest to adopt. Developers don't have to learn rebase, and main still ends up reasonably clean.
How the Workflows Compare
Here's a table that lays out the tradeoffs.
| Workflow | main history |
Developer skill required | Rebase needed | Revert granularity |
|---|---|---|---|---|
| Merge commit | Branching graph with merge commits | Low | No | Per-PR or per-commit |
| Rebase + fast-forward | Linear, one commit per logical change | High | Yes | Per-commit |
| Squash + merge | Linear, one commit per PR | Low | No | Per-PR only |
Each one has a place. The merge commit workflow is best for teams that value history over cleanliness. The rebase + fast-forward workflow is best for teams that value a readable log and are willing to invest in learning rebase. The squash + merge workflow is a pragmatic middle ground that most teams end up choosing.
Configuring Your Platform
The workflows aren't just about commands—they're about the defaults you set on Bitbucket, GitLab, and GitHub.
GitHub. Repository settings let you allow or disable merge commits, squash merges, and rebases. The default is to allow all three. Most teams I've worked with configure "Allow squash merging" as the only option, which enforces the squash workflow.
GitLab. Merge methods are set at the project level. You choose one of: merge commit, merge commit with semi-linear history, or fast-forward merge. Within that, you can also enable or disable squash. For the rebase + fast-forward workflow, choose "Fast-forward merge" and enable "Squash commits when merging" if you want both linear history and per-PR commits.
Bitbucket. Bitbucket offers the most granular options: merge commit, fast-forward, fast-forward only, rebase + merge, rebase + fast-forward, squash, and squash + fast-forward only. For the rebase workflow, choose "Rebase, fast-forward." For the squash workflow, choose "Squash, fast-forward only."
We covered the Bitbucket and GitLab options in more detail in our platform-specific guide.
A Real Story: Rolling Out Rebase Across a Team
A few years ago, I joined a team that had been using merge commits for years. Their main branch was 3,000 commits deep, half of them merge commits. The git log --oneline | wc -l output looked like a phone number. Nobody could tell what had shipped last week without digging through the graph.
We proposed a switch to rebase + fast-forward. The pitch was simple: linear history, readable log, easy bisect. The response was mixed. Half the team was excited. The other half saw it as unnecessary churn.
We ran a trial for a month. During the trial, we configured Bitbucket to use "Rebase, fast-forward" for merges to main, and we documented the workflow in the contributing guide. We also set up a team rule: no rebasing shared branches, no force-pushing to main, and always use --force-with-lease when force-pushing a feature branch.
The first two weeks were rough. Developers hit the "behind on main" state often, and the answer was always "rebase your branch." Somebody would forget and try to merge, and the platform would reject it. The learning curve was real.
By week three, the team had adapted. The rebases became second nature. The main history started to look linear. By the end of the month, the git log --oneline on main was a clean sequence of descriptive commits. The team voted to keep the workflow.
The lesson: the migration is disruptive, but the destination is worth it—if the team is willing to invest in learning the workflow. For teams that aren't, squash-and-merge gets you 80% of the benefit with 20% of the disruption.
Choosing Your Workflow
Here's the decision framework I use when advising teams.
If your team is small (1–5 developers) and comfortable with Git: Use rebase + fast-forward. The clean history is worth the effort, and the learning curve is manageable in a small team.
If your team is growing (5–20 developers) or includes less-experienced developers: Use squash + merge. It's the pragmatic middle ground. Individual developers commit freely, and main gets a clean, linear history. The tradeoff—losing granular commit history—is usually acceptable.
If your team is large, distributed, or has compliance requirements: Use merge commits. The non-destructive nature of merge commits is safest at scale, and the complete branch history is valuable for auditing and debugging.
If you're unsure: Start with squash + merge. It's the least disruptive of the clean-history options, and it's easy to switch to rebase + fast-forward later if the team wants more control.
Whatever you choose, document it. Write it in the contributing guide. Set the platform defaults to match. Reviewers should know what pattern to expect when they open a PR. Ambiguity is the source of most Git friction on teams.
Common Mistakes to Avoid
Rebasing shared branches. This is the cardinal sin. If anyone else has pulled your branch, don't rebase it. Force-pushing creates chaos for everyone else.
Force-pushing without --force-with-lease. --force overwrites whatever's on the remote. --force-with-lease checks that the remote hasn't changed since you last fetched. Use the latter.
Squashing and losing authorship. When you squash commits, the squashed commit is authored by whoever runs the squash. If you want to preserve the original author's attribution, use an interactive rebase with squash and reword commands rather than the platform's squash button.
Assuming fast-forward always works. Fast-forward requires the target to be an ancestor of the source. If main has moved on, fast-forward fails. Rebase first, then fast-forward.
Mixing workflows inconsistently. If your team uses squash for some PRs and merge commits for others, the history becomes inconsistent. Pick one workflow and stick with it.
Merging without understanding the history. Before you merge, know what the history will look like afterward. git log --graph --oneline before and after helps you see the shape.
If you're working with rebase conflict resolution, our git rebase tutorial covers the --continue, --skip, and --abort commands in detail.
Wrapping Up
Git rebase, merge, and fast-forward produce three different histories. Fast-forward is the simplest, but it only works when the target hasn't moved. Merge preserves structure but adds noise. Rebase produces a clean linear history at the cost of rewriting commits.
The real-world workflows built on top of these commands—merge commit, rebase + fast-forward, squash + merge—each serve different team sizes and skill levels. Most teams I've worked with end up on squash + merge because it balances simplicity and cleanliness. Smaller, Git-savvy teams often go further and adopt rebase + fast-forward. Larger teams often stick with merge commits for safety.
The right choice isn't about which one is "best." It's about which one fits your team's skill level, your tolerance for disruption, and the value you place on a clean commit history.
For deeper dives, see our rebase vs merge comparison for the fundamentals, our rebase tutorial for hands-on practice, and our Bitbucket/GitLab guide for platform-specific configuration.
Now go make your git log something you don't dread reading.
Need help standardizing your team's Git workflow or migrating to a cleaner history? Red Surge Technology helps teams adopt version control practices that scale. Get in touch to discuss your workflow.