Skip to content

fix(refs): read references by exact name instead of revision guessing - #2313

Closed
Keerthana-64 wants to merge 1 commit into
gitpython-developers:mainfrom
Keerthana-64:refs-exact-name-lookup
Closed

Keerthana-64 wants to merge 1 commit into
gitpython-developers:mainfrom
Keerthana-64:refs-exact-name-lookup

Conversation

@Keerthana-64

Copy link
Copy Markdown
Contributor

Repro: fetch a tag named refs/heads/main (a valid ref name any remote can advertise, stored as refs/tags/refs/heads/main) into a repository whose HEAD points at the unborn refs/heads/main. On the Git CLI backend repo.head.is_valid() is True, repo.head.commit is the tag's commit, and repo.index.commit("initial") records that commit as the parent and writes refs/heads/main on top of it, while git rev-parse --verify HEAD exits 1. A tag named REBASE_HEAD makes repo.currently_rebasing_on() return a commit with no rebase in progress.

Cause: SymbolicReference._get_ref_info_helper and Repo.currently_rebasing_on() resolve a non-symbolic name with git rev-parse --verify, which applies the gitrevisions guessing rules (refs/<name>, refs/tags/<name>, refs/heads/<name>, ...) when the exact name is missing. The Gix backend reads the exact name and already reports the reference as missing, so the two backends disagreed.

Fix: read with git show-ref --verify --hash -- <path>, and probe REBASE_HEAD with git show-ref --exists first, so only the named reference counts. The missing-reference ValueError is unchanged. Checked against Git 2.52.0 for HEAD, ORIG_HEAD, FETCH_HEAD, MERGE_HEAD, refs/stash, nested tags, a linked worktree's HEAD, and both files and reftable storage. The new lookalike fixture tests fail before and pass after on both backends; the full suite, Ruff, mypy and ShellCheck pass.

`SymbolicReference._get_ref_info_helper` resolved a reference that is not a
symbolic ref with `git rev-parse --verify --end-of-options <path>`, and
`Repo.currently_rebasing_on()` read `REBASE_HEAD` the same way. `rev-parse`
applies the revision-guessing rules from `gitrevisions(7)`: when `<path>`
itself does not exist it also tries `refs/<path>`, `refs/tags/<path>`,
`refs/heads/<path>` and `refs/remotes/<path>`. A reference that does not
exist was therefore reported as existing whenever a look-alike name did.

`refs/heads/main` is a valid tag name, and any fetch or clone delivers it as
`refs/tags/refs/heads/main`. In a repository whose `HEAD` points at the
unborn `refs/heads/main`, Git reports an unborn branch (`git rev-parse
--verify HEAD` exits 1 and `git show-ref --verify refs/heads/main` fails),
while GitPython's CLI backend reported `head.is_valid()` as `True`,
`head.commit` as the tag's commit, and `repo.index.commit()` created the
first commit with that foreign commit as its parent and then wrote
`refs/heads/main` on top of the tag's history. The same lookup through the
Gix backend already answered "does not exist", so the two backends
disagreed. A tag named `REBASE_HEAD` likewise made
`currently_rebasing_on()` return a commit while no rebase was in progress.

Read the exact name instead. `git show-ref --verify --hash -- <path>` reads
only the named reference and fails for a missing one; it resolves `HEAD`,
`ORIG_HEAD`, `FETCH_HEAD`, `MERGE_HEAD`, `refs/stash`, nested tags and a
linked worktree's `HEAD` the same way `rev-parse --verify` did, on both
files and reftable storage. `currently_rebasing_on()` first runs
`git show-ref --exists -- REBASE_HEAD`, which exits 2 for a missing
reference, so an absent rebase is still reported as `None` without matching
diagnostic text. The `ValueError` raised for a missing reference is
unchanged, and the Gix backend's native lookup is unaffected.

The `lookalike` case in `tests/fixtures/reference-security.sh` tags the
initial commit as `refs/heads/missing` and `REBASE_HEAD` and points `HEAD`
at the unborn branch. `test_missing_references_do_not_resolve_to_look_alike_names`
checks the lookup, `is_valid()`, tag access and `currently_rebasing_on()`
through the native and the CLI reader, and
`test_commit_on_unborn_head_ignores_look_alike_tag` checks that the first
commit has no parent. Both fail before this change on each backend and pass
after it.

Validation: the full test suite with Git 2.52.0 on the CLI and Gix backends
(a bootstrapped clone, `init-tests-after-clone.sh`), Ruff check and format,
mypy, and ShellCheck on the fixture script.
Byron pushed a commit that referenced this pull request Oct 27, 2026
Fetching a valid tag named `refs/heads/unborn` stores it under
`refs/tags/refs/heads/unborn`. When `HEAD` names the absent
`refs/heads/unborn`, the CLI reference reader used `git rev-parse --verify`
and guessed that tag instead. `head.is_valid()` became true and
`IndexFile.commit()` gave the local initial commit a foreign parent. A tag
named `REBASE_HEAD` also made `Repo.currently_rebasing_on()` report a rebase.

Read direct references with `git show-ref --verify --hash` instead of
revision parsing. Probe `REBASE_HEAD` with `git show-ref --exists` to
distinguish absence from lookup errors, then use
`SymbolicReference.dereference_recursive()` and `Repo.commit()` so the rebase
reader shares exact lookup and symbolic-reference validation while retaining
recursive annotated-tag peeling.
Revision APIs intentionally retain Git's name guessing. Existing native Gix
reference reads are unchanged; root-reference existence and reftable retain
documented CLI fallbacks.

Independently written regressions fetch the misleading tags into an unborn
repository and cover SHA-1/SHA-256, files/reftable, forced CLI fallback,
parentless initial commits, and absent/present/deleted `REBASE_HEAD`.
Positive cases compare existing root refs, packed annotated tags, and detached
`HEAD` against Git. All 16 original regressions failed on the CLI baseline;
14 failed under Gix, whose files-based initial commits were already protected.
All 26 final reference regression cases pass on each backend. Post-commit
review found that `SymbolicReference.commit` peels only one annotated tag;
four new regressions failed with `TypeError` before switching to the existing
recursive commit resolver.

Broader validation exposed the existing GIX-17 grammar bug for this work
branch: Gix reads `2313-supersede` as blob
`23136a1d1dd1f02de7cde0f7b55f49d76ac77d55`, while Git reads the branch's
commit. Extend the existing conservative CLI grammar guard to hexadecimal
prefixes followed by a dash. A deterministic fixture verifies existing
`<blob-prefix>-topic` and missing `<blob-prefix>-missing` names; its test
fails before the guard and passes afterward. The GIX-17 ledger retains the
upstream bug as open and records the evidence and adapter fallback.

An additional exact-name check exposed GIX-33: when the root `REBASE_HEAD`
is absent and `refs/tags/REBASE_HEAD` is symbolic, Gix's partial lookup
returns that tag and native `symbolic_ref` dispatch follows its target.
Git's equivalent `symbolic-ref --quiet --no-recurse` exits 1. Compare the
returned native reference name with the requested bytes and retain CLI for
a lookalike. Two new SHA-1/SHA-256 regressions fail before the guard and
pass afterward, including forced CLI fallback. The ledger keeps this
compatibility bug open until Gix exposes a verified exact-name mode.

Final caller validation passes 98 CLI reference tests and 200 Gix
reference/backend tests, with 24 passing subtests each. The preceding
intermediate commit `9ddf778` also passed the full local suites: 1,969 CLI
tests and 2,072 Gix tests, with 85 passing subtests each. Earlier targeted
revision/installation checks passed 48 CLI tests and 50 Gix tests. Ruff
check/format, mypy, basedpyright, ShellCheck, and `git diff --check` pass.

Behavioral comparisons use `git version 2.54.0 (Apple Git-157)` and
`GixPython 0.3.0`. The Git reference checkout at
`6de20f6092dcf9bdb1c8efe03db4b70c82b423dd` documents exact lookup in
`Documentation/git-show-ref.adoc`, name guessing in
`Documentation/revisions.adoc`, and parentless root commits in
`Documentation/git-commit-tree.adoc`.

Source report and investigation: Keerthana-64's PR
#2313.
The replacement does not incorporate that PR's implementation or tests.
@Byron

Byron commented Oct 27, 2026

Copy link
Copy Markdown
Member

This PR is violating the contribution guidelines (impersonation) which require the agent to identify themselves, with the PR text clearly being LLM generated. This is why it will be regenerated and superseded.

Put yourself into my shoes, I might interact with a human, but there is no use in responding to an agent. Not making clear who is behind the PR forces me to fallback to assume it's all made without human interaction, a bot on a cron job, who has been told to not follow self-disclosure or intentionally impersonate. This isn't acceptable.

@Byron Byron closed this Oct 27, 2026
Byron added a commit that referenced this pull request Oct 27, 2026
<!-- agent -->
Fetching a valid tag named `refs/heads/unborn` stores it under
`refs/tags/refs/heads/unborn`. When `HEAD` names the absent
`refs/heads/unborn`, the CLI reference reader used `git rev-parse --verify`
and guessed that tag instead. `head.is_valid()` became true and
`IndexFile.commit()` gave the local initial commit a foreign parent. A tag
named `REBASE_HEAD` also made `Repo.currently_rebasing_on()` report a rebase.

Read direct references with `git show-ref --verify --hash` instead of
revision parsing. Probe `REBASE_HEAD` with `git show-ref --exists` to
distinguish absence from lookup errors, then use
`SymbolicReference.dereference_recursive()` and `Repo.commit()` so the rebase
reader shares exact lookup and symbolic-reference validation while retaining
recursive annotated-tag peeling.
Revision APIs intentionally retain Git's name guessing. Existing native Gix
reference reads are unchanged; root-reference existence and reftable retain
documented CLI fallbacks.

Independently written regressions fetch the misleading tags into an unborn
repository and cover SHA-1/SHA-256, files/reftable, forced CLI fallback,
parentless initial commits, and absent/present/deleted `REBASE_HEAD`.
Positive cases compare existing root refs, packed annotated tags, and detached
`HEAD` against Git. All 16 original regressions failed on the CLI baseline;
14 failed under Gix, whose files-based initial commits were already protected.
All 26 final reference regression cases pass on each backend. Post-commit
review found that `SymbolicReference.commit` peels only one annotated tag;
four new regressions failed with `TypeError` before switching to the existing
recursive commit resolver.

Broader validation exposed the existing GIX-17 grammar bug for this work
branch: Gix reads `2313-supersede` as blob
`23136a1d1dd1f02de7cde0f7b55f49d76ac77d55`, while Git reads the branch's
commit. Extend the existing conservative CLI grammar guard to hexadecimal
prefixes followed by a dash. A deterministic fixture verifies existing
`<blob-prefix>-topic` and missing `<blob-prefix>-missing` names; its test
fails before the guard and passes afterward. The GIX-17 ledger retains the
upstream bug as open and records the evidence and adapter fallback.

An additional exact-name check exposed GIX-33: when the root `REBASE_HEAD`
is absent and `refs/tags/REBASE_HEAD` is symbolic, Gix's partial lookup
returns that tag and native `symbolic_ref` dispatch follows its target.
Git's equivalent `symbolic-ref --quiet --no-recurse` exits 1. Compare the
returned native reference name with the requested bytes and retain CLI for
a lookalike. Two new SHA-1/SHA-256 regressions fail before the guard and
pass afterward, including forced CLI fallback. The ledger keeps this
compatibility bug open until Gix exposes a verified exact-name mode.

Final caller validation passes 98 CLI reference tests and 200 Gix
reference/backend tests, with 24 passing subtests each. The preceding
intermediate commit `9ddf778` also passed the full local suites: 1,969 CLI
tests and 2,072 Gix tests, with 85 passing subtests each. Earlier targeted
revision/installation checks passed 48 CLI tests and 50 Gix tests. Ruff
check/format, mypy, basedpyright, ShellCheck, and `git diff --check` pass.

Behavioral comparisons use `git version 2.54.0 (Apple Git-157)` and
`GixPython 0.3.0`. The Git reference checkout at
`6de20f6092dcf9bdb1c8efe03db4b70c82b423dd` documents exact lookup in
`Documentation/git-show-ref.adoc`, name guessing in
`Documentation/revisions.adoc`, and parentless root commits in
`Documentation/git-commit-tree.adoc`.

Source report and investigation: Keerthana-64's PR
#2313.
The replacement does not incorporate that PR's implementation or tests.

Assisted-by: GPT 6.1 Sol
Co-authored-by: GPT 6.1 Sol <codex@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants