diff --git a/README.md b/README.md index ff509a4..737b9a2 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,33 @@ -## README – Updating Stacked PRs After a Squash‑Merge +## Stacked PRs with squash & merge -### Why this script exists -When you work with **stacked pull‑requests**—a chain of feature branches where each PR is based on the previous one—life is good right up to the moment the bottom PR is merged with **"Squash & Merge."** -A squash merge rewrites history and then deletes the source branch, which breaks every open PR that depended on it. +### The problem -**Pain points the script eliminates** +If you want stacked pull requests on GitHub, one way to do it that stays easy for people who aren't rebase wizards is to use a simple `git push` / `git merge` workflow while working on your PRs. -| Pain | Why it happens | Consequence in GitHub UI | -|------|----------------|---------------------------| -| 1. **Descendant branches lose their base** | The commit they were branched from no longer exists on the target branch. | GitHub shows a red *"This branch is out‑of‑date with the base branch"* banner. | -| 2. **Diffs are garbage** | GitHub compares the child‑branch to `main`, not to the commit it actually diverged from. | Reviewers see a giant diff containing code from *all* earlier PRs, making review impossible. | -| 3. **"Update branch" button explodes** | The missing commits mean Git can't perform a clean rebase or merge. | Clicking *Update branch* opens a web conflict‑editor with dozens of unrelated hunks. | -| 4. **Manual recovery is tedious** | Each branch must be rebased/merged and force‑pushed one‑by‑one. | Hours of menial work and risk of errors. | +When you merge the lower PR in the stack, you just need to update the upper PR. This works fine if you use regular merge commits, but your trunk history becomes very hard to read with normal tooling like the GitHub commit history page (though you can still navigate it with `git log --first-parent`). -This action automates that recovery: -1. Replays the missing history onto every direct child PR with a synthetic three‑parent merge. -2. Recursively updates indirect descendants so they stay clean and reviewable. -3. Updates each PR's base branch so GitHub's diff & merge logic are correct. -4. Deletes the merged branch. +If you use squash & merge instead, your main branch history stays nice and clean, but now the upper PR in the stack gets a garbage diff and merge conflicts when you try to update it. This happens because the squash commit rewrites history, and GitHub can't figure out what the PR is actually trying to change. -The net result: **your stack stays green and reviewers only see the intended diff**. +### The solution + +This action tries to fix that in a transparent way. Install it, and hopefully the workflow of stacking + merge during dev + squash merge when landing works. --- -### How the action works (high level) -1. **Trigger** – Fires when a PR is closed *and* `merged == true` *and* has `merge_commit_sha` (i.e. a squash merge). -2. **Discover hierarchy** – Uses `gh pr list` to find PRs whose `base` was the merged branch; walks the tree recursively. -3. **Direct children** – For each child branch it creates a synthetic merge commit that records three parents: (a) the child's old tip, (b) the deleted branch tip, (c) the squash commit. This preserves history without re‑introducing code. -4. **Indirect descendants** – Simply merge the now‑updated parent branch; no custom commit needed. -5. **PR metadata** – Switches each direct child PR's base to the trunk branch (or next living base). -6. **Push & clean up** – Force‑pushes updated branches where necessary and deletes the obsolete branch on the remote. +### How it works + +1. Triggers when a PR is squash merged +2. Finds PRs that were based on the merged branch +3. For direct children: creates a synthetic merge commit with three parents (child tip, deleted branch tip, squash commit) to preserve history without re-introducing code +4. For indirect descendants: merges the updated parent branch +5. Updates each PR's base branch to point to trunk +6. Force-pushes updated branches and deletes the merged branch --- -### Using this action in your repository +### Setup -#### As a GitHub Action workflow -1. Create a `.github/workflows/update-pr-stack.yml` file with the following content: +Create a `.github/workflows/update-pr-stack.yml` file: ```yaml name: Update Stacked PRs on Squash Merge @@ -60,27 +51,19 @@ jobs: fetch-depth: 0 - name: Update PR stack - uses: username/test-stack@v1 + uses: Phlogistique/autorestack-action@main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` -2. Replace `username/test-stack@v1` with the appropriate repository reference (e.g., `your-username/your-repo@main` or use a version tag). -#### Repository contents -| File | Purpose | -|------|---------| -| `update-pr-stack.sh` | Bash script that performs all git/gh operations. | -| `.github/workflows/update-pr-stack.yml` | GitHub Actions workflow that runs the script after every squash merge. | -| `action.yml` | GitHub Action definition for reuse in other repositories. | +### Notes ---- - -### Caveats & Tips -* Only supports **squash merges** for the base PR. -* TODO: If a merge in the chain hits a conflict the workflow exits neutral, comments on the PR, and waits for the developer to resolve & push. A follow‑up label or comment automatically resumes the stack update. -* Very large stacks may hit GitHub rate limits; TODO: throttle or batch API calls if needed.--- +* Currently only supports squash merges +* If a merge hits a conflict, you'll need to resolve it manually +* Very large stacks might hit GitHub rate limits --- ### Credits -Inspired by *Graphite* and *Gerrit* workflows but implemented with plain git + GitHub CLI. + +Inspired by Graphite and Gerrit workflows but implemented with plain git + GitHub CLI.