Read Anthropic’s case study about Graphite Reviewer
Background gradient

When you submit a pull request (PR) on GitHub, you have a few options for merging the changes from your branch into the base branch. The three main strategies are:

  1. Create a merge commit

  2. Squash and merge

  3. Rebase and merge

Choosing the right merge method hinges on your project's goals and the importance of its history. This guide covers the range of that decision-making process, explaining when and why to select each strategy—along with practical examples of how to execute merges.

Before we dive into the specifics of each method, let’s briefly summarize how they differ at a high level:

  • Merge commit: All commits from your branch are added to the base branch history along with an extra merge commit. This method is valuable for maintaining a complete audit trail, especially for collaborators in open-source projects prioritizing transparency.

  • Squash and merge: Your branch commits get combined or “squashed” into a single commit that is then added to the base branch. Squashing simplifies history from experiments, prototypes, and bug hunts.

  • Rebase and merge: Your branch commits get “replayed” one by one on top of the latest base commit. Rebasing keeps centralized stories linear, which can be great for teams with a “mainline” workflow.

How do you decide when to create a merge commit versus squash versus rebase? Here is a quick comparison of the main tradeoffs:

Let’s explore each strategy in more depth.

When merging a pull request on GitHub, the default option creates a "merge commit" that joins the two branch histories. For example, if you have:

Terminal
* e3851e8 (HEAD, feature/new-module) Complete new module
* a867b4af Fix linting errors
* 6c24fe3 Add initial module code
| \
| * d1485a3 (main) Add login form
* | b6a79f2 Style login button
* | 924565f Fix login bug

Merging this branch would add all three commits to main, plus an additional merge commit:

Terminal
* e3851e8 (HEAD, feature/new-module) Complete new module
* a867b4af Fix linting errors
* 6c24fe3 Add initial module code
*
* d7e89c1 Merging pull requests #123
|\
| * d1485a3 (main) Add login form
* | b6a79f2 Style login button
* | 924565f Fix login bug

This approach keeps all commits intact, doesn't edit history, and marks the point of integration.

Behind the scenes, GitHub performs a git merge and commits with the --no-ff flag set to force a merge commit to occur, which guarantees the creation of a merge commit even when it's not strictly necessary.

To merge pull requests, you must have write permissions in the repository.

Here are some situations when you want to use a merge commit when merging a PR:

  • You want to preserve the entire commit history from a feature branch for context.

  • You need to maintain authorship/contribution tracking at a granular level.

  • You want to mark the precise end of a long-running feature branch.

For example, you might use merge commits when integrating work from outside collaborators into an open-source project. Seeing each of their incremental commits can make reviews easier.

The merge commit strategy is best for teams that want to retain a complete record of all changes and commits related to a feature.

The “squash and merge” option on GitHub condenses all the commits from your branch into a single combined commit. For example, if you had three commits:

Terminal
- Add login form
- Style login button
- Fix login bug

These would get squashed into a single commit:

Terminal
- Implement login feature

The summarized squash commit replaces the individual commits on your branch.

This approach keeps the default branch history clean without pulling over every small incremental change.

Technically, GitHub performs a fast-forward merge with a single squashed commit. The branch pointer simply progresses to include the work without requiring an explicit merge commit.

Here are a few reasons you may want to use the squash and merge method:

  • You want a simplified commit history focusing on meaningful milestones.

  • You need to clean up work-in-progress commits before merging.

  • Your team simply prefers to squash certain types of branches—such as feature or experimental branches—to keep the main or development branch history clean and focused on significant changes.

Squashing condenses related changes into meaningful milestones. However, you will end up losing authorship details from specific squashed commits.

Teams that prioritize a clean project history over preserving every commit find squash and merge to be the best method.

The "rebase and merge" strategy integrates a feature branch by replaying its commits onto the main branch.

For example, consider a feature branch with commits:

Terminal
* Add login form
* Style login button
* Fix login bug

A main branch with recent work:

Terminal
* Login page created

To rebase and merge:

  1. The engineer individually takes the feature branch commits and applies (or “rebases”) them after the latest main commit, effectively moving the entire feature branch to the head of the main branch.

  2. With the branches now one linear history, main can fast-forward to include the feature work.

The result is a clean project history without unnecessary merge commits:

Terminal
* Add login form
* Style login button
* Fix login bug
* Login page created

Consider applying a rebase and merge strategy in these cases:

  • Your team values maintaining a linear commit history on main branches.

  • You want to avoid merge commits cluttering up the repository.

  • You still need to preserve some commit history from the PR branch.

However, rebasing comes with a few drawbacks, including the increased likelihood of conflicts, overwriting shared commit IDs, and loss of original author timestamps. To mitigate these issues and implement rebase and merge effectively, you need to establish team-wide guidelines on what is appropriate while rebasing.

For example, require pull requests to be small and focused to minimize complexity and conflicts. As outlined in code review best practices, keeping changes contained in each pull request to less than 200 lines of code makes them easier to review and rebase without issues.

Also set a policy around appropriate rebasing and small focused pull requests, making this powerful workflow tool easier on both authors and reviewers.

That said, rebasing is appropriate for teams that want linear project history but need more commit context than a full squash would provide. With the cleaned-up commit timeline that rebasing enables, developers can more easily revert changes when needed—they can identify the specific commit that introduced an issue without lots of merge commit clutter getting in the way.

Along with that, the linear history lends itself better to following best practices like "keeping main green," where the main branch always remains in a releasable state—a branch that always successfully compiles and passes all build steps.

Having such an organized, sequential commit history of changes makes it easier for teams to track when issues may have been introduced and quickly fix them before they make it too far downstream.

When choosing a merge strategy, you should weigh the factors:

  • Commit history: Do engineers need to preserve complete records of incremental developments, or is a summarized view of meaningful milestones preferred?

  • Long-running branches: Long-lived feature branches that remain active for a long time can be difficult to maintain: they can cause issues like complex conflicts during integration or loss of context. Instead, consider using practices like short-lived topic branches.

  • Type of repository: Some repos may benefit from a more simplified history, particularly those focusing on the end product rather than the development process, making a cleaner history more valuable for readability and maintenance.

  • Team preferences: Get consensus from the team on commit history standards.

Rather than enforcing one merge scheme, thoughtfully analyze your collaboration needs and priorities and identify what would work best for you. Whichever merge practice you choose, ensure that you have it documented along with any exception-handling steps.

Is there a better way to merge?

A major source of complexity from merge methods comes from long-lived branches containing lots of commits. These tangled histories make merging harder:

  • Merge commits can intertwine disjointed work, cluttering the main branch with a convoluted history.

  • Squashing many unrelated commits can obscure the development narrative, complicating project management.

Rebasing helps resolve these problems when paired with practices like short-lived branches. This enables a stacking workflow that uses rebasing to continually integrate changes into main in small increments helping maintain a clean, linear history on main.

As you eliminate messy merge commits, you start getting a better understanding of how the code evolved by looking at the clean main branch.

Also, when the branches are kept small and short-lived, you minimize complicated merge conflicts that usually occur when rebasing large PRs.

However, the fundamental problem is that massive new features get dumped all at once in a big commit or PR that becomes difficult to manage later.

“The biggest time waster is that PRs wait for review for hours or days. We analyzed historical data for 15 million pull requests, and PRs exist which waited several years to be merged!” — Tomas Riemers

That's where stacking comes in.

Stacking uses rebasing as the primary merging method to keep history linear. Changes get incorporated frequently with stacking workflows rather than massive merges.

Engineering teams at high-velocity companies like Meta, Pinterest, and Coinbase use stacking to simplify code review and integration while unblocking developers.

The result is better teamwork through a linear main history having clear separate steps reflecting incremental work.

The key ideas behind stacking are:

  • Break large tasks into granular, independent chunks.

  • Open small pull requests continually as you make progress.

  • Build features vertically by stacking commits on top of each other.

For example, instead of a single giant PR like this:

Terminal
- #401: Feature: Added image upload

You would instead have something like this:

Terminal
- #401: Create uploader UI component
- #402: Add API post endpoint
- #403: Integrate uploader with post content
- #404: Process images upon upload
- #405: Display images in feed posts

Each unit of work gets reviewed and merged quickly without blocking other changes.

The result is a similar feature, delivered iteratively across a sequence of small pull requests rather than one batch.

Graphite is a dedicated tool for automating stacking workflows on GitHub.

It helps you:

  • Auto-sync dependent branches.

  • Create and update stacked PRs in bulk.

  • Visualize relationships between branches.

Here’s a high-level comparison of the stacked PR workflow between GitHub and Graphite CLI:

The same stack can be achieved with just three commands when using Graphite.

For example, to create a new stack:

Terminal
# Add changes
gt add -A
# Commit to new branch and add all changed files automatically
gt create -am "Part 1"
# Stack more commits and add all the changed files to the commit
gt create -am "Part 2"
gt create -am "Part 3"
# Open PRs
gt submit

Graphite handles all the underlying Git commands needed to rebase branches and propagate changes.

This simplifies merging by structuring work into a series of small, incremental pull requests rather than a single batch.

Many top tech companies have worked with stacking PR workflows. It works because PRs under 400 lines of code have a higher defect discovery rate than larger PRs.

With stacking, you want the PRs to be less than 200 lines and—ideally—50 lines of code.

The productivity benefits can be immense, but the shift does require adjusting some ingrained processes. Here are a few things you can try to help with the transition.

Encourage developers to divide features into small segments during group sessions and to create small, incremental pull requests that build upon each other. This approach helps the team become familiar with the workflow.

Prioritize tracking fully completed, shippable features over counting the number of pull requests opened. Adjust dashboards to emphasize the completion of features rather than the number of requests.

Accelerate the review process by adding more people with the authority to approve and merge changes to the foundational stack. This strategy prevents delays caused by a few individuals.

Leverage purpose-built tools—such as Graphite—to automate complex tasks like updating dependent branches and simplifying the process for everyone involved. You can also consider implementing code linters to expedite initial checks.

During meetings, call out great stacking examples and offer praise to new and experienced stackers to encourage these practices. With enough early support and motivation, it may help teams shift to implementing stacked pull requests.

Whether you choose to adopt a stacking workflow or not, the underlying principle remains: be strategic about choosing the best merge method. Here's a breakdown for applying different strategies to suit the needs of your project:

  • Open-source libraries: Use merge commits to keep all contributor history. Don't rewrite public past changes.

  • Prototypes and experiments: Squash everything into major milestones. Hide unrelated small edits.

  • Module libraries: Rebase and merge PRs to maintain clean, linear version histories that are easy to manage.

  • Fast-moving teams in production environments: Default to a stacking workflow that continually rebases into main. This integrates changes frequently in small increments while retaining authorship details. Small, stacked pull requests also enable rapid development while keeping history linear for easy debugging, reverting, and “keeping main green.”

Here's the truth—there's no magic formula we can provide to determine optimal merge practices. 

Every team and project has unique dynamics spanning the spectrum from large enterprise projects to open-source projects with hundreds of collaborators. Ultimately, it's best to identify the workflows that mesh well with your developers' approaches and the level of visibility your team seeks.

Luckily, Graphite automates many potential merge headaches that surface as a dev team grows. It makes managing pull requests and merges easy by fixing the root cause—messy commits.

With Graphite’s integrations across your GitHub repositories, messy merges get automated away behind a friendly interface that just works. You get complete visibility into each step of the feature creation process without the messy branching from main.

Want to try stacking workflows for your team? Give Graphite a spin with the free account and see how the new way of commits and merges can seriously speed up work!

Built for the world's fastest engineering teams, now available for everyone