Live streamJoin us for "State of code review 2024" on July 18thRegister today
Background gradient

As a developer creating pull requests in a git repo, you almost certainly use one of two distinct workflows for integrating changes from one branch into another: merging or rebasing. There are three key points in the development process where you need to do this:

  1. Updating your code change before merging to trunk: you can either rewrite the initial change commit or create a second commit on your feature branch

  2. Pulling in changes from the trunk branch to your outstanding branch: you can either rebase your branch onto the latest updates from trunk or generate a “merge commit” on your branch.

  3. Merging your changes into the trunk branch: you can either craft a merge commit that links your branch’s history with the trunk or rebase your branch onto the trunk. This latter approach simulates the instant creation of your feature as fresh trunk commits, rendering the separate branch obsolete.

Merge vs. rebase has long been a hotly debated topic among developers, but the recent popularity of trunk-based development among fast-moving companies is starting to tip the scales in favor of rebase.

Over the past decade, more and more closed-source repos have started banning merge commits on trunk and shifting to a squash-rebase-and-merge workflow. The benefits are clear: rebasing creates a cleaner, more understandable history & state of the world without the clutter of merge commits. Trunk branches remain linear, and branches function as brief, atomic diffs off the trunk. Some operations become more complex (largely due to incomplete/missing Git tooling), but the end state is a tidier history.

In a context where there's no single trunk and branches are long-lived (i.e. open-source development), forking and merging hold more significance. However, in closed-source development, having one authoritative source of truth (the trunk branch) with small, transient changes avoids the pain of merging a long-lived feature branch. Google's 2016 paper Why Google Stores Billions of Lines of Code in a Single Repository describes this well:

Trunk-based development is beneficial in part because it avoids the painful merges that often occur when it is time to reconcile long-lived branches. Development on branches is unusual and not well supported at Google, though branches are typically used for releases. Release branches are cut from a specific revision of the repository. Bug fixes and enhancements that must be added to a release are typically developed on mainline, then cherry-picked into the release branch (see Figure 6). Due to the need to maintain stability and limit churn on the release branch, a release is typically a snapshot of head, with an optional small number of cherry-picks pulled in from head as needed. Use of long-lived branches with parallel development on the branch and mainline is exceedingly rare.

Figure 6. Release branching model.

credit: Rachel Potvin, Josh Levenberg

Lastly, rebasing makes reverting much less painful, especially as projects grow to contain a larger and larger history. Merge commits have more than one parent, and Git doesn’t automatically know which parent was the trunk, and which parent was the branch you want to un-merge. The squash, rebase, and merge strategy leaves you with a single commit in main that’s easily revertible without running git revert --help.

The most common critique of the rebase workflow can be boiled down to inadequate support from native Git and GitHub. Rebase commands can be intimidating to execute, and recovering previous commit versions often requires unnerving dives into the Git ref-log. Even with everything done right, a rebase-centric workflow will flood your GitHub PR timeline with force push events. These force pushes happen even when you've merely rebased your changes onto a newer trunk, leaving the diff unchanged.

Despite these hurdles, rebasing is slowly gaining acceptance as the superior workflow for closed-source development; it just requires better tooling. One notable example of rebase-centric tooling is Phabricator, Facebook’s internal code review platform. Phabricator enforces a single trunk branch per repo, and developers create atomic “diffs” off of the trunk branch, which are then rebased onto trunk once tested and approved.


Note

Greg spends full workdays writing weekly deep dives on engineering practices and dev-tools. This is made possible because these articles help get the word out about Graphite. If you like this post, try Graphite today, and start shipping 30% faster!


Even without specialized internal tooling, rebasing is quickly becoming the preferred workflow for fast-moving teams. Based on data from the tens of thousands of repos where engineers are using Graphite, over 60% of large repos (more than 10k PRs) ban merge commits. Notably, these large repos are more than twice as likely to ban merge commits than small repos with less than 10k PRs (sample size 217 large repos, 48,560 small repos).

From a sample of 2k repos with a minimum of 1k pull requests, we can see that newer repos are also more reliably banning merge commits.

While the merge vs. rebase debate will almost certainly continue to rage across Twitter & HN for years to come, it’s pretty clear that large tech companies and the next wave of fast-moving startups are increasingly banning merge commits in favor of a rebase-centric workflow.

While the trend is clear more and more developers are moving away from merge commits, rebasing manually with the git CLI can be intimidating at first. Rebase commands can be intimidating to execute, and recovering previous commit versions often requires unnerving dives into the git ref-log. Even with everything done right, GitHub floods your feature branch timeline with “force push” events. This occurs even when you've merely rebased your changes onto a newer trunk, leaving the diff unchanged.

Luckily however there is tooling in place that can automate away a lot of the complexity associated with manually rebasing your changes. Using the Graphite CLI for instance, you can "stack" dependent PRs on top of one another, and the CLI will automatically, recursively rebase the entire stack for you upon upstream changes. This makes it easy to keep your branches in sync without having to deal with messy merge conflicts, while also helping you avoid merge commits in favor of the squash-rebase-merge workflow.

Rebasing is scary, but it doesn't have to be. Join the thousands of other companies already banning merge commits, and try out the squash-rebase-merge workflow today.


Give your PR workflow
an upgrade today

Stack easier | Ship smaller | Review quicker

Or install our CLI.
Product Screenshot 1
Product Screenshot 2