TASK-017, human product owner priority-1 request. Repository list now requests sort=updated&order=desc from Gitea, loads 5 repositories per page with explicit prev/next navigation, and each visible row independently fetches and shows its most recently committed branch. Removes the legacy eager-loading RepositoryListApi in favor of the existing paged GiteaRepositoryBrowser.listRepositories. Fixes two issues found in self-review: missing error handling on page navigation, and repositories with no default branch (empty repos) previously failing the whole page instead of just that row. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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; 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(Giteaupdated_at) toRepositorySummaryand request server-sidesort=updated&order=descso ordering is correct across pages. - Replace the legacy eager-loading
RepositoryListApi/AccessibleRepositoriesGateway(which pages internally and returns one aggregated list) with the existing pagedGiteaRepositoryBrowserApi.listRepositories(page, pageSize)from TASK-004, wired through the domainPage<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.timestamptoBranchSummary(mapped from Gitea's branch-listcommit.timestampfield) and, for each repository visible on the current page only, fetch its branches and surface the one with the latestcommit.timestampas "last pushed branch."
- Add
- 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.xmltasks/017-repository-list-recent-push.mddocs/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.updatedAtis 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/AccessibleRepositoriesGatewaycode path and its now-unused tests are removed once the ViewModel is migrated toGiteaRepositoryBrowserApi.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
startOperationpattern). - 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:
RepositorySummarygainedupdatedAtEpochMillis: Long;defaultBranchchanged from non-nullRepositoryRefto nullableRepositoryRef?(an empty/newly-created Gitea repository can have no default branch — previously this threwApiFailure.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).BranchSummarygainedcommitAtEpochMillis: Long. - Data:
GiteaRepositoryBrowserApi.listRepositoriesnow requestssort=updated&order=descand mapsupdated_at;listBranchesmapscommit.timestamp. A sharedString.toEpochMillisOrInvalid()helper replaced three near-duplicateInstant.parse/catch blocks (also used bymapCommit, no behavior change there). - Removed
RepositoryListApi.kt/AccessibleRepositoriesGateway/AccessibleRepositoryand their test file — this was a second, independent repository-list implementation with its own internal eager-all-pages loop; the ViewModel now uses onlyGiteaRepositoryBrowser.listRepositories. RepositoryListViewModel: repository loading is now page-based (REPOSITORY_PAGE_SIZE = 5) withnextPage()/previousPage()/refreshRepositories(). Each visible row launches its own cancellable branch-fetch job (tracked inrowBranchJobs, keyed byRepositoryCoordinates) that pages through that repository's branches and picks the maximumcommitAtEpochMillis;Contentrows carry aLastPushedBranchUiState(Loading/Loaded/Empty/Failed). Row jobs are cancelled and relaunched on every page load/refresh (cancelRowBranchJobs()at the top ofloadRepositoryPage) and cancelled on navigating away to references or logging out;backToRepositories()resumes only rows stillLoadingrather than restarting completed ones. A row'sAuthenticationExpiredfailure clears the session the same way every other authenticated call does, instead of being swallowed as a per-rowFailedstate.retryRowLastPushedBranchretries 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). MainActivitypassesrepository.coordinatesdirectly instead of re-validating owner/name throughRepositoryCoordinates.ofa 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 (
maxByOrNullkeeps 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.