diff --git a/README.md b/README.md new file mode 100644 index 0000000..66b877f --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ +# 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 + +```mermaid +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: + +```sh +./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= # 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 + +```go +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 + +```sh +./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).