Repository navigation
Conversation
git config joins an unquoted value that ends in a backslash with the next line: the backslash and the newline are removed and the next line is appended verbatim, so 'k = line1\' followed by ' line2' reads back as 'line1 line2'. GitPython only implemented multi-line values for quoted strings; for unquoted values the continuation line was silently dropped, truncating whatever was stored in the config. Read the continuation inside _read and join it, chaining across lines that themselves end in a backslash. An even number of trailing backslashes is an escaped one, so the value ends there; a single backslash right at end-of-file is dropped, matching git. Verified against git itself for every case covered by the new tests.
<!-- agent --> Git treats # and ; outside quotes as the start of a comment. A trailing backslash in that ignored text therefore cannot continue the value. The continuation loop counted trailing slashes without lexical context, so it consumed the next option and a later writable flush silently dropped that setting. - Track quote and escape state while checking the accumulated value. - Stop continuation scanning at an unquoted comment. - Cover preservation of the following option across a writable flush. Assisted-by: GPT 5.6 Co-authored-by: GPT 5.6 <codex@openai.com>
Git removes each backslash-newline pair before parsing the resulting logical value. Appending physical lines after processing only the first line left trailing whitespace, comments, quotes, and recognized escapes literal. That divergence returned values unlike git config and could preserve comment text as configuration data. - Accumulate continuation text before parsing it. - Apply whitespace, comment, quote, and escape handling once to the complete value. - Cover each affected syntax form with Git-compatible expectations. Assisted-by: GPT 5.6 Co-authored-by: GPT 5.6 <codex@openai.com>
…backslash-continuation fix: join backslash line continuations when reading config values
…est-bare-worktree-subdir-discovery test: cover subdirectory discovery and pathspec commands in bare-repository worktrees
git accepts yes/no and on/off for a boolean as well as true/false
(git_parse_maybe_bool_text in parse.c), and ConfigParser.getboolean on this
class already accepted all of them. _string_to_value handled only true/false,
under a comment claiming to "try boolean values as git uses them", so the two
accessors disagreed about the same file:
value get_value() getboolean() git config --type=bool
yes 'yes' True true
no 'no' False false
on 'on' True true
off 'off' False false
Returning them as strings was worse than merely inexact. "no" and "off" are
non-empty, so a caller testing the result of get_value got True for a value
git reads as false — the inversion is silent, since nothing raises.
_string_to_value now recognises the same spellings getboolean does. A value
that is not a boolean is untouched, so "meld" is still returned as a string,
and numeric values keep their existing behavior.
Not changed here: get_value also diverges on numeric bases and suffixes
("0x10" and "1k" come back as strings, "010" as 10 where git reads octal 8).
Those change the value rather than its type and are worth their own commit.
Validation: test_get_value_reads_git_boolean_spellings covers the ten
spellings and asserts the two accessors agree; it fails on the previous
revision. test/test_config.py passes (40 passed, 2 skipped), ruff check and
ruff format are clean. test/test_repo.py errors on this clone because
init-tests-after-clone.sh has not been run, unchanged by this commit.
…-boolean-spellings Read git's yes/no and on/off booleans in get_value
…thon-developers#2230) <!-- agent --> Reading head.reference or active_branch raises TypeError when HEAD points directly to a commit, but the public documentation did not clearly explain how to access that commit. Document head.commit.hexsha for attached and detached HEADs, explain the reference setter/getter asymmetry, and clarify that active_branch requires an attached HEAD. Preserve the exception type and existing message prefix while adding a hint to use .commit or .object. Writable test fixtures assumed cloning produced an attached HEAD, so their branch access could fail when the source checkout was detached. Have with_rw_repo create and attach master at the requested revision when its clone is detached, retaining the clone's branch and tracking configuration otherwise. Explicitly attach the temporary bare remote to its own master branch before cloning it for remote tests. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
…s/detached-head Clarify detached HEAD access and isolate test branch state
Also, sloppy review of the tests which are assumpted to not make things worse. <!-- agent --> Submodule.move() checked lexical containment but did not validate intermediate destination components before filesystem and repository updates. GHSA-gq48-pqfc-9p58 identifies the resulting checkout-path boundary violation. The new regression failed before the fix because move() returned successfully. Share the existing abspath component walk with move() and validate the normalized destination before any mutation, including configuration-only and module-only calls. Preserve the no-op early return and existing final-component symlink handling; abspath still rejects every symlink component. This addresses pre-existing links, not concurrent directory replacement races. The Git reference checkout at 1630431f326e15fcde608827b5ff38422528eb59 uses has_symlink_leading_path() in builtin/mv.c and tests rejection without index changes in t/t7001-mv.sh. The fix follows that intermediate-component rule while retaining GitPython leaf-link compatibility. Validation: the 30 new parameterized cases pass, covering relative and absolute destinations and link targets, internal and dangling links, all move flag combinations, unchanged repository state after rejection, ordinary and no-op moves, and leaf-link compatibility. The complete test/test_submodule.py suite passes: 75 passed, 3 skipped, 1 xfailed. Test-process commit.gpgsign=false avoids sandbox GPG failures. Ruff lint and format checks, mypy (45 source files), and git diff --check pass. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
A bit of a sloppy review, rubber-stamping the tests based on the assumption that they are validating it's conforming to Git, probably also while increasing coverage. <!-- agent --> Submodule.add() could clone through a checkout symlink after module_exists() swallowed the validation error. Metadata paths had a similar gap: locally planted symlinks under .git/modules could redirect cloning, reconnecting, renaming, updating, or removing a submodule. Some failures were detected only after changing configuration or moving or removing checkout directories. Reuse the checkout component check for metadata paths and validate checkout paths in the shared clone helper, including legacy embedded repositories. Check .gitfiles, submodule configuration files, and the actual repository path named by a gitfile, which can differ from .git/modules/<name>. Reject symlinked .gitmodules files as well. Preflight move and rename sources and destinations before mutation, including the implicit metadata rename when a default-named submodule moves. Keep module_exists()'s boolean contract and the existing supported replacement of a leaf symlink during a move. Add 56 regression cases covering checkout and metadata links, dangling links, redirected gitfiles, legacy clone layouts, and rejected operations preserving external targets, configuration, the index, and an empty move destination. The initial 36 cases reproduced failures before the fix. These checks reject existing symlinks; they do not prevent concurrent filesystem replacement between validation and use. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
A quick rubber-stamp, admittedly. V4 will probably review all tests and make it more proper, if there can be such a thing in python anyway. <!-- agent --> Both Windows failures came from os.renames() pruning a directory symlink above the source after the rename succeeded. Unlike POSIX, Windows rmdir() can remove a directory symlink even when its target is nonempty. Moving a checkout through a worktree alias therefore deleted the alias and broke configuration updates and rollback. Renaming metadata through a linked .git/modules directory deleted that link and broke config.lock creation. Route checkout moves, rollback, and metadata renames through one helper. It creates destination parents and renames the source, then prunes empty source parents only until it reaches a symlink or a directory it cannot remove. This keeps ordinary empty-directory cleanup while preserving parent links and their targets, including targets that become empty. Leaf symlinks continue to move as links rather than moving their targets. Exercise Windows directory-symlink removal semantics on POSIX in the existing compatibility tests, and use native behavior on Windows. Both reported failures reproduced locally before the fix. Strengthen assertions that worktree and metadata parent aliases survive, their targets remain directories, and leaf metadata symlinks move without moving their targets. A further Windows run exposed a separate sharing violation during the checkout move. Submodule.add() read HEAD through its temporary Repo but left that Repo's persistent cat-file processes open. Those processes can hold the checkout as their current directory and prevent its rename. Close the owned Repo with a context manager when reading HEAD, including on read failure, instead of waiting for garbage collection. Add a regression that observes the real cat-file processes started for the new checkout and requires them to have exited before add() returns. It failed before the fix and now passes, along with the immediate move. The remaining metadata failures also reproduce with Python 3.7's Windows path semantics: ntpath.realpath is an alias of abspath and does not resolve symlinks. Relative core.worktree values were calculated from the metadata alias instead of the repository directory Git actually opens. This broke add/reconnect HEAD reads and made moves and renames point at nonexistent worktrees. The SHA/dubious-ownership message was a secondary read failure. Use pathlib.Path.resolve(), which resolves Windows symlinks on Python 3.7, for both endpoints of gitfile/config rewrites and for metadata removal. Run metadata and worktree-alias tests with native and simulated Windows 3.7 realpath behavior. The simulation reproduced all eight reported failures plus a leaf-symlink removal failure before this change. Reference: https://github.com/python/cpython/blob/3.7/Lib/ntpath.py and https://github.com/python/cpython/blob/3.7/Lib/pathlib.py. Validation: 183 passed, 3 skipped, and 1 expected failure across the submodule and diff suites plus the commit-message hook success test on macOS. Ruff lint and formatting, mypy for the changed module, and git diff --check passed. Native Windows validation remains for CI. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
rubber stamp <!-- agent --> The Python 3.7 Windows path simulation wrapped the entire os.path module in Mock. On Windows, all six simulated removal cases hit sharing violations that the default error handling converted into skipped tests. Use a SimpleNamespace copy of the path module and replace only realpath with abspath. Patch only the submodule module's osp binding so pathlib keeps its own resolver and other path operations remain ordinary calls. This retains Python 3.7 compatibility and allows the removal cases to run successfully without Windows permission-error suppression. Before removing a submodule, resolve and verify its metadata directory, then assert that removal deletes it as well as the checkout. Checking only the checkout could miss metadata left behind through a directory symlink. Validation: all 57 focused submodule compatibility and process-cleanup tests passed on Windows with Python 3.10 and HIDE_WINDOWS_KNOWN_ERRORS=0. Ruff 0.16.5 lint and formatting checks and git diff --check passed. Native Python 3.7 was unavailable; its realpath behavior is simulated. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
<!-- agent --> Ubuntu CI reported FileNotFoundError for maintenance.lock while running test_timeout_funcs. The test performs normal pull/fetch calls before its forced timeouts, and Git can launch detached automatic maintenance from those operations. Maintenance can then race with the fixture's recursive removal of the temporary repository, removing a lock file after cleanup has enumerated it. Disable maintenance.auto in this test's temporary repository and set gc.auto to zero for older Git versions that use automatic garbage collection. This removes background housekeeping unrelated to the timeout assertions without weakening repository cleanup or changing library behavior. Use mock.patch.object for the global forced termination status so an assertion failure cannot leak the override into subsequent tests. Validation: Git Trace2 recorded three detached maintenance launches in the original test and none with the fix. The timeout test then passed 20 consecutive runs locally on macOS. Ruff lint and formatting checks and git diff --check passed. The original Ubuntu cleanup exception was not reproduced locally; tracing verified removal of the suspected race source. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
rubber stamp <!-- agent --> Removing a submodule retains its metadata alias but deletes the target. Adding the same submodule again then passes a dangling directory symlink to git clone --separate-git-dir. Git for Windows fails while copying its template files through that alias. Both native-realpath and simulated Windows 3.7 remove-leaf cases reproduced this failure locally. When the metadata destination is a leaf symlink, pass its target to Git and leave the alias intact. Resolve relative targets against the link's parent, create missing target parents through the existing clone setup, and let Git create the repository directory itself. Precreating that directory is insufficient because Git rejects an existing separate git repository destination. Read the link explicitly because Python 3.7 on Windows cannot resolve a dangling link with Path.resolve(). Normalize the Windows namespace prefix returned by newer os.readlink implementations, including UNC targets, and use forward slashes before passing the path through Git's URL logic. The shared clone helper covers add() and initialization through update(). Add regression coverage for direct cloning through absolute and relative dangling metadata links with missing target parents. Verify that the link and its stored target are retained, metadata is created at the target, and the resulting checkout works under both realpath modes. Validation: 61 focused tests passed on Windows/Python 3.10 with HIDE_WINDOWS_KNOWN_ERRORS=0, including both reported failures. Ruff 0.16.5 lint and formatting checks and git diff --check passed. Native Python 3.7 and UNC network shares were not available for execution. A broader run also exposed sharing violations in sibling-reinitialization tests before cloning; a representative case also failed with the unchanged HEAD clone helper loaded in memory. That separate removal issue is not changed here. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
rubber stamp, just get this through CI OMG <!-- agent --> The dangling-metadata regression passed pathlib.Path objects to os.readlink() when capturing and checking the symlink target. Windows Python 3.7 requires a string argument, so all four parameter combinations failed with TypeError before exercising the clone fix. Convert the path to str at both calls. The production clone helper already passes a string and needs no change. Keep the target-preservation assertions and the absolute/relative and realpath-mode coverage intact. Validation: reproduced all four TypeErrors on Windows/Python 3.10 with an in-memory readlink wrapper enforcing the Python 3.7 string requirement. After the conversions, all four cases passed with the same wrapper and Windows permission-error suppression disabled. Ruff 0.16.5 lint and formatting checks and git diff --check passed. Native Python 3.7 was not available locally. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
…s/fix-submodule-move Reject submodule move destinations through intermediate symlinks
<!-- agent --> Head.checkout checked keyword options but omitted the serialized reference from its existing unsafe-option validation (GHSA-23mf-xhv8-69c2). Reference names can originate in a cloned repository, so callers could reach behavior that normally requires explicit opt-in without supplying any checkout options themselves. Pass self through the shared option-candidate helper, matching the argument actually sent to Git. This applies the existing policy to direct and cloned references, including abbreviated option spellings, while retaining allow_unsafe_options=True and ordinary checkout semantics. A leading -- separator would instead make the reference a pathspec and break branch switching. Add direct-reference regression coverage and a local clone regression using synthetic file content; the latter also verifies explicit opt-in. Both regression tests failed before the guard change. All 30 tests in test/test_refs.py pass with Python 3.12.14 and Apple Git 2.50.1; git diff --check passes. Git behavior reference: local git/git checkout at 1630431f326e15fcde608827b5ff38422528eb59, builtin/checkout.c checkout_main pathspec_from_file handling, which parses file contents as pathspecs. No Git source was copied. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
…s/clone-check-head-ref fix: validate checkout positional reference options
Git.execute used ps --ppid to find direct children before enforcing kill_after_timeout. On macOS this option is rejected: the parent is killed, but a child can continue running and hold captured output pipes open. Use pgrep -P for the child lookup, with POSIX ps PID/PPID output as a fallback when pgrep is absent. Filter the fallback by the original parent PID and reap the lookup subprocess in both paths. Keep the existing parent-first SIGKILL order, direct-child scope, and Windows guard, and update the documented command requirements. Systems without either lookup facility and the existing PID-reuse race remain limitations. Add real-process regressions for native pgrep and the ps fallback, plus a test that excludes unrelated processes and grandchildren from the fallback. Both real-process cases failed on the original code on macOS. The command module now passes 105 tests with 1 skip on macOS 27.0 / Python 3.13.5. Ruff check and format, codespell, mypy (45 files), basedpyright, and diff whitespace checks pass. Linux and Cygwin were not run locally; Cygwin's default ps lacks the required options, so the real-process cases skip it. Fixes gitpython-developers#1756 Signed-off-by: Mingyang Wu <129849514+aprylewu@users.noreply.github.com>
Several public annotations rejected supported inputs or lost the relationship between input options and return types. Describe Git.execute process, text, bytes, and extended-output results with overloads, accept stdin file descriptors, and account for absent stdout. Correct remote-removal, object, database, blame, and index-entry types, preserve entry subclasses and the supported tuple shapes, and accept streams with only the required read or write methods. Normalize absent previous stderr before appending process errors in AutoInterrupt.wait. Add runtime and static regressions for these interfaces, include them in mypy and basedpyright, make required imports explicit, and reduce the basedpyright baseline to the remaining diagnostics. Keep mock available to typecheck the Python 3.7 import branches. Make the output-type regression emit its payload without a newline. Its original print call produced CRLF on Windows; Git.execute strips the final LF as documented, leaving a carriage return that failed the test in all 11 Windows jobs. Omitting the newline keeps the same text, bytes, and tuple assertions independent of platform newline translation. Validation on macOS with Python 3.12.14: all four test/test_typing.py tests pass. Forcing the child stdout to translate newlines to CRLF reproduces the old failure and passes with the corrected command. Mypy passes for 46 source files; basedpyright --warnings reports no errors or warnings; Ruff lint and format checks for test/test_typing.py and git diff --check pass. Native Windows validation is delegated to the PR CI matrix. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
…portable-timeout-child-lookup Fix timeout child-process lookup on non-GNU systems
This was mostly a rubber-stamp, knowing the the whole implementation is quite a hack that is held together with ductape. Ideally, it will just work well enough at some point, to gain time for v4 to be made. <!-- agent --> GitConfigParser discarded keys written without an assignment. Reading such an option raised NoOptionError, and editing an unrelated setting silently removed it. Git treats a bare key as true but an explicitly empty value as false, so representing both as an empty string would lose their meaning. Store bare entries as None, following RawConfigParser's allow_no_value representation. Raw get/items access preserves the distinction, while get_value/get_values return an empty string as requested. Convert None to true and an empty string to false in getboolean, retaining the standard boolean spellings. Write None entries without an equals sign and exclude them from string validation and include-path expansion. The existing ordered multi-dict preserves repeated bare and assigned entries together. Update the regression that expected bare color.ui to disappear, and add a Git-backed read-modify-write test covering trailing whitespace, EOF without a newline, quoted and unquoted empty values, repeated keys, and a valueless non-path option in an include section. Both regressions failed before the fix. Compare Git's NUL-delimited listing before and after an unrelated edit and check the resulting repeated values with --type=bool --get-all. Git reference: checkout 1630431f326e15fcde608827b5ff38422528eb59, t/t1300-config.sh tests for novalue.variable and emptyvalue.variable, and parse.c:git_parse_maybe_bool_text. Runtime comparison used Git 2.50.1 (Apple Git-155). Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
…s/read-implicit-bool Preserve implicit boolean config keys
Note that this is just a fixup, on a huge hack which is the native git-config parsing. Let's just hope this holds up until v4. <!-- agent --> GitConfigParser accepted entries such as "enabled # comment" and "enabled ; comment" even though Git rejects them. The comment became part of the option name, and an equals sign or colon inside the comment could make the entry look like an assignment. Silently stripping the comment would also accept configuration that Git considers invalid. Exclude both comment markers from the shared option-name expression and require a full-line match for valueless options. The assignment pattern cannot cross a comment marker, and the valueless fallback cannot accept just the valid-looking prefix. Such lines now raise the existing ParsingError during reading or an attempted edit. Ordinary bare keys retain their implicit true value and round-trip behavior. Add six regression cases covering both markers, spaces, tabs, adjacent comments, and assignment delimiters inside comments. Compare rejection with git config, check both getboolean and an unrelated edit raise ParsingError, and verify that the failed edit leaves the original bytes untouched. All six cases failed before full-line matching was added. Git reference: checkout 1630431f326e15fcde608827b5ff38422528eb59, config.c:get_value. Without an assignment, that parser requires the line to end after the key and optional whitespace. Runtime comparisons used Git 2.50.1 (Apple Git-155), which rejected all six inputs with exit status 128. Validation on Python 3.12.14: 42 configuration tests and six regression subtests passed, with two existing skips. Ruff lint and formatting and git diff --check passed. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
…s/2238-follow-up Reject comments after implicit boolean config keys
…s/dependabot/github_actions/github-actions-a055b02c66 build(deps): bump astral-sh/setup-uv from 10.2.0 to 10.3.0 in the github-actions group
<!-- agent --> `Repo.blame_incremental()` copied Git's `filename` field into `BlameEntry.orig_path` without interpreting its C-style quoting. After renaming `café.txt` to `current.txt`, callers received `"caf\303\251.txt"` instead of `café.txt`, so following an attribution back to its original tree entry failed. Quotes, backslashes and control characters had the same problem, even with `core.quotePath=false`. Decode quoted filenames with the existing `git.diff._unquote_path()` helper before constructing each entry. This shared point handles both newly encountered and cached commits, leaves unquoted names unchanged, and retains `safe_decode()`'s byte-preserving `surrogateescape` behavior. `Repo.blame(..., incremental=True)` receives the same correction. Blame continues to use Git's CLI on both backends; this is an output-parser fix, not a new Gix implementation. Add an independently written tree-based fixture and regression covering eight historical filenames with both `core.quotePath` settings. Renaming then editing the middle line produces two separated entries attributed to the same original commit, exercising both parser branches. Check the commit, original path and both line ranges through both public entry points. Tree construction avoids platform restrictions on checking out control-character and invalid-UTF-8 filenames. The regression failed in ten quoted-filename subcases on each backend before the fix and passes afterward. Repository and diff tests pass on both backends; Ruff lint/format, mypy and ShellCheck also pass. Git evidence: the comparison binary is `git version 2.54.0 (Apple Git-157)`. Reference checkout revision `6de20f6092dcf9bdb1c8efe03db4b70c82b423dd` documents the terminating `filename` field in `Documentation/git-blame.adoc`; its `builtin/blame.c:write_filename_info()` uses `write_name_quoted()`. The regression's real `git blame --incremental HEAD -- current.txt` output quotes the historical path in both entries for the original commit, matching that format. Independent solution to the problem reported and investigated by `Keerthana-64` in gitpython-developers#2314. No implementation or tests were taken from that PR. Assisted-by: GPT 6.1 Sol Co-authored-by: GPT 6.1 Sol <codex@openai.com>
The `test (windows, 3.15)` and `test (windows, 3.12, gix)` CI jobs fail the new historical-path regression for its combined control-character filename under both `core.quotePath` settings. Their assertion messages truncate the actual and expected entries and omit the full diff, leaving the precise filename mismatch unavailable in the job logs. Set `TestCase.maxDiff` to `None` in this regression so CI reports the complete values. This preserves every assertion and the implementation; it provides the evidence needed to distinguish fixture handling from a parser defect before changing behavior. The focused regression passes locally on the CLI and Gix backends with all sixteen filename/quoting subcases. Ruff lint and formatting checks and `git diff --check` pass.
Windows CI with Git `2.56.0.windows.2` fails the historical-path regression for its combined control-character filename under both `core.quotePath` settings. The attributed original commit is `e5ed22d659d5aa4ff5f7527069752d20f74fc2d9`, which a local Git reproduction matches exactly when removing the carriage return from the original path. Keeping that byte produces `73813b4d58d085503f9426c6e50ce5c201f715b6`. The failure therefore comes from fixture creation losing the byte, rather than incremental blame returning a different path from the stored tree. Represent special paths as Git C-quoted text and feed the original tree through `git mktree` instead of sending raw control bytes to `mktree -z`. Git decodes the escapes while constructing the tree. This keeps carriage returns out of the raw fixture input and retains coverage for every control character, invalid UTF-8, literal escapes, and both quoting settings. The parser implementation and expected results are unchanged. The focused regression passes locally on both backends with all sixteen subcases. Raw `git ls-tree -z HEAD~2` confirms the carriage return is present, and the original commit retains its expected ID. ShellCheck, `bash -n`, and `git diff --check` pass. Windows CI must verify the fixture on its native Git/Bash combination.
<!-- agent --> Fetching a valid tag named `refs/heads/unborn` stores it under `refs/tags/refs/heads/unborn`. When `HEAD` names the absent `refs/heads/unborn`, the CLI reference reader used `git rev-parse --verify` and guessed that tag instead. `head.is_valid()` became true and `IndexFile.commit()` gave the local initial commit a foreign parent. A tag named `REBASE_HEAD` also made `Repo.currently_rebasing_on()` report a rebase. Read direct references with `git show-ref --verify --hash` instead of revision parsing. Probe `REBASE_HEAD` with `git show-ref --exists` to distinguish absence from lookup errors, then use `SymbolicReference.dereference_recursive()` and `Repo.commit()` so the rebase reader shares exact lookup and symbolic-reference validation while retaining recursive annotated-tag peeling. Revision APIs intentionally retain Git's name guessing. Existing native Gix reference reads are unchanged; root-reference existence and reftable retain documented CLI fallbacks. Independently written regressions fetch the misleading tags into an unborn repository and cover SHA-1/SHA-256, files/reftable, forced CLI fallback, parentless initial commits, and absent/present/deleted `REBASE_HEAD`. Positive cases compare existing root refs, packed annotated tags, and detached `HEAD` against Git. All 16 original regressions failed on the CLI baseline; 14 failed under Gix, whose files-based initial commits were already protected. All 26 final reference regression cases pass on each backend. Post-commit review found that `SymbolicReference.commit` peels only one annotated tag; four new regressions failed with `TypeError` before switching to the existing recursive commit resolver. Broader validation exposed the existing GIX-17 grammar bug for this work branch: Gix reads `2313-supersede` as blob `23136a1d1dd1f02de7cde0f7b55f49d76ac77d55`, while Git reads the branch's commit. Extend the existing conservative CLI grammar guard to hexadecimal prefixes followed by a dash. A deterministic fixture verifies existing `<blob-prefix>-topic` and missing `<blob-prefix>-missing` names; its test fails before the guard and passes afterward. The GIX-17 ledger retains the upstream bug as open and records the evidence and adapter fallback. An additional exact-name check exposed GIX-33: when the root `REBASE_HEAD` is absent and `refs/tags/REBASE_HEAD` is symbolic, Gix's partial lookup returns that tag and native `symbolic_ref` dispatch follows its target. Git's equivalent `symbolic-ref --quiet --no-recurse` exits 1. Compare the returned native reference name with the requested bytes and retain CLI for a lookalike. Two new SHA-1/SHA-256 regressions fail before the guard and pass afterward, including forced CLI fallback. The ledger keeps this compatibility bug open until Gix exposes a verified exact-name mode. Final caller validation passes 98 CLI reference tests and 200 Gix reference/backend tests, with 24 passing subtests each. The preceding intermediate commit `9ddf778` also passed the full local suites: 1,969 CLI tests and 2,072 Gix tests, with 85 passing subtests each. Earlier targeted revision/installation checks passed 48 CLI tests and 50 Gix tests. Ruff check/format, mypy, basedpyright, ShellCheck, and `git diff --check` pass. Behavioral comparisons use `git version 2.54.0 (Apple Git-157)` and `GixPython 0.3.0`. The Git reference checkout at `6de20f6092dcf9bdb1c8efe03db4b70c82b423dd` documents exact lookup in `Documentation/git-show-ref.adoc`, name guessing in `Documentation/revisions.adoc`, and parentless root commits in `Documentation/git-commit-tree.adoc`. Source report and investigation: Keerthana-64's PR gitpython-developers#2313. The replacement does not incorporate that PR's implementation or tests. Assisted-by: GPT 6.1 Sol Co-authored-by: GPT 6.1 Sol <codex@openai.com>
The existing `AGENTS.md` guidance requires PRs to describe the problem, but only commit bodies explicitly require motivation. Port the PR motivation requirement from the Gitoxide worktree so reviewers can understand why an agent pursued the work. Require agent-created PR descriptions to identify the underlying motivation, such as the benefiting project or where the bug was discovered. Clarify that restating the fix is insufficient. Validation: reviewed the wording against the Gitoxide requirement and ran `git diff --check`. No tests were run because this changes repository instructions only.
…s/2314-supersede fix(repo): return actual historical paths from incremental blame
The Ubuntu 3.14t CI job rejected correctly redacted clone output because its randomly generated `/tmp/.tmpg8ibob` fixture path contained `bob`. The credential assertions scanned the whole error and trace, so ordinary source and destination paths could masquerade as leaked credentials. Replace only the known fixture directory with `<fixture>` before checking the error and trace for credentials. Keep the exact redacted-option assertions and the credential checks outside that directory. Include `alice-bob-user-token` in the input paths so this false positive is covered deterministically, rather than depending on a random name. The Windows CLI and Gix jobs then failed all four trace subtests because `Repo._clone()` uses `Git.polish_url()` for the source path: the command and stderr contain forward slashes, while `str(directory)` contains backslashes. Copilot's review requested: "Remove both the native and POSIX spellings of the fixture directory before checking for credentials." Remove both exact spellings from the error and trace to address that feedback without weakening the credential checks elsewhere. Focused CLI and Gix runs each pass three redaction tests and four subtests. An assert-based check runs the new replacement loop against four actual Windows CLI/Gix CI failure messages: all pass, and a credential outside the fixture path remains visible. `ruff check`, `ruff format --check`, and `git diff --check` also pass. Windows execution remains for remote CI.
…s/2313-supersede fix(refs): keep missing references from resolving to lookalike tags
The public `join_path` helper drops the slash in `join_path("parent", "/child")`, yielding `parentchild`. Strip leading slashes from later tokens before the normal joining logic so they remain relative to the accumulated prefix and receive a separator. Document this retained relative-token contract and cover string/PathLike inputs, chained tokens, roots, empty tokens and repeated leading separators.
Five of eleven regression cases fail before this change. Afterward the complete utility module passes 128 tests with 39 skips on each CLI/Gix backend. Tree/reference/remote caller modules pass 230 tests with seven stalled-remote timeout failures per backend; the exact same seven failures reproduce on unchanged main. Repository pre-commit hooks pass. Tested on Linux with Python 3.12.14, Git 2.52.0 and GixPython 0.3.0; no other platform execution is claimed.
Assisted-by: OpenAI Codex (model: unknown, autonomous)
Python 3 sends slices to `Tree.__getitem__`, but its legacy slice implementation only exists in `__getslice__`. `tree[:2]` therefore raises `TypeError`. `reversed(tree)` also raises because it applies `reversed` to a generator. Handle slices through the existing cache-to-object conversion and reverse cached entries before lazy conversion. Add return-type overloads, public API documentation and regressions for ordering, stride, negative/empty ranges, zero steps, independent results and nested paths. Validation: all eight new cases fail on the original CLI implementation. Afterward, the complete tree and typing modules pass 110 tests on each CLI/Gix backend with Python 3.12.14, Git 2.52.0 and GixPython 0.3.0 on Linux. Mypy (40 source files), basedpyright and applicable repository pre-commit hooks pass. Full suites and other platforms were not run. Assisted-by: OpenAI Codex (model: unknown, autonomous)
`altz_to_utctz_str` accepts float seconds, but floor division preserves their float type. Formatting `19800.0` therefore emitted `-5.030.0` rather than Git's `-0530` offset. Compute whole minutes as an integer and split them with `divmod`, preserving existing sign and sub-minute truncation behavior. Add six regressions covering zero, positive/negative offsets and fractional seconds. All six fail before the fix and pass afterward; applicable pre-commit checks pass. Assisted-by: OpenAI Codex (model: unknown, autonomous)
…ath-leading-separators fix(util): preserve separators before relative path tokens
…ne-float-formatting fix: format float timezone offsets as integer fields
`assure_directory_exists("notes.txt", is_file=True)` attempted to create the empty dirname and raised `FileNotFoundError`, despite the current directory already being present.
Interpret an empty parent as `os.curdir` for file inputs. Preserve the helper's return convention, nested parent creation and failures when a parent is an existing regular file.
Use a native scripted filesystem fixture for string and pathlib inputs. Two current-directory cases fail before the fix, while six controls pass. All eight cases pass afterward; utility and typing modules pass on both backends (129 passed, 39 skipped each). Applicable pre-commit checks, including shellcheck, pass.
Assisted-by: OpenAI Codex (model: unknown, autonomous)
`IterableList.__getitem__` advertises `SupportsIndex`, but routes every non-`int` index through named lookup. Valid objects implementing `__index__` therefore raise a string-concatenation `TypeError` instead of selecting an element. Delegate non-string, non-slice indices to the built-in list implementation, which already handles the index protocol and bounds checks. Keep named lookup and explicit slice rejection unchanged. Six protocol/bounds regressions fail before this fix; a lookup/slice control passes. Utility and typing modules pass with both CLI and Gix backends (128 passed, 39 skipped each). Applicable pre-commit checks pass. Assisted-by: OpenAI Codex (model: unknown, autonomous)
…s/fix/current-directory-file-parent fix/current directory file parent
…s/fix/iterable-list-index-protocol fix/iterable list index protocol
…equence-access fix(tree): support slices and reverse iteration
`stream_copy` stopped after any read shorter than `chunk_size`, truncating valid unbuffered streams. Continue until an empty chunk reports end of input, preserving the existing writer contract and byte count. Add binary/text short-read and empty-input regressions, plus a synchronized real POSIX pipe reproduction. The regressions fail before the fix; utility and typing modules pass with both CLI and Gix backends (126 passed, 39 skipped each). Applicable pre-commit checks pass. Assisted-by: OpenAI Codex (model: unknown, autonomous)
…s/fix/stream-copy-short-reads fix/stream copy short reads
`git.util.rmfile` exists so that a read-only file can be deleted on Windows, where `os.remove` fails on files with the read-only attribute. On Windows it calls `os.chmod` to clear that attribute before removing the file. Nothing tested this. Before this change, the only callers of `rmfile` were `LockFile._release_lock` and a lock-file helper in `test/test_repo.py`, and no test called either of them on a read-only file. Add `TestRmfile.test_deletes_readonly_file`, alongside the analogous `TestRmtree.test_deletes_dir_with_readonly_files`. Its initial state comes from a new fixture script, `tests/fixtures/rmfile.sh`. The script creates a file named `readonly` and removes its write permissions with `chmod a-w`, as `tests/fixtures/cleanup.sh` does for its read-only case. On Windows this should give the file the read-only attribute. The test uses a copy of the cached fixture output, which keeps the file's mode, so it needs no `execute=True`. The script is separate from `tests/fixtures/rmtree.sh`, whose cases are keyed by `TestRmtree` test names through the `tmp_path` fixture in `test/test_util.py`, and which that fixture runs in place for all but one of them. `rmfile` silently does nothing if the path is not a file, so the test would pass vacuously if the fixture made no file. It therefore first checks that the file exists and is not owner-writable, and only then that `rmfile` removes it. On Windows, `os.stat` reports the read-only attribute as the absence of write bits, so this also confirms that the fixture produced the condition the test is about, rather than assuming it. The test runs on every platform, but it is only meaningful on native Windows. Unlike the `rmtree` test, it is not skipped on Cygwin. That skip's reason, that Cygwin can't set the needed permissions, hasn't been checked for this test. On Cygwin `sys.platform` is `"cygwin"`, so `rmfile` uses no special handling there, and the test only checks that a read-only file can be removed, as on Unix. This adds coverage ahead of changing the mode `rmfile` passes to `os.chmod`: the test passes with the current `0o777`, and on Windows it would fail if the mode lacked the owner-write bit. Validation: on macOS with Python 3.12, the four `TestRmtree` and `TestRmfile` tests pass, the copied fixture file has mode `0o444`, and the precondition fails when the path does not exist. `shellcheck` passes on the new script. Neither Windows nor Cygwin was run locally. The `import-cygwin` workflow does not run the test suite, so Cygwin is not tested in CI either. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
On Windows, `git.util.rmfile` calls `os.chmod(path, 0o777)` before
`os.remove` so that read-only files can be deleted. With the CodeQL
`security-extended` suite now enabled for this repository, that call is
reported as `py/overly-permissive-file` ("Overly permissive mask in
chmod sets file to world writable"), at high severity.
The finding is a false positive in effect. On Windows, `os.chmod` can
only set or clear the read-only attribute, and the Python documentation
says "All other bits are ignored". CPython's implementation confirms
this. Despite its name, `Modules/posixmodule.c` is also the Windows
implementation: `os` takes its functions from a built-in module that is
`posix` on Unix and `nt` on Windows, and both are compiled from that
file, with the Windows code under `MS_WINDOWS`. There, `chmod` tests
only `mode & _S_IWRITE`, clearing `FILE_ATTRIBUTE_READONLY` when that
bit is set and setting it otherwise. This is the same in 3.8, the
oldest Python GitPython supports, as in `main`, including the newer
handle-based path. `_S_IWRITE` is `0o200`, the same value as
`stat.S_IWUSR` (and `stat.S_IWRITE`). The call is only reached when
`sys.platform == "win32"`, so every bit in `0o777` except `0o200` was
always ignored.
Pass `stat.S_IWUSR` instead. On Windows this behaves identically: it
clears the read-only attribute and does nothing else. It states the
intent exactly, and it matches the `os.chmod(path, stat.S_IWUSR)`
already used for the same purpose by the `rmtree` error handler a few
lines above, which CodeQL does not flag.
Behavior is unchanged on all platforms, so no test can fail before this
change and pass after it. `TestRmfile.test_deletes_readonly_file`
covers the behavior being preserved, and on Windows would fail if the
mode lacked the owner-write bit.
Validation: on macOS with Python 3.12, the `TestRmtree` and `TestRmfile`
tests pass both before and after this change, and `ruff check` and
`ruff format --check` pass. Windows, where the changed line actually
runs, was not run locally and is left to CI.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…mfile-chmod-owner-write refactor(util): clear read-only in `rmfile` with `stat.S_IWUSR`
`IndexFile.remove()` passed every item to `git rm` as a pathspec. Path strings are documented to accept globs, but index entries, blobs and submodules name one exact path, and Git still expanded glob characters and pathspec magic in those names. The names are chosen by whoever authored the repository: - Removing the entry for a file named `*.txt` also removed `a.txt` and `b.txt`. - Removing the entry for `:(glob)**` removed every tracked path, and with `working_tree=True` deleted those files from disk. `Submodule.remove()` (for a submodule that is not checked out, or with `module=False`) and `Submodule.move(module=False)` passed the `.gitmodules` path string to the same call with `force=True`. A superproject declaring a submodule at path `*` therefore had every tracked file staged for deletion and the index written. The neighboring `git rm` and `git mv` calls in `Submodule` already run with `GIT_LITERAL_PATHSPECS=1`. Prefix the paths of entry, blob and submodule items with Git's `:(literal)` pathspec magic and leave path strings unchanged, so a mixed call keeps the documented glob behavior for its strings. The submodule callers now pass their index entry. `git mv` reads its sources literally, so `IndexFile.move()` needs no change. `git rm` has no native Gix handler and runs through the CLI on both backends, so `doc/gix-backend.md` is unchanged. Limitation: when a caller exports `GIT_LITERAL_PATHSPECS=1`, Git does not parse pathspec magic. Removing an entry, blob or submodule then fails with "did not match any files" and removes nothing; path strings stay literal in that environment as before. Add a `remove-literal` scenario to `tests/fixtures/index.sh`, a `glob-path` scenario to `tests/fixtures/submodule-special.sh`, and regression tests in `test/test_index.py` and `test/test_submodule.py`. The three new test cases fail before the change and pass after it with both `GIT_PYTHON_BACKEND=cli` and `GIT_PYTHON_BACKEND=gix` (Git 2.52.0, GixPython 0.3.0, Python 3.11.17, macOS). Full suites with the change: CLI 1,884 passed and Gix 1,987 passed. Each run also had 108 failures, and the same 108 tests fail on the unpatched tree. They need history and tags such as `0.1.6` and `0.3.2.1`, which the shallow local clone lacks. `ruff check`, `ruff format --check`, `mypy --python-version=3.11`, `basedpyright --warnings`, ShellCheck and `git diff --check` pass.
…remove-literal-paths **fix(index): entries, blobs aur submodules ko literal path se remove karo**
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes two duplicated words in docstrings and log messages.
git/objects/submodule/root.py: the warning emitted when the current submodule sha is not contained in the tracking branch at the new remote read "setting it the the remote's tracking branch"; it now reads "setting it to the remote's tracking branch".git/util.py: theIterableObjdocstring said "subclasses are obliged to to yield all items"; it now reads "obliged to yield all items".Both are comment/string only, no behavior change.
AI assistance disclosure (per CONTRIBUTING.md "Prevent agent impersonation"): the wording review and the two proposed one-word edits were produced with AI assistance, and the commit carries a
Co-authored-bytrailer. The person submitting this PR is responsible for the change.