Implemented directly at human product owner's direction, independent of Codex's separate uncommitted attempt at the same task (not read or referenced here). - CommitDetail (reusing CommitSummary) with per-file added/modified/ deleted/renamed/other status; deleted-author and missing-rename-path cases handled without crashing or inventing data. - BoundedPatch for commit and comparison diffs, sourced from Gitea's .diff raw-text endpoints (JSON commit/compare objects only carry file status, not diff content); byte limit then line limit, both constructor-injected and validated positive. - Comparison uses one combined base...head path segment, built by concatenating both refs before a single addPathSegment call so each is escaped exactly once; pagination computed locally since Gitea's compare endpoint has no server-side paging. - Reuses Page<T> and ApiFailure; no put/delete/postForm call anywhere in the file (read-only, as required). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7.6 KiB
TASK-009: Implement history and diff API contracts
Status: implemented; all required checks pass; pending Codex review and contract freeze 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
- Domain contracts distinguish added, modified, renamed, and deleted files (
FileChangeStatus; anOTHERfallback covers Gitea statuses beyond those four, e.g.typechange, so an unrecognized value degrades instead of failing a whole commit closed). - Commit history and comparison pagination is explicit and immutable — commit listing reuses the existing
Page<CommitSummary>from TASK-004'sGiteaRepositoryBrowser.listCommits; comparison pagination reuses the samePage<T>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). - Patch size and line limits produce an explicit truncated state —
BoundedPatch(text, isTruncated); byte limit applied before line limit, both independently tested. - Base and head refs are encoded independently exactly once — Gitea's compare endpoint takes one combined
base...headpath segment; both ref values are concatenated first and passed through a singleaddPathSegmentcall so each character (including any/inside a branch name) is escaped exactly once, verified by a test using a ref containing/. - Empty repository, missing ref, 401, 403, 404, 409, 422, rate-limit, network, parse, and server failures map to typed results — reuses the existing
ApiFailuresealed 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). - Representative JSON and fake-server tests cover commit detail, rename/delete, comparison, pagination, and truncation (14 tests; see Evidence).
- No write endpoint is exposed —
GiteaHistoryApionly callsHttpTransport.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
./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'smodules/structs/{repo_commit,issue,pull,pull_review,issue_comment}.gorather than guessed: a commit'sfilesarray carriesfilename/status(CommitAffectedFiles); apull_requestfield on an issue DTO marks it as actually a PR;Compare{ total_commits, commits: []Commit }has no separate paging mechanism. HistoryRepositorydeliberately does not re-expose commit-history listing, sinceGiteaRepositoryBrowser.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}.difffor a commit,/repos/{owner}/{repo}/compare/{base}...{head}.difffor 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 sameLong-safe from/to-index slicing pattern as TASK-004'slistTree.
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):
./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. whetherchanged/removedsynonyms actually appear on this Gitea version) was not exhaustively confirmed; both recognized synonyms and a genericOTHERfallback are handled defensively either way. - The
.diff-suffixed compare endpoint's exact path was inferred from Gitea's documented single-commit.diff/.patchconvention 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.