Skip to content

Make HEAD a symbolic ref that tracks the current branch - #69

Merged
seanwevans merged 1 commit into
mainfrom
claude/symbolic-head
Jul 5, 2026
Merged

seanwevans merged 1 commit into
mainfrom
claude/symbolic-head

Conversation

@seanwevans

Copy link
Copy Markdown
Owner

Summary

Fixes the data-model flaw where committing on one branch silently moved others. Previously HEAD stored a bare commit hash with no notion of a current branch, so commit_index advanced HEAD and every branch that happened to point at the old commit. checkout, reset, and merge shared the same ambiguity.

This models the ref graph the way Git does.

Model

A ref is now either direct (commit_hash set) or symbolic (symbolic_target set, naming another ref). HEAD is normally symbolic and points at the current branch (e.g. master); a detached HEAD is direct. A CHECK enforces exactly one of the two columns.

New helpers (in the schema fragment):

  • resolve_ref(repo, name) — follow symbolic refs to a commit hash
  • current_branch(repo) — the branch HEAD tracks, or NULL if detached
  • set_head_symbolic / set_head_detached
  • advance_head(repo, commit) — move the current branch (or the detached HEAD)

Behaviour changes (only the current branch moves)

  • init_repository: HEAD is symbolic → master
  • commit_index / cherry_pick / revert: advance_head after committing
  • reset_soft / reset_mixed: advance_head
  • merge_branches: advance_head for the default HEAD target, else move the named branch
  • checkout_branch: attach HEAD symbolically (creating the branch first with -c)
  • create_branch: create at resolved HEAD without moving HEAD
  • list_branches: is_current compares against current_branch(), so branches sharing a commit are no longer all "current"

All other HEAD readers (get_log, get_status, create_tag, blame, grep, whatchanged, create_archive, stash_save, push) resolve through resolve_ref, and gc ignores the symbolic HEAD's NULL commit_hash when collecting reachability roots.

Tests

  • New test/sql/symbolic_head_test.sql (12 assertions): committing advances only the current branch while a sibling branch and master stay put; checkout switches the current branch; a detached HEAD advances itself and leaves every branch untouched.
  • Existing assertions that read HEAD's commit_hash column were updated to use resolve_ref('HEAD') (that column is NULL for a symbolic HEAD).

make test-core and RUN_INTEGRATION=1 RUN_PERF=1 make test-all — PASS.

Notes for reviewers

🤖 Generated with Claude Code


Generated by Claude Code

Previously HEAD stored a bare commit hash, with no notion of a "current
branch". commit_index advanced HEAD and then every branch that happened to
point at the old commit, so committing on one branch silently moved its
siblings; checkout, reset and merge had the same ambiguity.

Model the ref graph the way Git does. A ref is now either direct
(commit_hash set) or symbolic (symbolic_target set, naming another ref).
HEAD is normally symbolic and points at the current branch (e.g. 'master');
a detached HEAD is direct. A CHECK enforces exactly one of the two columns.

New helpers in the schema fragment:
- resolve_ref(repo, name)   - follow symbolic refs to a commit hash
- current_branch(repo)      - the branch HEAD tracks, or NULL if detached
- set_head_symbolic / set_head_detached
- advance_head(repo, commit) - move the current branch (or detached HEAD)

Rewrites so only the current branch moves:
- init_repository: HEAD is symbolic -> master
- commit_index / cherry_pick / revert: advance_head after committing
- reset_soft / reset_mixed: advance_head
- merge_branches: advance_head for 'HEAD', else move the named branch
- checkout_branch: attach HEAD symbolically (or create then attach)
- create_branch: create at resolved HEAD without moving HEAD
- list_branches: is_current compares against current_branch()

All other HEAD readers (log, status, tag, blame, grep, whatchanged,
archive, stash, push) now resolve through resolve_ref, and gc ignores the
symbolic HEAD's NULL commit_hash when collecting roots.

Tests: add symbolic_head_test.sql (commit advances only the current branch,
siblings and master stay put, detached HEAD advances itself). Existing
assertions that read HEAD's commit_hash column now use resolve_ref('HEAD').
@seanwevans
seanwevans force-pushed the claude/symbolic-head branch from 7f7a4a0 to 4148448 Compare July 5, 2026 16:22
@seanwevans
seanwevans merged commit ca7b3c4 into main Jul 5, 2026
1 check passed
@seanwevans
seanwevans deleted the claude/symbolic-head branch July 5, 2026 16:22
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.

2 participants