# TASK-017: Sort repositories by recent push, page the list, and show the last-pushed branch Status: implemented; all required checks pass; pending Codex review Owner: Claude (human product owner priority-1 request, 2026-08-26) Reviewer: Codex ## Outcome The repository list shows the signed-in account's repositories most-recently-pushed first, loads them a page at a time instead of eagerly loading everything, and shows each visible repository's most recently pushed branch. ## Scope - Included: - Add `updatedAt` (Gitea `updated_at`) to `RepositorySummary` and request server-side `sort=updated&order=desc` so ordering is correct across pages. - Replace the legacy eager-loading `RepositoryListApi`/`AccessibleRepositoriesGateway` (which pages internally and returns one aggregated list) with the existing paged `GiteaRepositoryBrowserApi.listRepositories(page, pageSize)` from TASK-004, wired through the domain `Page` contract. - Reduce the repository list page size to 5 (human product owner decision, 2026-08-26) with explicit load-more/pagination UI, replacing today's single unpaginated load. - Add `commit.timestamp` to `BranchSummary` (mapped from Gitea's branch-list `commit.timestamp` field) and, for each repository visible on the current page only, fetch its branches and surface the one with the latest `commit.timestamp` as "last pushed branch." - Excluded: - Any write/mutation of repository or branch state. - Favorites/starring (TASK-018). - Sorting or paging the branch/tag list itself (already paged; unaffected). ## Writable paths - `app/src/main/java/com/ubisam/ubgitea/domain/model/RepositoryModels.kt` (add fields only; no existing field removed) - `app/src/main/java/com/ubisam/ubgitea/data/repositories/` - `app/src/main/java/com/ubisam/ubgitea/feature/repositories/` - `app/src/test/java/com/ubisam/ubgitea/domain/model/` - `app/src/test/java/com/ubisam/ubgitea/data/repositories/` - `app/src/test/java/com/ubisam/ubgitea/feature/repositories/` - `app/src/main/res/values/strings_repositories.xml` - `tasks/017-repository-list-recent-push.md` - `docs/development-log.md` (append-only) ## Acceptance criteria - [x] Repository list requests `sort=updated&order=desc`; ordering is correct across page boundaries (verified by a fake-transport test asserting the query parameters, not by trusting client-side re-sorting). - [x] `RepositorySummary.updatedAt` is populated and covered by DTO-mapping tests (present, absent/malformed → typed failure, consistent with existing `.required()` fail-closed pattern). - [x] The repository list screen loads 5 repositories per page with explicit next-page loading; no behavior change for accounts with 5 or fewer repositories. - [x] The legacy `RepositoryListApi`/`AccessibleRepositoriesGateway` code path and its now-unused tests are removed once the ViewModel is migrated to `GiteaRepositoryBrowserApi.listRepositories`; no duplicate repository-list implementation remains. - [x] Each of the 5 visible repository rows independently fetches and displays its last-pushed branch name once available; a slow or failed per-row branch fetch does not block or fail the rest of the page, and is retried independently. - [x] Repository row branch-timestamp fetches are cancelled when their row scrolls out of the current page/is replaced by a refresh, to avoid orphaned requests (races/cancellation, matching the existing `startOperation` pattern). - [x] Loading, empty, offline, authentication-expired, and server-error states remain accessible and localized for both the repository page and the per-row branch fetch. - [x] Tests cover: sort/pagination query construction, page-boundary correctness, per-row branch-timestamp selection (including a repository with zero branches, and a tie on identical timestamps), and cancellation on row replacement. ## Security and privacy No additional concerns — read-only, no new OAuth scope, no credential or token exposure. ## Required checks ```text ./gradlew assembleDebug ./gradlew testDebugUnitTest ./gradlew lintDebug ``` ## Evidence Implemented on branch `claude/task-017-recent-push-favorites` from commit `fdda164` (TASK-004/005/006 reviewed-and-frozen base). - Domain: `RepositorySummary` gained `updatedAtEpochMillis: Long`; `defaultBranch` changed from non-null `RepositoryRef` to nullable `RepositoryRef?` (an empty/newly-created Gitea repository can have no default branch — previously this threw `ApiFailure.InvalidResponse()` for the *entire* page instead of marking just that one repository, a latent bug surfaced by routing the main list through the already-existing TASK-004 paged endpoint). `BranchSummary` gained `commitAtEpochMillis: Long`. - Data: `GiteaRepositoryBrowserApi.listRepositories` now requests `sort=updated&order=desc` and maps `updated_at`; `listBranches` maps `commit.timestamp`. A shared `String.toEpochMillisOrInvalid()` helper replaced three near-duplicate `Instant.parse`/catch blocks (also used by `mapCommit`, no behavior change there). - Removed `RepositoryListApi.kt`/`AccessibleRepositoriesGateway`/`AccessibleRepository` and their test file — this was a second, independent repository-list implementation with its own internal eager-all-pages loop; the ViewModel now uses only `GiteaRepositoryBrowser.listRepositories`. - `RepositoryListViewModel`: repository loading is now page-based (`REPOSITORY_PAGE_SIZE = 5`) with `nextPage()`/`previousPage()`/`refreshRepositories()`. Each visible row launches its own cancellable branch-fetch job (tracked in `rowBranchJobs`, keyed by `RepositoryCoordinates`) that pages through that repository's branches and picks the maximum `commitAtEpochMillis`; `Content` rows carry a `LastPushedBranchUiState` (`Loading`/`Loaded`/`Empty`/`Failed`). Row jobs are cancelled and relaunched on every page load/refresh (`cancelRowBranchJobs()` at the top of `loadRepositoryPage`) and cancelled on navigating away to references or logging out; `backToRepositories()` resumes only rows still `Loading` rather than restarting completed ones. A row's `AuthenticationExpired` failure clears the session the same way every other authenticated call does, instead of being swallowed as a per-row `Failed` state. `retryRowLastPushedBranch` retries a single row without touching the rest of the page. - UI: `RepositoryListScreen`'s repository list gained a refresh button, a per-row last-pushed-branch line (with localized loading/empty/failed+retry states), and a previous/page-number/next footer row, all localized (`strings_repositories.xml`). - `MainActivity` passes `repository.coordinates` directly instead of re-validating owner/name through `RepositoryCoordinates.of` a second time. Checks (JDK 21 via `D:\AndroidStudio\jbr`, Android SDK at `D:\Android\WhatToEat\sdk`, `local.properties` created locally, not committed): ```text ./gradlew assembleDebug testDebugUnitTest lintDebug --rerun-tasks → BUILD SUCCESSFUL - 105 unit tests across 22 suites, 0 failures/errors - 0 lint errors git diff --check → only CRLF-normalization notices, no real whitespace errors sensitive-pattern scan of changed files → no matches ``` Known limitations: - No physical device was available; page navigation, per-row branch loading, and the removed legacy path have not been manually verified on-device. - Tie-breaking when two branches share the exact same commit timestamp is deterministic (`maxByOrNull` keeps the first match) but arbitrary from a product standpoint; acceptable since this is a rare edge case and not user-configurable. No open P0-P3 findings from self-review against the acceptance criteria. Requesting Codex read-only review. ## Review findings None yet.