Netscoot.Shared/Common/Git.ps1

function Get-RepositoryRoot {
    # Walk up from $StartPath looking for a .git dir/file; fall back to the start path.
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$StartPath)
    $dir = Get-Item -LiteralPath $StartPath
    if (-not $dir.PSIsContainer) { $dir = $dir.Directory }
    while ($null -ne $dir) {
        if (Test-Path (Join-Path $dir.FullName '.git')) { return $dir.FullName }
        $dir = $dir.Parent
    }
    return (Get-Item -LiteralPath $StartPath).FullName
}

function Get-NestedWorktreePath {
    # Absolute paths of git worktrees that live strictly inside $Root - linked worktrees (e.g.
    # under .claude/worktrees/<id>/) hold duplicate copies of the repository's solutions/projects and
    # would poison a recursive scan (double-counted membership, etc.). Callers exclude these.
    # Empty when git is unavailable, $Root is not in a repository, or nothing nests under it.
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Root)
    if (-not (Test-GitAvailable)) { return @() }
    $rootFull = Resolve-FullPath $Root
    if (-not (Test-Path -LiteralPath $rootFull)) { return @() }
    $lines = $null
    Push-Location $rootFull
    try { $lines = & git worktree list --porcelain 2>$null; $ok = ($LASTEXITCODE -eq 0) }
    catch { $ok = $false }
    finally { Pop-Location }
    if (-not $ok) { return @() }
    $nested = @()
    foreach ($l in $lines) {
        if ($l -match '^worktree\s+(.+)$') {
            $wt = Resolve-FullPath ($Matches[1].Trim())
            if (Test-PathUnder -Path $wt -Dir $rootFull) { $nested += $wt }   # strictly under root only
        }
    }
    return $nested
}

function Move-PathTracked {
    # Move one path: git mv when tracked (preserves history), else Move-Item. Creates the
    # destination parent if needed. Shared by every move cmdlet's filesystem step.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][bool]$UseGit,
        [Parameter(Mandatory)][string]$Source,
        [Parameter(Mandatory)][string]$Destination,
        [Parameter(Mandatory)][string]$RepositoryRoot
    )
    $parent = Split-Path -Parent $Destination
    if ($parent -and -not (Test-Path -LiteralPath $parent)) {
        New-Item -ItemType Directory -Path $parent -Force | Out-Null
    }
    if ($UseGit -and (Test-GitTracked -Path $Source)) {
        Push-Location $RepositoryRoot
        try { & git mv -- $Source $Destination; if ($LASTEXITCODE -ne 0) { throw "git mv failed: $Source -> $Destination" } }
        finally { Pop-Location }
    } else {
        # No -Force on purpose: a plain Move-Item refuses an existing destination instead of
        # clobbering it. With Resolve-MoveTarget already rejecting an existing target, this keeps the
        # non-git path non-destructive even if a tampered journal sets Force. Do NOT add -Force here.
        Move-Item -LiteralPath $Source -Destination $Destination
    }
}

function Test-GitTracked {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Path)
    $dir = Split-Path -Parent $Path
    try {
        Push-Location $dir
        & git ls-files --error-unmatch -- $Path *> $null
        return ($LASTEXITCODE -eq 0)
    } catch { return $false }
    finally { Pop-Location }
}