使用 git rebase —onto 进行堆叠差异
Stacked Diffs with git rebase —onto

原始链接: https://dineshpandiyan.com/blog/stacked-diffs-with-rebase-onto/

## 使用 `git rebase --onto` 掌握堆叠式 Pull Request 使用依赖特性(堆叠的 diff/PR)可以提供更小、更易于审查的代码变更,但保持分支同步可能具有挑战性。`git rebase --onto` 是干净管理此工作流程的关键。 与简单地重放提交(如常规 `git rebase`)不同,`git rebase --onto ` 会选择性地将 `` 上 *在* `` 之后的提交移动到 `` 上。 这避免了引入早期依赖项中不需要的提交。 **流程:** 1. **建立标记:** 在分支时(例如,从 `feature-1` 分支 `feature-2`),创建一个标记分支 (`feature-2-base`),指向原始 `feature-1` 提交。 2. **同步:** 当 `main` 或依赖项 (`feature-1`) 更新时,首先将依赖项 rebase 到新的 `main` 上。 然后,使用 `git rebase --onto ` 将你的分支 rebase。 *至关重要的是,更新标记分支以指向新的依赖项提交。* 3. **清理:** 在依赖项合并后,使用交互式 rebase (`git rebase -i main`) 从你的分支历史记录中删除其提交。 这种方法需要强制推送 (`git push --force-with-lease`) 以及在更新标记分支方面的纪律。 虽然比单个分支更复杂,但它能为大型特性提供更干净的历史记录和更快的审查。

这个Hacker News讨论围绕着Git中管理堆叠的拉取请求(PR)。dineshpandiyan.com上的原始文章详细介绍了一种使用`git rebase —onto`的方法,以便在处理多个依赖性更改时获得更清晰的历史记录。 然而,评论者提出了替代方案。一位用户推荐`git rebase --update-refs`作为更简单的解决方案。另一位用户高度赞扬`git-spice`,这是一种旨在改善GitHub和GitLab等平台上小型、专注PR工作流程的工具。第三位用户更喜欢交互式变基(`git rebase origin/main -i`),以便明确控制和检查更改。 最后一位评论者,一位中级开发人员,表示犹豫,认为所提出的方法过于复杂,并且可能存在历史管理风险,同时承认了对更小PR的需求。这次对话突出了复杂的Git命令与易用性/风险之间的权衡。
相关文章

原文

tldr; Use git rebase --onto to cleanly rebase a dependent branch without dragging along commits that don’t belong to it.

1git rebase --onto <new-base> <old-base> <branch>

If you’ve ever worked on a larger feature and split your work into multiple PRs that depend on each other, you’ve probably experienced the pain of keeping them in sync. This workflow is called stacked diffs (or stacked PRs), and it’s incredibly powerful. But it comes with a learning curve. The secret weapon? git rebase --onto.

Here’s what we’ll cover:

  • Why stacked diffs are worth the effort
  • The difference between a regular git rebase and git rebase --onto
  • Step-by-step: first sync, ongoing syncs, and post-merge cleanup

# Why Stacked Diffs?

Let’s say you’re building a large feature. You could dump everything into one massive PR, but reviewers hate that. Large PRs get superficial reviews (or no reviews at all), and you end up waiting forever for approvals.

Stacked diffs solve this by breaking your work into smaller, dependent PRs:

main
  └── feature-1 (auth layer)
        └── feature-2 (user profile)
              └── feature-3 (profile settings)

Each PR is small, focused, and easy to review. The catch? When main updates or when feature-1 gets rebased, you need to sync all the downstream branches. That’s where most people get stuck.

# Regular rebase vs rebase –onto

# Regular rebase

A regular git rebase main replays your commits on top of the target branch:

Before:
main:      A---B---C
                \
feature:         D---E

After git rebase main:
main:      A---B---C
                    \
feature:             D'---E'

Simple enough. But what happens with stacked branches?

# The Problem with Stacked Branches

Here’s a typical stacked setup:

main:        A---B---C
                  \
feature-1:         D---E
                        \
feature-2:               F---G

Now main gets updated with new commits:

main:        A---B---C---H---I
                  \
feature-1:         D---E
                        \
feature-2:               F---G

You rebase feature-1 onto main:

main:        A---B---C---H---I
                  \          \
old:               D---E      D'---E'  ← feature-1 (new hashes!)
                        \
feature-2:               F---G  ← Still based on old D---E!

See the problem? feature-2 is still based on the old D---E commits. If you try a regular git rebase feature-1 on feature-2, git will try to include those old commits again and you’ll end up with duplicates or conflicts.

# Enter: git rebase –onto

This is where git rebase --onto shines. It lets you specify exactly which commits to move and where to put them:

git rebase --onto <new-base> <old-base> <branch>
                      ↑          ↑          ↑
                new parent  old parent   branch to rebase

Think of it as saying: “Take everything after <old-base> on <branch>, and replay it onto <new-base>.”

# Step-by-Step: Using rebase –onto

# First rebase –onto

When you first create feature-2 off of feature-1, also create a marker branch:

1git checkout feature-1
2git checkout -b feature-2
3
4# feature-2-base is your marker
5# when you update feature-1 later,
6# the marker will have feature-1 branch's previous state
7git branch feature-2-base feature-1

The first time main updates and you need to sync your stack:

1# 1. Rebase feature-1 onto main
2git checkout feature-1
3git rebase main
4
5# 2. Rebase feature-2 onto the updated feature-1
6git rebase --onto feature-1 feature-2-base feature-2
7
8# 3. Update the marker branch after successful rebase
9git branch -f feature-2-base feature-1

That last step is critical. Without it, your next sync will break.

# Syncing main

Every time main updates, you repeat the same pattern:

1# Rebase feature-1 onto main
2git checkout feature-1
3git rebase main
4
5# Sync feature-2
6git rebase --onto feature-1 feature-2-base feature-2
7git branch -f feature-2-base feature-1  # ← Don't forget!

The marker update isn’t optional. It’s what makes repeat syncs work.

# Once a feature branch merges

When feature-1 finally lands in main, you no longer need its commits in your feature-2 history. Here’s how to clean up:

1git checkout feature-2
2git rebase -i main

In the interactive rebase, you’ll see all commits including the ones from feature-1:

pick abc123 D' ...  ← DELETE (from feature-1)
pick def456 E' ...  ← DELETE (from feature-1)
pick 789ghi F  ...  ← KEEP (your work)
pick 012jkl G  ...  ← KEEP (your work)

Delete (or mark as drop) the commits from feature-1, and Git will replay only your feature-2 commits directly onto main.

# Putting it all together visually

BEFORE REBASE:
==============
main:             A---B---C---H---I
                       \
feature-1:              D---E              (needs rebase onto main)
feature-2-base:             * (marker pointing to E)
                             \
feature-2:                    F---G


AFTER REBASING FEATURE-1:
=========================
main:             A---B---C---H---I
                       \          \
old commits:            D---E      D'---E'  (feature-1, new hashes!)
feature-2-base:             * (still pointing to old E!)
                             \
feature-2:                    F---G         (orphaned on old commits)


AFTER REBASE --ONTO:
====================
main:             A---B---C---H---I
                                  \
feature-1:                         D'---E'
feature-2-base:                         * (updated to new E')
                                         \
feature-2:                                F'---G'  (synced!)

# Closing Thoughts

git rebase --onto is one of those commands that looks intimidating but becomes second nature once you understand what each parameter does:

The marker branch pattern takes the guesswork out of tracking the old base. Use it, update it, and your stacked diffs will stay clean.

Here are a few thoughts to keep in mind when using this workflow:

  1. Force pushes are required: Every rebase changes commit hashes, so you’ll be doing git push --force-with-lease a lot.

  2. Marker branches need discipline: If you forget to update your marker, your next sync will be painful. Consider aliasing the full command:

    1alias gsync='git rebase --onto $1 $2-base $2 && git branch -f $2-base $1'
    
  3. Merge conflicts multiply: If you have conflicts when rebasing feature-1, you might hit them again when rebasing feature-2. That’s the nature of the beast.

  4. Don’t stack too deep: Two or three levels is manageable. Beyond that, the maintenance overhead outweighs the benefits. I personally try to keep it at 2 levels max.

Is this workflow more complex than just having one big branch? Absolutely. But the payoff (smaller PRs, faster reviews, and cleaner history) is worth the investment. Just remember to update those marker branches!

Happy rebasing! Have a great day!

联系我们 contact @ memedata.com