Skip to main content

git-merge-hazards

Traps in GitHub's merge machinery. Use when merging a PR, merging a stacked or serial PR chain, auditing whether a branch really landed, or merging over red CI.

Informations de source

Dépôt
laurigates/claude-plugins
Dernière activité de la source
17 octobre 2026 à 17:21
Langue détectée de SKILL.md
anglais
Étoiles
59
Forks
6

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
9 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
git-merge-hazards
description
Traps in GitHub's merge machinery. Use when merging a PR, merging a stacked or serial PR chain, auditing whether a branch really landed, or merging over red CI.
allowed-tools
Read, Grep, Glob, Bash(gh pr *), Bash(gh issue *), Bash(gh api *), Bash(git cherry *), Bash(git merge-tree *), Bash(git rev-parse *), Bash(git rev-list *), Bash(git log *), Bash(git reflog *), Bash(git rebase *), Bash(git merge *), Bash(git checkout *), Bash(git worktree *), Bash(git diff *), Bash(git add *), Bash(git commit *), Bash(git -C *), Bash(git merge-base *), Bash(grep *), Bash(awk *), Bash(jq *), Bash(git push *), Bash(git fetch *), Bash(git cat-file *), Bash(sleep *), Bash(just *), Bash(bash *), TodoWrite
created
2026-08-19T00:00:00.000Z
modified
2026-10-17T00:00:00.000Z
reviewed
2026-08-21T00:00:00.000Z
# Git Merge Hazards ## When to Use This Skill | Use this skill when... | Use something else when... | |---|---| | Merging a PR, or proving a branch landed | The PR's green CI ran against an older `main` → `git-plugin:worktree-stale-base-merge` | | Deciding whether a red PR can merge (§4) | Deciding whether a red AI-review check is a real finding → `github-actions-plugin:ai-review-max-turns` | | Merging an ad-hoc stacked chain (§2, §3) | Release-please PRs are piling up or conflicting → `git-plugin:release-please-pr-workflow` | The numbered sections are the verbatim text of the promoted `~/.claude/rules/pr-merge-hazards.md` rule. The maintainer notes on what differs from that rule, including the skill-only §2 bullets, are in [references/sync-notes.md](references/sync-notes.md). Six traps in GitHub's merge machinery, one law: "PR merged" says nothing about *content* — and a red check is not proof of failure. Each: the trap, the 5-second check, the fix. Sibling: `git-plugin:git-local-hazards` (local git). ## 1. `--merged` misses squash-merged branches A squash-merge collapses a branch into one fresh-SHA commit on `main`, so the branch's own commits are never ancestors — `git branch --merged` (and any ancestry check) reports it **unmerged**. "Files identical to main" also fails once `main` drifts the same files. - **Check**, in order of authority: - `gh pr list --state all --head <branch> --json state` → a MERGED PR is **authoritative**. Reach for this first; the git-side checks below are all one-way. - `git cherry main <branch>` → marks a commit `-` when a patch-equivalent commit is already upstream, `+` when it is not. Survives squash **and** cherry-pick, and does not care that `main` drifted. - `git merge-tree --write-tree main <branch>` equals `git rev-parse main^{tree}` → contained. **A match proves containment; a non-match proves nothing.** - **Not immune to drift** (corrected 2026-07): once `main` moves on over the same files, merging an already-merged branch back would re-introduce its older versions, so the trees differ and merge-tree reports **not contained** for work that fully landed. Observed reporting three merged branches as unmerged. Same trap as "files identical to main". Use the PR state or `git cherry` to decide; keep merge-tree only as a positive-containment shortcut. - **Fix**: use the encoded recipe rather than re-deriving: `just -g branch-audit` (in `private_dot_config/just/git.just`) prints MERGED vs REVIEW + a paste-ready delete. Its REVIEW bucket measured ~90% false positives (#2268), so a REVIEW line is a question to settle with `gh pr list --state all --head <branch>`, not a verdict. - A non-match is "review", **not** proof of unmerged — don't force the count to zero. ## 2. Merging a stacked base auto-CLOSES the child PR When PR B is based on PR A's branch, merging A and deleting its branch auto-closes B (GitHub does **not** retarget it), and a closed PR whose base branch is gone **cannot be reopened**. - **Check the base first** (skill-only): `gh pr view <base> --json state,mergeStateStatus,mergeable` before retargeting. Proceed only when `state` is `OPEN` and `mergeable` is `MERGEABLE`; the `mergeStateStatus` values and the `UNKNOWN` wait are in [references/no-force-push-stack.md](references/no-force-push-stack.md#check-the-base-first). - **Fix — order matters**: retarget the child **first**, while the base PR is open: 1. `gh pr edit <child> --base main` 2. `gh pr merge <base> --squash --delete-branch` 3. `git rebase --onto origin/main <old-base-tip> <child-branch>` (drops the already-squashed base commits) + `git push --force-with-lease` 4. merge the child. - **No-force alternative (skill-only, preferred when the child builds only on its base)**: merge `main` into the child with no force-push, after a classification that decides whether `-X ours` is lossless. Recipe: [references/no-force-push-stack.md](references/no-force-push-stack.md). The rebase path in the Fix steps is the fallback. - **If already auto-closed**: the head branch survives — rebase as above, `gh pr create` fresh, comment "Superseded by #new" on the closed one. - **Nothing tells you this happened.** The auto-close is silent: no failed check, no notification, and the PR list just looks one shorter. claude-plugins #2049 sat stranded for a day; a sweep then found 26 dead branches, two carrying work that had **never had a PR opened at all** (so no event ever fired for them either). A scheduled sweep is the only thing that finds this class — an event handler on `pull_request: closed` is too late by construction (the base ref is already deleted, so the reopen window is gone) and is blind to never-PR'd branches. `claude-plugins scripts/check-stranded-work.sh` is the encoded audit; it takes `--repo`, so one run sweeps the portfolio. - **Telling an accident from a decision**: a closed-unmerged PR whose base ref **404s** was auto-closed; one whose base ref is still **alive** was closed by a human (duplicate/superseded). That single check is the discriminator — 11 of those 26 branches were deliberate closes and must not be resurrected. ## 3. Stacked-chain merges: push by SHA, never `HEAD:` — and expect auto-close races Read [references/stacked-chain-push.md](references/stacked-chain-push.md) before any force-push or merge in a stacked-PR chain. It carries the push-by-SHA refspec form (braced variable, full `refs/heads/` refname, the zsh word-modifier table), the empty-diff auto-close, the mergeability and CI-registration races, the pre-push check, and auto-close recovery. ## 4. A red PR may still be mergeable — `UNSTABLE` is not `BLOCKED` `mergeStateStatus` separates **required** failing checks (`BLOCKED` — merge refused) from merely-present ones (`UNSTABLE` — plain `gh pr merge` works), so `--admin` on an `UNSTABLE` PR takes a privilege you didn't need. Read it first. Merging over red needs **two** checks: `--json files` (config/docs can't break a compile) **and** the same check already failing on `main`. Either alone is a guess — and a stale-green `main` lies, so check `createdAt` (2026-07: a "green" run was 21 days old; main hadn't compiled for three weeks). A `500` from the merge call may be GitHub, not the PR: run the stale-SHA control in [references/merge-endpoint-failures.md](references/merge-endpoint-failures.md) before changing the PR. ## 5. A **negated** closing keyword still closes the issue GitHub matches `close|fixes|resolves|…` + an issue reference without parsing the sentence around it, so the natural way to *disclaim* closure closes the issue. Markdown emphasis between verb and number doesn't break the match either: ``` Does **not** close #162 → GitHub reads `close #162`, and closes it ``` - **Check**: `gh issue view <n> --json closedByPullRequestsReferences` names the closer; a `closedAt` one second after a merge is automation, not a decision. - **Fix**: never let a closing verb precede an issue number you don't mean to close — `Related: #162`, `Unblocks #162`, or `#162 stays open — it needs …`. - **Recovery**: `gh issue reopen` works (nothing was deleted, unlike #2), then comment so the next reader doesn't re-derive it. The trap is wider than negation: plain past tense, a markdown-linked cross-repo reference, and a keyword you are only *quoting* all match too. Before merging a PR whose body mentions issue numbers, read [references/closing-keyword-cases.md](references/closing-keyword-cases.md) for those cases, the audit-with-a-control procedure, and the incidents. ## 6. A serial merge chain merges commits nobody verified `gh pr update-branch` re-runs every `pull_request` workflow, and a retarget (`gh pr edit --base`) can start a bot too, so a bot that writes to PR branches can push after your review; its commits pass CI and merge unread. Before each merge, refuse any non-merge commit newer than your review timestamp, and merge with `--match-head-commit <sha>`. A PR that changes such a bot goes last in the chain. Guard snippet, the `gh --jq --arg` trap and recovery: [references/serial-merge-chain.md](references/serial-merge-chain.md). ## Agentic Optimizations | Context | Command | |---|---| | Did the branch land? (§1, authoritative) | `gh pr list --state all --head <branch> --json number,state,mergedAt` | | Can a red PR merge? (§4) | `gh pr view <n> --json mergeStateStatus,mergeable` — `UNSTABLE` merges, `BLOCKED` refuses | | Merge-over-red evidence (§4) | `gh pr view <n> --json files --jq '.files[].path'` and `gh run list --branch main --workflow <wf> -L 1 --json conclusion,createdAt` | | Before a force-push (§3) | `git log --oneline origin/main..<sha>` — exactly the child's commits | | Who closed an issue (§5) | `gh issue view <n> --json closedByPullRequestsReferences,closedAt` | | Commits a PR gained after review (§6) | `gh pr view <n> --json commits`, filtered with `jq --arg` on `committedDate` |
Voir sur GitHub