FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(repo): retain revision tree context and path errors · gitpython-developers/GitPython@86b89ac · GitHub

Repository navigation

Commit 86b89ac

Browse files
authored andcommitted
fix(repo): retain revision tree context and path errors
MCP Server Git 2026.10.19's unchanged suite exposes three regressions in `Repo.rev_parse()`: `HEAD:` returns a root `Tree` without a path, so directory listing fails, and a missing `HEAD:<path>` raises generic `BadName` instead of the established tree lookup `KeyError`. All three tests pass with released GitPython 3.2.1 and fail with both local backends before this correction. Retain an empty path for successfully resolved root trees when Git's `objectmode` atom or Gix's `RevisionSpec.path_and_mode()` omits that context. Factor the existing Git-validated colon boundary query so missing paths use the public backend-managed `Tree.join()` for missing-component diagnostics, including nested paths and colons inside commit-message searches. Git/Gix continue to resolve revisions and read trees; no storage or Git semantics are implemented separately in Python. The diagnostic prefix resolver retains a documented CLI fallback until GixPython exposes compatible failed-path context. Add a scripted fixture and eight regression cases across SHA-1/SHA-256 and files/reftable; all eight fail before the correction. The repository/revision and downstream suites pass 184 tests, two skips and 14 subtests on CLI; adding native backend parity tests passes 286 tests, two skips and 14 subtests on Gix. MCP Server Git's full unchanged suite now passes all 186 tests on each backend. Ruff lint/format, ShellCheck for the fixture, mypy for `git/repo/fun.py`, and `git diff --check` pass with Python 3.12.15, Git 2.53.0 and GixPython 0.2.0.
1 parent 8977c5b commit 86b89ac

5 files changed

Lines changed: 91 additions & 17 deletions

File tree

‎doc/gix-backend.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ adapter return an invalid remote reference, preventing angr 10.0.4.post1 from
6161
reporting a valid local tracking branch. A native replacement must expose Git's
6262
local-upstream resolution, including its validation and diagnostics.
6363

64+
Revision path results retain the root tree's empty path so `Repo.rev_parse("HEAD:")`
65+
can be iterated, as required by MCP Server Git 2026.10.19. Git's `objectmode`
66+
atom and Gix's `RevisionSpec.path_and_mode()` omit metadata for that root;
67+
the adapter supplies its fixed empty path after successful tree resolution.
68+
Missing tree paths retain GitPython's `Tree.join()` `KeyError` diagnostics.
69+
Git validates the tree-ish prefix, including colons inside commit-message
70+
searches, and existing backend-managed tree queries identify the missing
71+
component. This error path uses a CLI prefix resolver; GixPython would need to
72+
expose compatible failed-path context and component diagnostics to replace it.
73+
6474
`IndexFile` queries immutable entry records; it does not retain a parsed index.
6575
The compatibility `entries` property projects those queries into a read-only
6676
snapshot keyed by path and stage, preserving readers in dlt and Renku. It uses

‎doc/source/changes.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ Git executable and safety
5151
API changes
5252
-----------
5353

54+
* Revision root trees such as ``Repo.rev_parse("HEAD:")`` retain their empty
55+
path and can be iterated. Missing paths within a valid tree revision retain
56+
``Tree.join()``'s ``KeyError`` and missing-component message, restoring MCP
57+
Server Git's directory display and path-error reporting.
5458
* ``Head.tracking_branch()`` returns a local ``Head`` when Git configures the
5559
current repository (``branch.<name>.remote=.``) as the upstream, instead of
5660
constructing an invalid ``refs/remotes/./...`` reference. Git resolves the

‎git/repo/fun.py‎

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,31 @@ def to_commit(obj: AnyGitObject) -> "Commit":
122122
return obj
123123

124124

125+
def _tree_path(repo: "Repo", rev: str) -> Optional[tuple[str, str]]:
126+
"""Ask Git which colon follows a tree-ish, including message-search revisions."""
127+
if rev.startswith(":"):
128+
return None
129+
for index, char in enumerate(rev):
130+
if char != ":":
131+
continue
132+
try:
133+
tree = repo.git._call_process_safe(
134+
"rev_parse", "--verify", "--quiet", "--end-of-options", rev[:index] + "^{tree}"
135+
)
136+
except GitCommandError as exc:
137+
if exc.status != 1:
138+
raise
139+
continue
140+
return tree, rev[index + 1 :]
141+
return None
142+
143+
125144
def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
126145
"""Resolve a Git revision to an object using Git's native revision grammar.
127146
128-
Invalid or missing revisions raise :class:`gitdb.exc.BadName`. Option-like
129-
revisions and embedded command delimiters are rejected before invoking Git.
147+
Invalid or missing revisions raise :class:`gitdb.exc.BadName`. Missing tree
148+
paths retain the tree lookup's :class:`KeyError`. Option-like revisions and
149+
embedded command delimiters are rejected before invoking Git.
130150
"""
131151
rev = Git._check_operand(rev, "revision")
132152
native = _backend.revision_info(repo.git, rev) if ":" in rev else NotImplemented
@@ -139,6 +159,12 @@ def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
139159
except GitCommandError as exc:
140160
if exc.status != 1 and "Invalid regular expression" not in exc.stderr:
141161
raise
162+
if exc.status == 1 and ":" in rev:
163+
context = _tree_path(repo, rev)
164+
if context is not None and context[1]:
165+
# Reuse the public, backend-managed tree query for its legacy
166+
# missing-component diagnostics; do not parse Git storage here.
167+
repo.tree(context[0]).join(context[1])
142168
raise BadName(rev) from exc
143169
if not repo.re_hexsha_only.fullmatch(oid):
144170
raise BadName(rev)
@@ -149,6 +175,9 @@ def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
149175
path_bytes, obj.mode = native[1]
150176
path = path_bytes.decode(defenc, "surrogateescape")
151177
obj.path = posixpath.normpath(path) if path else ""
178+
elif obj.type == "tree":
179+
# Gix omits path metadata for a successfully resolved root tree.
180+
obj.path = ""
152181
return obj
153182
# Git resolves the mode, including index stages and executable/symlink
154183
# entries. No object storage or revision grammar is decoded in Python.
@@ -165,20 +194,13 @@ def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
165194
path = rev[3:] if len(rev) > 2 and rev[1] in "0123" and rev[2] == ":" else rev[1:]
166195
obj.path = posixpath.normpath(path) if path else ""
167196
else:
168-
# Git has no path format atom. Ask it which colon follows a
169-
# tree-ish; earlier colons can belong to commit-message regexes.
170-
for index, char in enumerate(rev):
171-
if char != ":":
172-
continue
173-
try:
174-
repo.git._call_process_safe(
175-
"rev_parse", "--verify", "--quiet", "--end-of-options", rev[:index] + "^{tree}"
176-
)
177-
except GitCommandError as exc:
178-
if exc.status != 1:
179-
raise
180-
continue
181-
path = rev[index + 1 :]
197+
context = _tree_path(repo, rev)
198+
if context is not None:
199+
path = context[1]
182200
obj.path = posixpath.normpath(path) if path else ""
183-
break
201+
elif obj.type == "tree":
202+
# Git's objectmode atom is empty for the root tree, too.
203+
context = _tree_path(repo, rev)
204+
if context is not None and not context[1]:
205+
obj.path = ""
184206
return obj

‎test/test_downstream_compatibility.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,35 @@
1010
from test.lib.fixtures import fixture_repo
1111

1212

13+
@pytest.mark.parametrize("object_format", ["sha1", "sha256"])
14+
@pytest.mark.parametrize("ref_format", ["files", "reftable"])
15+
def test_revision_root_tree_retains_iterable_path_context(object_format, ref_format):
16+
with fixture_repo("downstream-revision-path.sh", object_format, ref_format) as repo:
17+
for revision in ("HEAD:", "HEAD^{}:", "HEAD^{/initial: with colon}:"):
18+
tree = repo.rev_parse(revision)
19+
assert tree.path == ""
20+
assert tree.mode == 0o40000
21+
assert [entry.path for entry in tree] == ["dir", "tracked"]
22+
assert tree["dir/file"].data_stream.read() == b"nested"
23+
24+
25+
@pytest.mark.parametrize("object_format", ["sha1", "sha256"])
26+
@pytest.mark.parametrize("ref_format", ["files", "reftable"])
27+
def test_missing_revision_paths_retain_tree_lookup_errors(object_format, ref_format):
28+
with fixture_repo("downstream-revision-path.sh", object_format, ref_format) as repo:
29+
for revision, missing in (
30+
("HEAD:nope", "nope"),
31+
("HEAD:dir/nope", "nope"),
32+
("HEAD:nope/child", "nope"),
33+
("HEAD:tracked/child", "tracked/child"),
34+
("HEAD^{}:nope", "nope"),
35+
("HEAD^{/initial: with colon}:nope", "nope"),
36+
):
37+
with pytest.raises(KeyError) as error:
38+
repo.rev_parse(revision)
39+
assert error.value.args == ("Blob or Tree named %r not found" % missing,)
40+
41+
1342
@pytest.mark.parametrize("object_format", ["sha1", "sha256"])
1443
@pytest.mark.parametrize("ref_format", ["files", "reftable"])
1544
def test_local_tracking_branch_uses_gits_upstream_location(object_format, ref_format):
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
git init -q --object-format="$1" --ref-format="$2" repo
4+
cd repo
5+
mkdir dir
6+
printf tracked > tracked
7+
printf nested > dir/file
8+
git add .
9+
git commit -qm 'initial: with colon'

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL