Commit b0c3109
Cover submodule working trees in safe.directory on Cygwin
Add the gitdb and smmap submodule working tree paths to the
`safe.directory` config in the Cygwin CI workflow. Without this,
when GitPython opens a submodule as a `Repo` and runs
`git cat-file --batch-check` against it, git rejects the repository
for dubious ownership. The user-visible failure modes documented in
commit 1 (`ValueError`, `IndexError`, `AssertionError`) all trace
back to this rejection.
Why `[gitdb]` fails and `[smmap]` incidentally passes
-----------------------------------------------------
The trust check in `test/test_fixture_health.py` fails for `[gitdb]`
but passes for `[smmap]`, even though neither submodule's working
tree is in the workflow's `safe.directory` list before this commit.
The asymmetry comes down to which Owner SID NTFS records on each
path, and which paths git's ownership check requires to be owned.
Of the six paths git's check might consider for the two submodules
-- gitdb's `.git` gitfile, worktree directory `git/ext/gitdb`, and
gitdir `<repo>/.git/modules/gitdb`; smmap's `.git` gitfile,
worktree directory `git/ext/gitdb/gitdb/ext/smmap`, and gitdir
`<repo>/.git/modules/gitdb/modules/smmap` -- exactly one has NTFS
Owner = `BUILTIN\Administrators` (Cygwin uid 544): gitdb's worktree
directory. The other five have Owner = runneradmin (Cygwin uid
197108). Cygwin's `geteuid()` is 197108.
This pattern reproduces both with and without `submodules: recursive`
set on `actions/checkout`. The single Admin-owned path is gitdb's
worktree directory; all other paths -- including the ones that
Windows git's recursive submodule clone has just produced when
`submodules: recursive` is set -- are runneradmin-owned. The
differentiator is not which git binary clones the submodules, but
the fact that `git/ext/gitdb` is created by the outer `git clone`'s
checkout phase. When `git checkout` materializes a tree entry of
mode 160000, it calls `mkdir(path, 0777)` to create an empty
submodule directory (see git's `entry.c::checkout_entry`, case
`S_IFGITLINK`). The directory created by that call inside
`actions/checkout`'s git process ends up with NTFS Owner =
`BUILTIN\Administrators`, the same Owner as the surrounding
workspace and the rest of the tree that outer `git clone` checkout
produces. Subsequent submodule-update operations -- both Windows
git when `actions/checkout` drives the recursive clone, and Cygwin
git when `init-tests-after-clone.sh` runs `git submodule update
--init --recursive` later -- produce paths that NTFS records as
runneradmin-owned. Why the operations that come after the outer
checkout produce a different Owner from the outer checkout itself
was not pinned down in source. The empirical pattern is consistent
across all observed scenarios (see "Verification" below) and the
fix does not depend on the precise cause.
Cygwin's `lstat().st_uid` reports the actual NTFS Owner SID mapped
through Cygwin's SID-to-uid table. Git's
`is_path_owned_by_current_user` reduces to
`lstat(p).st_uid == geteuid()` on Cygwin (no special-casing). Git's
`ensure_valid_ownership` short-circuits acceptance when ALL of
(gitfile, worktree, gitdir) pass that check; otherwise it falls
through to matching `safe.directory` against the worktree's
`real_pathdup`.
For gitdb the three Owners are runneradmin, **Administrators**, and
runneradmin, so the short-circuit fails on the worktree. The
workflow's `safe.directory` before this commit contains only
`$(pwd)` and `$(pwd)/.git`, neither of which exact-matches
`git/ext/gitdb`, so the safe.directory fallback also fails, and
git rejects the repository. For smmap the three Owners are all
runneradmin, so the short-circuit accepts.
Cygwin's `chown` cannot rewrite the gitdb worktree's Owner SID from
Administrators to runneradmin: it returns `Permission denied`,
verified in the v3/v4 diag runs cited below. Adding both submodule
worktree paths to `safe.directory` is the correct fix and is
robust against shifts in which paths inherit which Owner.
Why `actions/checkout`'s own `safe.directory` does not help
-----------------------------------------------------------
`actions/checkout`'s `set-safe-directory: true` default adds the
main repository path to `safe.directory`, but on Cygwin that
addition lands in the Windows-side global git config (under
runneradmin's Windows HOME), not the Cygwin-side one (under the
Cygwin user's HOME). Cygwin git reads only its own global config,
so `actions/checkout`'s entry is invisible to it. The cygwin-test
workflow therefore explicitly populates Cygwin git's
`safe.directory` itself; this commit extends that to cover the
gitdb and smmap working trees.
The Cygwin-vs-Windows-git distinction is also why the bug surfaces
only on the Cygwin workflow rather than across every Windows
workflow. Git's `compat/mingw.c` defines
`is_path_owned_by_current_sid` (lines 2935-2943 in v2.51.0), which
accepts `BUILTIN\Administrators`-owned paths when the current user
is a member of Administrators. Cygwin git compiles against the
POSIX path (`is_path_owned_by_current_uid` in `git-compat-util.h`)
without that leniency. So the same Admin-owned `git/ext/gitdb`
that Cygwin git rejects is silently accepted by Windows git, and
the `Python package` workflow's `windows-latest` jobs never trip
the trust check.
Verification
------------
The `reproduce-safe-dir` matrix on the previous commit produces
failures for all three affected tests; this commit's CI run is
expected to show those tests passing instead.
The Owner-SID claim above is verifiable from three diagnostic CI
runs on temporary branches. The branches have been deleted, but
the runs themselves remain accessible:
- https://github.com/EliahKagan/GitPython/actions/runs/25542627150
Ownership and `chown` experiment after `actions/checkout` with
default settings (no recursive submodules) and Cygwin's
`git submodule update --init --recursive`. `git/ext/gitdb` Owner
= `BUILTIN\Administrators`; every other relevant path Owner =
runneradmin; `chown` of the worktree returns `Permission denied`.
- https://github.com/EliahKagan/GitPython/actions/runs/25543128283
Placeholder timing check: `git/ext/gitdb` exists as an empty
directory with Owner = `BUILTIN\Administrators` immediately
after `actions/checkout`, before any submodule update has run.
Confirms the Owner is set at the outer `git clone`'s checkout
phase, not by the submodule update.
- https://github.com/EliahKagan/GitPython/actions/runs/25557770540
Recursive-clone replication: with `submodules: recursive` on
`actions/checkout` so Windows git does the recursive submodule
clone itself, the same pattern holds. `git/ext/gitdb` is
Admin-owned; every other submodule path -- including ones
Windows git has just created -- is runneradmin-owned.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 1f986ba commit b0c3109
1 file changed
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
| 61 | + | |
| 62 | + | |
61 | 63 | | |
62 | 64 | | |
63 | 65 | | |
| |||
0 commit comments