Skip to content

feat: record HEAD at creation, heal a dangling HEAD on push, admin route to move it - #99

Open
jonasgrosch wants to merge 4 commits into
tobi:mainfrom
conxai-technologies:conxai/default-head-upstream
Open

jonasgrosch wants to merge 4 commits into
tobi:mainfrom
conxai-technologies:conxai/default-head-upstream

Conversation

@jonasgrosch

Copy link
Copy Markdown

A repository whose first pushes contain no main keeps HEAD → refs/heads/main, which does not exist: the UI reports an empty repository and clones warn "remote HEAD refers to nonexistent ref" although branches exist. Receive-pack cannot set HEAD.

HEAD stays WAL state (checkpoint head_target + symbolic HEAD updates in log transactions); an unrecorded HEAD means IMPLICIT_HEAD (refs/heads/main). No proto change.

  • Creation: git.default_branch (default main) and PUT /{o}/{r}?default_branch=<name> (short name, validated). main records nothing (one Create, unchanged); any other branch is the repository's first entry — its log slot claimed like a publish and listed by the manifest Create, so the repository appears with its HEAD atomically (2 requests, once per repository). walgit repo create --default-branch.
  • Heal on push: if a publish creates refs/heads/* while HEAD resolves to nothing before and after it, the same entry carries HEAD → default_branch if created, else the lexicographically first created branch — computed on each attempt's CAS basis in process_batch, zero extra requests, recomputed on retry. Explicit HEAD updates (import) win; the client report is unchanged.
  • Admin route: PUT /{o}/{r}/api[-browser]/head {"branch": "…"} — admin, target must exist (re-checked on every CAS attempt), idempotent (seq: 0), 409 for an unknown branch. SDK repo.setHead(), repo.create({defaultBranch}).
  • Fix: apply_txn_to_map tracks a HEAD retarget as head_target, so later requests in a batch see it; the sim's truth fold skips symbolic HEAD updates.

Round trips: push unchanged; create 1 (main) / 2 (other, once per repository); HEAD route = refs sync → log PUT → CAS. healthy_request_round_trip_budgets unchanged.

Stacked on #93 (the pushed-HEAD "funny refname" fix): this branch's first commit is #93's; review from e3cab23.

Tests: WAL (create records only a non-implicit HEAD, survives a lost Create reply; heal by name order; default-branch preference; explicit HEAD respected; one heal per batch; publish_head existence/idempotence/stale replica), HTTP (create override, heal on an auto-created push, admin route auth/validation/409/404/both lanes). Standalone on git 2.47: fmt, clippy --workspace --all-targets -D warnings, api_v1 6/6, policy 3/3. Two sim tests (base_rebuild_resumes_after_a_kill_between_any_two_phases, geometric_fold_never_touches_the_base_or_the_history_pack) fail in our container on main 80e9a20 as well, serially — pre-existing there, unchanged by this PR. Combined with #96/#97/#98 the full workspace suite is green (conxai-technologies/walgit conxai/main).

Part of #94.

🤖 Generated with Claude Code

jonasgrosch-conxai and others added 4 commits September 30, 2026 22:39
A receive-pack command naming `HEAD` (e.g. `<old> <new> HEAD`) was
accepted and moved HEAD's branch through the symref, under a name that
no `protect` rule on refs/heads/* matches. Any principal with write
could therefore move a protected branch that HEAD points at. git's own
receive-pack refuses such commands as a funny refname; walgit now does
the same under every policy, including none.

Reproduced against a running server: with HEAD -> refs/heads/production
and production protected, a push to refs/heads/production is rejected
by the rule while a raw `HEAD` command answered `ok HEAD` and moved
production. Plain `git push` cannot send it (the client wants a full
refname), but any HTTP client can.

Symbolic HEAD updates (import) are not pushed commands and still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ute to move it

A repository created by a push that contains no `main` kept HEAD -> refs/heads/main
(nonexistent): the UI showed "This repository is empty" and clones warned "remote HEAD
refers to nonexistent ref" although the repository had branches. Receive-pack cannot
set HEAD, and only `walgit import` wrote symbolic HEAD updates.

HEAD stays where refs live (checkpoint RefSnapshot.head_target + symbolic HEAD updates in
log transactions); no record means walgit_git::IMPLICIT_HEAD (refs/heads/main), now a
named format constant. No proto change.

- `git.default_branch` (default "main", validated as a short Git branch name) and
  `PUT /{o}/{r}?default_branch=` name HEAD's branch for a new repository. The implicit
  target records nothing (one manifest Create, as before); any other target is the
  repository's first entry (HEAD-only REF_UPDATE): its log slot is claimed as a publish
  claims one and the manifest Create lists it, so the repository appears with its HEAD in
  one commit. Lost Create replies resolve through cas_landed. `walgit repo create
  --default-branch`, `Registry::create_with_default_branch`.
- Heal: a publish that creates refs/heads/* while HEAD resolves to nothing before and
  after it appends `HEAD -> <target>` to its own entry's transaction: default_branch if
  created, else the lexicographically first created branch. Computed on each attempt's
  CAS basis inside process_batch, so it commits with the refs that justify it. A txn that
  names HEAD keeps it; deleting HEAD's branch leaves it dangling until such a publish.
  Clients never see HEAD in report-status.
- `PUT /{o}/{r}/api[-browser]/head {"branch"}` (admin): `RepoHandle::publish_head` through
  the same publisher; the target must exist on the attempt's CAS basis (re-checked on
  every retry); idempotent (seq 0). 409 when the branch does not exist.
- Policy: the pushed-`HEAD <oid>` refusal (`ng funny refname` under every policy) comes
  from the base, conxai/receive-pack-refname (689d96e); this commit only rewords its
  comment to name the heal rule and the admin route as HEAD's symbolic writers.
- Fix: apply_txn_to_map stored a symbolic HEAD update in the ref map instead of
  head_target, so later requests of the same batch saw the old HEAD.
- Events: unchanged. Symbolic retargets already emit nothing.

Round trips (docs/ROUNDTRIPS.md): push/publish unchanged (heal is 0 extra); create
1 -> 1 for the implicit HEAD, 2 sequential for any other (once per repository); admin
HEAD route = refs sync -> log PUT -> CAS (3 rounds, like a settings publish). Budget test
healthy_request_round_trip_budgets unchanged.

Docs: AGENTS.md D50, walgit.example.toml, web/API.md, SDK (repo.create({defaultBranch}),
repo.setHead), docs/CONTRACT.md, docs/POLICY.md, docs/ROUNDTRIPS.md.

Rebased onto conxai/receive-pack-refname from 846d8ed, dropping its duplicate of the
funny-refname check and test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
default_branch_is_chosen_at_creation_healed_by_push_and_moved_by_admin declared
two consts after statements; clippy -D warnings (items_after_statements, all
targets) failed on them. They are promoted 'static lets now, renamed so they do
not shadow the test's array-valued writer/admin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every sim scenario pushes refs/heads/p<N> into a fresh repository, so since the HEAD
heal (D52) each first push's log entry also carries `HEAD -> refs/heads/p0` (a
symbolic update, no oid). check_truth folded every update's new_oid and then parsed
each as an object id: `A hash sized 0 hexadecimal characters is invalid`, 10 of 19
sim tests red under `just sim`. conxai/default-head did not touch sim.rs; the
failure is the topic's, surfaced by the full tree run. The fold now tracks object
moves only; HEAD's target is not an object to materialize.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@0bserver07

Copy link
Copy Markdown
Contributor

Recording HEAD at creation and fixing it inside the same entry looks right to me. One problem with the heal though: when the default branch isn't one of the new branches, it picks created().min(), so whichever sorts first. A git push --all with master and bugfix/x ends up with HEAD on bugfix/x, and an existing repo with a dangling HEAD jumps to whatever feature branch gets created next. Healing only when there were no branches before, and preferring main or master, would avoid that.

Also, now that HEAD can move between branches on the same commit, the refs ETag (just the head sha) can answer 304 with the old branch name.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants