Files

5.7 KiB

mopac-gitea-go

A 100% Go client library and CLI (mgit) for the Gitea API (v1). Replaces shell/tea glue for every forge operation the MOPAC verticals need: one static binary, zero third-party modules (stdlib only), machine-parseable JSON output, and a token that never appears in flags, logs, or error strings. Built for the harness PR flow: branch list -> commit status -> PR create/list/merge.

Status: 2026-08-29 — v0 complete and green: repo create/list/show, branch list, commit statuses (report + combined), pull requests (create/list/show/merge, all five merge styles), token-scoped auth via GITEA_URL/GITEA_KEY, 0600 env-file config, -o json on every command, exit codes 0/1/2 with one-line API-error stderr. Built and tested entirely against a fake Gitea (unit suite + end-to-end smoke); the live forge (git.knownelement.com) attaches with zero code change via GITEA_URL/GITEA_KEY.

Architecture

flowchart LR
    subgraph host[Host]
        M[mgit CLI\nbin/mgit] --config 0600 env --> M
        S[smoke/smoke.sh]
    end
    subgraph docker[Digest-pinned Docker builder — ALL dev]
        B[go build / vet / test]
        F[smoke/fakegitea\nstateful in-memory Gitea]
    end
    subgraph wire[Library: package gitea]
        C[Client\nAuthorization: token header only]
    end
    M --> C
    C --> F
    C -.-> R[(real Gitea\ngit.knownelement.com)]
    S --> B
    S --> F

All compilation, vetting, and testing runs inside the digest-pinned golang:1.26-bookworm builder (./dev.sh); the host never runs a Go toolchain. Tests and smoke talk only to the in-process/containerized fake; the real forge is never contacted by the test suite.

Quickstart (verified against the fake)

This is the exact flow the smoke run (./dev.sh smoke) executes:

./dev.sh check          # build + vet + test inside the Docker builder
./dev.sh smoke          # boots fake Gitea on 127.0.0.1:8602, drives bin/mgit

# against a real forge instead:
export GITEA_URL=https://git.knownelement.com
export GITEA_KEY=<api token>            # or: --config ~/.config/mgit/env (0600)

mgit repo create -n vertical-x --owner ukrrs --desc "one vertical" --private --auto-init
mgit repo list                          # your repos
mgit repo list --owner ukrrs            # an org's / user's repos
mgit repo show ukrrs/Mopac-gitea-go
mgit branch list ukrrs/MOPAC
mgit status create ukrrs/MOPAC feat/quota --state success \
    --context ci/lint --description "lint clean" --target-url https://ci.example/run/1
mgit status list ukrrs/MOPAC feat/quota          # combined + per-check
mgit pr create ukrrs/MOPAC --title "Quota accounting" --body - \
    --base main --head feat/quota <<'EOF'
## Scope
- usage accounting per acting identity
EOF
mgit pr list ukrrs/MOPAC                          # open by default
mgit pr list ukrrs/MOPAC --state all
mgit pr show ukrrs/MOPAC 1
mgit pr merge ukrrs/MOPAC 1 --do squash

Add -o json to any command for machine output. REF arguments accept anything the server resolves (full or short sha, branch, tag); PR head may be branch or owner:branch (fork syntax).

Command reference

Command Effect Key flags
repo create -n NAME create repo (under your account, or an org) --owner ORG, --desc, --private, --auto-init, --default-branch
repo list list repos --owner OWNER (default: yours)
repo show OWNER/REPO one repo
branch list OWNER/REPO branches with tip shas
status create OWNER/REPO REF report a commit status --state success|pending|error|failure, --context, --description, --target-url
status list OWNER/REPO REF combined status + latest per check
pr create OWNER/REPO open a PR --title, --body FILE|-, --base, --head
pr list OWNER/REPO list PRs (open by default) --state open|closed|all
pr show OWNER/REPO NUMBER one PR with body
pr merge OWNER/REPO NUMBER merge an open PR --do merge|rebase|rebase-merge|squash|fast-forward
help usage

Flags may appear before or after positionals (mgit pr merge ukrrs/MOPAC 1 --do squash parses).

Configuration

Source Keys Discipline
env vars GITEA_URL, GITEA_KEY wins over the file
--config PATH (or MGIT_CONFIG, or ~/.config/mgit/env) same keys must be 0600 or stricter; refused before any read

The token travels ONLY in the Authorization: token header. Response bodies are never surfaced in error strings — the fake Gitea deliberately echoes the presented token back in every error body, and the suite plus the smoke redaction pass prove it appears nowhere else.

Library

import "git.knownelement.com/ukrrs/mopac-gitea-go/gitea"

c := gitea.New(gitea.Config{BaseURL: url, Token: key})
prs, err := c.ListPullRequests(ctx, "ukrrs", "MOPAC", gitea.PRStateOpen)
err := c.MergePullRequest(ctx, "ukrrs", "MOPAC", prs[0].Number, gitea.MergeParams{Do: gitea.MergeSquash})

Sentinel errors (ErrAuth, ErrNotFound, ErrValidation, ErrServer, ErrUnreachable, ErrMalformedResponse) wrap-check with errors.Is; every API failure carries an http NNN code in its one-line message. This is the surface the harness turns use for their PR flow — no copy-paste, no shell parsing.

Development

./dev.sh build|vet|test|check|smoke|shell   # everything runs in the digest-pinned builder

The fake Gitea (internal/fakegitea) implements exactly the endpoints the client uses, records every request for round-trip assertions, honors FailSpec for pinned error responses, and maintains real semantics: latest-status-per-context folding, worst-of combined status, short-sha resolution, merge-style validation, 409 on duplicate open PRs and re-merges.

License

AGPLv3 (see LICENSE).