--- name: cleanup-branches description: > Delete all merged local branches and fast-forward development to origin. Handles both feature branches (merged to development) and hotfix branches (ending in -hotfix, merged to any production branch). Stashes and restores persistence.xml local JNDI settings automatically. Use after PRs are merged to clean up local git state in one command. allowed-tools: Bash --- # Cleanup Merged Local Branches Delete every local branch whose PR has already been merged — plus any branch with no PR of its own whose changes are all already represented on `origin/development` (e.g. a `gh pr checkout ` review checkout) — then bring `development` up to date. Leave `persistence.xml` with local JNDI names restored (unstaged) at the end. ## Step 1 — Stash Local persistence.xml Changes Only stash `persistence.xml` — not all uncommitted work. This prevents unrelated WIP from being moved onto `development` when the stash is popped. ```bash git stash push -- src/main/resources/META-INF/persistence.xml ``` If this reports "No local changes to save", that is fine — continue. Note whether a stash was created so you know whether to pop it in Step 7. ## Step 2 — Fetch Latest from Origin ```bash git fetch origin --prune ``` `--prune` removes remote-tracking refs for branches deleted on GitHub. ## Step 3 — Collect All Local Branches Except development List every local branch except `development`: ```bash git branch --format='%(refname:short)' | grep -v '^development$' ``` For each branch, determine whether it is safe to delete. ### Protected branches — never delete, never classify Before the feature/hotfix split, skip any branch that is `master` or ends with `-prod` (the local mirrors of admin-managed / production branches — see Notes for the full list). They diverge from `development` by design, so a naïve patch-equivalence check could still misfire on them; the explicit skip is the guarantee. List them in the report under their own "Protected — not touched" heading (they are expected, not a problem to flag). ### Feature branches (do NOT end with `-hotfix`) The comparison base is `origin/development`. Look for a merged PR from this head: ```bash gh pr list --head --base development --state merged --repo hmislk/hmis --json number,title,mergedAt --jq '.[0]' # if that is empty, retry without --base — the PR's base may have been changed: gh pr list --head --state merged --repo hmislk/hmis --json number,title,baseRefName,mergedAt --jq '.[0]' ``` Record the PR number/title if found. If the second query returns a PR with a `baseRefName` other than `development`, the comparison base is `origin/`, not `origin/development`. Whether or not a PR is found, continue to **Vet the branch tip** — a merged PR does not by itself prove the local branch is safe to force-delete (it may carry post-merge commits, or have been reused). ### Hotfix branches (end with `-hotfix`) Hotfix PRs target a production branch. Look for a merged PR: ```bash gh pr list --head --state merged --repo hmislk/hmis --json number,title,baseRefName,mergedAt --jq '.[0]' ``` - **No merged PR** → **skip** (warn the user). A hotfix branch is never vetted by patch-equivalence against `development`; its commits legitimately are not there. - **Merged PR found** → record the PR number/title and its `baseRefName`, then **Vet the branch tip** with the comparison base `origin/`. ### Vet the branch tip (both branch kinds) Carry two facts forward for each branch: its **PR reference** — either `# → ` (from the `gh pr list` step) or *none* (a no-PR review checkout) — and its comparison base ``: `origin/development` for a feature branch or a no-PR checkout, `origin/` for a feature PR whose base was changed, `origin/` for a merged hotfix. ```bash git merge-base --is-ancestor && echo CONTAINED || echo AHEAD ``` - **CONTAINED** — the branch tip is already reachable from ``; it holds nothing unmerged and no post-merge commits. **Mark for deletion.** - **AHEAD** — the tip is not reachable from ``. Normal for a squash- or rebase-merged PR, but also how a branch with genuine post-merge commits (or a reused branch) looks. Decide with a **final-tree guard**: are ``'s versions of the files it touched already identical to ``? ```bash mb=$(git merge-base ) git diff --quiet -- $(git diff --name-only "$mb" ) ``` This compares final content, not per-commit patches, so it is correct where `git cherry` is not: a clean multi-commit squash-merge (no per-commit equivalent) passes; a branch whose change was applied to `` and later reverted there fails; a merge commit that carried unique content in fails. - **exit `0`** — every file the branch touched already matches ``; deleting the branch loses nothing (a clean squash/rebase merge, or a fully-absorbed no-PR checkout such as `pr-23617`). **Mark for deletion.** - **exit `1`** — some file the branch touched differs from ``: genuine post-merge work, a reused branch, conflict-resolution content, or a squash/rebase that did not land identical content. **Skip** and warn, quoting the branch's real `` and its PR reference (or noting it has none): "*ahead of `` — if PR #`` was squash/rebase-merged, `git branch -D ` manually; otherwise inspect for unmerged work first*". **Empty touched-file list gotcha**: if `git diff --name-only "$mb" ` is empty, the command above degrades to a full-tree `git diff --quiet ` (no pathspec) and will almost always exit `1` — not because `` differs from ``, but because `` has since changed unrelated files `` never touched. Don't trust that exit code in this case. Instead compare tree hashes directly: ```bash [ "$(git rev-parse ^{tree})" = "$(git rev-parse "$mb"^{tree})" ] && echo IDENTICAL || echo DIVERGED ``` - **IDENTICAL** — ``'s tree is byte-identical to the merge-base (its "ahead" commits are no-ops, e.g. an empty merge), and the merge-base is by definition an ancestor of ``. The branch holds zero unique content. **Mark for deletion.** - **DIVERGED** — genuinely different content despite the empty per-commit diff (rare). **Skip** and warn. ## Step 4 — Switch to development ```bash git checkout development ``` ## Step 5 — Delete Marked Branches Step 3's **Vet the branch tip** fully decided every marked branch — each is either CONTAINED in its comparison base, or AHEAD but proven fully absorbed (the final-tree guard found every file it touched already identical to ``). Branches with post-merge or unmerged work were skipped there. Step 5 only deletes; it does not re-decide safety. ```bash git branch -d || git branch -D ``` `git branch -d` refuses when the tip is not reachable from *local* `development` — normal here, because a squash/rebase merge leaves the tip off `development` and local `development` is not fast-forwarded until Step 6. The `|| git branch -D` completes the delete; Step 3 already established the branch is safe to drop. `git branch -D` prints the deleted SHA (`Deleted branch X (was 906d5ebbb4)`) and the reflog keeps it ~30 days, so a mistaken delete is still recoverable. Do **not** re-check with `git log origin/..`: Step 2's `git fetch --prune` has already removed the `origin/` upstream, so that command errors with `unknown revision or path not in the working tree` instead of returning empty. ## Step 6 — Fast-Forward development to origin/development Use `--ff-only` so git stops with an error if the local `development` has diverged (e.g. local commits not yet pushed), rather than silently discarding them: ```bash git merge --ff-only origin/development ``` If this fails with "Not possible to fast-forward", the local `development` has commits that are not on `origin/development`. Report this to the user and stop — do not force-reset. The user must resolve the divergence manually. ## Step 7 — Restore persistence.xml If a stash was created in Step 1 (only `persistence.xml` was stashed): ```bash git stash pop ``` If the stash pop reveals conflicts (unlikely but possible), report them to the user and stop — do not resolve conflicts automatically. If no stash was created, leave `persistence.xml` as-is. ## Step 8 — Report Print a summary: ```text ✓ Deleted branches: - (PR #NNN merged → ) - (no PR; all changes already represented on ) ... ⚠ Skipped branches: - (PR #NNN merged → , but tip is ahead of it — squash/rebase merge? `git branch -D` manually; else inspect for unmerged work) - (no PR; tip has commit(s) not on ) - (hotfix, no merged PR found) ... • Protected — not touched: - master, -prod ✓ development is now at () ✓ persistence.xml restored to local JNDI settings (unstaged) ``` Each *vetted* deleted / skipped line carries the branch's real PR reference (or "no PR") and its real comparison base — never assume a PR exists or that the base is `development`. The unvetted "hotfix, no merged PR found" line is the one exception: it is skipped before any base is chosen, so it carries neither. Omit any section with no entries. If nothing was stashed, replace the last line with: `✓ persistence.xml unchanged (no local changes were present)` ## Notes - Never delete `development`, `master`, or any `*-prod` branch. - The known production branches are: `coop-prod`, `ruhunu-prod`, `southernlanka-prod`, `rmh-prod`, `digasiri-prod`, `mp-prod`, `roseth-prod`, `horizon-prod`, `asiripharmacy-prod`, `engagewellness-prod` - After this skill completes the developer is on `development`, up to date, with only unmerged feature branches remaining locally.