lib core (start/finish/init, transactional bootstrap) + migrate + 57-test suite + aiguillage; skills/gitflow + gitignore template; CLAUDE.md gitflow rule; wiring init-project (5f/8/11), onboard (2.6), ship-feature (0/4/9), feat/bugfix/hotfix aiguillage.
96 lines
6.2 KiB
Bash
Executable File
96 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gitflow-migrate.sh — migrate an existing repo to the gitflow model.
|
|
# LOCAL (no token): gitflow init existing → master→main, develop, socle, hook.
|
|
# PROBE (token, READ-ONLY): identity + scope/rights, before any write.
|
|
# REMOTE (token, DESTRUCTIVE): push, default→main, protection, delete master.
|
|
# Writes ordered reversible→irreversible; DELETE master is LAST and only
|
|
# runs if every prior step succeeded. Halts on first failure.
|
|
# No `... | grep -q` under pipefail (SIGPIPE false-negative gotcha). Never echo the token.
|
|
set -uo pipefail
|
|
GITEA="${GITEA_URL:-https://git.bchanot.fr}"
|
|
OWNER="${GITEA_OWNER:-bchanot}"
|
|
|
|
# ── LOCAL half (token-free) ──────────────────────────────────────────────────
|
|
migrate_local() { # <repo-path>
|
|
local repo="$1" renamed="no"
|
|
cd "$repo" || { echo " ✗ cannot cd $repo" >&2; return 1; }
|
|
[ -z "$(git status --porcelain)" ] || { echo " ✗ working tree not clean — stash/commit first" >&2; return 2; }
|
|
{ [ -n "$(git config user.name)" ] && [ -n "$(git config user.email)" ]; } \
|
|
|| { echo " ✗ git identity unset (user.name/user.email) — set it before migrating $repo" >&2; return 3; }
|
|
git show-ref --verify -q refs/heads/master && renamed="yes"
|
|
bash "$HOME/.claude/lib/gitflow.sh" init || return 1
|
|
git show-ref --verify -q refs/heads/main || { echo " ✗ no main" >&2; return 1; }
|
|
git show-ref --verify -q refs/heads/develop || { echo " ✗ no develop" >&2; return 1; }
|
|
[ "$(git config core.hooksPath)" = ".githooks" ] || { echo " ✗ hook not active" >&2; return 1; }
|
|
[ -z "$(git status --porcelain)" ] || { echo " ✗ tree dirty after init" >&2; return 1; }
|
|
echo " ✓ local: main+develop, hook active, tree clean (master→main: $renamed)"
|
|
}
|
|
|
|
# ── Gitea API helper (token in header only; never printed) ────────────────────
|
|
_gitea() { # <METHOD> <api-path> [json-body]
|
|
local m="$1" p="$2" body="${3:-}"
|
|
curl -fsS -X "$m" -H "Authorization: token $GITEA_TOKEN" \
|
|
-H "Content-Type: application/json" ${body:+-d "$body"} "$GITEA/api/v1$p"
|
|
}
|
|
_json() { python3 -c "import sys,json;$1" 2>/dev/null; } # tiny JSON field reader
|
|
|
|
# ── PROBE (READ-ONLY: identity informational, rights = the real gate) ─────────
|
|
# /user needs read:user (cosmetic — the migration never calls it) → informational.
|
|
# The gates are the repo-scoped rights the writes actually require: admin+push on
|
|
# the repo, and admin scope confirmed by a readable branch_protections list.
|
|
gitea_probe() { # <repo-name to test rights against>
|
|
local name="$1" me pj perm
|
|
[ -n "${GITEA_TOKEN:-}" ] || { echo " ✗ GITEA_TOKEN unset" >&2; return 1; }
|
|
|
|
# [a] identity — INFORMATIONAL (needs read:user scope the migration never uses)
|
|
if me=$(_gitea GET "/user" 2>/dev/null | _json "print(json.load(sys.stdin).get('login','?'))") && [ -n "$me" ]; then
|
|
echo " ✓ token identity: $me"
|
|
else
|
|
echo " ⚠ token identity unavailable (no read:user scope) — cosmetic, migration is repo-scoped"
|
|
fi
|
|
|
|
# [b] repo rights — GATE: admin AND push must be true (default_branch, protections, push)
|
|
pj=$(_gitea GET "/repos/$OWNER/$name") \
|
|
|| { echo " ✗ GET /repos/$OWNER/$name failed — token lacks repo read scope" >&2; return 1; }
|
|
perm=$(printf '%s' "$pj" | _json "p=json.load(sys.stdin).get('permissions',{});print('admin=%s push=%s pull=%s'%(p.get('admin'),p.get('push'),p.get('pull')))")
|
|
printf '%s' "$pj" | _json "p=json.load(sys.stdin).get('permissions',{});sys.exit(0 if (p.get('admin') and p.get('push')) else 1)" \
|
|
|| { echo " ✗ insufficient rights on $name ($perm) — need admin+push" >&2; return 1; }
|
|
echo " ✓ rights on $name: $perm (admin+push confirmed)"
|
|
|
|
# [c] admin-scope canary — GATE: branch_protections readable (POST/PATCH/DELETE need repo-admin)
|
|
_gitea GET "/repos/$OWNER/$name/branch_protections" >/dev/null \
|
|
|| { echo " ✗ cannot read branch_protections — token lacks repo-admin scope; protection step would fail" >&2; return 1; }
|
|
echo " ✓ repo-admin scope confirmed (branch_protections readable → POST/PATCH/DELETE OK)"
|
|
}
|
|
|
|
# ── REMOTE half (DESTRUCTIVE; reversible→irreversible; delete master LAST) ────
|
|
_protect() { # <repo-name> <branch> (Option 1: owner-pushable)
|
|
_gitea POST "/repos/$OWNER/$1/branch_protections" \
|
|
"{\"branch_name\":\"$2\",\"enable_push\":true,\"enable_push_whitelist\":true,\"push_whitelist_usernames\":[\"$OWNER\"]}"
|
|
}
|
|
migrate_remote() { # <repo-name> (cwd = the local repo)
|
|
local name="$1"
|
|
[ -n "${GITEA_TOKEN:-}" ] || { echo " ✗ GITEA_TOKEN unset" >&2; return 1; }
|
|
echo " [1/4] push main + develop (ADDITIVE/reversible)…"
|
|
git push -u origin main || { echo " ✗ push main failed (push scope?) — STOP, nothing irreversible done" >&2; return 1; }
|
|
git push -u origin develop || { echo " ✗ push develop failed — STOP" >&2; return 1; }
|
|
echo " [2/4] default_branch → main (REVERSIBLE — scope canary)…"
|
|
_gitea PATCH "/repos/$OWNER/$name" '{"default_branch":"main"}' >/dev/null \
|
|
|| { echo " ✗ PATCH default_branch failed (admin/write scope?) — STOP before protection & delete" >&2; return 1; }
|
|
echo " [3/4] branch protection main + develop (REVERSIBLE)…"
|
|
_protect "$name" main >/dev/null || { echo " ✗ protect main failed — STOP before delete" >&2; return 1; }
|
|
_protect "$name" develop >/dev/null || { echo " ✗ protect develop failed — STOP before delete" >&2; return 1; }
|
|
echo " [4/4] DELETE remote master (IRREVERSIBLE — last; default already repointed)…"
|
|
git push origin --delete master || { echo " ✗ delete master failed (left in place — safe)" >&2; return 1; }
|
|
echo " ✓ remote: default=main, main/develop protected (owner-pushable), remote master deleted"
|
|
}
|
|
|
|
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
case "${1:-}" in
|
|
local) migrate_local "$2" ;;
|
|
probe) gitea_probe "$2" ;;
|
|
remote) migrate_remote "$2" ;;
|
|
*) echo "usage: gitflow-migrate.sh {local <repo>|probe <name>|remote <name>}" >&2; exit 2 ;;
|
|
esac
|
|
fi
|