Split out from #83 / PR #91 (the new-project form unification).
Reframed scope (2026-07-19): for local repos, branch is display-only, never an input axis. No branch picker for local. This drops the original "part 1 branch picker (backend)" and focuses on showing the checked-out branch and fixing the local-repo breakage caused by treating local sources as having a (src, branch) identity.
What to build
1. No branch axis for a local source
A submitted local path always carries branch: undefined. sourceKey(src, branch), CURRENT_SOURCE, the manifest cache key, and especially the URL must omit the branch for local (it is dynamic, whatever is checked out on disk, so a stored branch would be a lie). GET /api/branches stays remote-only (git ls-remote); no new local endpoint.
2. Recents dedupe by src only for local
Recents are keyed ${src}:${branch} today, so opening a local path at different checked-out branches spawns two rows. Local recents should be one row per path. Fix the recents keying / pushRecent / active-matching, and don't render a stored @branch identity pill on a local recent row (the branch is not part of its identity).
3. Display the current local branch (read-only)
The manifest already carries a branch field (api/models/manifest.py, types/manifest.ts). Populate it from the local repo's HEAD (git symbolic-ref / rev-parse --abbrev-ref HEAD), gated behind CODECITY_ALLOW_LOCAL_REPOS + path validation. The header branch pill falls back to manifest.branch (via SOURCE_INFO / resolveBranch) when no branch was explicitly chosen, so a loaded local repo shows its checked-out branch. Display only, not a control.
4. Audit the rest of the local-repo breakage
More than the recents dupes breaks for local. Sweep every place that assumes a (src, branch) identity and confirm it degrades sanely for a branch-less local source: source-picker prefill / re-open, header pill, cache namespacing, live-update polling, deep-link URL round-trip. List findings, fix the local breakage.
Out of scope
- A branch picker for local repos (explicitly dropped) and any local branch-list endpoint.
Split out from #83 / PR #91 (the new-project form unification).
Reframed scope (2026-07-19): for local repos, branch is display-only, never an input axis. No branch picker for local. This drops the original "part 1 branch picker (backend)" and focuses on showing the checked-out branch and fixing the local-repo breakage caused by treating local sources as having a
(src, branch)identity.What to build
1. No branch axis for a local source
A submitted local path always carries
branch: undefined.sourceKey(src, branch),CURRENT_SOURCE, the manifest cache key, and especially the URL must omit the branch for local (it is dynamic, whatever is checked out on disk, so a stored branch would be a lie).GET /api/branchesstays remote-only (git ls-remote); no new local endpoint.2. Recents dedupe by
srconly for localRecents are keyed
${src}:${branch}today, so opening a local path at different checked-out branches spawns two rows. Local recents should be one row per path. Fix the recents keying /pushRecent/ active-matching, and don't render a stored@branchidentity pill on a local recent row (the branch is not part of its identity).3. Display the current local branch (read-only)
The manifest already carries a
branchfield (api/models/manifest.py,types/manifest.ts). Populate it from the local repo's HEAD (git symbolic-ref/rev-parse --abbrev-ref HEAD), gated behindCODECITY_ALLOW_LOCAL_REPOS+ path validation. The header branch pill falls back tomanifest.branch(viaSOURCE_INFO/resolveBranch) when no branch was explicitly chosen, so a loaded local repo shows its checked-out branch. Display only, not a control.4. Audit the rest of the local-repo breakage
More than the recents dupes breaks for local. Sweep every place that assumes a
(src, branch)identity and confirm it degrades sanely for a branch-less local source: source-picker prefill / re-open, header pill, cache namespacing, live-update polling, deep-link URL round-trip. List findings, fix the local breakage.Out of scope