Skip to content

Recognize linked worktree .git files in QuarantineTools - #19157

Closed
Adam Ratzman (adamint) wants to merge 19 commits into
microsoft:mainfrom
adamint:adamint/fix-quarantinetools-worktree-root
Closed

Recognize linked worktree .git files in QuarantineTools#19157
Adam Ratzman (adamint) wants to merge 19 commits into
microsoft:mainfrom
adamint:adamint/fix-quarantinetools-worktree-root

Conversation

@adamint

@adamint Adam Ratzman (adamint) commented Aug 7, 2026

Copy link
Copy Markdown
Member

Description

Resolve the repository root through bounded git rev-parse --show-toplevel instead of trusting an ancestor .git marker alone.

The command drains stdout and stderr concurrently, has a bounded timeout, and accepts the result only when its canonical path matches the nearest .git marker root. This supports normal checkouts and linked worktrees while refusing malformed or redirected metadata that points at another tree. Repository-root validation failures return exit code 4 rather than editing a different checkout.

Verification

  • Real git repository and linked-worktree root resolution.
  • Nested-directory invocation.
  • Missing or failed git command handling.
  • Wrong-tree refusal, including an outer-ancestor core.worktree redirection.
  • Focused root suite: 9 passing.
  • Full QuarantineTools suite: 50 passing.
  • Formatting and diff checks pass.

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes. It fails closed rather than editing a repository root that does not match the caller's nearest Git marker.
    • No

QuarantineTools located the repository root by walking up the directory tree
looking for a `.git` *directory*. In a linked git worktree `.git` is a regular
file holding a `gitdir:` pointer, so the walk stepped over the worktree root and
kept climbing. When the worktree was nested inside another checkout of the same
repository, the walk terminated on the outer checkout's real `.git` directory
and the tool rewrote test sources in the wrong tree.

The failure was silent: the edit succeeded and the tool reported success with a
repo-relative path, which looks identical to a correct run. Against a file with
uncommitted work the stray edit would merge into it unnoticed.

Ask `git rev-parse --show-toplevel` instead, and keep the directory walk as a
fallback for when git is unavailable - now matching `.git` as a file or a
directory so it stops at the worktree root too. Also refuse to run when the
resolved root is not the working directory or one of its ancestors, which turns
a silent wrong-tree write into an error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings August 7, 2026 20:55
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19157

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19157"

@github-actions github-actions Bot added the area-engineering-systems infrastructure helix infra engineering repo stuff label Aug 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Resolves QuarantineTools repository roots through Git, preventing edits to an outer checkout from nested worktrees.

Changes:

  • Adds Git-based root discovery with timeout and fallback.
  • Adds ancestor safety validation.
  • Adds linked-worktree regression tests.
Show a summary per file
File Description
tools/QuarantineTools/QuarantineTools.csproj Exposes internals to tests.
tools/QuarantineTools/Quarantine.cs Implements root discovery and validation.
tests/QuarantineTools.Tests/RepoRootTests.cs Tests worktree and path scenarios.
tests/QuarantineTools.Tests/QuarantineTools.Tests.csproj References the production tool.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread tools/QuarantineTools/Quarantine.cs Outdated
Comment thread tools/QuarantineTools/Quarantine.cs
Two fixes in the repo-root resolution added by this PR.

IsSameOrAncestorDirectory decided case sensitivity from the operating
system, which is not a reliable proxy for the volume: macOS APFS can be
formatted case-sensitive, Windows exposes a per-directory case-sensitivity
flag that WSL sets, and Linux can mount case-insensitive volumes. Folding
case on such a volume lets two genuinely different trees whose paths differ
only by case satisfy the guard, which is the one outcome it exists to
prevent. Comparing ordinally everywhere is not the answer either, because
the Windows current directory keeps whatever casing the process was given
while git canonicalizes --show-toplevel, so an ordinary run could then be
refused over a meaningless difference. Probe the volume instead.

TryGetGitTopLevelAsync linked the probe timeout to the caller's token and
then caught every OperationCanceledException, so Ctrl+C was reported as an
ordinary probe failure. FindRepoRootAsync then ran the fallback walk and
ExecuteAsync enumerated the whole tests tree before cancellation was next
observed. Re-throw caller cancellation; fall back only for the timeout.

Both are covered by tests that fail without the change: the case test
returns True instead of False, and the cancellation test throws nothing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 21:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (1)

tools/QuarantineTools/Quarantine.cs:517

  • The linked-worktree tests always have git available, so they return from the primary probe and never exercise this fallback. Reverting only File.Exists(gitPath) would therefore leave the suite green and reintroduce wrong-tree writes whenever git is unavailable. Add a regression test that forces the git probe to fail and verifies the nested worktree's .git file is selected instead of the outer checkout.
            if (Directory.Exists(gitPath) || File.Exists(gitPath))
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Follow-up to the repo-root fix, from review feedback:

- Return a distinct exit code (4) when refusing to edit a tree the caller is
  not standing in. 2 already means "tests folder not found", so reusing it made
  the two indistinguishable to a caller that branches on the exit code.

- Resolve symlinks before giving up on the ancestor check. A Windows junction or
  `subst` drive lets the caller's directory read as `D:\src\aspire` while git
  reports `C:/src/aspire`, and refusing that would block a legitimate run. This
  runs only on the failure path, so a gap in it can only rescue a run that was
  already being refused, never block one that was about to succeed. Resolution
  proceeds one hop at a time because a link's stored target may be relative and
  may itself route through another link, and is depth-capped for cycles.

- Extract the marker walk as FindRepoRootByMarker and test it directly. The git
  probe answers first, so the `.git`-as-a-file branch - the line the bug lived
  on - was not otherwise reachable from a test.

- Assert the refusal message names both the resolved root and the working
  directory, so a later reword cannot quietly drop one.

- Skip the git-dependent tests when git is absent instead of hard-failing, and
  make test cleanup non-throwing so a mid-setup failure cannot mask the real
  assertion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 21:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (1)

tools/QuarantineTools/Quarantine.cs:690

  • remainingDepth is decremented for every ordinary parent component, not only when following a link. A valid path with more than 40 components therefore exhausts the link budget before reaching a symlinked ancestor, leaves that ancestor unresolved, and makes the new wrong-tree guard reject a legitimate run. Keep the budget unchanged while walking parents; only decrement it when substituting a link target.
            ? Path.Combine(CanonicalizeCore(parent.FullName, remainingDepth - 1), info.Name)
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread tools/QuarantineTools/Quarantine.cs Outdated
The existing coverage pins the ExitCodeWrongTree constant, but a constant-pinning
test cannot fail when the `return` changes, so the exit-code contract was
effectively untested. This runs the built tool as a child process with a stale
GIT_DIR/GIT_WORK_TREE pointing at a second repository and asserts the exit code,
that the message names both paths, and that the other tree's file is byte
identical afterwards.

ExitCodeWrongTree becomes internal so the test binds to the contract instead of
repeating the literal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 22:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

tools/QuarantineTools/Quarantine.cs:775

  • This probe still accepts a wrong tree when a case-sensitive parent contains two checkouts named with inverse casing (for example, OUTER and outer). The flipped path exists because it is a distinct sibling, so this reports case-insensitive behavior; IsSameOrAncestorDirectory then approves OUTER as an ancestor of outer/tools and permits the cross-tree write. The new rejection test bypasses this probe by passing caseSensitive: true. Distinguish an alternate lookup of the same directory from a distinct sibling, or conservatively reject this ambiguous case.
        // A case-sensitive volume that happens to hold a real sibling differing only by case is read as
        // case-insensitive here. That is acceptable: it only restores the behavior this guard had
        // before the probe existed, and such a pair is not something a checkout layout produces.
        return !Directory.Exists(Path.Combine(parent, flipped));

tools/QuarantineTools/Quarantine.cs:691

  • remainingDepth is intended to cap symlink hops, but this branch decrements it for every ordinary parent component. From a path deeper than 40 segments, canonicalization can stop before reaching a symlink near the root, leaving equivalent real/link spellings different and incorrectly returning exit code 4. Keep the counter unchanged while walking non-link parents; only decrement it when resolving a link.
        return info.Parent is { } parent
            ? Path.Combine(CanonicalizeCore(parent.FullName, remainingDepth - 1), info.Name)
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 8, 2026 18:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (1)

tools/QuarantineTools/Quarantine.cs:602

  • Trim() removes valid whitespace from the repository path, not just git's line terminator. On Unix a checkout directory can end in a space, so this resolves a different path and the new guard incorrectly refuses a valid run. Remove only CR/LF terminators.
            var trimmed = standardOutputTask.Result.Trim();
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread tools/QuarantineTools/Quarantine.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 8, 2026 19:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

tools/QuarantineTools/Quarantine.cs:693

  • remainingDepth is decremented for every ordinary parent component, not only when a link is followed. A path deeper than 40 components therefore exhausts the link budget before reaching a symlink near the root, leaving the two spellings uncanonicalized and incorrectly refusing a valid run. Preserve the budget while walking ordinary parents; only decrement it when resolving a link.
        return info.Parent is { } parent
            ? Path.Combine(CanonicalizeCore(parent.FullName, remainingDepth - 1), info.Name)
            : Path.TrimEndingDirectorySeparator(info.FullName);

tools/QuarantineTools/Quarantine.cs:821

  • This new Windows-native case-sensitivity path is not exercised by CI: QuarantineTools.Tests.csproj explicitly disables both Windows and macOS runs, while the delegate-based test only verifies dispatch and the filesystem test runs on Linux. Enable platform runs for this project or move focused tests to platform-enabled projects so the P/Invoke behavior and macOS fallback/symlink behavior are covered.
        using var handle = CreateFile(
  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread tools/QuarantineTools/Quarantine.cs Outdated
Comment thread tools/QuarantineTools/Quarantine.cs
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread tools/QuarantineTools/Quarantine.cs Outdated
ResolveLinkTarget reports reparse points, so Canonicalize covers symlinks and junctions but
not DOS device mappings, which are per-session drive aliases. The comment listed `subst`
alongside junctions as a case it handles, which is wrong.

Records what is actually covered, why the gap is not currently reachable (a process under a
mapped drive sees the mapped spelling from getcwd and git reports the same spelling, so the
plain comparison already matches), and that the failure mode is a refusal to run rather than
a wrong-tree write.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 487b899d-d79d-4349-a84b-a026861bde7f
Copilot AI review requested due to automatic review settings August 9, 2026 23:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (1)

tests/QuarantineTools.Tests/RepoRootTests.cs:440

  • This summary claims the guard handles subst, but Program.Canonicalize explicitly does not resolve DOS device mappings (Quarantine.cs:663-669), and this test only creates POSIX symlinks. Limit the summary to the link aliases this regression actually covers so it does not imply nonexistent subst coverage.
    /// A junction or `subst` drive on Windows lets the caller's directory and git's answer name the same
    /// tree with different spellings. Refusing that would block a legitimate run, so the guard resolves
    /// links before it gives up. Symlink creation needs elevation on Windows, so this is POSIX-only.
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Copilot AI review requested due to automatic review settings August 10, 2026 07:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

tests/QuarantineTools.Tests/RepoRootTests.cs:441

  • This summary still implies that the canonicalization path handles subst, but Canonicalize only resolves symlinks/junctions and the production comment at tools/QuarantineTools/Quarantine.cs:663-669 explicitly documents subst as unsupported. This test is POSIX-only and exercises symlinks, so remove the subst claim to avoid documenting behavior that is neither implemented nor tested.
    /// A junction or `subst` drive on Windows lets the caller's directory and git's answer name the same
    /// tree with different spellings. Refusing that would block a legitimate run, so the guard resolves
    /// links before it gives up. Symlink creation needs elevation on Windows, so this is POSIX-only.

tests/QuarantineTools.Tests/RepoRootTests.cs:532

  • This expectation contradicts the PR's verification claim that stale GIT_DIR/GIT_WORK_TREE exercises the wrong-tree guard, exits with code 4, and names both paths. The implementation now strips those variables and this test correctly observes the ordinary missing-tests exit code 2 instead. Update the PR description to describe the current behavior; if an end-to-end exit-4 regression is still required, it needs a different setup.
        Assert.Equal(2, exitCode);
        Assert.Contains(Path.Combine(here, "tests"), standardError, StringComparison.Ordinal);
        Assert.Contains(here, standardError, StringComparison.Ordinal);
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0d1cf760-b1bc-46ba-a6d4-628354b00f2c
Copilot AI review requested due to automatic review settings August 10, 2026 14:56
@adamint Adam Ratzman (adamint) changed the title Resolve QuarantineTools repo root via git instead of a .git directory probe Recognize linked worktree .git files in QuarantineTools Aug 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (1)

tools/QuarantineTools/Quarantine.cs:484

  • The implementation in this hunk does not match the PR's stated fix: it still resolves the root solely by walking for a .git marker. There is no git rev-parse --show-toplevel probe, timeout/concurrent stream draining, ancestor safety check, or distinct exit code 4 anywhere in Quarantine.cs. As a result, the behavior and verification described in the PR—including the real git-backed worktree scenarios and wrong-tree refusal—are not present in the submitted changes. Please either restore the described implementation and tests or update the PR scope and description to reflect this narrower marker-file fix.
            var gitPath = Path.Combine(dir.FullName, ".git");
            if (Directory.Exists(gitPath) || File.Exists(gitPath))
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@adamint
Adam Ratzman (adamint) marked this pull request as ready for review August 11, 2026 03:17
@adamint
Adam Ratzman (adamint) marked this pull request as draft August 11, 2026 06:44
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3253974d-4f18-486f-863c-281a607656d4
Copilot AI review requested due to automatic review settings August 11, 2026 08:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

tools/QuarantineTools/Quarantine.cs:635

  • None of the new end-to-end tests sets GIT_DIR or GIT_WORK_TREE; the core.worktree tests exercise a different redirect path. Without a regression test, removing either environment cleanup reintroduces the outer-checkout write this guard prevents. Add a nested-worktree test that points both variables at the outer checkout and verifies only the nested file is changed.
        foreach (var name in s_gitRepositoryLocationEnvironmentVariables)
        {
            startInfo.Environment.Remove(name);

tools/QuarantineTools/Quarantine.cs:816

  • RepoRootTests only calls this method with identically-cased path segments, so this casing-dependent safety branch is never exercised. A regression here can treat case-distinct checkouts as the same directory and permit edits in the wrong tree. Add focused tests through the injected casing delegate for sensitive and insensitive parents, including the case-only-sibling and unknown-probe outcomes.
            if (!string.Equals(ancestorSegments[i], currentSegments[i], StringComparison.Ordinal)
                && (isCaseSensitiveDirectory(parent)
                    || !string.Equals(ancestorSegments[i], currentSegments[i], StringComparison.OrdinalIgnoreCase)))
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Adds a nested linked-worktree test that runs the tool with GIT_DIR and
GIT_WORK_TREE pointed at the outer repository. GIT_TRACE2 is used to assert
the tool does not forward those overrides to its own git invocations, which
is what previously made it resolve the outer root and rewrite the wrong
sources.

Recovered from an uncommitted working tree after the authoring session was
killed by a CLI out-of-memory crash on 2026-08-11.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f1bd51d5-cb9f-4370-a07b-c94416baad61
Copilot AI review requested due to automatic review settings August 11, 2026 20:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

tools/QuarantineTools/Quarantine.cs:816

  • The case-sensitive wrong-tree guard has no regression coverage in RepoRootTests: the current ancestry theory uses only lowercase paths, and no test calls the injected casing overloads or child-probe seam. A regression that treats a case-sensitive parent or distinct case-flipped siblings as insensitive would therefore let a wrong checkout through without failing the suite. Restore focused tests for per-parent casing rules, the Windows metadata/fail-closed branches, and the child probe's distinct-sibling and unknown outcomes.
            if (!string.Equals(ancestorSegments[i], currentSegments[i], StringComparison.Ordinal)
                && (isCaseSensitiveDirectory(parent)
                    || !string.Equals(ancestorSegments[i], currentSegments[i], StringComparison.OrdinalIgnoreCase)))

tools/QuarantineTools/Quarantine.cs:579

  • The caller-cancellation test uses an already-canceled token, so it never exercises this internal timeout or the process-termination path. Removing CancelAfter, failing to kill the child, or incorrectly skipping marker fallback would still leave the suite green despite the PR's bounded-probe guarantee. Add a fake git process that blocks, use an injectable short timeout, and assert that the probe terminates it and falls back within the bound.

This issue also appears on line 814 of the same file.

            using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            timeoutSource.CancelAfter(s_gitProbeTimeout);
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-engineering-systems infrastructure helix infra engineering repo stuff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants