# Gitea backup and remote attach (Alpine BE)

Scripts for the BE Gitea instance at `https://apps.f0xx.org/app/androidcast_project/git/`.

**Safety:** The main working tree under  
`/var/www/localhost/htdocs/apps/app/androidcast_project/android_cast`  
is **never** modified. Only Gitea-owned paths from `/etc/gitea/app.ini` are backed up or reset.

## Temp files (/tmp)

Gitea scripts use `mktemp` work dirs for `git clone --mirror` fallbacks. Each `*.mirror.git` tree is **removed after every repo** (success or failure) and the work dir is removed on script exit.

Stale dirs (e.g. after `kill -9`) are pruned automatically on the next run (default: older than **60 minutes**). Manual cleanup:

```bash
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_cleanup_tmp.sh
# immediate wipe of all gitea-* temps:
GITEA_TMP_MAX_AGE_MIN=0 sudo ./gitea_cleanup_tmp.sh
```

## Prerequisites (Alpine BE)

- Gitea installed (`apk add gitea`), config at `/etc/gitea/app.ini`
- Run backup as **root** (service control + mysqldump)
- For attach: `git`, `curl`; **`GITEA_ADMIN_TOKEN`** (admin PAT from Gitea UI)

## 1. Full backup + optional reset

```bash
cd /path/to/android_cast/examples/crash_reporter/backend/scripts/gitea
chmod +x gitea_collect_backup.sh gitea_attach_remote.sh gitea_ini.sh

# default output: /var/backups/gitea/gitea-collect-YYYYMMDD-HHMMSS/
./gitea_collect_backup.sh
```

Collects:

| Item | Source (from `app.ini`) |
|------|-------------------------|
| Config | `/etc/gitea/app.ini` |
| App data | `[server] APP_DATA_PATH` (caches, indexes, attachments) |
| Bare repos | `[repository] ROOT` |
| LFS | `[lfs] PATH` (if set) |
| Logs | `[log] ROOT_PATH` |
| DB | `mysqldump` / `pg_dump` / sqlite file copy |
| Built-in dump | `gitea dump` when available |

At the end:

```text
Would you like to reset Gitea repositories state to the default (empty)? [y/N]
```

- **`N`** (default): backup only
- **`y`**: stops Gitea, `gitea admin repo-delete` per repo, clears **only** `[repository] ROOT` (must be under `/var/lib/gitea/`), restarts Gitea

## 2. Attach canonical remote as pull mirror

After backup (and optional reset), point Gitea at the real remote:

```bash
./gitea_attach_remote.sh git://10.7.0.10/android_cast
# or: GITEA_MIRROR_URL=git://f0xx.org/android_cast ./gitea_attach_remote.sh
```

### Do I need `GITEA_ADMIN_TOKEN`?

**Usually no** when run **as root on the BE**. The script tries `gitea admin user create-repo` and `gitea admin user generate-access-token` before push.

**Optional** if CLI fails — create in Gitea UI: **Avatar → Settings → Applications → Generate New Token** (scopes: All), then:

```bash
export GITEA_ADMIN_TOKEN='gitea_…'
./gitea_attach_remote.sh git://10.7.0.10/android_cast
```

**Why auth?** Gitea rejects anonymous `git push`. The token (or CLI-generated token) proves write access to `foxx/android_cast`.

**Prerequisite:** a **Gitea user** must exist (you have `admin`). Default owner is **`admin`** (override: `GITEA_OWNER=...`).

**Run the script as root** (so it can `su` to `RUN_USER` from `app.ini`, usually `gitea`):

```bash
sudo ./gitea_attach_remote.sh git://10.7.0.10/android_cast
```

Or run directly as the Gitea service user:

```bash
sudo -u gitea ./gitea_attach_remote.sh git://10.7.0.10/android_cast
```

Running `gitea admin ...` as `foxx` fails with `Expect user 'gitea' but current user is: foxx` — that is normal.

List users (on BE):

```bash
sudo -u gitea gitea -c /etc/gitea/app.ini admin user list
```

**`remote: Not found`** = repo did not exist yet and nothing created it. Re-sync the updated script and re-run.

Local push URL is `http://127.0.0.1:3000/foxx/android_cast.git` (no `/app/.../git/` — nginx strips subpath on :3000).

Override:

```bash
GITEA_MIRROR_URL='git://10.7.0.10/android_cast' \
GITEA_OWNER='foxx' GITEA_REPO='android_cast' \
./gitea_attach_remote.sh
```

**Method (no NFS/SSHFS):**

1. **Preferred:** Gitea API `POST /api/v1/repos/migrate` with `"mirror": true` — UI updates on Gitea’s mirror interval (`10m` default).
2. **Fallback:** `git clone --mirror` from remote + `git push --mirror` to local Gitea HTTP.

Ongoing sync: Gitea pull mirror or cron `gitea admin repo-sync-mirrors`.

## Allow local git mirror (`git://10.7.0.10/...`)

Gitea blocks private/local hosts by default (SSRF protection). For a LAN git daemon:

Edit the **live** config (often `/var/lib/gitea/custom/conf/app.ini`, not only `/etc/gitea/app.ini`):

```ini
[migrations]
ALLOW_LOCALNETWORKS = true
ALLOWED_DOMAINS = *,github.com,*.github.com,chromium.googlesource.com,git.zx2c4.com
BLOCKED_DOMAINS =
```

Notes:

- Section name is **`[migrations]`** (plural) — `[migration]` is ignored.
- `10.7.0.10` is a private IP → **`ALLOW_LOCALNETWORKS = true`** is required.
- On some Gitea versions an empty `ALLOWED_DOMAINS` still blocks UI mirrors; use **`*`** if needed.
- Tighter allowlist (optional): `ALLOWED_DOMAINS = 10.7.0.10` plus `ALLOW_LOCALNETWORKS = true`.

Restart Gitea, then re-sync the mirror:

```bash
sudo rc-service gitea restart
```

In UI: repo → **Settings → Mirror settings** → set interval (e.g. `10m`) → **Sync now**.

CLI (as `gitea` user):

```bash
sudo -u gitea gitea -c /var/lib/gitea/custom/conf/app.ini admin repo-sync-mirrors
```

Ensure BE can reach the git daemon:

```bash
git ls-remote git://10.7.0.10/android_cast HEAD
```

Optional faster default interval for all mirrors:

```ini
[repository]
DEFAULT_MIRROR_INTERVAL = 10m
MIRROR_UPDATE_INTERVAL = 10m
```

## AndroidCast organization (team repos)

Project mirrors live under the **`AndroidCast`** org (not personal `admin/`):

`https://apps.f0xx.org/app/androidcast_project/git/AndroidCast`

| Role | Who | Access |
|------|-----|--------|
| **Site admin** | Gitea user `admin` | Instance users/settings + org **Owners** |
| **Org Owners** | `admin`, `foxx` | Full control of all `AndroidCast/*` repos |
| **Future members** | Add to org teams in UI | Read/write per team (create e.g. Developers later) |

**One-time org setup** (idempotent; org already created in UI is fine):

```bash
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_migrate_org_setup.sh
```

This will:

- Ensure org **AndroidCast** exists (skip create if you made it in UI)
- Create Gitea user **`foxx`** if missing (`--must-change-password` — set password or SSH key in UI later)
- Add **`admin`** and **`foxx`** to org **Owners** team (org-admin on all repos)
- **Transfer** existing `admin/android_cast`, `admin/libvpx`, … → `AndroidCast/*` when present

Then migrate mirrors under the org:

```bash
cd /var/www/.../androidcast_project/android_cast
GITEA_OWNER=AndroidCast sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_migrate_project.sh .
```

Defaults: `GITEA_OWNER=AndroidCast`, `GITEA_ORG_OWNERS="admin foxx"`, `GITEA_TRANSFER_FROM=admin`.

Clone URLs become:

`https://apps.f0xx.org/app/androidcast_project/git/AndroidCast/android_cast.git`

## Full project migration (main + external submodules)

**`gitea_migrate_project.sh`** — one script for BE:

1. Pull-mirror **`android_cast`** from FE git (`git://10.7.0.10/android_cast`) with a **short interval** (default `1m`).
2. Read **external** submodules from a checkout (`.gitmodules` + **pinned SHA** at `HEAD` / `--ref`).
3. Create read-only pull mirrors for each upstream (libvpx, opus, speex, …).
4. Verify the **same commit digest** the superproject pins is reachable on each Gitea mirror.

Canonical `.gitmodules` stays on upstream URLs — no remote edit.

```bash
cd /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast
chmod +x examples/crash_reporter/backend/scripts/gitea/*.sh

sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_migrate_project.sh .

# Re-run submodules only (main mirror already exists):
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_migrate_project.sh . --skip-main

# Pin check at a tag/branch:
GITEA_REF=next sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_migrate_project.sh .
```

### Troubleshooting: only `android_cast` visible (Repositories 1)

`gitea_attach_remote.sh` mirrors **only the main repo**. Submodule repos need a separate step.

```bash
# 1) See what BE would mirror / what exists in Gitea:
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_diagnose_mirrors.sh .

# 2) Create submodule mirrors (main already exists):
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_migrate_project.sh . --skip-main
# or:
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_mirror_submodules.sh .gitmodules
```

Expect **4 repos** under **`AndroidCast`**: `android_cast`, `libvpx`, `opus`, `speex`. If migrate summary shows `submodules ok=0` and a WARN about empty discovery, `.gitmodules` is missing on the BE checkout — the updated migrate script falls back to `submodules.defaults.conf`.

In Gitea UI, use **Explore** or **Site Administration → Repositories** if the profile counter hides mirror repos.

### Fast sync for local `android_cast` mirror only

Gitea **default minimum** pull interval is **10m**. To use **1m** for the FE git mirror:

```ini
[mirror]
MIN_INTERVAL = 1m

[cron.update_mirrors]
SCHEDULE = @every 1m
RUN_AT_START = true
```

Restart Gitea. The migrate script sets `admin/android_cast` mirror interval to `GITEA_MAIN_MIRROR_INTERVAL` (default `1m`). External submodule mirrors default to `24h` (`GITEA_SUBMODULE_MIRROR_INTERVAL`).

**Near-immediate sync on each FE push** (better than polling):

| Method | Command |
|--------|---------|
| BE cron every minute | `* * * * * root /path/to/gitea_sync_mirror.sh android_cast` |
| FE `post-receive` hook | Copy `fe-android_cast-post-receive.example` → FE bare repo `hooks/post-receive`; set `GITEA_SYNC_CMD` (ssh to BE or curl `mirror-sync` API) |
| Manual | `sudo ./gitea_sync_mirror.sh android_cast` |

```bash
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_sync_mirror.sh android_cast
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_sync_mirror.sh --all
```

There is no built-in “push mirror from FE git daemon → Gitea” in stock git; **pull mirror + trigger** (`mirror-sync` API or hook) is the usual integration when git lives on FE and Gitea on BE.

## Submodule mirrors (libvpx, opus, speex, …)

Gitea does not “nest” submodules inside one repo — each submodule is its **own pull mirror** (`admin/libvpx`, `admin/opus`, …). That works **without** changing committed `.gitmodules`.

Prefer **`gitea_migrate_project.sh`** for the full flow; or submodule-only:

**1. Allow upstream hosts** in `app.ini` (see `[migrations]` block above — includes GitHub, googlesource, zx2c4).

**2. Run on BE (as root):**

```bash
cd /path/to/android_cast
chmod +x examples/crash_reporter/backend/scripts/gitea/*.sh

# From bundled list (libvpx, opus, speex, wireguard-android):
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_mirror_submodules.sh

# Or parse live .gitmodules from a checkout:
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_mirror_submodules.sh \
  /var/www/.../android_cast/.gitmodules
```

**3. Keep canonical `.gitmodules` unchanged** (upstream GitHub / googlesource URLs). After clone, rewrite submodule fetch URLs **locally**:

```bash
# Per checkout (.git/config only — does not touch .gitmodules):
./examples/crash_reporter/backend/scripts/gitea/gitea_submodule_local_rewrite.sh --apply

# Or machine-wide (url.insteadOf in ~/.gitconfig):
./examples/crash_reporter/backend/scripts/gitea/gitea_submodule_local_rewrite.sh --apply --global

git submodule update --init --recursive
```

On the BE working tree, run `--apply` once per checkout (e.g. after `git pull` in `androidcast_project/android_cast`).

**Optional:** commit Gitea URLs in `.gitmodules` on `git://10.7.0.10/android_cast` so every clone defaults to Gitea without the rewrite script. Skip this if you want upstream URLs to stay canonical.

**4. Add local submodules** — edit `submodules.defaults.conf` or pass a custom list:

```text
# repo|upstream_url|submodule_path
mylib|git://10.7.0.10/mylib|third-party/mylib
```

```bash
GITEA_SUBMODULES_CONF=/root/my-submodules.list sudo ./gitea_mirror_submodules.sh
```

**5. Sync all mirrors periodically:**

```bash
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`.

Apply the same Gitea rules to other AndroidCast repos:

```bash
GITEA_REPO=url-shortener GITEA_PROTECT_BRANCHES="next master" \
  sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_protect_branches.sh
```

(Branches that do not exist yet are skipped when using `gitea_attach_url_shortener.sh`.)

## url-shortener (first-party submodule mirror)

The url-shortener service lives in its **own** canonical repo  
`git://f0xx.org/androidcast_project/url-shortener` (submodule `backend/url-shortener`).  
It is **not** mirrored by `gitea_mirror_submodules.sh` (local `f0xx.org` URLs are skipped).

**One command on BE** (org + pull mirror + default `next` + protect `master`/`next` when present):

```bash
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_attach_url_shortener.sh
```

This reuses `gitea_attach_remote.sh`, `gitea_set_default_branch.sh`, and `gitea_protect_branches.sh` — no duplicate mirror logic.

| Step | What |
|------|------|
| Org | Ensures **AndroidCast** + **Owners** (`admin`, `foxx`) |
| Transfer | `admin/url-shortener` or `androidcast_project/url-shortener` → `AndroidCast/url-shortener` if present |
| Mirror | Pull from `git://f0xx.org/androidcast_project/url-shortener` (`GITEA_PRIVATE=false` — team-visible under public org) |
| Default branch | `next` |
| Protection | `next` and `master` only (skips branches not on canonical remote yet) |

Clone URL:

`https://apps.f0xx.org/app/androidcast_project/git/AndroidCast/url-shortener.git`

**FE canonical hook** (primary enforcement for `git push` to `git://f0xx.org/...`):

```bash
cp examples/git/hooks/pre-receive.android_cast.example \
   /path/to/url-shortener.git/hooks/pre-receive
chmod +x /path/to/url-shortener.git/hooks/pre-receive
```

Ensure `app.ini` `[migrations]` allows **`f0xx.org`** (in addition to `10.7.0.10` for the main repo).

`gitea_polish_project.sh` calls `gitea_attach_url_shortener.sh` automatically when the mirror is missing.

## Polish (finish line — safe BE run)

```bash
cd /var/www/.../androidcast_project/android_cast
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_polish_project.sh .
```

Idempotent: **foxx** → AndroidCast **Owners**, transfer `admin/*` repos, **sync mirrors now**, create missing **libvpx/opus/speex**, default branch **`next`**.

If `gitea_diagnose_mirror_sync.sh` shows `mirror=false` for `android_cast`:

```bash
GITEA_RECREATE_MAIN_MIRROR=yes sudo ./gitea_polish_project.sh .
```

**foxx UI:** browse **`…/git/AndroidCast`** (org), not only profile “Your repositories”.

**Email/2FA (deferred):** `REGISTER_EMAIL_CONFIRM = false` until SMTP; passwords via `gitea_user_admin.sh`.

## Verify

```bash
# Remote reachable from BE
git ls-remote git://f0xx.org/android_cast HEAD

# Gitea UI
curl -sS -o /dev/null -w '%{http_code}\n' \
  'https://apps.f0xx.org/app/androidcast_project/git/'

# After attach
git ls-remote "https://apps.f0xx.org/app/androidcast_project/git/foxx/android_cast.git" HEAD
```

## Troubleshooting

### 1) Mirror synced once, then nothing (1h+ stale)

Common cause: repo was created via **`git push --mirror` fallback**, not API **pull mirror** — Gitea shows the repo but **`mirror: false`** and cron never pulls again.

```bash
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_diagnose_mirror_sync.sh
```

If `api mirror=false`:

1. Delete `AndroidCast/android_cast` in Gitea (mirror copy only).
2. Re-attach with API migrate (must succeed):

```bash
GITEA_OWNER=AndroidCast sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_attach_remote.sh git://10.7.0.10/android_cast
```

Ensure `app.ini`:

```ini
[cron.update_mirrors]
ENABLED = true
SCHEDULE = @every 10m
RUN_AT_START = true

[migrations]
ALLOW_LOCALNETWORKS = true
```

Manual sync / stuck queue:

```bash
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_sync_mirror.sh android_cast
# Admin UI -> Monitor -> Queues -> Remove All (if log says "already in queue")
sudo rc-service gitea restart
```

### 2) Default branch `next` in UI

Per-repo (Gitea UI): **Settings → Repository → Default Branch → `next`**.

Or script (all repos under org):

```bash
GITEA_OWNER=AndroidCast GITEA_DEFAULT_BRANCH=next \
  sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_set_default_branch.sh
```

Run after mirror has fetched `next` from upstream. Each developer can still pick another branch locally; this only changes the **web UI default**.

### 3) Register says “user exists”, login fails (`foxx`)

`gitea_migrate_org_setup.sh` **pre-creates** `foxx` — do **not** use **Register**. The account has an admin-set random password (`must-change-password`).

**Site admin** sets password:

```bash
sudo ./examples/crash_reporter/backend/scripts/gitea/gitea_user_admin.sh reset-password foxx 'choose-a-password'
```

Or Gitea UI: **Site Administration → Users → foxx → Edit**.

Then **Sign In** (not Register). Add SSH key under **Settings → SSH / GPG Keys**.

If self-registration is intended instead: delete `foxx` in admin UI and use **Register** once, or set in `app.ini`:

```ini
[service]
DISABLE_REGISTRATION = false
REGISTER_EMAIL_CONFIRM = false
```

## Related docs

- [docs/INFRA.md](../../../../docs/INFRA.md) — `git://f0xx.org/android_cast`, disk layout
- [docs/TICKETS_ROADMAP.md](../../../../docs/TICKETS_ROADMAP.md) — Gitea URL / `ROOT_URL`
- [docs/GIT_FLOW.md](../../../../docs/GIT_FLOW.md) — branch flow after remote is wired
