Squash merge detection union
Skill kjuhwa/skills-hub/skills/workflow/squash-merge-detection-union
Classify "can this branch be cleaned up?" using the union of `git branch --merged` + `git cherry` patch-equivalence + `gh pr list` state, with clear semantics for each signal.From its SKILL.md
npx -y skills add kjuhwa/skills-hub --skill squash-merge-detection-unionAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
5.2 KB, ~1.3k tokens by cl100k_base, as published. Nobody here has run it
Three-Way Merged-Branch Detection (--merged ∪ cherry ∪ PR state)
When to use
- You clean up stale git worktrees / branches and want to distinguish "safe to delete" from "unmerged, don't touch."
- Squash merges break
git branch --merged mainbecause the squashed commit onmainhas a different SHA than any of the feature commits. Ancestry-based checks say "unmerged" even when the work is long shipped. - You have
ghCLI available for GitHub repos and want to use it as a tie-breaker when git alone is ambiguous.
Steps
Run checks in this order, returning early on a definitive signal:
-
Fast path —
git branch --merged <main>. If the branch appears in the output (minus any leading*), it was fast-forward or merge-commit merged. Return "safe, no open PR."const { stdout } = await exec('git', ['branch', '--merged', mainBranch]); const merged = stdout.split('\n').map(b => b.trim().replace(/^\* /, '')); if (merged.includes(branchName)) return { safe: true, openPr: false }; -
Squash-merge detection —
git cherry <base> <branch>. Lines starting with-mean "patch already in upstream" (squash-merged or cherry-picked). Lines starting with+mean "genuinely not in upstream." If every line is-(or the list is empty), the branch is patch-equivalent to upstream — safe to delete.const lines = stdout.split('\n').filter(l => l.trim()); if (lines.length === 0) return true; return lines.every(line => line.startsWith('-')); -
PR state —
gh pr list --head <branch> --state all --json state --limit 1. Parse the JSON, mapMERGED→ safe,CLOSED→ safe-if-includeClosed-flag,OPEN→ unsafe (and flagopenPr: true), anything else → unknown (treat as unsafe). -
Error handling per signal:
- For
branch --merged/git cherry: expected errors (unknown revision, bad revision, not a git repo, ENOENT) → returnfalse/null, let the next signal try. Unexpected errors (permission denied, corruption) → throw. - For
gh: missing binary (ENOENT or "command not found") → return'NONE'silently (gh is a soft dependency). Other errors → log at warn with stdout preview. - For the remote-URL precheck: if origin isn't
github.com, return'NONE'and skipgh. Non-GitHub remotes are out of scope.
- For
-
Cache PR-state lookups per cleanup invocation.
getPrStatetakes an optionalMap<string, PrState>so repeated queries across many branches don't re-execgh(it's slow, ~100-300 ms per call). -
Combine signals with OR logic in a wrapper (
computeBranchCleanupSafetyor similar): the branch is safe iff any of the three signals says so. The return type should include{ safe: boolean, openPr: boolean }so callers can message the user differently for "has open PR" vs "just stale."
Counter / Caveats
- Do not run
git cherrywithout the--mergedfast path — it's O(commits-on-branch) whereas--mergedis O(heads). Order matters. git cherryon an unpushed branch vs. a branch that has been rebased ontomaincan give surprising output (the rebased commits are patch-equivalent only to specific upstream commits). Test with a rebase scenario.ghasks the GitHub API, which is rate-limited. The per-invocation cache is mandatory for bulk cleanup. Don't switch to unauthenticated API even as a fallback.ghreturnsstate: 'MERGED'even if the merge commit was force-pushed (a maintainer amend). Treating it as safe is correct because the platform considers it merged.- Non-GitHub remotes (Gitea, GitLab, custom) will fall through to the ancestry checks only. That's fine — just document it.
Evidence
packages/git/src/branch.ts:isBranchMerged(lines 186-221) —git branch --merged.isPatchEquivalent(lines 236-271) —git cherrywith lines starting with-meaning already-upstream.
packages/isolation/src/pr-state.ts(91 lines):gh pr list --json statewrapper with per-invocation cache, GitHub-only remote guard, graceful handling of missingghbinary.packages/core/src/services/cleanup-service.ts:562-578: the three-signal union insidecomputeBranchCleanupSafety/classifyForCleanup:if (await isBranchMerged(...)) return { safe: true, openPr: false }; if (await isPatchEquivalent(...)) return { safe: true, openPr: false }; const prState = await getPrState(...); if (prState === 'MERGED') return { safe: true, openPr: false }; if (prState === 'CLOSED') return { safe: includeClosed, openPr: false }; if (prState === 'OPEN') return { safe: false, openPr: true };- Commit SHA: d89bc767d291f52687beea91c9fcf155459be0d9.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.