From 88ca1b9e7044aed1e1e398be469fb6182f2042ab Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Thu, 11 Jun 2026 10:41:44 +0200 Subject: [PATCH] gitea sync --- .../backend/scripts/gitea/README.md | 34 ++++ .../scripts/gitea/gitea_polish_project.sh | 9 +- .../scripts/gitea/gitea_protect_branches.sh | 172 ++++++++++++++++++ .../hooks/pre-receive.android_cast.example | 70 +++++++ 4 files changed, 284 insertions(+), 1 deletion(-) create mode 100755 examples/crash_reporter/backend/scripts/gitea/gitea_protect_branches.sh create mode 100644 examples/git/hooks/pre-receive.android_cast.example diff --git a/examples/crash_reporter/backend/scripts/gitea/README.md b/examples/crash_reporter/backend/scripts/gitea/README.md index c0d662e..10f5d4d 100644 --- a/examples/crash_reporter/backend/scripts/gitea/README.md +++ b/examples/crash_reporter/backend/scripts/gitea/README.md @@ -331,6 +331,40 @@ GITEA_SUBMODULES_CONF=/root/my-submodules.list sudo ./gitea_mirror_submodules.sh sudo -u gitea gitea -c /var/lib/gitea/custom/conf/app.ini admin repo-sync-mirrors ``` +## Branch protection (`master`, `next` only) + +Server-side rules per [docs/GIT_FLOW.md](../../../../docs/GIT_FLOW.md) §7. **Two places:** + +| Where | Tool | +|-------|------| +| **Gitea** `AndroidCast/android_cast` | `gitea_protect_branches.sh` | +| **Canonical FE** `git://10.7.0.10/android_cast` | `examples/git/hooks/pre-receive.android_cast.example` | + +**Gitea (BE):** + +```bash +sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_protect_branches.sh +sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_protect_branches.sh --list +``` + +- `master` and `next` only (no `*` wildcard — `feature/*` stays free) +- Force-push **disabled for everyone** +- Push/merge allowlist: **admin**, **foxx**, org team **Owners** +- Admins cannot bypass merge rules + +**FE bare repo** (primary enforcement for `git push origin`): + +```bash +# on FE, in bare repo hooks/ +cp examples/git/hooks/pre-receive.android_cast.example \ + /path/to/android_cast.git/hooks/pre-receive +chmod +x /path/to/android_cast.git/hooks/pre-receive +``` + +Rejects on `master` / `next`: branch delete, non-fast-forward (force-push). Other branches unchanged. + +`gitea_polish_project.sh` runs branch protection automatically unless `GITEA_SKIP_BRANCH_PROTECT=1`. + ## Polish (finish line — safe BE run) ```bash diff --git a/examples/crash_reporter/backend/scripts/gitea/gitea_polish_project.sh b/examples/crash_reporter/backend/scripts/gitea/gitea_polish_project.sh index c3f0ce6..71503de 100755 --- a/examples/crash_reporter/backend/scripts/gitea/gitea_polish_project.sh +++ b/examples/crash_reporter/backend/scripts/gitea/gitea_polish_project.sh @@ -200,7 +200,12 @@ if command -v "$SCRIPT_DIR/gitea_set_default_branch.sh" >/dev/null 2>&1; then sh "$SCRIPT_DIR/gitea_set_default_branch.sh" || warn "default branch set failed (sync first)" fi -# 7) Summary +# 7) Branch protection (master, next) +if [ "${GITEA_SKIP_BRANCH_PROTECT:-0}" -eq 0 ]; then + sh "$SCRIPT_DIR/gitea_protect_branches.sh" || warn "branch protection failed — run manually" +fi + +# 8) Summary log "=== done ===" cat <&2; } +die() { printf '[gitea-protect] ERROR: %s\n' "$*" >&2; exit 1; } + +gitea_api_token() { + _t="${GITEA_ADMIN_TOKEN:-}" + [ -n "$_t" ] || _t="$(gitea_resolve_push_token)" || return 1 + printf '%s' "$_t" +} + +gitea_list_branch_protections() { + command -v curl >/dev/null 2>&1 || return 1 + _token="$(gitea_api_token)" || return 1 + _api="$(gitea_api_base)" + curl -sS "$_api/repos/$GITEA_OWNER/$GITEA_REPO/branch_protections" \ + -H "Authorization: token $_token" +} + +gitea_protection_exists() { + _branch="$1" + gitea_list_branch_protections 2>/dev/null | grep -q "\"branch_name\":\"$_branch\"" +} + +gitea_protect_payload() { + _branch="$1" + _prio="$2" + _users_json="" + for _u in $GITEA_PROTECT_PUSH_USERS; do + [ -n "$_u" ] || continue + _users_json="$_users_json\"$_u\"," + done + _users_json="${_users_json%,}" + + cat </dev/null 2>&1 || return 1 + _token="$(gitea_api_token)" || return 1 + _api="$(gitea_api_base)" + _payload="$(gitea_protect_payload "$_branch" "$_prio")" + _resp="$(mktemp /tmp/gitea-protect-XXXXXX.json)" + + if gitea_protection_exists "$_branch"; then + log "update protection: $GITEA_OWNER/$GITEA_REPO::$_branch" + _http="$(curl -sS -o "$_resp" -w '%{http_code}' \ + -X PATCH "$_api/repos/$GITEA_OWNER/$GITEA_REPO/branch_protections/$_branch" \ + -H "Authorization: token $_token" \ + -H "Content-Type: application/json" \ + -d "$_payload")" + else + log "create protection: $GITEA_OWNER/$GITEA_REPO::$_branch" + _http="$(curl -sS -o "$_resp" -w '%{http_code}' \ + -X POST "$_api/repos/$GITEA_OWNER/$GITEA_REPO/branch_protections" \ + -H "Authorization: token $_token" \ + -H "Content-Type: application/json" \ + -d "$_payload")" + fi + + case "$_http" in + 200|201|204) gitea_rm_path "$_resp"; return 0 ;; + *) + warn "HTTP $_http for $_branch:" + head -c 500 "$_resp" >&2 2>/dev/null || true + printf '\n' >&2 + gitea_rm_path "$_resp" + return 1 + ;; + esac +} + +list_protections() { + gitea_load_config || exit 1 + gitea_ensure_cli_access || exit 1 + log "branch protections: $GITEA_OWNER/$GITEA_REPO" + gitea_list_branch_protections | tr ',' '\n' | grep -E 'branch_name|enable_force_push|enable_push' || true +} + +# --- main --- + +case "${1:-}" in + -h|--help|help) + sed -n '2,16p' "$0" + exit 0 + ;; + --list|list) + list_protections + exit 0 + ;; +esac + +gitea_load_config || exit 1 +gitea_ensure_cli_access || exit 1 + +if ! gitea_repo_exists_api "$GITEA_OWNER" "$GITEA_REPO"; then + die "repo $GITEA_OWNER/$GITEA_REPO not found — run gitea_polish_project.sh first" +fi + +log "repo=$GITEA_OWNER/$GITEA_REPO branches=$GITEA_PROTECT_BRANCHES" +log "push allowlist: users=$GITEA_PROTECT_PUSH_USERS team=$GITEA_PROTECT_PUSH_TEAM" +log "force-push: disabled for all | side branches: unprotected" + +_prio=1 +_fail=0 +for _b in $GITEA_PROTECT_BRANCHES; do + gitea_apply_protection "$_b" "$_prio" || _fail=1 + _prio=$((_prio + 1)) +done + +cat <&2 + return 1 +} + +while read -r oldrev newrev refname; do + case "$refname" in + refs/heads/*) ;; + *) continue ;; + esac + branch="${refname#refs/heads/}" + is_protected "$branch" || continue + + check_user || exit 1 + + # Deletion + if [ "$newrev" = "$zero" ]; then + echo "hooks: refusing to delete protected branch '$branch'" >&2 + exit 1 + fi + + # New branch creation on protected name (allow only if empty repo edge case) + if [ "$oldrev" = "$zero" ]; then + continue + fi + + # Non-fast-forward => force-push or history rewrite + if ! git merge-base --is-ancestor "$oldrev" "$newrev" 2>/dev/null; then + echo "hooks: refusing non-fast-forward push to protected branch '$branch'" >&2 + echo "hooks: use feature/* branch + merge (see docs/GIT_FLOW.md)" >&2 + exit 1 + fi +done + +exit 0