Skip to content

fix: preserve legacy path bytes and repair tag, tree and ref edge cases - #2308

Draft
Byron wants to merge 11 commits into
mainfrom
tuning
Draft

Byron wants to merge 11 commits into
mainfrom
tuning

Conversation

@Byron

@Byron Byron commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Tasks

This section is for Byron only. Models continuing this PR must not add, remove, check, uncheck, rename, or reorder checkboxes here.

  • refackiew

Everything below this line was generated by Codex GPT-6.

Created by Codex on behalf of Byron. Byron will review before this is ready to merge.

Preserve legacy filename bytes in raw diffs and native reference inputs, clarify supported APIs, and fix three concrete failures from the follow-up review of #2274. Missing tagger fields are initialized without losing tag messages, failed tree reads cannot expose a partial cache, and renamed heads retain the actual destination Git created.

The five open issues of type Task or Bug are addressed in separate commits, with Tasks first:

Issue Resolution
#1771 Clarify that Tree.cache and TreeModifier are supported in-memory editors. Correct the historical removal claim and test that serialization creates a new object without implicitly updating parents. Construction already delegates to Git/Gix.
#1848 Use Sphinx's built-in :meta public: docstring metadata for documented subclass interfaces, remove the traversal allowlist, and repair the traversal link.
#2030 Parse raw diff fields as bytes and decode only ASCII metadata. Test add/delete/modify/rename/copy paths and staged Windows-1252 filename/content bytes.
#2064 Packed-tag enumeration was already delegated to Git/Gix. Preserve surrogate-escaped bytes when names are passed back into supported Gix validation, lookup and revision APIs; test lightweight/annotated tags in SHA-1/SHA-256 repositories and forbid native-resolution CLI fallback on POSIX; Windows enumeration remains covered and byte-name lookup has the documented GIX-32 native fallback.
#2101 Document the existing Repo context manager and close() contract, followed by strict git.util.rmtree() deletion. Test release of object readers and native state before removing a packed repository. All instances and separately opened readers must be closed; Windows read-only files are a separate reason plain shutil.rmtree() can fail.

Fixes #1771
Fixes #1848
Fixes #2030
Fixes #2064
Fixes #2101

The three review items are addressed in separate follow-up commits:

Review item Fix
Missing tagger fields 7c77a2f6: project native absence to the existing empty actor and zero date/offset. Correct CLI optional-header parsing so it retains the first message line. Gix reads remain native.
Partial tree cache 88958c9d: attach the cache only after every entry has parsed and validated. Repeated reads of an invalid tree raise the same error.
Literal branch rename 265a9517: validate and retain the namespace-prefixed destination corresponding to the literal short name passed to Git. The returned head points to the renamed branch, even when topic already exists.

All three review reports reproduce with real repository fixtures; none was dismissed as a nit or hypothetical concern. Tag and tree regressions cover SHA-1/SHA-256 and empty/nonempty messages or invalid first/last entries. The rename regression preserves a different existing topic to catch selecting the wrong branch.

Validation on CPython 3.12.14/macOS, Git 2.54.0 and GixPython 0.3.0:

  • All review regressions fail before their respective fixes and pass afterwards on both backends. Native tag metadata tests forbid Git.execute() calls. Tree reads use native ls_tree; branch rename retains its documented CLI path.
  • Final reference tests: 50 passed and 24 subtests per backend. Tree tests: 97 passed per backend. Reference plus object/index tests passed after the tag fix: 105 passed and 24 subtests per backend.
  • Ruff, ShellCheck, mypy and basedpyright pass after the review fixes. Strict Sphinx and rendered subclass-interface checks passed for the earlier documentation changes.
  • Windows Gix: 1,930 passed, 111 skipped, nine expected failures, three unexpected passes and 79 subtests. All ten new tag, tree-cache and branch-rename regression cases are explicitly PASSED, with none skipped. Native tag tests retain their no-CLI guard on Windows.
  • Each of the eleven issue, CI and review-fix commits received one Codex review with no actionable findings.
  • All 64 CI checks pass on final head 265a9517b20f03bafa691b1f4c7bc0c527b2a673, including Linux, macOS, Windows, Gix, legacy import checks, type/lint/docs checks and backend comparison. The earlier issue-fix head also passed all 64 checks. No new PR comments, reviews or review threads were present after final CI completed.
  • Complete final-head suites with global Git configuration isolated and separate daemon ports: CLI had 1,903 passed, 153 skipped, one expected failure and 77 subtests in 593.18 seconds; Gix had 2,005 passed, 51 skipped, one expected failure and 77 subtests in 295.92 seconds. CLI launch totals were 56,075 and 16,722 respectively. Counts cover the pytest process, excluding fixture Bash launches and direct helper subprocesses. Initial local attempts exposed sandbox socket restrictions, shared daemon-port interference and the user-level fetch.prune=true setting; the isolated complete runs both pass.

Three separate CI commits pin legacy Python jobs to Ubuntu 24.04, separate packed-tag enumeration from byte-name resolution, and record the Windows native lookup gap (GIX-32). Windows enumeration remains covered on both backends. Successful byte-name resolution remains POSIX-only because Windows CLI arguments cannot preserve the original non-UTF-8 bytes and native Windows lookup currently falls back.

Native operations use supported GixPython APIs. Existing upstream capability gaps and compatibility bugs remain documented and open in doc/gix-backend.md, including GIX-28's strict tag-writing gap and GIX-32's Windows byte-reference lookup gap.

codex added 11 commits October 25, 2026 08:51
`Tree.cache` and `TreeModifier` were announced as removed in an old
changelog, but remain exported and supported. Their documentation also
suggested sorting alone would make edits persistent, leaving their status
and practical limits unclear.

Document that cache edits affect one instance and do not update its object
ID, stored tree, parents, index, or commits. Correct the historical removal
claim and recommend `IndexFile.write_tree()` for staged path changes.
Serialization already delegates construction to Git or Gix; no deprecation
or replacement implementation is needed. Git's `Documentation/git-mktree.adoc`
confirms that `mktree` normalizes entry order without pre-sorted input.

Add a regression covering local subtree edits, serialization into a new
object, and unchanged parent references. The focused tree tests passed
with both CLI and Gix backends (3 tests each), using Git 2.54.0 and
GixPython 0.3.0. The HTML build rendered the updated documentation; its
strict check reported one existing field-list warning in
`git.objects.submodule.util.is_relative_url`.
Sphinx omits underscore-prefixed methods by default, hiding documented
extension points such as `RemoteProgress._parse_progress_line()` and
`LockFile._obtain_lock()`. Traversal documentation used a separate allowlist,
and `Tree.traverse()` linked to its implementation using an ordinary URL.

Mark supported subclass interfaces with Sphinx's built-in `:meta public:`
docstring metadata. Remove the traversal-specific `:private-members:` list,
use a method reference for traversal documentation, and explain this
convention in `CONTRIBUTING.md`. Private implementation helpers stay hidden.
Repair an existing malformed `is_relative_url()` return field so strict
HTML validation can complete.

Before this change, the HTML reference lacked the progress and lock
extension-point targets. The strict Sphinx HTML build now passes. Assertions
against its output verify all five selected interface targets, the traversal
link, and omission of `LockFile._lock_file_path()`. Runtime logic is unchanged.
Aider-style staged-file reads fail with `UnicodeDecodeError` when a raw
diff contains a non-UTF-8 filename. Index entries and blob streams already
preserve the legacy bytes, but `Diff._handle_diff_line()` decoded the entire
NUL-delimited output before separating metadata from paths.

Keep raw diff fields as bytes, decode only the ASCII mode/OID/status
metadata, and pass original path bytes into `Diff`. This also preserves
rename and copy destinations without requiring callers to reencode them.
The existing text path properties retain their display behavior.
Git 2.54.0's `Documentation/diff-options.adoc` specifies that `--raw -z`
returns unmodified pathnames separated by NULs.

All six new regressions failed before the fix: five cover raw add/delete/
modify/rename/copy records, and one stages Windows-1252 filename and content
bytes in a real index and reads its diff and blobs. All diff and index tests
now pass with CLI and Gix backends: 150 passed, 2 skipped, and 9 subtests on
each. Ruff and ShellCheck pass. Windows execution remains for CI validation;
no conversion of the legacy files is needed.
Packed-reference enumeration now delegates to Git/Gix and no longer has the
original UTF-8 file-reader failure. However, tag paths retain undecodable
bytes as surrogate escapes, and passing those strings back into GixPython
raises `UnicodeEncodeError` before validation or reference lookup.

Make `safe_encode()` the byte-preserving inverse of `safe_decode()`. Use it
at the supported Gix reference validation, lookup, revision, reflog and edit
boundaries, and when serializing Git reference transactions. GixPython 0.3.0
accepts bytes in these APIs, so this fixes adapter conversion without adding
a reference-storage implementation or requiring an upstream workaround.

Add a scripted packed-ref fixture with lightweight and annotated non-UTF-8
tag names for both SHA-1 and SHA-256. Git 2.54.0's `for-each-ref` supplies
the reference inventory; `git-for-each-ref.adoc` documents the `refname` and
`objectname` projections used for the baseline. The regression checks byte
round trips, tag/commit resolution, CLI parity and absence of CLI fallback
during native resolution. Packed records are fixture inputs because APFS
rejects those bytes in loose reference filenames.

Validation: 42 reference tests and 24 subtests passed with CLI; 144 reference
and backend tests and 24 subtests passed with Gix. Ruff, ShellCheck and mypy
passed. Windows behavior will be validated by CI.
Reading `repo.head.commit` can open persistent object readers or retain a
native repository handle. Deleting the repository while its handles remain
open is not a supported sequence on Windows. Git's read-only object files
can independently prevent plain `shutil.rmtree()` from removing a clone.

`Repo.close()` and its context-manager support already release the retained
handle and terminate persistent Git readers synchronously. Document that
contract in the API reference, tutorial and README, replacing the old advice
to invoke destructors manually. Demonstrate closing the repository first,
then using the existing strict `git.util.rmtree()` helper for read-only files.
All instances and separately opened readers must be closed by their owners.

Add a packed-repository fixture and a tutorial regression that reads HEAD
and blob data, verifies reader processes have exited and caches are released,
and removes the directory without suppressing deletion errors. The test
passes with both CLI and Gix backends on macOS with Git 2.54.0. Sphinx's
strict HTML build, Ruff and ShellCheck pass. Windows execution is left to CI;
no automatic cleanup of a still-live repository is introduced.
The dependency `test (gitdb, 3.9)` and project
`install/import (ubuntu-latest, 3.9)` checks fail before installing code.
`ubuntu-latest` now selects Ubuntu 26.04, and `actions/setup-python` reports
that no Python 3.9 x64 build exists for that image.

Run the dependency matrix and the Linux legacy-runtime import checks on
Ubuntu 24.04. Preserve the import-check matrix names and the existing
macOS/Windows runners. The main modern-Python suite continues to cover the
current Ubuntu image.

Validated both workflow files as YAML and checked the upstream
`actions/python-versions` manifest for available Ubuntu 24.04 x64 builds of
Python 3.8, 3.9 and 3.10. This is a CI environment repair discovered while
validating the five issue-specific commits, not a runtime code change.
The Windows Python 3.15 free-threaded CI job enumerated the non-UTF-8
packed tags correctly, then failed both object-format variants when the
new regression tried to resolve their surrogate-escaped names through
the CLI. Git 2.56.0.windows.2 returned exit 128 from `git rev-parse
--verify --end-of-options` with `fatal: Needed a single revision`.
Git's Windows `wmain()` converts UTF-16 arguments to UTF-8, so those
arguments cannot carry the original invalid UTF-8 reference bytes.

Separate packed-tag enumeration from reference resolution. Keep the
reported enumeration regression on both backends and every platform;
skip only the Windows CLI resolution variant. Gix resolution still runs
on every platform with `Git.execute()` forbidden, using the supported
byte-capable native APIs. Document the demonstrated CLI limitation and
the coverage boundary in `doc/gix-backend.md`.

Validation: all 44 reference tests and 24 subtests pass locally on each
backend; Ruff check/format and `git diff --check` pass. Windows execution
remains for CI.
The Windows Gix job also failed both packed-tag resolution regressions:
enumeration preserved the names, but `reference_info()` selected its native
read diagnostic fallback before the no-CLI guard caught a Git launch.
The run used GixPython 0.3.0, Git 2.56.0.windows.2 and CPython 3.12.10.
The pinned `gix-ref` 0.69.1 lookup attempts loose-reference path conversion
before consulting packed refs, which cannot represent invalid UTF-8 bytes
on Windows. The CI log demonstrates the fallback, but does not contain
the underlying Gix diagnostic text.

Keep byte-name resolution coverage on POSIX and check the existing native
fallback explicitly on Windows. Packed-tag enumeration remains covered
on both backends on every platform, resolving the reported enumeration
problem without claiming unsupported native lookup coverage. Record the
open GIX-32 capability gap and required upstream lookup support in
`doc/gix-backend.md`. Git's Windows CLI also cannot address those original
bytes through UTF-16 arguments, so this evidence is not classified as a
demonstrated Git compatibility mismatch. No library behavior changes.

Validation: all four focused packed-tag regressions pass on each local
backend; Ruff check/format and `git diff --check` pass. The Windows fallback
assertion remains for CI.
Review item #2274 (comment)
reports that "reading them raises `AttributeError`" when native decoding has
no tagger, and requests preserving initialized fields through a CLI fallback.
Four SHA-1/SHA-256 regressions reproduce the native failure. The two cases
with a message also expose CLI loss of the first message line, so falling
back alone would not restore complete metadata.

Adapt `Tag.tagger()` returning `None` to the existing empty `Actor` and zero
date/offset defaults through `parse_actor_and_date("")`. Gix already exposes
the absence correctly; no new Git semantics or fallback is required. Make
the CLI parser recognize the optional tagger header and preserve the entire
message. Git 2.54.0's `tag.c::parse_tag_buffer()` accepts an absent tagger
and sets its date to zero. The existing GIX-28 strict writer gap remains open.

All four regressions now pass on both backends; the native cases forbid
`Git.execute()` during metadata access. Reference and CLI object/index tests
pass on CLI (105 tests, 24 subtests) and Gix (105 tests, 24 subtests).
Ruff, ShellCheck and mypy pass.
Review item #2274 (comment)
reports that "subsequent iteration or lookup silently accepts that truncated
tree" after an entry fails validation. Stored trees containing a `.git`
entry reproduce this on both backends: the first iteration rejects the name,
but the second accepts an empty tree or the valid prefix.

Build the entries in a local list and attach `_cache` only after every record
passes parsing and validation. Failed reads leave the lazy cache unset, so
later accesses retry and raise the same error instead of exposing partial data.
No backend behavior or tree validation policy changes. Git 2.54.0's `mktree`
and `ls-tree` accept and emit the fixture's `.git` entry; GitPython applies
its existing metadata-path guard while consuming that output.

Four regressions cover invalid entries first and last in SHA-1/SHA-256 trees.
All fail before the fix and pass afterwards on CLI and native Gix `ls_tree`.
All 97 tree tests pass on each backend. Ruff, ShellCheck and mypy pass.
Review item #2274 (comment)
reports that "the returned head points to `refs/heads/topic`, which may be
missing or identify a different branch" after renaming to the short branch
name `refs/heads/topic`. A repository with an existing `topic` at commit A
and a source branch at commit B reproduces the incorrect returned ref on
both backends.

Compute the destination by prepending `refs/heads/` to the literal name
passed to `git branch`. Validate that same destination and retain it only
after Git successfully renames the branch. Git 2.54.0's
`builtin/branch.c::copy_or_rename_branch()` uses `refs.c::check_branch_ref()`,
which always prepends that namespace. The general `to_full_path()` helper
continues to normalize already-qualified references for its other callers.

The SHA-1/SHA-256 regressions fail before this fix and pass afterwards.
They check the returned path, name and commit, current `HEAD`, and that the
original `topic` still points to commit A. All 50 reference tests and 24
subtests pass on each backend, including ordinary and forced renames.
Ruff, ShellCheck, mypy and basedpyright pass.
@Byron Byron changed the title fix: preserve legacy path bytes and clarify supported APIs fix: preserve legacy path bytes and repair tag, tree and ref edge cases Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants