File: Git\GitWorktreeTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.Git;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Utils;
using Aspire.Hosting.Utils;
 
namespace Aspire.Cli.Tests.Git;
 
public class GitWorktreeTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public void TryGetLinkedWorktreeRoot_PrimaryCheckout_ReturnsNull()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, ".git"));
        var appHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost", "AppHost.csproj");
 
        Assert.Null(GitWorktree.TryGetLinkedWorktreeRoot(appHostPath));
        Assert.Null(GitWorktree.TryGetLinkedWorktreeRoot(workspace.WorkspaceRoot.FullName));
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_StandardCommonGitDirectory_ReturnsWorktreeRoot()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var fixtureRoot = workspace.WorkspaceRoot.FullName;
        var worktreeRoot = Directory.CreateDirectory(Path.Combine(fixtureRoot, "worktree")).FullName;
        TestGitWorktree.WriteLinkedWorktreeMetadata(
            worktreeRoot,
            Path.Combine(fixtureRoot, "primary", ".git"));
        var appHostPath = Path.Combine(worktreeRoot, "AppHost", "AppHost.csproj");
 
        var linkedRoot = GitWorktree.TryGetLinkedWorktreeRoot(appHostPath);
 
        Assert.NotNull(linkedRoot);
        Assert.Equal(PathNormalizer.ResolveSymlinks(worktreeRoot), linkedRoot);
    }
 
    [Theory]
    [InlineData("repo.git")]
    [InlineData("separate-git")]
    public void TryGetLinkedWorktreeRoot_NonDotGitCommonDirectory_ReturnsWorktreeRoot(string commonGitDirectoryName)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var fixtureRoot = workspace.WorkspaceRoot.FullName;
        var worktreeRoot = Directory.CreateDirectory(Path.Combine(fixtureRoot, "worktree")).FullName;
        TestGitWorktree.WriteLinkedWorktreeMetadata(
            worktreeRoot,
            Path.Combine(fixtureRoot, commonGitDirectoryName));
 
        var linkedRoot = GitWorktree.TryGetLinkedWorktreeRoot(worktreeRoot);
 
        Assert.NotNull(linkedRoot);
        Assert.Equal(PathNormalizer.ResolveSymlinks(worktreeRoot), linkedRoot);
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_RelativeGitDir_ReturnsWorktreeRoot()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var worktreeRoot = workspace.WorkspaceRoot.FullName;
        TestGitWorktree.WriteLinkedWorktreeMetadata(
            worktreeRoot,
            Path.Combine(worktreeRoot, "primary", ".git"),
            useRelativePaths: true);
 
        var linkedRoot = GitWorktree.TryGetLinkedWorktreeRoot(worktreeRoot);
 
        Assert.NotNull(linkedRoot);
        Assert.Equal(PathNormalizer.ResolveSymlinks(worktreeRoot), linkedRoot);
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_CheckoutAliasWithRelativeMetadata_ReturnsCanonicalWorktreeRoot()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var fixtureRoot = workspace.WorkspaceRoot.FullName;
        var worktreeRoot = Directory.CreateDirectory(
            Path.Combine(fixtureRoot, "physical", "checkouts", "feature")).FullName;
        TestGitWorktree.WriteLinkedWorktreeMetadata(
            worktreeRoot,
            Path.Combine(fixtureRoot, "physical", "primary", ".git"),
            useRelativePaths: true);
        var aliasRoot = Path.Combine(fixtureRoot, "aliases", "feature");
        Directory.CreateDirectory(Path.GetDirectoryName(aliasRoot)!);
 
        try
        {
            try
            {
                ReparsePoint.CreateOrReplace(aliasRoot, worktreeRoot);
            }
            catch (UnauthorizedAccessException ex)
            {
                Assert.Skip($"Cannot create a directory symlink or junction in this environment: {ex.Message}");
            }
            catch (IOException ex)
            {
                Assert.Skip($"Directory symlink or junction creation failed in this environment: {ex.Message}");
            }
 
            var linkedRoot = GitWorktree.TryGetLinkedWorktreeRoot(aliasRoot);
 
            Assert.NotNull(linkedRoot);
            Assert.Equal(PathNormalizer.ResolveSymlinks(worktreeRoot), linkedRoot);
        }
        finally
        {
            ReparsePoint.RemoveIfExists(aliasRoot);
        }
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_CaseVariantCheckoutPath_ReturnsFilesystemCanonicalCasing()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var fixtureRoot = workspace.WorkspaceRoot.FullName;
        var worktreeRoot = Directory.CreateDirectory(Path.Combine(fixtureRoot, "worktree")).FullName;
        TestGitWorktree.WriteLinkedWorktreeMetadata(
            worktreeRoot,
            Path.Combine(fixtureRoot, "primary", ".git"));
        var caseVariantRoot = Path.Combine(fixtureRoot, "WORKTREE");
        if (!Directory.Exists(caseVariantRoot))
        {
            Assert.Skip("The test requires a case-insensitive filesystem.");
        }
 
        var linkedRoot = GitWorktree.TryGetLinkedWorktreeRoot(caseVariantRoot);
 
        Assert.Equal(PathNormalizer.ResolveSymlinks(worktreeRoot), linkedRoot);
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_AdminAliasWithRelativeBackPointer_ReturnsCanonicalWorktreeRoot()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var fixtureRoot = workspace.WorkspaceRoot.FullName;
        var worktreeRoot = Directory.CreateDirectory(Path.Combine(fixtureRoot, "checkout")).FullName;
        var adminDirectory = Directory.CreateDirectory(
            Path.Combine(fixtureRoot, "physical", "primary", ".git", "worktrees", "feature")).FullName;
        var aliasDirectory = Path.Combine(fixtureRoot, "aliases", "admin");
        Directory.CreateDirectory(Path.GetDirectoryName(aliasDirectory)!);
 
        try
        {
            try
            {
                ReparsePoint.CreateOrReplace(aliasDirectory, adminDirectory);
            }
            catch (UnauthorizedAccessException ex)
            {
                Assert.Skip($"Cannot create a directory symlink or junction in this environment: {ex.Message}");
            }
            catch (IOException ex)
            {
                Assert.Skip($"Directory symlink or junction creation failed in this environment: {ex.Message}");
            }
 
            var gitFilePath = TestGitWorktree.WriteGitDirFile(worktreeRoot, aliasDirectory);
            File.WriteAllText(
                Path.Combine(adminDirectory, "gitdir"),
                Path.GetRelativePath(adminDirectory, gitFilePath) + Environment.NewLine);
 
            var linkedRoot = GitWorktree.TryGetLinkedWorktreeRoot(worktreeRoot);
 
            Assert.NotNull(linkedRoot);
            Assert.Equal(PathNormalizer.ResolveSymlinks(worktreeRoot), linkedRoot);
        }
        finally
        {
            ReparsePoint.RemoveIfExists(aliasDirectory);
        }
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_GitDirWithTrailingDirectorySeparator_ReturnsWorktreeRoot()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var worktreeRoot = workspace.WorkspaceRoot.FullName;
        var adminDirectory = TestGitWorktree.WriteLinkedWorktreeMetadata(
            worktreeRoot,
            Path.Combine(worktreeRoot, "primary", ".git"));
        File.WriteAllText(
            Path.Combine(worktreeRoot, ".git"),
            $"gitdir: {adminDirectory}{Path.DirectorySeparatorChar}{Environment.NewLine}");
 
        var linkedRoot = GitWorktree.TryGetLinkedWorktreeRoot(worktreeRoot);
 
        Assert.NotNull(linkedRoot);
        Assert.Equal(PathNormalizer.ResolveSymlinks(worktreeRoot), linkedRoot);
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_UppercaseWorktreesAdminDirectory_UsesPlatformCasing()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var fixtureRoot = workspace.WorkspaceRoot.FullName;
        var worktreeRoot = Directory.CreateDirectory(Path.Combine(fixtureRoot, "worktree")).FullName;
        var adminDirectory = Path.Combine(fixtureRoot, "primary", ".git", "WORKTREES", "feature");
        var gitFilePath = TestGitWorktree.WriteGitDirFile(worktreeRoot, adminDirectory);
        File.WriteAllText(
            Path.Combine(adminDirectory, "gitdir"),
            gitFilePath + Environment.NewLine);
 
        var linkedRoot = GitWorktree.TryGetLinkedWorktreeRoot(worktreeRoot);
        var expectedRoot = OperatingSystem.IsWindows()
            ? PathNormalizer.ResolveSymlinks(worktreeRoot)
            : null;
 
        Assert.Equal(expectedRoot, linkedRoot);
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_CaseVariantBackPointer_UsesFilesystemIdentity()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var worktreeRoot = workspace.WorkspaceRoot.FullName;
        var adminDirectory = TestGitWorktree.WriteLinkedWorktreeMetadata(
            worktreeRoot,
            Path.Combine(worktreeRoot, "primary", ".git"));
        var caseVariantGitFile = Path.Combine(worktreeRoot, ".GIT");
        File.WriteAllText(
            Path.Combine(adminDirectory, "gitdir"),
            caseVariantGitFile + Environment.NewLine);
 
        var linkedRoot = GitWorktree.TryGetLinkedWorktreeRoot(worktreeRoot);
        var expectedRoot = File.Exists(caseVariantGitFile)
            ? PathNormalizer.ResolveSymlinks(worktreeRoot)
            : null;
 
        Assert.Equal(expectedRoot, linkedRoot);
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_SymlinkAliasBackPointer_ReturnsWorktreeRoot()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var fixtureRoot = workspace.WorkspaceRoot.FullName;
        var worktreeRoot = Directory.CreateDirectory(Path.Combine(fixtureRoot, "worktree")).FullName;
        var adminDirectory = TestGitWorktree.WriteLinkedWorktreeMetadata(
            worktreeRoot,
            Path.Combine(fixtureRoot, "primary", ".git"));
        var aliasRoot = Path.Combine(fixtureRoot, "worktree-alias");
 
        try
        {
            try
            {
                ReparsePoint.CreateOrReplace(aliasRoot, worktreeRoot);
            }
            catch (UnauthorizedAccessException ex)
            {
                Assert.Skip($"Cannot create a directory symlink or junction in this environment: {ex.Message}");
            }
            catch (IOException ex)
            {
                Assert.Skip($"Directory symlink or junction creation failed in this environment: {ex.Message}");
            }
 
            File.WriteAllText(
                Path.Combine(adminDirectory, "gitdir"),
                Path.Combine(aliasRoot, ".git") + Environment.NewLine);
 
            var linkedRoot = GitWorktree.TryGetLinkedWorktreeRoot(worktreeRoot);
 
            Assert.NotNull(linkedRoot);
            Assert.Equal(PathNormalizer.ResolveSymlinks(worktreeRoot), linkedRoot);
        }
        finally
        {
            ReparsePoint.RemoveIfExists(aliasRoot);
        }
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_SubmoduleInsideLinkedWorktree_ReturnsNull()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var fixtureRoot = workspace.WorkspaceRoot.FullName;
        var worktreeRoot = Directory.CreateDirectory(Path.Combine(fixtureRoot, "worktree")).FullName;
        var adminDirectory = TestGitWorktree.WriteLinkedWorktreeMetadata(
            worktreeRoot,
            Path.Combine(fixtureRoot, "primary", ".git"));
        var submoduleRoot = Directory.CreateDirectory(Path.Combine(worktreeRoot, "extern", "dep")).FullName;
        TestGitWorktree.WriteGitDirFile(
            submoduleRoot,
            Path.Combine(adminDirectory, "modules", "dep"));
 
        Assert.Null(GitWorktree.TryGetLinkedWorktreeRoot(Path.Combine(submoduleRoot, "AppHost.csproj")));
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_ReciprocalBackPointerOutsideWorktrees_ReturnsNull()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var fixtureRoot = workspace.WorkspaceRoot.FullName;
        var worktreeRoot = Directory.CreateDirectory(Path.Combine(fixtureRoot, "worktree")).FullName;
        var adminDirectory = Path.Combine(fixtureRoot, "primary", ".git", "modules", "dep");
        var gitFilePath = TestGitWorktree.WriteGitDirFile(worktreeRoot, adminDirectory);
        File.WriteAllText(
            Path.Combine(adminDirectory, "gitdir"),
            gitFilePath + Environment.NewLine);
 
        Assert.Null(GitWorktree.TryGetLinkedWorktreeRoot(worktreeRoot));
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_BackPointerToDifferentCheckout_ReturnsNull()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var fixtureRoot = workspace.WorkspaceRoot.FullName;
        var commonGitDirectory = Path.Combine(fixtureRoot, "primary", ".git");
        var worktreeRoot = Directory.CreateDirectory(Path.Combine(fixtureRoot, "worktree")).FullName;
        var adminDirectory = TestGitWorktree.WriteLinkedWorktreeMetadata(worktreeRoot, commonGitDirectory);
        var otherWorktreeRoot = Directory.CreateDirectory(Path.Combine(fixtureRoot, "other-worktree")).FullName;
        var otherGitFile = TestGitWorktree.WriteGitDirFile(
            otherWorktreeRoot,
            Path.Combine(commonGitDirectory, "worktrees", "other"));
        File.WriteAllText(
            Path.Combine(adminDirectory, "gitdir"),
            otherGitFile + Environment.NewLine);
 
        Assert.Null(GitWorktree.TryGetLinkedWorktreeRoot(worktreeRoot));
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_DecoyWithoutBackPointer_ReturnsNull()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var fixtureRoot = workspace.WorkspaceRoot.FullName;
        var worktreeRoot = Directory.CreateDirectory(Path.Combine(fixtureRoot, "worktree")).FullName;
        TestGitWorktree.WriteGitDirFile(
            worktreeRoot,
            Path.Combine(fixtureRoot, "primary", ".git", "worktrees", "stale"));
 
        Assert.Null(GitWorktree.TryGetLinkedWorktreeRoot(worktreeRoot));
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_SubmoduleGitFile_ReturnsNull()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, ".git"));
        var submoduleRoot = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "extern", "dep")).FullName;
        TestGitWorktree.WriteGitDirFile(
            submoduleRoot,
            Path.Combine(workspace.WorkspaceRoot.FullName, ".git", "modules", "dep"));
        var appHostPath = Path.Combine(submoduleRoot, "AppHost.csproj");
 
        Assert.Null(GitWorktree.TryGetLinkedWorktreeRoot(appHostPath));
    }
 
    [Fact]
    public void TryGetLinkedWorktreeRoot_NotAGitRepo_ReturnsNull()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        Assert.Null(GitWorktree.TryGetLinkedWorktreeRoot(workspace.WorkspaceRoot.FullName));
        Assert.Null(GitWorktree.TryGetLinkedWorktreeRoot(null));
        Assert.Null(GitWorktree.TryGetLinkedWorktreeRoot(""));
    }
 
    [Fact]
    public void IsSameWorktreeScope_NestedLinkedWorktree_IsOutOfScopeOfPrimary()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var primaryRoot = workspace.WorkspaceRoot.FullName;
        Directory.CreateDirectory(Path.Combine(primaryRoot, ".git"));
        var worktreeRoot = Directory.CreateDirectory(Path.Combine(primaryRoot, ".worktrees", "feature")).FullName;
        TestGitWorktree.WriteLinkedWorktreeMetadata(worktreeRoot, Path.Combine(primaryRoot, ".git"));
        var nestedAppHost = Path.Combine(worktreeRoot, "AppHost.csproj");
        var primaryAppHost = Path.Combine(primaryRoot, "AppHost.csproj");
 
        Assert.False(GitWorktree.IsSameWorktreeScope(nestedAppHost, primaryRoot));
        Assert.True(GitWorktree.IsSameWorktreeScope(primaryAppHost, primaryRoot));
        Assert.True(GitWorktree.IsSameWorktreeScope(nestedAppHost, worktreeRoot));
        Assert.False(GitWorktree.IsSameWorktreeScope(primaryAppHost, worktreeRoot));
    }
 
    [Fact]
    public void IsSameWorktreeScope_SubmoduleInsideLinkedWorktree_UsesEnclosingWorktree()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var primaryRoot = workspace.WorkspaceRoot.FullName;
        Directory.CreateDirectory(Path.Combine(primaryRoot, ".git"));
        var worktreeRoot = Directory.CreateDirectory(Path.Combine(primaryRoot, ".worktrees", "feature")).FullName;
        var adminDirectory = TestGitWorktree.WriteLinkedWorktreeMetadata(worktreeRoot, Path.Combine(primaryRoot, ".git"));
        var submoduleRoot = Directory.CreateDirectory(Path.Combine(worktreeRoot, "extern", "dep")).FullName;
        TestGitWorktree.WriteGitDirFile(submoduleRoot, Path.Combine(adminDirectory, "modules", "dep"));
        var submoduleAppHost = Path.Combine(submoduleRoot, "AppHost.csproj");
 
        Assert.Null(GitWorktree.TryGetLinkedWorktreeRoot(submoduleAppHost));
        Assert.True(GitWorktree.IsSameWorktreeScope(submoduleAppHost, worktreeRoot));
        Assert.False(GitWorktree.IsSameWorktreeScope(submoduleAppHost, primaryRoot));
    }
 
    [Fact]
    public void IsSameWorktreeScope_Submodule_RemainsInScopeOfPrimary()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var primaryRoot = workspace.WorkspaceRoot.FullName;
        Directory.CreateDirectory(Path.Combine(primaryRoot, ".git"));
        var submoduleRoot = Directory.CreateDirectory(Path.Combine(primaryRoot, "extern", "dep")).FullName;
        TestGitWorktree.WriteGitDirFile(
            submoduleRoot,
            Path.Combine(primaryRoot, ".git", "modules", "dep"));
        var submoduleAppHost = Path.Combine(submoduleRoot, "AppHost.csproj");
 
        Assert.True(GitWorktree.IsSameWorktreeScope(submoduleAppHost, primaryRoot));
    }
 
    [Fact]
    public void IsSameWorktreeScope_AppHostNestedInSamePrimaryCheckout_IsSameScope()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var primaryRoot = CreatePrimaryCheckout(workspace.WorkspaceRoot, "repoA");
        var appHostPath = Path.Combine(primaryRoot, "sub", "App", "App.csproj");
 
        Assert.True(GitWorktree.IsSameWorktreeScope(appHostPath, primaryRoot));
    }
 
    [Fact]
    public void IsSameWorktreeScope_AppHostInDifferentPrimaryCheckout_IsDifferentScope()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var repoARoot = CreatePrimaryCheckout(workspace.WorkspaceRoot, "repoA");
        var repoBRoot = CreatePrimaryCheckout(workspace.WorkspaceRoot, "repoB");
        var repoBAppHost = Path.Combine(repoBRoot, "App", "App.csproj");
 
        Assert.False(GitWorktree.IsSameWorktreeScope(repoBAppHost, repoARoot));
        Assert.False(GitWorktree.IsSameWorktreeScope(Path.Combine(repoARoot, "App", "App.csproj"), repoBRoot));
    }
 
    [Fact]
    public void IsSameWorktreeScope_SubmodulesInDifferentPrimaryCheckouts_AreDifferentScopes()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var repoARoot = CreatePrimaryCheckout(workspace.WorkspaceRoot, "repoA");
        var repoBRoot = CreatePrimaryCheckout(workspace.WorkspaceRoot, "repoB");
        var repoBSubmoduleRoot = Directory.CreateDirectory(Path.Combine(repoBRoot, "extern", "dep")).FullName;
        TestGitWorktree.WriteGitDirFile(
            repoBSubmoduleRoot,
            Path.Combine(repoBRoot, ".git", "modules", "dep"));
        var repoBSubmoduleAppHost = Path.Combine(repoBSubmoduleRoot, "AppHost.csproj");
 
        Assert.True(GitWorktree.IsSameWorktreeScope(repoBSubmoduleAppHost, repoBRoot));
        Assert.False(GitWorktree.IsSameWorktreeScope(repoBSubmoduleAppHost, repoARoot));
    }
 
    [Fact]
    public void IsSameWorktreeScope_PrimaryCheckoutAndNestedLinkedWorktree_AreDifferentScopes()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var primaryRoot = CreatePrimaryCheckout(workspace.WorkspaceRoot, "repoA");
        var worktreeRoot = Directory.CreateDirectory(Path.Combine(primaryRoot, ".worktrees", "feature")).FullName;
        TestGitWorktree.WriteLinkedWorktreeMetadata(worktreeRoot, Path.Combine(primaryRoot, ".git"));
 
        Assert.False(GitWorktree.IsSameWorktreeScope(Path.Combine(worktreeRoot, "App", "App.csproj"), primaryRoot));
        Assert.False(GitWorktree.IsSameWorktreeScope(Path.Combine(primaryRoot, "App", "App.csproj"), worktreeRoot));
    }
 
    [Fact]
    public void IsSameWorktreeScope_BothOutsideAnyGitRepository_IsSameScope()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var workingDirectory = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "plain")).FullName;
        var appHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "other", "App.csproj");
 
        Assert.True(GitWorktree.IsSameWorktreeScope(appHostPath, workingDirectory));
    }
 
    [Fact]
    public void IsSameWorktreeScope_AppHostOutsideAnyGitRepository_IsDifferentScopeFromCheckout()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var primaryRoot = CreatePrimaryCheckout(workspace.WorkspaceRoot, "repoA");
        var unmanagedDirectory = Directory.CreateDirectory(Path.Combine(workspace.WorkspaceRoot.FullName, "outside")).FullName;
        var unmanagedAppHost = Path.Combine(unmanagedDirectory, "App.csproj");
 
        Assert.False(GitWorktree.IsSameWorktreeScope(unmanagedAppHost, primaryRoot));
        Assert.False(GitWorktree.IsSameWorktreeScope(Path.Combine(primaryRoot, "App", "App.csproj"), unmanagedDirectory));
    }
 
    /// <summary>
    /// Creates a primary checkout (a real <c>.git</c> directory) under <paramref name="parent"/> so
    /// scope comparisons are deterministic regardless of whether the test host's temp directory
    /// happens to sit inside a git repository.
    /// </summary>
    private static string CreatePrimaryCheckout(DirectoryInfo parent, string name)
    {
        var root = Directory.CreateDirectory(Path.Combine(parent.FullName, name)).FullName;
        Directory.CreateDirectory(Path.Combine(root, ".git"));
 
        return root;
    }
}