| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 8977c5b commit 86b89ac
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -61,6 +61,16 @@ adapter return an invalid remote reference, preventing angr 10.0.4.post1 from | |||
| 61 | 61 | reporting a valid local tracking branch. A native replacement must expose Git's | |
| 62 | 62 | local-upstream resolution, including its validation and diagnostics. | |
| 63 | 63 | ||
| 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 | + | ||
| 64 | 74 | `IndexFile` queries immutable entry records; it does not retain a parsed index. | |
| 65 | 75 | The compatibility `entries` property projects those queries into a read-only | |
| 66 | 76 | snapshot keyed by path and stage, preserving readers in dlt and Renku. It uses | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -51,6 +51,10 @@ Git executable and safety | |||
| 51 | 51 | API changes | |
| 52 | 52 | ----------- | |
| 53 | 53 | ||
| 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. | ||
| 54 | 58 | * ``Head.tracking_branch()`` returns a local ``Head`` when Git configures the | |
| 55 | 59 | current repository (``branch.<name>.remote=.``) as the upstream, instead of | |
| 56 | 60 | constructing an invalid ``refs/remotes/./...`` reference. Git resolves the | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -122,11 +122,31 @@ def to_commit(obj: AnyGitObject) -> "Commit": | |||
| 122 | 122 | return obj | |
| 123 | 123 | ||
| 124 | 124 | ||
| 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 | + | ||
| 125 | 144 | def rev_parse(repo: "Repo", rev: str) -> AnyGitObject: | |
| 126 | 145 | """Resolve a Git revision to an object using Git's native revision grammar. | |
| 127 | 146 | ||
| 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. | ||
| 130 | 150 | """ | |
| 131 | 151 | rev = Git._check_operand(rev, "revision") | |
| 132 | 152 | native = _backend.revision_info(repo.git, rev) if ":" in rev else NotImplemented | |
@@ -139,6 +159,12 @@ def rev_parse(repo: "Repo", rev: str) -> AnyGitObject: | |||
| 139 | 159 | except GitCommandError as exc: | |
| 140 | 160 | if exc.status != 1 and "Invalid regular expression" not in exc.stderr: | |
| 141 | 161 | 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]) | ||
| 142 | 168 | raise BadName(rev) from exc | |
| 143 | 169 | if not repo.re_hexsha_only.fullmatch(oid): | |
| 144 | 170 | raise BadName(rev) | |
@@ -149,6 +175,9 @@ def rev_parse(repo: "Repo", rev: str) -> AnyGitObject: | |||
| 149 | 175 | path_bytes, obj.mode = native[1] | |
| 150 | 176 | path = path_bytes.decode(defenc, "surrogateescape") | |
| 151 | 177 | 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 = "" | ||
| 152 | 181 | return obj | |
| 153 | 182 | # Git resolves the mode, including index stages and executable/symlink | |
| 154 | 183 | # entries. No object storage or revision grammar is decoded in Python. | |
@@ -165,20 +194,13 @@ def rev_parse(repo: "Repo", rev: str) -> AnyGitObject: | |||
| 165 | 194 | path = rev[3:] if len(rev) > 2 and rev[1] in "0123" and rev[2] == ":" else rev[1:] | |
| 166 | 195 | obj.path = posixpath.normpath(path) if path else "" | |
| 167 | 196 | 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] | ||
| 182 | 200 | 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 = "" | ||
| 184 | 206 | return obj | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -10,6 +10,35 @@ | |||
| 10 | 10 | from test.lib.fixtures import fixture_repo | |
| 11 | 11 | ||
| 12 | 12 | ||
| 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 | + | ||
| 13 | 42 | @pytest.mark.parametrize("object_format", ["sha1", "sha256"]) | |
| 14 | 43 | @pytest.mark.parametrize("ref_format", ["files", "reftable"]) | |
| 15 | 44 | def test_local_tracking_branch_uses_gits_upstream_location(object_format, ref_format): | |
| Original file line number | Diff line number | Diff 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' | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments