普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月23日iOS

Maintaining shadow branches for GitHub PRs

作者 MaskRay
2026年1月22日 16:00

I've created pr-shadow, a tool thatmaintains a shadow branch for GitHub pull requests (PR) that neverrequires force-pushing. This addresses pain points I described in Reflectionson LLVM's switch to GitHub pull requests.

The problem

GitHub structures pull requests around branches, enforcing abranch-centric workflow. When you force-push a branch after a rebase,the UI displays "force-pushed the BB branch from X to Y". Clicking"compare" shows git diff X..Y, which includes unrelatedupstream commits—not the actual patch difference. For a project likeLLVM with 100+ commits daily, this makes the comparison essentiallyuseless.

Inline comments suffer too: they may become "outdated" or misplacedafter force pushes.

Additionally, if your commit message references an issue or anotherPR, each force push creates a new link on the referenced page,cluttering it with duplicate mentions. (You can work around this byadding backticks around the link text, but it is not ideal.)

Due to these difficulties, some recommendations suggest less flexibleworkflows that only append new commits and discourage rebases. Whenworking with both the latest main branch and the pull request branch,switching between branches results in numerous rebuilds.

In a large repository, avoiding rebases isn't realistic—other commitsfrequently modify nearby lines, and rebasing is often the only way todiscover that your patch needs adjustments due to interactions withother landed changes.

The solution

pr-shadow maintains a separate PR branch (e.g.,pr/feature) that only receives commits—never force-pushed.You work freely on your local branch (rebase, amend, squash), then syncto the PR branch using git commit-tree to create a commitwith the same tree but parented to the previous PR HEAD.

1
2
3
4
5
6
Local branch (feature)     PR branch (pr/feature)
A A
| |
B (amend) C1 "Fix bug"
| |
C (rebase) C2 "Address review"

Reviewers see clean diffs between C1 and C2, even though theunderlying commits were rewritten.

When a rebase is detected (merge-base with main/masterchanged), the new PR commit is created as a merge commit with the newmerge-base as the second parent. GitHub displays these as "condensed"merges, preserving the diff view for reviewers.

Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Initialize and create PR
git switch -c feature
prs init # Creates pr/feature, pushes, opens PR
# prs init --draft # Same but creates draft PR

# Work locally (rebase, amend, etc.)
git rebase main
git commit --amend

# Sync to PR
prs push "Fix bug"
prs push --force "Rewrite" # Force push if remote diverged

# Update PR title/body from local commit message
prs desc

# Run gh commands on the PR
prs gh view
prs gh checks

The tool supports both fork-based workflows (pushing to your fork)and same-repo workflows (for branches likeuser/<name>/feature). It also works with GitHubEnterprise, auto-detecting the host from the repository URL.

Related work

The name "prs" is a tribute to spr, which implements asimilar shadow branch concept. However, spr pushes user branches to themain repository rather than a personal fork. While necessary for stackedpull requests, this approach is discouraged for single PRs as itclutters the upstream repository. pr-shadow avoids this by pushing toyour fork by default.

I owe an apology to folks who receiveusers/MaskRay/feature branches (if they use the defaultfetch = +refs/heads/*:refs/remotes/origin/* to receive userbranches). I had been abusing spr for a long time after LLVM'sGitHub transition to avoid unnecessary rebuilds when switchingbetween the main branch and PR branches.

If I need stacked pull requests, I will probably use pr-shadow withthe base patch and just rebase stacked ones - it's unclear how sprhandles stacked PRs.

❌
❌