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>
69 lines
7.6 KiB
Markdown
69 lines
7.6 KiB
Markdown
# 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
|
|
|
|
- [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<CommitSummary>` from TASK-004's `GiteaRepositoryBrowser.listCommits`; comparison pagination reuses the same `Page<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).
|
|
- [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.
|