fix: pin --untracked-files so status.showUntrackedFiles cannot hide work [patch] - #113
Conversation
…ork [patch] GitStatusBuilder only emitted --untracked-files when a caller called WithUntrackedFiles. Without it git resolves the mode from the host's status.showUntrackedFiles, so a machine that sets it to "no" reported the same working copy as IsClean with untracked files sitting in it. The flag is now emitted unconditionally, defaulting to git's own documented "normal", the way GitCommandBuilder already pins --no-pager, core.quotepath and color.ui. WithUntrackedFiles replaces that default rather than adding a second value. Fixes #112 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVrdn7dcoGasLJEgrnb67u
SonarCloud's analysis of the new tests raised three MSTest suggestions, all in code this branch added: - MSTEST0037 -> Assert.ContainsSingle for the "exactly one --untracked-files argument" check, which also folds the following DoesNotContain into an equality check on the single argument found - MSTEST0068 -> resolved by that fold; the CollectionAssert call is gone - MSTEST0037 -> Assert.Contains for the untracked-entry check, keeping the failure message MSTEST0054 (TestContext.CancellationToken over TestContext.CancellationTokenSource.Token) is left alone: the older form is used at roughly 250 call sites across 32 files here, and changing one new line would make it the only outlier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVrdn7dcoGasLJEgrnb67u
6d82315 to
dccc7a8
Compare
|
|
Notes on the two CI signals above, so neither needs chasing down: SonarCloud's remaining "1 New issue" is It is left as-is deliberately. The older form is this suite's universal idiom — roughly 250 call sites across 32 files — and it is flagged here only because the line is new. Changing this one line would make it the sole outlier in the repository rather than fixing anything. If the newer property is wanted, it is a suite-wide sweep of its own, not a drive-by in a fix PR. The other three findings from the first analysis were real and are fixed (
All checks are green on Generated by Claude Code |



Fixes #112
What was wrong
GitStatusBuilderadded--untracked-files=<mode>only when a caller had calledWithUntrackedFiles(...). With no such call the vector was justgit status --porcelain=v2 --branch -z, and git then resolves the mode fromstatus.showUntrackedFiles— a variable a minimal CI image or a developer's~/.gitconfigcan set tonoto keepgit statusfast in a large tree.Reproduced against git 2.43, the version the issue reports:
So the same repository state produced
IsClean == truewith an untracked file sitting in it — silently, and precisely for the caller asking "is there work here I would destroy?" before discarding a working copy.This is the one verb that still left a host-configurable default unpinned.
GitCommandBuilder.BuildArgumentsalready pins--no-pager,core.quotepath=falseandcolor.ui=falseunconditionally, andRunCommandGitProcessRunnerforcesLC_ALL=CandGIT_TERMINAL_PROMPT=0, for the same reason.The change
GitStatusBuilder.DefaultUntrackedFiles(GitUntrackedFilesMode.Normal— git's own documented default, so nothing changes on a host that has not set the variable) initialises the field, and the flag is emitted unconditionally.WithUntrackedFiles(...)replaces it. The vector never carries two--untracked-filesvalues, so a caller passingNodoes not first pay for anormalwalk it declined.IGitStatusBuilder.WithUntrackedFilesand the class summary record the pinned default; CLAUDE.md gets design point 14 beside the other non-obvious, load-bearing choices.GitStatusParser's doc comment is left alone: it names the output format it reads, which this flag does not change.Tests
Builder tier (
GitIntegration.Test/Builders/GitStatusBuilderTests.cs):BuildsTheDefaultStatusVector--untracked-files=normalis in the vector withWithUntrackedFilesnever calledChoosingAModeReplacesThePinnedDefaultRatherThanJoiningIt--untracked-files=argument, and it is the caller's; the hazard an unconditional default introducesIntegration tier (
GitIntegration.Test/Integration/GitRoundTripTests.cs), which is where a config-resolution bug can actually be caught:StatusReportsUntrackedWorkEvenWhereTheHostHidesItAsync— setsstatus.showUntrackedFiles = noin the throwaway repository's own config (the same variable resolved from the nearest scope, so the test neither depends on nor disturbs the runner's global config), then assertsStatus()still reportsIsClean == falseand an entry for the untracked file. It closes by assertingWithUntrackedFiles(No)still reports clean, so the test cannot pass by the builder simply ignoring the caller.Verification
dotnet buildanddotnet build -c Release: 0 warnings, 0 errors.KTSU_GIT_INTEGRATION_TESTS_REQUIRED=1 GIT_CONFIG_NOSYSTEM=1 dotnet test: 590 passed, 0 failed, 0 skipped — integration tier exercised rather than skipped, andGIT_CONFIG_NOSYSTEM=1set per CLAUDE.md so the POSIX path is what ran.if (_untrackedFiles is GitUntrackedFilesMode mode)guard fails exactlyBuildsTheDefaultStatusVectorandStatusReportsUntrackedWorkEvenWhereTheHostHidesItAsync(2 failed / 588 passed).ChoosingAModeReplacesThePinnedDefaultRatherThanJoiningItpasses either way, as a guardrail on behaviour this change must not disturb. The tree was re-verified against a pre-mutation copy afterwards, and the full suite re-run green.🤖 Generated with Claude Code
https://claude.ai/code/session_01WVrdn7dcoGasLJEgrnb67u
Generated by Claude Code