Skip to content

fix(docs): correct duplicated words in submodule and util docstrings - #2332

Closed
Jah-yee wants to merge 1065 commits into
gitpython-developers:masterfrom
Jah-yee:fix/doc-typos-the-the-to-to
Closed

Jah-yee wants to merge 1065 commits into
gitpython-developers:masterfrom
Jah-yee:fix/doc-typos-the-the-to-to

Conversation

@Jah-yee

@Jah-yee Jah-yee commented Oct 31, 2026

Copy link
Copy Markdown

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: the IterableObj docstring 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-by trailer. The person submitting this PR is responsible for the change.

NK and others added 30 commits September 4, 2026 02:39
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 <[email protected]>
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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
<!-- 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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
…s/2238-follow-up

Reject comments after implicit boolean config keys
codex and others added 28 commits October 27, 2026 12:06
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 <[email protected]>
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)
- simplify test (remove threads)
…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 <[email protected]>
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 <[email protected]>
…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**
@Byron

Byron commented Nov 1, 2026

Copy link
Copy Markdown
Member

What a mess, maybe try to put things on top of the latest main.

@Byron Byron closed this Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.