ghls-stack-pr
v0.1.5
Published
Stacked PR CLI — make small, reviewable PRs the default workflow
Maintainers
Readme
⚡ ghls
Git, but with Stacked PR Superpowers
Stop shipping 2,000-line PRs that nobody wants to review.
┌─────────────────────────────────┐
│ ⚡ G H L S │
│ Git + Stacked PRs, Supercharged│
└─────────────────────────────────┘Quick Start · Commands · How It Works · Configuration · GitHub Actions
What is this?
ghls is a drop-in replacement for git that adds stacked PR superpowers on top. Every command you already know (ghls status, ghls commit, ghls log) passes straight through to git. But when you need to ship a stack of small, reviewable PRs — that's where the magic kicks in.
You write code normally. ghls does the rest.
┌─ #4 fix: edge case in notifications +12 -3 ✅ CI passing
├─ #3 feat: notification preferences +45 -12 🔄 In review
├─ #2 feat: user settings page +120 -30 ✅ Approved
└─ #1 feat: settings data model +80 -5 ✅ Approved
▼
mainWhen PR #1 gets merged, run ghls land and #2, #3, #4 automatically rebase onto the new main. No manual branch juggling. No merge conflicts from stale bases. Just vibes.
Why Stacked PRs?
| Mega PR | Stacked PRs with ghls | |---|---| | 1,500 lines in one shot | 3 focused PRs of ~200 lines each | | "LGTM" after 2 days of procrastination | Meaningful review in minutes | | Merge conflicts from hell | Auto-rebase keeps everything fresh | | Blocks the whole team | Land PRs independently as they're approved | | Reviewer: 😩 | Reviewer: 😎 |
🚀 Quick Start
Prerequisites
Before installing ghls, make sure you have:
| Dependency | Version | How to check | Install |
|---|---|---|---|
| Node.js | >= 18 | node --version | nodejs.org |
| GitHub CLI | any | gh --version | cli.github.com |
| Git | any | git --version | You probably have this already |
Important: Make sure you're authenticated with GitHub CLI: run
gh auth statusto check. If not, rungh auth login.
Install
npm install -g ghls-stack-prYour first stack in 60 seconds
# 1. Navigate to any GitHub repo
cd your-repo
# 2. Initialize ghls (one-time setup, takes 5 seconds)
ghls init
# 3. Write code and make commits as usual
git add .
git commit -m "feat: add settings data model"
git commit -m "feat: add settings UI"
# 4. Ship it as stacked PRs
ghls submit
# 5. See your beautiful stack
ghls stack
# 6. After PR #1 is approved and merged on GitHub...
ghls land # rebases the rest onto main automaticallyThat's it. You're stacking. 🎉
Pro tip: alias git to ghls
Since ghls passes unknown commands to git, you can replace git entirely:
# Add to your ~/.zshrc or ~/.bashrc
alias git=ghlsNow git commit, git push, git log all work normally, but you also get git submit, git stack, git land for free.
📖 Commands
Stack Commands
ghls init
One-time repo setup. Creates a .ghls.yml config file, detects your GitHub remote, and verifies gh is installed.
ghls initghls submit (alias: up)
Creates or updates stacked PRs from your commits. The heart of ghls.
ghls submit # interactive mode
ghls submit --one-click # fully automated, no prompts
ghls submit --draft # create as draft PRs
ghls submit --reviewer alice,bob # assign reviewers
ghls submit --dry-run # see the plan without executing
ghls submit --force # bypass size limits
ghls submit --group semantic # choose grouping strategy| Flag | Short | Description |
|---|---|---|
| --one-click | | Non-interactive mode — auto-groups, skips prompts, uses defaults |
| --draft | -d | Create PRs as drafts |
| --reviewer <names> | -r | Comma-separated list of GitHub usernames to request review from |
| --yes | -y | Skip confirmation prompts |
| --force | | Bypass size validation limits |
| --dry-run | | Show what would happen without doing it |
| --group <strategy> | -g | Grouping strategy (see Grouping Strategies) |
ghls land (alias: merge)
Merges the bottom PR(s) and rebases the rest of the stack.
ghls land # merge bottom PR (interactive)
ghls land --one-click # merge all, no prompts
ghls land --all # merge entire stack
ghls land --count 2 # merge bottom 2 PRs
ghls land --dry-run # preview the plan
ghls land --conflict theirs # auto-resolve conflicts| Flag | Short | Description |
|---|---|---|
| --one-click | | Equivalent to --all --yes — merges everything non-interactively |
| --all | | Land all PRs in the stack (bottom-up) |
| --count <n> | -n | Number of PRs to land from the bottom |
| --yes | -y | Skip confirmation |
| --dry-run | | Show the merge plan without executing |
| --conflict <strategy> | | Conflict resolution: manual, ours, theirs, abort |
ghls sync
Rebases your entire stack onto the latest base branch. Run this when main has been updated.
ghls sync # rebase stack onto latest main
ghls sync --push # also force-push branches after sync
ghls sync --conflict ours # auto-resolve with local changes
ghls sync --continue # continue after manual conflict resolution
ghls sync --abort # abort an in-progress rebase| Flag | Description |
|---|---|
| --push | Force-push branches after rebase |
| --force | Overwrite remote branches even if they're ahead |
| --conflict <strategy> | manual (default), ours, theirs, abort |
| --continue | Resume after you've resolved conflicts manually |
| --abort | Cancel the in-progress rebase and rollback |
ghls stack (alias: sk)
Visualize your current stack. Read-only, never modifies anything.
ghls stack # show stack overview
ghls stack --size # include detailed size breakdown
ghls stack --no-remote # skip fetching PR info from GitHubExample output:
Stack: 3 PRs on main
┌─ #103 feat: notification preferences +45 -12 🔄 Review pending
├─ #102 feat: user settings page +120 -30 ✅ Approved
└─ #101 feat: settings data model +80 -5 ✅ Approved, CI passing
▼
main (2 commits behind)ghls abandon
Close all PRs in the stack, delete remote branches, and clean up metadata.
ghls abandon # interactive confirmation
ghls abandon --keep-local # close PRs but keep local branches/metadataUtility Commands
ghls config
View or update configuration.
ghls config --show # display all merged config
ghls config mergeMethod # get a single value
ghls config mergeMethod rebase # set a value
ghls config maxPrSize 400 --global # set in user-level config| Flag | Description |
|---|---|
| --show | Display the fully merged configuration |
| --global | Read/write from user-level config (~/.config/ghls/config.yml) |
ghls stats
Your personal stacked PR analytics dashboard.
ghls statsShows: total commands run, stacks created, PRs created & landed, sync count, conflicts resolved, average PR size trends, weekly size charts, and estimated review time savings.
ghls history
View the operation log — every submit, land, sync, and abandon is recorded with timestamps and backup references.
ghls history # show recent operationsghls undo
Revert the last ghls operation by restoring from its automatic backup.
ghls undo # restore previous stateghls cleanup
Remove old backup branches (older than 7 days).
ghls cleanupCommand Aliases Cheat Sheet
ghls submit → ghls up
ghls stack → ghls sk
ghls land → ghls merge🔧 How It Works
The Core Idea
Each commit on your working branch becomes its own PR. PRs are chained so that each one's base is the previous PR's branch:
Your commits: The PRs ghls creates:
commit C PR #3 (base: PR #2's branch)
commit B PR #2 (base: PR #1's branch)
commit A PR #1 (base: main)This means reviewers see small, focused diffs — not the entire feature at once.
Metadata: How ghls Tracks the Stack
ghls stamps git trailers on your commits to track everything:
feat: add settings data model
ghls-id: a1b2c3d4-5678-9abc-def0-1234567890ab
ghls-stack: e5f6a7b8
ghls-order: 1| Trailer | Purpose |
|---|---|
| ghls-id | Unique ID per commit (UUID) |
| ghls-stack | Shared identifier for the entire stack (8-char UUID prefix) |
| ghls-order | Position in the stack (1 = bottom, first to merge) |
These trailers are added automatically during ghls submit and are used for stack detection, ordering, and integrity validation.
Branch Naming
ghls creates branches using a configurable template:
Default: ghls/{user}-{n}
Example for user "alice" with a 3-PR stack:
ghls/alice-1 → PR #1
ghls/alice-2 → PR #2
ghls/alice-3 → PR #3| Placeholder | Value |
|---|---|
| {user} | Your GitHub username |
| {stack} | Stack ID (8-char UUID) |
| {n} | Position in stack (1-based) |
Git Passthrough
Any command ghls doesn't recognize is forwarded directly to git:
ghls status → git status
ghls commit -m → git commit -m
ghls log --oneline → git log --oneline
ghls diff → git diff
ghls push → git push (with a helpful hint if you're in a stack)ghls intercepts push on stack branches to suggest using ghls submit instead, but still runs the push if you want it.
⚙️ Configuration
Config File
ghls init creates .ghls.yml in your repo root:
baseBranch: main
branchTemplate: "ghls/{user}-{n}"
mergeMethod: squash # squash | merge | rebase
maxPrSize: 500 # hard block above this
warnPrSize: 300 # warning above this
maxStackEntries: 30 # max PRs in one stack
excludePatterns:
- "*.lock"
- "*.generated.*"
- "package-lock.json"
- "yarn.lock"
- "pnpm-lock.yaml"
groupingStrategy: single # single | marker | semantic | file | interactive | auto
autoSync: false # auto-sync before submit
analytics: true # local usage analytics
remote: origin # GitHub remote nameConfig Hierarchy
Configuration is merged in this order (later wins):
┌─────────────────────────────────────────────────────┐
│ CLI flags (highest) │
│ .ghls.yml (repo) │
│ ~/.config/ghls/config.yml (user) │
│ Built-in defaults (lowest) │
└─────────────────────────────────────────────────────┘This means you can set personal defaults globally and override per-repo.
All Config Options
| Option | Default | Description |
|---|---|---|
| baseBranch | main | The branch your stack targets |
| branchTemplate | ghls/{user}-{n} | Branch naming pattern |
| mergeMethod | squash | How PRs are merged: squash, merge, or rebase |
| maxPrSize | 500 | Hard block — PRs above this line count are rejected |
| warnPrSize | 300 | Warning threshold — you'll get a heads-up |
| maxStackEntries | 30 | Maximum number of PRs in a single stack |
| excludePatterns | *.lock, *.generated.*, etc. | Glob patterns excluded from size counting |
| groupingStrategy | single | How commits are grouped into PRs |
| autoSync | false | Automatically rebase onto base before submit |
| analytics | true | Collect local usage analytics |
| remote | origin | Name of the GitHub remote |
📏 Size Validation
ghls keeps your PRs small by default. Not all lines are created equal, so different file types are weighted differently:
File Type Weight Example
───────── ────── ───────
Code 1.0x src/api/users.ts
Tests 0.5x tests/users.test.ts
Migration 0.7x migrations/001_add_users.sql
Config 0.3x tsconfig.json
Docs 0.2x docs/api.md
Generated 0.0x package-lock.json (excluded)How It Works
- ghls calculates the weighted line count for each PR
- Lines in
excludePatternsfiles are ignored entirely - The weighted total determines the verdict:
Weighted lines < 300 → ✅ Good to go
Weighted lines < 500 → ⚠️ Warning (still submits)
Weighted lines > 500 → 🚫 Blocked (use --force to override)- Review time estimate: ~200 weighted lines per hour
Use ghls stack --size for a full breakdown by category.
🔀 Grouping Strategies
When you have 2+ new commits, ghls submit shows an interactive picker:
How should these new commits become PRs?
1 Each commit = 1 PR (single)
2 Group all into 1 PR
3 Use suggested grouping above ← only shown if ghls found a smarter split
4 Group by [stack] markers
5 Group by type (feat/fix/refactor)
6 Group by file scope (directory)
7 Interactive — manually assign commits to PRs| # | Strategy | Behavior |
|---|---|---|
| 1 | single | Each commit becomes its own PR (default) |
| 2 | all-in-one | Every commit is squashed into a single PR — you pick the title |
| 3 | auto (suggested) | ghls analyzes marker/semantic/file strategies and suggests the best split. Only appears when a non-trivial grouping exists |
| 4 | marker | Groups commits that share a [stack:name] or [group:name] tag in their message |
| 5 | semantic | Groups consecutive commits with the same conventional commit type (feat, fix, refactor, etc.) |
| 6 | file | Groups commits by top-level directory scope — commits touching the same area go together |
| 7 | interactive | Shows all commits numbered, you type ranges (e.g. 1,2,3 or 1-3) to assign them to PRs manually |
With --one-click, ghls skips this prompt and uses the auto strategy automatically.
Set a default in .ghls.yml or override per-submit:
ghls submit --group semantic # skip the picker, use semantic
ghls submit --group file # skip the picker, group by directory
ghls submit --one-click # auto strategy, no prompts at all🔥 Conflict Resolution
When a rebase produces conflicts, ghls gives you options:
| Strategy | What happens |
|---|---|
| manual | Drops you into your editor to resolve, then ghls sync --continue |
| ours | Keeps your local changes, discards incoming |
| theirs | Accepts incoming changes, discards yours |
| abort | Cancels the rebase entirely and rolls back |
ghls includes an interactive conflict wizard that analyzes the conflicts, shows their difficulty, and lets you pick a strategy. It handles multiple conflict rounds in a loop until everything's resolved.
# Automatic resolution
ghls sync --conflict theirs
# Manual resolution workflow
ghls sync # hits a conflict
# ... fix the files ...
git add .
ghls sync --continue # resume🤖 GitHub Actions
ghls ships 4 optional workflows. Copy them from the actions/ directory into your repo's .github/workflows/:
cp node_modules/ghls-stack-pr/actions/*.yml .github/workflows/auto-rebase.yml
Trigger: PR merged
Automatically detects when a ghls-managed PR is merged, finds dependent PRs in the stack, updates their base branches, and attempts an auto-rebase. Comments on the PR with success/failure status.
stack-validate.yml
Trigger: PR opened or updated
Validates stack integrity — checks that base branch chaining is correct and the stack metadata is consistent. Posts a validation comment on the PR.
ci-optimize.yml
Trigger: After auto-rebase completes
Cancels redundant CI runs when branches get force-pushed. Groups workflow runs by branch, keeps the latest, cancels everything older. Saves your CI minutes.
review-preserve.yml
Trigger: PR force-pushed (sync event)
Detects "cosmetic" rebases (same tree hash, different commit hash) and re-requests reviews from previous approvers. This way, approvals aren't lost just because ghls rebased the stack.
🛡️ Safety
ghls is paranoid about your code. Here's how it keeps you safe:
| Feature | Description |
|---|---|
| Automatic backups | Every destructive op (submit, land, sync, abandon) creates a backup branch first |
| Undo | ghls undo restores the state from the last backup |
| Abort | ghls sync --abort cancels an in-progress rebase |
| Dry run | --dry-run on submit and land shows the plan without doing anything |
| Scoped force-push | Only force-pushes ghls-managed stack branches, never your main branch |
| Non-destructive | Works alongside regular git push — only touches its own PRs and branches |
| Operation log | Every operation is logged in ~/.config/ghls/history/operations.json |
| Backup cleanup | ghls cleanup removes backups older than 7 days (max 50 entries) |
Backup branches are stored as ghls-backup/{branch}/{operation}-{timestamp}.
📊 Analytics
ghls collects local-only usage analytics (nothing leaves your machine). Run ghls stats to see:
- Total commands run, stacks created, PRs shipped & landed
- Sync count and conflicts resolved
- Average & median PR sizes over time
- Weekly size trend charts
- Estimated review time savings
Disable with ghls config analytics false or set analytics: false in your config.
🧑💻 Full Workflow Example
Here's a real-world scenario: you're building a notifications feature.
# Start from main
git checkout main && git pull
# Create a feature branch
git checkout -b notifications
# Write the data layer
# ... code code code ...
git add . && git commit -m "feat: add notification data model"
# Write the API
# ... code code code ...
git add . && git commit -m "feat: add notification API endpoints"
# Write the UI
# ... code code code ...
git add . && git commit -m "feat: add notification bell component"
# Ship it as 3 stacked PRs
ghls submit
# → Creates PR #1: notification data model (base: main)
# → Creates PR #2: notification API (base: PR #1)
# → Creates PR #3: notification bell (base: PR #2)
# Check your stack
ghls stack
# ┌─ #103 feat: add notification bell component +95 -10
# ├─ #102 feat: add notification API endpoints +140 -25
# └─ #101 feat: add notification data model +60 -3
# ▼
# main
# PR #1 gets approved and merged on GitHub...
# Land it and rebase the rest
ghls land
# → Merges #101
# → Rebases #102 onto main, updates base
# → Rebases #103 onto #102, updates base
# → Force-pushes updated branches
# PR #2 gets approved...
ghls land
# ...and so on until the stack is empty 🎉🛠️ Development
git clone <repo>
cd ghls-stack
npm install
npm run build
npm link # makes `ghls` available globally
npm run dev # watch mode (tsc --watch)
npm test # vitest
npm run test:watch # vitest in watch mode
npm run lint # eslintProject Structure
ghls-stack/
├── bin/ghls.ts # CLI entrypoint & git passthrough
├── src/
│ ├── commands/ # Command implementations
│ │ ├── init.ts # ghls init
│ │ ├── submit.ts # ghls submit
│ │ ├── land.ts # ghls land
│ │ ├── sync.ts # ghls sync
│ │ ├── status.ts # ghls stack
│ │ ├── abandon.ts # ghls abandon
│ │ ├── config.ts # ghls config
│ │ ├── history.ts # ghls history, undo, cleanup
│ │ └── stats.ts # ghls stats
│ ├── core/ # Core logic
│ │ ├── stack.ts # Stack detection & ordering
│ │ ├── git.ts # Git operations
│ │ ├── github.ts # GitHub API (via gh CLI)
│ │ ├── conflicts.ts # Conflict resolution wizard
│ │ ├── backup.ts # Backup/restore system
│ │ └── metadata.ts # Commit trailer management
│ ├── validation/ # PR validation
│ │ ├── size.ts # Size analysis & weighting
│ │ ├── rules.ts # Pre-submit/pre-land checks
│ │ └── grouping.ts # Commit grouping strategies
│ ├── config/ # Configuration
│ │ ├── schema.ts # Zod config schema
│ │ └── loader.ts # Config hierarchy merger
│ ├── ui/ # Terminal UI
│ │ ├── tree.ts # ASCII stack visualization
│ │ ├── prompts.ts # Interactive prompts
│ │ ├── stages.ts # Progress stages
│ │ └── spinner.ts # Loading spinners
│ ├── analytics/ # Analytics
│ │ ├── collector.ts # Metrics tracking
│ │ └── storage.ts # PR history & trends
│ └── utils/ # Utilities
│ ├── errors.ts # Custom error types
│ ├── exec.ts # Process execution
│ └── logger.ts # Logging
├── actions/ # GitHub Actions workflows
├── tests/ # Unit & integration tests
└── .ghls.yml # Repo config (created by init)FAQ
Q: Does ghls modify my existing branches?
No. ghls only creates and manages its own branches (prefixed with ghls/). Your working branch is untouched.
Q: Can I use ghls with an existing PR?
ghls manages its own stack. If you have a regular PR, keep it. Start fresh with ghls submit for new work.
Q: What if I make more commits after submitting?
Run ghls submit again. It updates existing PRs or creates new ones as needed.
Q: Does it work with GitLab/Bitbucket?
Not yet — ghls depends on the GitHub CLI (gh). GitHub only for now.
Q: What merge methods are supported?
squash (default), merge, and rebase. Set via mergeMethod in your config.
Q: Can I have multiple stacks at once? Each branch gets its own stack. Switch branches to work on different stacks.
License
MIT — go build cool things.
