Reflect on your 2024 year in code

Rebasing and updating refs in git

Sara Verdi
Sara Verdi
Graphite software engineer
Try Graphite


Note

This guide explains this concept in vanilla Git. For Graphite documentation, see our CLI docs.


When you work on multiple features and bug fixes in parallel, it’s common to create multiple branches off a shared base branch, often the main branch, to keep work organized. Over time, these branches evolve, gain commits, and diverge from their original base. This can lead to complex code histories and complicated merge scenarios. One way to maintain a clean and understandable commit history is to use git rebase. Rebasing modifies the commit history by moving or "replaying" your commits on top of a new base commit, effectively creating a linear history without unnecessary merge commits.

In addition to just rebasing a single feature branch, a common scenario involves updating references (or "refs" in Git’s internal storage) to handle multiple interdependent branches. When you have a series of branches stacked on top of each other or a full branch hierarchy, you need a cohesive git rebase strategy for stacked branches. By carefully updating refs and ensuring that each child branch follows its parent’s updated position, you can maintain a logical progression of commits across multiple branches.

This guide will show you how to rebase and update refs in Git, as well as manage multiple branches with a clear rebase strategy.

Git stores the current state of your repository using references (refs). A ref is essentially a pointer to a commit hash. For instance, refs/heads/main points to the tip of your main branch. When you run a git rebase update refs operation, Git takes the existing commits from one branch and replays them onto another, recalculating their commit hashes and updating the refs accordingly. This process:

  • Reads the commits from your current branch.
  • Temporarily stores them as patches.
  • Checks out the target base you are rebasing onto (such as main).
  • Applies the stored patches on top of the new base.
  • Updates the ref of your current branch to the new tip commit.

By manipulating these refs, you can ensure that a whole stack of branches remains aligned, all pointing to the correct commits in a clean linear chain.

When you have a chain of branches dependent on one another, a git rebase strategy for stacked branches helps you maintain clarity and order. Stacked branches usually have a structure like this:

Terminal
main
\
feature-A
\
feature-B
\
feature-C

If main moves forward (for instance, when someone merges new changes into it), you might want to rebase feature-A onto the updated main, and then rebase feature-B onto feature-A, and finally feature-C onto feature-B. Doing so keeps all the branches current with the latest changes from upstream while preserving the logical chain of commits.

You can perform this operation manually by starting from the bottom of the stack and moving upwards:

Terminal
# update feature-A
git checkout feature-A
git rebase main
# now update feature-B
git checkout feature-B
git rebase feature-A
# finally update feature-C
git checkout feature-C
git rebase feature-B

This sequence ensures that each branch gets rebased onto its now-updated parent branch, preserving the hierarchy without introducing merge commits or tangled histories.

When you need to git rebase multiple branches in bulk, you might find it time-consuming to manually move each branch. Since git rebase is fundamentally about rewriting commit history, consider scripting or automating this process if you have a large number of branches. Under the hood, a scripted solution would:

  • Parse your list of branches.
  • Determine the correct base branch for each.
  • Run git rebase in sequence, updating refs step-by-step.

For example, a simple script might look like this in a terminal session:

Terminal
for BRANCH in feature-A feature-B feature-C
do
echo "Rebasing $BRANCH..."
git checkout $BRANCH
git rebase $(git merge-base $BRANCH main)
done

This script uses git merge-base (a Git command that finds the best common ancestor commit) to determine where each branch should be rebased onto main. After completion, you’ll have a clean hierarchy of branches all pointing to updated refs.

Rebasing can sometimes introduce conflicts. This happens when the patches applied during rebase clash with changes in the new base branch. To resolve these conflicts:

  1. Use git status to see which files have conflicts.
  2. Edit the conflicted files to fix the issues.
  3. Use git add to mark them as resolved.
  4. Run git rebase --continue to proceed with the rebase process.

Under the hood, Git’s merge machinery compares the changes made in each commit you are replaying against the target branch. When conflicts arise, you must manually guide Git on how to combine these changes before it can update the ref and finalize the new commit history.

Tools like Graphite introduce advanced capabilities for managing and automating these workflows. Graphite’s merge queue feature automates the final steps of merging your stacked branches into main, ensuring that once a branch is approved and passes all tests, it gets merged in the correct order.

Without a merge queue, you might have to manually rebase and merge each dependent branch to maintain a coherent chain. With Graphite’s merge queue, you can rely on automated checks and merges, so you always have an up-to-date main branch at the end. This simplifies the continuous integration pipeline and reduces the burden of manually updating refs every time one branch lands before the others.

Graphite's merge queue optimizes the merging of stacked branches by automating the rebase process. This feature is particularly useful for teams dealing with frequent breakages in the trunk branch or slowdowns from manual rebasing. Here's how it helps:

  • Automatic rebasing: Graphite's merge queue automatically rebases branches when added to the queue.
  • Parallel processing: The merge queue can process and validate entire stacks in parallel, significantly reducing the time it takes to merge complex branch hierarchies.
  • Conflict reduction: By ensuring that branches are tested against the latest trunk state before merging, the merge queue reduces the likelihood of semantic merge conflicts.
  • To set up the Graphite merge queue, you need to configure branch protection rules in your GitHub repository and connect it with Graphite's GitHub app. Once configured, your PRs can be managed through Graphite's interface, providing a seamless and efficient merging experience.

By combining a solid rebase strategy for stacked branches with an automated tool like Graphite’s merge queue, you can maintain a clean, linear commit history even in complex multi-branch workflows.

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