Ubisam-MobileGit/tasks/017-repository-list-recent-push.md
choiminyeong f6ac20c8ed TASK-063: task records say what is actually open
No task waits on a Codex review; the redesign tasks are confirmed on the
product owner's Z Fold7; TASK-016's leftovers are on hold; no biometric
reauthentication.

Agent: main (opus)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:25:49 +09:00

7.6 KiB

TASK-017: Sort repositories by recent push, page the list, and show the last-pushed branch

Status: implemented; all required checks pass; no review pending (Codex retired, 2026-09-29) 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<RepositorySummary> 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

  • 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).
  • RepositorySummary.updatedAt is populated and covered by DTO-mapping tests (present, absent/malformed → typed failure, consistent with existing .required() fail-closed pattern).
  • The repository list screen loads 5 repositories per page with explicit next-page loading; no behavior change for accounts with 5 or fewer repositories.
  • 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.
  • 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.
  • 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).
  • Loading, empty, offline, authentication-expired, and server-error states remain accessible and localized for both the repository page and the per-row branch fetch.
  • 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

./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):

./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.