# TASK-009: Implement history and diff API contracts Status: implemented; all required checks pass; no review pending (Codex retired, 2026-09-29) Owner: Claude (human product owner directed Claude to implement this directly rather than wait for Codex, 2026-08-26) Reviewer: Codex ## Outcome Provide frozen read-only domain and data contracts for commit history, changed files, bounded patches, and ref comparison so Claude can implement TASK-007 without editing shared layers. ## Writable paths - `app/src/main/java/com/ubisam/ubgitea/domain/history/` - `app/src/main/java/com/ubisam/ubgitea/data/history/` - `app/src/test/java/com/ubisam/ubgitea/domain/history/` - `app/src/test/java/com/ubisam/ubgitea/data/history/` - `tasks/009-history-diff-api-contract.md` ## Acceptance criteria - [x] Domain contracts distinguish added, modified, renamed, and deleted files (`FileChangeStatus`; an `OTHER` fallback covers Gitea statuses beyond those four, e.g. `typechange`, so an unrecognized value degrades instead of failing a whole commit closed). - [x] Commit history and comparison pagination is explicit and immutable — commit *listing* reuses the existing `Page` from TASK-004's `GiteaRepositoryBrowser.listCommits`; comparison pagination reuses the same `Page` type, computed locally (Gitea's compare endpoint returns the full commit list in one response with no server-side paging — confirmed against known Gitea API limitations, not assumed). - [x] Patch size and line limits produce an explicit truncated state — `BoundedPatch(text, isTruncated)`; byte limit applied before line limit, both independently tested. - [x] Base and head refs are encoded independently exactly once — Gitea's compare endpoint takes one combined `base...head` path segment; both ref values are concatenated first and passed through a single `addPathSegment` call so each character (including any `/` inside a branch name) is escaped exactly once, verified by a test using a ref containing `/`. - [x] Empty repository, missing ref, 401, 403, 404, 409, 422, rate-limit, network, parse, and server failures map to typed results — reuses the existing `ApiFailure` sealed class (no parallel hierarchy); 404/409 explicitly tested here (a missing ref and an empty-repository response are both plausible Gitea outcomes covered by those two codes). - [x] Representative JSON and fake-server tests cover commit detail, rename/delete, comparison, pagination, and truncation (14 tests; see Evidence). - [x] No write endpoint is exposed — `GiteaHistoryApi` only calls `HttpTransport.get`; confirmed by grep. - [ ] Evidence records the reviewed contract-freeze commit for TASK-007 — cannot be closed by this task alone; requires Codex's read-only review of this commit first, per the same freeze convention used for TASK-005→006 and requested for TASK-010→008. ## Required checks ```text ./gradlew assembleDebug ./gradlew testDebugUnitTest ./gradlew lintDebug ``` ## Evidence Implemented independently on branch `claude/task-009-history-diff-contract` from commit `7d122d4`, at the human product owner's explicit direction to proceed without waiting for Codex's separate, still-uncommitted TASK-009 attempt in the primary worktree. That uncommitted work was not read or referenced; this implementation was designed from this task file's acceptance criteria and Gitea's own published Go source structs, independently. - New files (writable scope only): `domain/history/HistoryModels.kt`, `domain/history/HistoryRepository.kt`, `data/history/GiteaHistoryApi.kt`, `data/history/GiteaHistoryApiTest.kt`. - Design grounded in Gitea's actual API shapes, checked against `go-gitea/gitea`'s `modules/structs/{repo_commit,issue,pull,pull_review,issue_comment}.go` rather than guessed: a commit's `files` array carries `filename`/`status` (`CommitAffectedFiles`); a `pull_request` field on an issue DTO marks it as actually a PR; `Compare{ total_commits, commits: []Commit }` has no separate paging mechanism. - `HistoryRepository` deliberately does not re-expose commit-history listing, since `GiteaRepositoryBrowser.listCommits` (TASK-004) already provides it as a proper paged endpoint; this contract only adds commit detail (with files), bounded patch text, and ref comparison. - Bounded patch: `GiteaHistoryApi(maxPatchBytes = 200_000, maxPatchLines = 4_000, ...)` — both constructor-injected and validated positive. Byte bound applied first (via UTF-8 byte truncation), then line bound, matching the truncation-ordering pattern already used by TASK-006A's classifier. - Raw diff/patch text is fetched from Gitea's `.diff`-suffixed endpoints (`/repos/{owner}/{repo}/git/commits/{sha}.diff` for a commit, `/repos/{owner}/{repo}/compare/{base}...{head}.diff` for a comparison) rather than expecting full diff content inside the JSON commit/compare objects, since Gitea's JSON responses only carry per-file status, not line-level diff text (a documented Gitea API limitation, not an assumption). - `compare`'s pagination is computed locally against the single JSON response Gitea returns (no server-side page/limit parameters exist for this endpoint), using the same `Long`-safe from/to-index slicing pattern as TASK-004's `listTree`. Test coverage (14 cases in `GiteaHistoryApiTest`): commit detail maps every recognized file status plus an unknown-status fallback (1), renamed file with no `previous_filename` maps to `null` instead of crashing (1), patch under both limits returned whole (1), patch over the byte limit truncated (1), patch over the line limit truncated (1), compare correctly encodes a ref containing `/` into one basehead segment (1), compare paginates one response across two pages (1), compare-patch requests the correct diff-suffixed URL (1), missing token fails closed (1), 404 and 409 status mapping (1), malformed commit response fails closed (1), two constructor-validation tests for non-positive limits (2). Checks run from the new worktree (`local.properties` created locally, not committed): ```text ./gradlew testDebugUnitTest --tests "com.ubisam.ubgitea.data.history.GiteaHistoryApiTest" → BUILD SUCCESSFUL, 14/14 passed ./gradlew assembleDebug testDebugUnitTest lintDebug --rerun-tasks → BUILD SUCCESSFUL - 141 unit tests across 25 suites, 0 failures/errors - 0 lint errors git diff --check → clean sensitive-pattern scan of changed files → no matches ``` Known limitations: - Gitea's exact per-file status vocabulary beyond `added`/`modified`/`deleted`/`renamed` (e.g. whether `changed`/`removed` synonyms actually appear on this Gitea version) was not exhaustively confirmed; both recognized synonyms and a generic `OTHER` fallback are handled defensively either way. - The `.diff`-suffixed compare endpoint's exact path was inferred from Gitea's documented single-commit `.diff`/`.patch` convention rather than independently confirmed for the compare route specifically; this should be verified against a live Gitea instance or its own compare-diff documentation during review. - No physical device or live Gitea instance was used; all coverage is fake-transport/unit-level, consistent with TASK-004/010's own pre-freeze verification. - Codex was independently attempting this same task in the primary worktree (uncommitted at the time this was written); that work was not inspected. Whoever reviews this should reconcile the two rather than assume this one is the only attempt. No open P0-P3 findings from self-review against the acceptance criteria. Requesting Codex read-only review and contract-freeze recording so TASK-007 can begin.