Skip to content

Shield seeds the wrong global ignore file on Git for Windows >= 2.46 (%APPDATA%\Git\ignore) #403

Description

@pbean

Found while fixing the $HOME half of the same branch on #385 (c4ba8e9), and deliberately not fixed there — see "Why not in #385" below. Recorded in _shield_home_git_ignore's docstring so the branch is not read as complete.

The defect

When core.excludesFile and XDG_CONFIG_HOME are both unset, the shield reproduces git's fallback to $HOME/.config/git/ignore and copies that file's patterns into the worktree's private exclude — it has to, because the worktree-scoped core.excludesFile it then activates SHADOWS the operator's global one (git resolves the key from the most specific scope and never concatenates).

c4ba8e9 made the $HOME part correct on every platform by asking git rather than Python. But $HOME/.config/git/ignore is the whole of the fallback only upstream. Git for Windows patches xdg_config_home_for in its fork to prefer %APPDATA%/Git/<file> whenever that file exists:

	home = getenv("HOME");
	if (home && *home)
		home_config = mkpathdup("%s/.config/%s/%s", home, subdir, filename);

	#ifdef WIN32
	{
		const char *appdata = getenv("APPDATA");
		if (appdata && *appdata) {
			char *appdata_config = mkpathdup("%s/Git/%s", appdata, filename);
			if (file_exists(appdata_config)) {
				if (home_config && file_exists(home_config))
					warning("'%s' was ignored because '%s' exists.", home_config, appdata_config);
				free(home_config);
				return appdata_config;
			}
			free(appdata_config);
		}
	}
	#endif

	return home_config;

git-for-windows/git, path.c:1584-1616 at v2.55.0.windows.3.

Note git itself warns that the $HOME file "was ignored because" the APPDATA one exists: git considers these two genuinely alternative locations, not a search path.

Version bounds (counted APPDATA in path.c)

ref occurrences
git-for-windows/git v2.55.0.windows.3 1
git-for-windows/git v2.46.0.windows.1 1
git-for-windows/git v2.45.0.windows.1 0
git-for-windows/git v2.20.0.windows.1 0
git/git master (upstream) 0

So: Git for Windows only, from 2.46 onward, never upstream.

Harm

Silent, and it is #384's own harm. An operator on a current Git for Windows who keeps their global ignores at %APPDATA%\Git\ignore gets:

  1. _shield_home_git_ignore returns $HOME/.config/git/ignore, which typically does not exist;
  2. is_file() is false, so the seed is empty — no fault, no reason, reason is None;
  3. the shield activates a worktree-scoped core.excludesFile that shadows the file git really reads.

Everything they globally ignore becomes visible to git add -A inside the worktree, and the unit's commit path (verify.commit_story, and the finalize path, both unconditional git add -A) sweeps it into the story commit.

The mirror direction matters too: if BOTH files exist, git uses APPDATA and we would seed the $HOME one — copying patterns git is not applying, so the worktree over-ignores and story files can go unstaged.

Why not in #385

  • It is a downstream fork's behavior, not git's — a different claim from everything else that branch verified.
  • It is gated at 2.46, far above the shield's own 2.20 floor, so a naive if APPDATA exists, prefer it would over-ignore on older Git for Windows. A correct fix needs a version gate too.
  • There is no way to measure it from a POSIX box, which is that branch's standing bar for a git claim. Windows CI cannot validate it either: the runners have no %APPDATA%\Git\ignore, so CI can only show nothing broke, not that the preference is right.

Fix shape

In _shield_home_git_ignore (src/bmad_loop/install.py), before falling back to the ~ probe:

if sys.platform == "win32" and _git_version_at_least(<version>, (2, 46)):
    appdata = os.environ.get("APPDATA")
    if appdata:
        candidate = Path(appdata) / "Git" / "ignore"
        if candidate.is_file():
            return candidate

The is_file() precondition is load-bearing and mirrors git's own file_exists — the preference applies only when the file is actually there.

Two caveats for whoever picks this up:

  • The version gate needs a git version reading in this function, which currently has none. _git_version_at_least already exists; check it parses 2.46.0.windows.1 (a four-component version string) before relying on it.
  • A test can pin our selection logic by faking APPDATA and sys.platform, but it cannot prove Git for Windows agrees. Quote the fork source in the test docstring and say so, rather than implying it was measured.

Provenance

Source-read of git-for-windows/git and git/git via raw.githubusercontent.com, version bounds counted per tag. Not measured on a Windows machine — no runtime observation of Git for Windows was possible.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions