Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@


# Stacked PR Squash Tool

A CLI tool that automates squashing stacked pull requests into single commits while preserving their unique changes. It solves the common problem of massive conflicts and duplicate histories when using GitHub's "Squash and Merge" workflow with stacked PRs.

## Features
- ✅ Validates PR stack structure before making changes
- 📦 Squashes each PR in a stack into a single commit
- 🔍 Extracts and applies only the unique diff for each PR, avoiding duplicate changes
- ⚔️ Interactive conflict resolution with `git apply --3way`
- 🛡️ Batched force-pushes with `--force-with-lease` for safety
- 🔗 Accepts branch names or GitHub PR URLs
- 📝 Uses PEP 723 inline script metadata for zero-config execution with `uv`

## Prerequisites
- Python 3.11+
- [`uv`](https://docs.astral.sh/uv/) package manager
- [`gh` CLI](https://cli.github.com/) (GitHub CLI) installed and authenticated
- Git

## Installation
No explicit installation is required. The script uses PEP 723 inline metadata, allowing `uv` to automatically manage Python versions and dependencies on the fly. Simply ensure `uv` is installed and available in your `PATH`.

## Usage

Run the tool directly from the repository root. All commands require the `--branches` (or `-B`) flag, where identifiers must be listed in order from the base of the stack to the top.

### Validate a Stack
Check if your branches form a valid stack and verify their target bases:
```bash
uv run stacked.py validate -B feature-part1,feature-part2,feature-part3
# Or using PR URLs:
uv run stacked.py validate -B "https://github.com/org/repo/pull/123,https://github.com/org/repo/pull/124"
```

### Squash a Stack
Prepare your stacked PRs for squash-and-merge:
```bash
uv run stacked.py squash -B feature-part1,feature-part2,feature-part3
```

**Options:**
- `-B, --branches`: Comma-separated list of branch names or PR URLs (in stack order)
- `-b, --base`: Target branch for the first PR (default: `develop`)
- `--fetch / --no-fetch`: Fetch remote before starting (default: `true`)

## How It Works

Manually squashing stacked PRs breaks Git's history chain, causing massive conflicts or duplicate changes. This tool avoids that by:

1. **Recording Original Tips:** Saves the original commit hashes of each branch before modifying anything.
2. **Squashing the Base PR:** Resets the first PR to its target base and creates a single commit.
3. **Extracting Unique Diffs:** For each subsequent PR, it calculates the diff between the *original* adjacent branch tips (`git diff OLD_PR1...OLD_PR2`) and applies it to the newly squashed branch using `git apply --3way`.
4. **Minimizing Conflicts:** You resolve conflicts at most once per PR, rather than once per original commit.
5. **Batched Push:** After processing, it shows a summary and force-pushes all branches with `--force-with-lease` upon confirmation.

## Post-Squash Workflow

After running the tool, your stack is ready for clean squash-merging:
1. Squash-merge the first PR into your base branch.
2. Change the target of the next PR: `gh pr edit <PR_NUMBER> --base <BASE_BRANCH>`
3. Squash-merge the second PR.
4. Repeat for the remaining PRs in the stack.

## Safety & Notes
- The tool checks for uncommitted changes and validates the stack structure before proceeding.
- All destructive operations require explicit confirmation.
- Force-pushes are batched and use `--force-with-lease` to prevent accidentally overwriting remote changes made by others.
- Requires `gh` CLI to fetch PR metadata and validate stack ordering.