chore(git-workflow): add 3-repo feature branch skills + helper scripts

This commit is contained in:
Gergő Törcsvári 2026-05-29 08:15:27 +02:00
commit 645f89b816
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
10 changed files with 500 additions and 0 deletions

65
.claude/settings.json Normal file
View file

@ -0,0 +1,65 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(git commit -m*)",
"Bash(git -C kicad commit -m*)",
"Bash(git -C wxwidgets commit -m*)",
"Bash(git add*)",
"Bash(git -C kicad add*)",
"Bash(git -C wxwidgets add*)",
"Bash(git fetch*)",
"Bash(git -C kicad fetch*)",
"Bash(git -C wxwidgets fetch*)",
"Bash(git checkout)",
"Bash(git checkout [!-]*)",
"Bash(git -C kicad checkout)",
"Bash(git -C kicad checkout [!-]*)",
"Bash(git -C wxwidgets checkout)",
"Bash(git -C wxwidgets checkout [!-]*)",
"Bash(git pull --ff-only*)",
"Bash(git -C kicad pull --ff-only*)",
"Bash(git -C wxwidgets pull --ff-only*)",
"Bash(./scripts/git-workflow/*.sh*)",
"Bash(scripts/git-workflow/*.sh*)",
"Bash(/Users/torcsi/dev/kicad-wasm/scripts/git-workflow/*.sh*)"
],
"ask": [
"Bash(git rebase*)",
"Bash(git -C kicad rebase*)",
"Bash(git -C wxwidgets rebase*)",
"Bash(git merge*)",
"Bash(git -C kicad merge*)",
"Bash(git -C wxwidgets merge*)",
"Bash(git push)",
"Bash(git push [!-]*)",
"Bash(git -C kicad push)",
"Bash(git -C kicad push [!-]*)",
"Bash(git -C wxwidgets push)",
"Bash(git -C wxwidgets push [!-]*)",
"Bash(git checkout -b*)",
"Bash(git -C kicad checkout -b*)",
"Bash(git -C wxwidgets checkout -b*)",
"Bash(git branch -d*)",
"Bash(git branch -D*)",
"Bash(git -C kicad branch -d*)",
"Bash(git -C kicad branch -D*)",
"Bash(git -C wxwidgets branch -d*)",
"Bash(git -C wxwidgets branch -D*)"
],
"deny": [
"Bash(git push --force*)",
"Bash(git push -f*)",
"Bash(git push --force-with-lease*)",
"Bash(git -C kicad push --force*)",
"Bash(git -C kicad push -f*)",
"Bash(git -C kicad push --force-with-lease*)",
"Bash(git -C wxwidgets push --force*)",
"Bash(git -C wxwidgets push -f*)",
"Bash(git -C wxwidgets push --force-with-lease*)",
"Bash(git reset --hard*)",
"Bash(git -C kicad reset --hard*)",
"Bash(git -C wxwidgets reset --hard*)"
]
}
}

View file

@ -0,0 +1,54 @@
---
name: git-feature-commit
description: Commit staged + unstaged work across all 3 repos. Submodules first (kicad, wxwidgets), then root with submodule pointer bumps. Asks the user to approve each commit separately and shows the pointer diff for the root commit. Usage - "/git-feature-commit [message]".
---
# git-feature-commit
Commit work-in-progress across root + kicad + wxwidgets in the correct order: **submodules first, then root** (so the root commit captures the new submodule pointers).
## Arguments
Optional positional message string. If omitted, ask the user for a message via `AskUserQuestion` before any commit step. Same message is reused across all repos (user can edit per-repo at the confirm step).
## Steps
1. **Status snapshot.** Run `bash /Users/torcsi/dev/kicad-wasm/scripts/git-workflow/repo-status.sh` to see which repos have changes.
2. **Show per-repo diff overview to the user first** (before any commit). For each repo, run:
- `git -C <path> status --short`
- `git -C <path> diff --stat HEAD`
so the user sees the full picture before being asked to commit anything.
3. **Submodule commits — kicad first, then wxwidgets.** For each submodule:
- If `git -C <path> status --porcelain` is empty AND no staged changes, print "kicad: no changes, skipping" and continue.
- Otherwise, prose-ask: "Commit kicad with message `<msg>`? Type `y`, `n`, or `edit` to change the message."
- On `y`: `git -C <path> add -A && git -C <path> commit -m "<msg>"` (auto-allowed).
- On `edit`: use `AskUserQuestion` to get a new message, then commit with that.
- On `n`: skip and continue.
4. **Root commit (last).** After submodules:
- Stage submodule pointer bumps explicitly: `git -C <root> add kicad wxwidgets` (only stages the gitlink pointer change if a submodule has a new HEAD).
- Stage other root changes: `git -C <root> add -A -- ':!kicad' ':!wxwidgets' ':!features'` (exclude submodule trees and features/ patch dir).
- Check what's staged: `git -C <root> diff --cached --stat`. If nothing is staged, print "root: no changes, skipping" and stop.
- Show the user:
- `git -C <root> diff --cached --stat` (overview)
- `git -C <root> diff --cached -- kicad wxwidgets` (the raw pointer-bump diff — the user wants to eyeball this every time)
- Prose-ask: "Commit root with message `<msg>`? `y`/`n`/`edit`."
- On `y`: `git -C <root> commit -m "<msg>"`.
5. **Report.** Print a summary of which repos committed and the new HEAD shas.
## Edge cases
- All 3 repos clean → print "nothing to commit anywhere" and stop.
- Only submodule changes, no other root changes → root commit still happens, but it'll only contain the pointer bumps. That's correct.
- Only root changes (no sub changes) → just root commits, submodules skipped.
- Submodule has commits but pointer was already bumped in a prior root commit → pointer add will be a no-op and that's fine.
## Safety
- Never use `git commit -a` (it ignores staging intent).
- Never `git add` paths outside the repo or with shell expansion that could grab unintended files.
- Always exclude `features/` from the root stage step (it contains patches that recursively include themselves if committed).
- Always echo commands before running.

View file

@ -0,0 +1,64 @@
---
name: git-feature-finish
description: Merge the current feature branch back to main (fast-forward only) and push, across all 3 repos. Submodules first, then root - so the root push includes the merged submodule SHAs. Stops for confirmation at each merge and push. Usage - "/git-feature-finish".
---
# git-feature-finish
Land the current feature branch into each repo's main and push. Uses `--ff-only` for clean linear history.
Per-repo main mapping is in `scripts/git-workflow/repos.sh` (root=`main`, subs=`wasm-port`).
## Pre-flight (all of these must pass before any merge happens)
1. Run `bash /Users/torcsi/dev/kicad-wasm/scripts/git-workflow/repo-status.sh`. Parse the JSON.
2. All 3 repos must have `dirty: false`. If any is dirty, STOP and tell the user to commit (`/git-feature-commit`) or stash first.
3. All 3 repos must be on the same feature branch (use root's branch as the reference). If not, STOP and report the mismatch.
4. All 3 repos must have `up_to_date_with_main: true` (i.e. the feature is rebased onto the latest main). If any isn't, STOP and instruct: "run `/git-feature-sync` first".
5. None may have `rebase_in_progress: true`. If so, STOP.
## Steps — order: kicad, wxwidgets, root
Submodules first so that when root checks out main and merges its feature branch, the pointer-bump commits land on root's main referencing the just-merged submodule mains.
For each repo in order [kicad, wxwidgets, root]:
1. Skip if already merged: run `git -C <path> branch --merged <main>` and check if the feature branch appears. If so, print "<repo>: feature already merged into <main>, skipping merge+push" and proceed to step 5 (branch delete prompt).
2. **Prose-ask:** "About to merge `<feature>` into `<main>` in <repo> (ff-only) — proceed?"
3. Run, echoing each:
- `git -C <path> checkout <main>` (auto-allowed)
- `git -C <path> pull --ff-only` (auto-allowed) — if this fails because main moved with non-ff changes, STOP and tell the user: "<repo>: main moved with non-ff changes since last sync. Run `/git-feature-sync` and try again." Do NOT try `--no-ff`.
- `git -C <path> merge --ff-only <feature>` (hits `ask` permission — user reconfirms at tool layer). If this fails because main moved in a way that breaks ff, STOP with the same message as above.
4. **Prose-ask:** "Push <repo>'s <main> to origin? — proceed?"
- `git -C <path> push` (hits `ask` permission).
5. **Prose-ask:** "Delete local feature branch `<feature>` in <repo>?"
- `git -C <path> branch -d <feature>` (hits `ask` permission). This is safe (`-d`, not `-D`) — git refuses if the branch isn't fully merged, which is exactly the safety we want.
## After all 3 repos done
Report a summary table:
```
kicad: merged feature/foo into wasm-port, pushed (origin/wasm-port now at <sha>), branch deleted
wxwidgets: merged feature/foo into wasm-port, pushed (origin/wasm-port now at <sha>), branch deleted
root: merged feature/foo into main, pushed (origin/main now at <sha>), branch deleted
```
If `features/<feature>/` exists from `scripts/create-feature-patches.sh`, mention it but do NOT auto-delete. The user may want to keep the patches as history.
## Edge cases
- **Feature already merged in some repos but not all:** the per-repo `--merged` check handles this naturally — finish skips re-merging and just offers branch delete.
- **No local feature branch left to delete in some repo:** `branch -d` will fail; report and continue. Don't make this fatal.
- **`-u` first-push:** this workflow never pushes feature branches, only mains. Mains always have tracking already.
## Safety
- `--ff-only` everywhere, no fallback to `--no-ff`.
- `branch -d` not `-D` — never force-delete.
- Never `--force` push. The settings.json `deny` rule blocks this regardless.
- Always echo commands before running.
- Always pose the prose confirmation BEFORE invoking the tool-layer prompt.

View file

@ -0,0 +1,50 @@
---
name: git-feature-start
description: Create a new feature branch across all 3 repos (root + kicad + wxwidgets submodules). Fetches each repo's main, fast-forward pulls, and creates the same feature branch in each. Usage - "/git-feature-start <branch-name>", e.g. "/git-feature-start feature/new-foo".
---
# git-feature-start
Create a new feature branch in all 3 repos (root, kicad submodule, wxwidgets submodule), based on each repo's main branch.
Per-repo main mapping (hardcoded in `scripts/git-workflow/repos.sh`):
- root → `main`
- kicad → `wasm-port`
- wxwidgets → `wasm-port`
## Arguments
A single positional argument: the feature branch name (e.g. `feature/my-thing`).
If the user invoked the skill without a branch name, ask for one via `AskUserQuestion` before proceeding.
## Steps
1. **Pre-flight: assert all 3 repos are clean.** Run `bash /Users/torcsi/dev/kicad-wasm/scripts/git-workflow/assert-clean.sh`. If it exits non-zero, STOP and tell the user which repos are dirty and that they need to commit, stash, or discard before starting a new feature.
2. **Pre-flight: check no repo is already on a non-main branch.** Run `bash /Users/torcsi/dev/kicad-wasm/scripts/git-workflow/repo-status.sh` and inspect each line's `branch` field. If any repo's `branch` is not its `main` (or is empty meaning detached HEAD), STOP and tell the user. Suggest: `/git-feature-sync` if they're already mid-feature, or manually checkout the main in that repo first. Do NOT silently switch off in-progress work.
3. **For each repo** in order [root, kicad, wxwidgets], run these commands. Echo what you're about to do in chat before each repo.
- `git -C <path> fetch origin`
- `git -C <path> checkout <main>`
- `git -C <path> pull --ff-only origin <main>`
- `git -C <path> checkout -b <branch-name>` — this hits the `ask` permission rule, so it will prompt. Also, you should prose-ask the user "About to create branch `<name>` in <repo> — proceed?" before invoking it (belt + suspenders per project rule).
4. **Report.** Print a summary table:
```
root: created <branch> from main@<sha>
kicad: created <branch> from wasm-port@<sha>
wxwidgets: created <branch> from wasm-port@<sha>
```
5. Suggest `/git-feature-commit` as the next step when the user has changes to record.
## Failure handling
If any step fails partway (e.g. checkout fails in wxwidgets after root and kicad succeeded), STOP and report exactly which repos already have the new branch and which don't. The user can manually finish or `/git-feature-finish` won't run until all 3 are aligned anyway.
## Safety
- Never use `-f` / `--force` flags.
- Never delete branches in this skill.
- Always echo the command before running it.

View file

@ -0,0 +1,75 @@
---
name: git-feature-sync
description: Rebase the current feature branch onto main in all 3 repos (root + kicad + wxwidgets). Naturally re-runnable - after the user resolves a conflict manually and runs `git rebase --continue`, re-invoke the skill and it picks up where it stopped. Usage - "/git-feature-sync".
---
# git-feature-sync
Rebase the current feature branch onto each repo's main, across root, kicad, and wxwidgets.
Per-repo main mapping is in `scripts/git-workflow/repos.sh` (root=`main`, subs=`wasm-port`).
## How "resume after conflict" works
There is **no state file**. State is derived live each run:
- `git -C <p> merge-base --is-ancestor origin/<main> HEAD` → if 0, the branch already contains the latest main; skip this repo.
- `.git/rebase-merge` or `.git/rebase-apply` directory present → rebase is mid-flight in that repo; refuse to do anything until the user finishes or aborts it.
So after the user resolves a conflict manually + runs `git rebase --continue` in the stopped repo, just re-running `/git-feature-sync` picks up at the next un-rebased repo.
## Steps
1. **Get status snapshot.** Run `bash /Users/torcsi/dev/kicad-wasm/scripts/git-workflow/repo-status.sh` and parse the per-repo JSON.
2. **Pre-flight checks.**
- If any repo has `rebase_in_progress: true`, STOP. Tell the user which repo, and that they need to resolve (`git -C <repo> rebase --continue` after `git add`-ing resolved files) or abort (`git -C <repo> rebase --abort`) before sync can proceed.
- Determine the feature branch from root's `branch` field. If root is on its main (`main`) or detached, STOP and say "no feature branch active in root — nothing to sync".
- For each submodule with `branch != <feature-branch>`:
- If branch is empty (detached), prose-ask: "wxwidgets is at detached HEAD `<sha>`. Want me to `git -C wxwidgets checkout <feature-branch>` first? (y/N)". On yes, run it (auto-allowed). On no, STOP.
- If branch is some other name, STOP and tell the user which repo is on which branch. Don't auto-switch.
3. **Determine work plan.** For each repo, mark "needs rebase" if `up_to_date_with_main: false`. If all three are up-to-date, print "all 3 repos already up to date with their mains" and stop cleanly.
4. **Execute per repo** in order [root, kicad, wxwidgets]. Skip any repo with `up_to_date_with_main: true`. For each repo that needs rebase:
- Prose-announce: "About to rebase <repo> (`<feature>`) onto `origin/<main>` — proceed?"
- `git -C <path> fetch origin`
- `git -C <path> rebase origin/<main>` (this hits the `ask` permission — user confirms again at tool layer)
- If the rebase command exits non-zero (conflict), STOP and emit the handoff message (see below).
- On success, continue to the next repo.
5. **After all 3 succeed:** check `git -C <root> status --short` for staged or unstaged changes to the `kicad` / `wxwidgets` submodule entries. If present, suggest:
> Submodule SHAs changed during rebase. When ready: `/git-feature-commit "sync: bump submodule pointers after rebase"`.
Do NOT auto-commit.
## Conflict handoff message — use this exact shape
When a rebase fails mid-flight in repo X, list the repos in the plan, what's been done, what's pending, and the manual commands to resolve. Derive everything live by re-running `repo-status.sh` if needed.
> **Completed:** root rebased onto origin/main (3 commits replayed).
> **Stopped:** kicad — conflict during rebase. Conflicted files:
> ```
> kicad/eeschema/foo.cpp
> kicad/common/bar.cpp
> ```
> **Pending:** wxwidgets (not started).
>
> **To resolve manually:**
> ```
> cd /Users/torcsi/dev/kicad-wasm/kicad
> # edit each conflicted file, resolve <<<<<<< markers
> git add eeschema/foo.cpp common/bar.cpp
> git rebase --continue
> ```
>
> Then re-run `/git-feature-sync` — kicad will be detected as already rebased and it will proceed with wxwidgets.
>
> To roll back kicad only: `cd /Users/torcsi/dev/kicad-wasm/kicad && git rebase --abort`. Note: already-rebased repos (root in this case) **stay rebased** — they are not rolled back.
Get the conflicted-files list from `git -C <path> diff --name-only --diff-filter=U`.
## Safety
- Never `--force` anything.
- Never `git rebase --skip` on the user's behalf — only `--continue` is safe and that's the user's job after manual resolution.
- Always echo the command before running it.
- Detached HEAD in a submodule is a confirmation point, never an auto-fix.

View file

@ -0,0 +1,33 @@
#!/bin/bash
# Exit 0 if all 3 repos are clean (no staged, no unstaged, no untracked files).
# Exit 1 with a readable message listing the dirty repos otherwise.
# Untracked files count as dirty - this is intentional: starting a new feature
# while you have uncommitted new files is almost always a mistake.
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./repos.sh
source "$SCRIPT_DIR/repos.sh"
dirty=()
for repo in "${REPOS[@]}"; do
p=$(repo_path "$repo")
# `git status --porcelain` is empty iff worktree clean (covers modified, staged, and untracked).
if [ -n "$(git -C "$p" status --porcelain)" ]; then
dirty+=("$repo")
fi
done
if [ ${#dirty[@]} -eq 0 ]; then
echo "All 3 repos clean."
exit 0
fi
echo "Dirty repos: ${dirty[*]}" >&2
for repo in "${dirty[@]}"; do
p=$(repo_path "$repo")
echo "--- $repo ---" >&2
git -C "$p" status --short >&2
done
exit 1

View file

@ -0,0 +1,36 @@
#!/bin/bash
# Assert all 3 repos are on the given branch.
# Exit 0 if all match. Exit 1 with details if any mismatch (including detached HEAD).
# Usage: ./assert-on-branch.sh <branch-name>
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./repos.sh
source "$SCRIPT_DIR/repos.sh"
if [ $# -ne 1 ]; then
echo "Usage: $0 <branch-name>" >&2
exit 2
fi
WANT="$1"
mismatched=()
for repo in "${REPOS[@]}"; do
p=$(repo_path "$repo")
cur=$(git -C "$p" branch --show-current)
if [ "$cur" != "$WANT" ]; then
mismatched+=("$repo: on '${cur:-DETACHED-HEAD}' (want '$WANT')")
fi
done
if [ ${#mismatched[@]} -eq 0 ]; then
echo "All 3 repos on branch: $WANT"
exit 0
fi
echo "Branch mismatch:" >&2
for line in "${mismatched[@]}"; do
echo " $line" >&2
done
exit 1

View file

@ -0,0 +1,26 @@
#!/bin/bash
# Run a git command in each of the 3 repos. Echoes before each run.
# Usage: ./for-each-repo.sh <git-subcommand-and-args>
# e.g. ./for-each-repo.sh status --short
# ./for-each-repo.sh fetch origin
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./repos.sh
source "$SCRIPT_DIR/repos.sh"
if [ $# -eq 0 ]; then
echo "Usage: $0 <git-subcommand> [args...]" >&2
exit 2
fi
for repo in "${REPOS[@]}"; do
p=$(repo_path "$repo")
echo "=== $repo ($p) ==="
echo "+ git -C $p $*"
if ! git -C "$p" "$@"; then
echo "FAILED in repo: $repo" >&2
exit 1
fi
done

View file

@ -0,0 +1,63 @@
#!/bin/bash
# Emit one JSON line per repo describing its state.
# Fields: repo, path, branch, dirty, ahead, behind, head_sha, main, rebase_in_progress, up_to_date_with_main
# "up_to_date_with_main" means origin/<main> is an ancestor of HEAD (i.e. rebase not needed).
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./repos.sh
source "$SCRIPT_DIR/repos.sh"
json_escape() {
# Minimal JSON string escape: backslash and double-quote only. Branch/sha never contain control chars in practice.
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
printf '%s' "$s"
}
for repo in "${REPOS[@]}"; do
p=$(repo_path "$repo")
main=$(repo_main "$repo")
branch=$(git -C "$p" branch --show-current || echo "")
head_sha=$(git -C "$p" rev-parse --short HEAD)
if [ -z "$(git -C "$p" status --porcelain)" ]; then
dirty="false"
else
dirty="true"
fi
ahead=0
behind=0
if [ -n "$branch" ] && git -C "$p" rev-parse --verify --quiet "origin/$main" >/dev/null; then
read -r behind ahead < <(git -C "$p" rev-list --left-right --count "origin/$main...HEAD" 2>/dev/null || echo "0 0")
fi
if [ -d "$p/.git/rebase-merge" ] || [ -d "$p/.git/rebase-apply" ]; then
rebase="true"
else
rebase="false"
fi
up_to_date="false"
if git -C "$p" rev-parse --verify --quiet "origin/$main" >/dev/null; then
if git -C "$p" merge-base --is-ancestor "origin/$main" HEAD 2>/dev/null; then
up_to_date="true"
fi
fi
printf '{"repo":"%s","path":"%s","branch":"%s","dirty":%s,"ahead":%s,"behind":%s,"head_sha":"%s","main":"%s","rebase_in_progress":%s,"up_to_date_with_main":%s}\n' \
"$(json_escape "$repo")" \
"$(json_escape "$p")" \
"$(json_escape "$branch")" \
"$dirty" \
"$ahead" \
"$behind" \
"$(json_escape "$head_sha")" \
"$(json_escape "$main")" \
"$rebase" \
"$up_to_date"
done

34
scripts/git-workflow/repos.sh Executable file
View file

@ -0,0 +1,34 @@
#!/bin/bash
# Sourced library. Single source of truth for the 3-repo layout.
# Usage: source "$(dirname "$0")/repos.sh"
ROOT_DIR="/Users/torcsi/dev/kicad-wasm"
REPOS=(root kicad wxwidgets)
PATH_root="$ROOT_DIR"
PATH_kicad="$ROOT_DIR/kicad"
PATH_wxwidgets="$ROOT_DIR/wxwidgets"
MAIN_root="main"
MAIN_kicad="wasm-port"
MAIN_wxwidgets="wasm-port"
repo_path() {
local var="PATH_$1"
echo "${!var}"
}
repo_main() {
local var="MAIN_$1"
echo "${!var}"
}
run_git() {
# Echo before run so the user always sees what we're doing.
local repo="$1"; shift
local p
p=$(repo_path "$repo")
echo "+ git -C $p $*" >&2
git -C "$p" "$@"
}