Files
web-terminal/android/api-client
Yaojia Wang 35d32f4670 feat(android): decode the v0.6 git-panel surface
The server shipped the project-detail git panel in 8fe1f52/f711db9/042c4cd,
all after the last Android commit, so the client silently ignored every new
field (they are additive and optional, so nothing crashed — it just rendered
the pre-w6 shape: one bare dirty dot and no sync information at all).

Added: SyncState + ProjectDetail.sync/dirtyCount, ProjectInfo.dirtyCount,
ProjectSessionRef.cwd, GitLogResult.upstream, CommitLogEntry.unpushed, plus
GET /projects/worktree/state (read) and POST /projects/git/fetch (write).

Two rules from the server's own commit message are encoded as behaviour
rather than left to each call site to remember:

  `SyncState.isFullySynced(now)` is the only sanctioned way to ask whether
  the green all-clear may be rendered, and it answers false for anything
  unknown. ahead/behind compare against @{u}, a locally cached ref that only
  a fetch moves, so `behind = 0` from a stale FETCH_HEAD proves nothing. Most
  importantly NO UPSTREAM leaves both counts undefined — which is not zero —
  and that is the normal state of a fresh worktree branch. The server calls
  that fall-through the easiest bug to ship here; there is a test per way it
  could regress, including a fetch timestamp in the future, which is clock
  skew rather than evidence that anything was checked.

  `GitLogResult.upstreamBoundaryIndex` finds the LAST unpushed commit rather
  than assuming the unpushed ones come first. git log is date-ordered, so
  merging an older branch interleaves unpushed commits below pushed ones, and
  the row-count shortcut fails in the dangerous direction — it would label an
  unpushed commit as pushed. It returns null, not -1, when no boundary may be
  drawn, so "no upstream" cannot be mistaken for "boundary before everything".

Origin policy is asserted in both directions: worktree/state carries no
Origin, git/fetch carries a byte-equal one, and a test also asserts the fetch
body contains no remote or refspec — the remote is derived server-side
precisely so a client cannot aim a fetch at an arbitrary URL. Fetch has its
own rate-limit bucket, so a 429 is surfaced rather than retried, and a failed
fetch leaves lastFetchMs unknown so the UI keeps saying "stale" instead of
pretending it refreshed.

Verified: :api-client:test + koverVerify green; 22 new tests.
2026-07-30 09:45:59 +02:00
..