Skip to content

fix: read reference arguments by exact name in revision APIs - #2336

Merged
Byron merged 2 commits into
gitpython-developers:mainfrom
Keerthana-64:ref-args-exact-name
Nov 4, 2026
Merged

Byron merged 2 commits into
gitpython-developers:mainfrom
Keerthana-64:ref-args-exact-name

Conversation

@Keerthana-64

Copy link
Copy Markdown
Contributor

Description:

Revision APIs mein reference objects ka short name use hone ki wajah se Git galat reference resolve kar sakta tha. Agar same naam ka tag aur branch exist karte the, toh Git branch ki jagah tag ko select kar leta tha. Isse merge_base, merge_tree aur reset jaise operations galat commit ya files use kar sakte the.

Fix: Ab reference objects ko unke exact reference name se resolve kiya jaata hai. Revision strings ke liye existing name resolution behaviour unchanged hai. Missing references par ValueError raise hota hai.

Security: Yeh issue security review ke dauraan identify hua, especially un merge bots aur CI jobs ke liye jo contributor forks fetch karke Head ya RemoteReference objects use karte hain.

Testing: test_reference_arguments_do_not_resolve_same_named_tags fix se pehle fail aur fix ke baad dono backends par successfully pass hota hai.

<!-- agent -->
`Reference.__str__` returns the short name, so `refs/heads/main` becomes
`main` and `refs/remotes/origin/main` becomes `origin/main`. The revision
APIs turned reference objects into revisions with `str(ref)` or
`Git._check_operand(ref, "revision")`, and Git's revision lookup
(gitrevisions(7)) tries `refs/tags/<name>` before `refs/heads/<name>` and
`refs/remotes/<name>`, only warning about the ambiguity on stderr. A tag
that shares a branch's short name therefore took the branch's place.
Whoever controls a remote can supply such tags: `git fetch` auto-follows
tags that point into fetched history, and a clone brings in every tag.

With a branch `main` and fetched tags `main` and `origin/main` on another
commit, `repo.heads.main.commit` still reported the branch, but these APIs
used the tag's commit on both backends:

- `Repo.commit()`, `Repo.tree()`, `Object.new()`, `Repo.iter_commits()`
- `Repo.merge_base()`, `Repo.is_ancestor()`, `Diffable.diff()`
- `Repo.blame()`, `Repo.blame_incremental()`, `Repo.archive()`
- `IndexFile.from_tree()`, `IndexFile.merge_tree()`, `IndexFile.reset()`
- `HEAD.reset()`, `Repo.create_head()`, `Repo.create_tag()`

In the documented merge workflow, `merge_base(new_branch, original_branch)`
followed by `index.merge_tree(original_branch, base=...)`, the merge commit
received the tag's tree while its parents named the branch commits.
`repo.head.reset(repo.heads.main, working_tree=True)` and
`repo.index.reset(ref, head=True)` moved the current branch to the tag's
commit and wrote its files into the working tree.

Add `Git._check_revision()`. For a `SymbolicReference` it reads the exact
reference through `dereference_recursive()`, the `show-ref`/Gix read behind
`ref.object`, and passes on the object ID, which is how
`SymbolicReference.set_object()` already resolves reference arguments.
Strings keep Git's name guessing, as the changelog already documents.
`merge_base()` resolves its top-level arguments before
`_unpack_args()` flattens them. A reference that does not exist now raises
`ValueError`, as `ref.object` does, instead of resolving to whatever Git
guesses for its short name. On the CLI backend each reference argument
costs one or two extra short Git calls; the Gix backend reads references
natively, and `doc/gix-backend.md` is unchanged.

Validation: the new `shadowed` case in `tests/fixtures/exact-refs.sh` backs
`test_reference_arguments_do_not_resolve_same_named_tags` in
`test/test_refs.py`, which checks the APIs above for a branch and a
remote-tracking branch. It fails before the change and passes after it with
`GIT_PYTHON_BACKEND=cli` and `GIT_PYTHON_BACKEND=gix` for SHA-1/SHA-256 and
files/reftable (Git 2.52.0, GixPython 0.3.0, Python 3.11, macOS). Full
suites with the change: CLI 1,915 passed and Gix 2,018 passed. Each run
also had 116 failures, and the same 116 tests fail on the unpatched tree;
they need history and tags such as `0.1.5`, which this shallow clone lacks.
`ruff check`, `ruff format --check`, ShellCheck, `mypy --python-version=3.11`
and `basedpyright --warnings` pass.

Agent-Model: claude-opus-5-5

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Reference objects nested in list or tuple arguments bypass revision validation, allowing same-named tags to affect merge-base results and missing references to escape the expected ValueError.

1 open issue
What changed in this PR

GitHub Copilot: This PR updates GitPython’s revision APIs to resolve reference objects by exact name, preventing same-named tags from shadowing branches. String revisions retain Git’s existing name resolution.

Changes:

  • Adds a shared revision validator and applies it across repository, reference, object, index, and diff APIs.
  • Adds regression coverage for shadowed local and remote-tracking branches.
  • Documents exact reference resolution and missing-reference errors.
File Description
tests/​fixtures/​exact-refs.sh Adds branches shadowed by same-named tags.
test/​test_refs.py Tests reference arguments across revision APIs.
git/​repo/​base.py Applies revision validation to repository operations.
git/​refs/​tag.py Resolves tag creation targets exactly.
git/​refs/​symbolic.py Resolves reference creation targets exactly.
git/​refs/​head.py Validates reset targets as revisions.
git/​objects/​commit.py Resolves references before history traversal.
git/​objects/​base.py Resolves reference inputs during object lookup.
git/​index/​base.py Uses revision validation for tree lookup.
git/​diff.py Resolves reference objects before diffing.
git/​cmd.py Adds the shared revision validator.
doc/​source/​changes.rst Documents updated reference argument behavior.

Review effort: Balanced

Comment thread git/repo/base.py Resolved
<!-- agent: Codex -->

Converting reference arguments to object IDs prevents same-named tags from
replacing branches, but loses the reference identity required by several Git
operations. Reflog walks then return no commits, fork-point lookup fails with
`No such ref`, and the default `upload-archive` server rejects the literal ID.
References inside nested `merge_base()` arguments also still became short names
and could select a shadowing tag.

Keep the exact-reference existence and symbolic-chain checks in
`Git._check_revision()`, then pass the validated full ref name. This preserves
Git's ref-dependent behavior without operation-specific conversion switches.
Strings retain Git's revision interpretation and commit objects retain their
IDs. Normalize `os.PathLike` operands through `os.fsdecode()` before validation
so their filesystem paths, including byte paths, are used instead of their
display strings. Flatten nested list/tuple merge-base arguments while preserving
reference objects until each operand is validated.

Extend `tests/fixtures/exact-refs.sh` with an upstream advance and rewind plus
a topic based on its old tip. Regressions compare branch, remote-tracking and
HEAD behavior with Git 2.54.0 across SHA-1/SHA-256 and files/reftable repositories.
They cover shadowing tags, missing refs, nested operands, option spellings,
remote archives and safe/unsafe `PathLike` values. Update the changelog and
document the retained CLI fallbacks: GixPython 0.3.0 exposes neither the required
reflog-walk/fork-point operations nor an upload-archive client.

Assisted-by: GPT 6.1 Sol
Co-authored-by: GPT 6.1 Sol <codex@openai.com>
@Byron
Byron force-pushed the ref-args-exact-name branch from a1025f3 to 6de90f1 Compare November 4, 2026 06:35
@Byron
Byron merged commit 652cb3f into gitpython-developers:main Nov 4, 2026
55 of 64 checks passed
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.

3 participants