diff --git a/lib/gitflow-migrate.sh b/lib/gitflow-migrate.sh deleted file mode 100755 index 6cbc9b4..0000000 --- a/lib/gitflow-migrate.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/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() { # - 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() { # [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() { # - 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() { # (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() { # (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 |probe |remote }" >&2; exit 2 ;; - esac -fi