Skip to content

Commit 652cb3f

Browse files
authored
Merge pull request #2336 from Keerthana-64/ref-args-exact-name
fix: read reference arguments by exact name in revision APIs
2 parents 9689fb2 + 6de90f1 commit 652cb3f

13 files changed

Lines changed: 228 additions & 20 deletions

File tree

‎doc/gix-backend.md‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ and predate the current fallback; they do not establish native coverage.
374374
| Worktree inventory, `worktree list` | Main and linked worktree metadata, including bare main repositories and locks | Prunable entries; GIX-14 |
375375
| `Repo.merge_base`, `Repo.is_ancestor` | Native graph queries for two revisions | Octopus/fork-point and other options |
376376
| `Commit.count` | Reachable commit count, skip/limit/first-parent | Path filters and other revision options |
377-
| `Commit.iter_items`, `Repo.iter_commits` | First-parent walks and a single tip | General history ordering; GIX-8 |
377+
| `Commit.iter_items`, `Repo.iter_commits` | First-parent walks and a single tip | Reflog walks, general history ordering; GIX-8 |
378378
| Index entry reads, `ls_files` | Stage/mode/OID/path and exposed index flags | Custom and sparse indexes; GIX-3/4 |
379379
| `IndexFile.version`, `update_index` query | Native index version | Actual `update-index` mutations |
380380
| Index editing and `IndexFile.write` | Opaque byte snapshots; atomic locked publication | Private-file edits always use Git: arbitrary native index loading and strict pathname validation are missing; GIX-3/4/13/26 |
@@ -392,6 +392,25 @@ and predate the current fallback; they do not establish native coverage.
392392
| `Repo.untracked_files` | Native directory walk | Extra status options and non-root command directories |
393393
| `Repo.ignored` | Native excludes with tracked-file suppression | Symlink/submodule traversal, unsupported path normalization, and unavailable path metadata |
394394

395+
Revision APIs preserve a reference object's validated full name. The shared
396+
`Git._check_revision()` first uses the exact reference reader to verify existence
397+
and the symbolic-reference chain; it passes the full name rather than the resolved
398+
object ID. Strings retain Git's name guessing, and commit objects retain their IDs.
399+
This prevents same-named tags from replacing branches without losing the ref
400+
identity needed by reflog walks, fork-point lookups and remote archives.
401+
`merge_base()` validates nested list/tuple operands before string conversion.
402+
`exact-refs.sh` covers these paths with shadowing tags and missing references in
403+
SHA-1/SHA-256 and files/reftable repositories on both backends.
404+
405+
These name-sensitive operations retain CLI fallback. GixPython 0.3.0's
406+
`Repository.rev_walk()` has no Git-compatible reflog-walk mode,
407+
`Repository.merge_base()` exposes graph merge bases rather than reflog-based
408+
fork-point selection, and no upload-archive client API is exposed. The regressions
409+
compare Git 2.54.0's reflog history, old fork point after an upstream rewind,
410+
and default upload-archive server behavior, which rejects literal object IDs.
411+
Native replacements must expose matching reflog walking and fork-point selection
412+
and the upload-archive protocol; no Python implementation substitutes for them.
413+
395414
Reference reads require an exact name, unlike revision APIs that intentionally
396415
apply Git's name guessing. `reference_info()` uses
397416
`Repository.try_find_reference()` and verifies the returned name for `HEAD` and

‎doc/source/changes.rst‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,14 @@ API changes
5757
* Reference reads require the exact name. A missing branch no longer resolves to
5858
a similarly named tag, so an unborn branch's initial commit has no parent.
5959
A tag named ``REBASE_HEAD`` no longer reports a rebase in progress. Revision
60-
APIs such as ``Repo.commit(rev)`` retain Git's name guessing.
60+
APIs such as ``Repo.commit(rev)`` retain Git's name guessing for strings.
61+
A reference object passed as a revision, for example to ``Repo.commit()``,
62+
``Repo.iter_commits()``, ``Repo.merge_base()``, ``IndexFile.from_tree()`` or
63+
``HEAD.reset()``, is validated by its exact name and passed using its full ref
64+
name, so a tag named like a branch or remote-tracking branch no longer takes
65+
its place. The ref name also preserves reflog walks, fork-point lookups and
66+
remote archives. A missing reference raises ``ValueError``, as its ``object``
67+
property does.
6168
* Packed tag names containing non-UTF-8 bytes are represented losslessly as
6269
surrogate-escaped strings. ``safe_encode()`` recovers their original bytes,
6370
and Gix reference validation and resolution receive those bytes directly.

‎git/cmd.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,24 @@ def _check_operand(value: Any, label: str = "operand") -> str:
10231023
raise UnsafeOptionError(f"Invalid {label}: {value!r}")
10241024
return value
10251025

1026+
@staticmethod
1027+
def _check_revision(value: Any, label: str = "revision") -> str:
1028+
"""Validate a revision, preserving a reference object's full name.
1029+
1030+
A reference converts to its short name, for which Git's revision lookup tries
1031+
other references, such as a same-named tag, before ``refs/heads/`` and
1032+
``refs/remotes/``. Verify the exact reference exists before passing its full
1033+
name, retaining its identity for reflog walks and remote archives.
1034+
"""
1035+
from git.refs.symbolic import SymbolicReference
1036+
1037+
if isinstance(value, SymbolicReference):
1038+
value.dereference_recursive(value.repo, value.path)
1039+
value = value.path
1040+
if isinstance(value, os.PathLike):
1041+
value = os.fsdecode(value)
1042+
return Git._check_operand(value, label)
1043+
10261044
def _require_version(self) -> None:
10271045
key = None
10281046
if self._version_info_token is not self._refresh_token:

‎git/diff.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,10 @@ def diff(
261261
clusterable_short_options="46abceflmnpqrstuvwzBCDMNRW",
262262
)
263263

264-
if isinstance(other, str):
265-
other = self.repo.rev_parse(Git._check_operand(other, "revision")).hexsha
264+
from git.refs.symbolic import SymbolicReference
265+
266+
if isinstance(other, (str, SymbolicReference)):
267+
other = self.repo.rev_parse(Git._check_revision(other)).hexsha
266268
native = _backend.tree_diff(self.repo, self, other, paths, create_patch, kwargs)
267269
if native is not NotImplemented:
268270
return native

‎git/index/base.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,7 @@ def _tree_oid(self, tree: Treeish) -> str:
280280
tree = tree[0]
281281
if isinstance(tree, bytes):
282282
tree = tree.hex() if len(tree) == self.repo._oid_size else tree.decode("ascii")
283-
value = str(tree)
284-
Git._check_operand(value, "tree revision")
283+
value = Git._check_revision(tree, "tree revision")
285284
return self.repo.git._call_process_safe("rev_parse", "--verify", "--end-of-options", value + "^{tree}")
286285

287286
def _index_path(self) -> PathLike:

‎git/objects/base.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import gitdb.typ as dbtyp
1111

12+
from git.cmd import Git
1213
from git.compat import force_text
1314
from git.exc import WorkTreeRepositoryUnsupported
1415
from git.util import LazyMixin, bin_to_hex, join_path_native, stream_copy
@@ -138,7 +139,7 @@ def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> AnyGitObject:
138139
This cannot be a ``__new__`` method as it would always call :meth:`__init__`
139140
with the input id which is not necessarily a binsha.
140141
"""
141-
return repo.rev_parse(str(id))
142+
return repo.rev_parse(Git._check_revision(id))
142143

143144
@classmethod
144145
def new_from_sha(cls, repo: "Repo", sha1: bytes) -> AnyGitObject:

‎git/objects/commit.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,8 +465,8 @@ def iter_items(
465465
args_list.extend(paths_tup)
466466
# END if paths
467467

468-
Git._check_operand(str(rev), "revision")
469-
native = _backend.history(repo.git, str(rev), paths, kwargs)
468+
rev = Git._check_revision(rev)
469+
native = _backend.history(repo.git, rev, paths, kwargs)
470470
if native is not NotImplemented:
471471
return (cls(repo, hex_to_bin(oid)) for oid in native)
472472
proc = repo.git._call_process_safe("rev_list", rev, args_list, as_process=True, **kwargs)

‎git/refs/head.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def reset(
121121
# END working tree handling
122122

123123
try:
124-
oid = self.repo.rev_parse(Git._check_operand(commit, "revision")).hexsha
124+
oid = self.repo.rev_parse(Git._check_revision(commit)).hexsha
125125
self.repo.git._call_process_safe("reset", mode, oid, "--", paths, **kwargs)
126126
except GitCommandError as e:
127127
# git nowadays may use 1 as status to indicate there are still unstaged

‎git/refs/symbolic.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ def _create(
575575
) -> T_References:
576576
full_path = cls.to_full_path(path)
577577
cls._get_validated_ref_path(repo, full_path)
578-
target = repo.rev_parse(str(reference)) if resolve else reference
578+
target = repo.rev_parse(Git._check_revision(reference)) if resolve else reference
579579
ref = cls(repo, full_path)
580580
if force:
581581
ref.set_reference(target, logmsg)

‎git/refs/tag.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def create(
170170

171171
name = Git._check_operand(path, "tag")
172172
cls._get_validated_ref_path(repo, cls.to_full_path(name))
173-
reference = Git._check_operand(reference, "revision")
173+
reference = Git._check_revision(reference)
174174
oid = repo.rev_parse(reference).hexsha
175175
options = [] if allow_unsafe_options else ["--no-sign"]
176176
repo.git._call_process_safe("tag", *options, "--", name, oid, **kwargs)

0 commit comments

Comments
 (0)