git-helpers.ps1

function global:Register-GitWorktreeProvider {
    [CmdletBinding()]
    param()

    $providerName = 'WtProvider'
    $providerLoaded = $null -ne (Get-PSProvider -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $providerName })

    if (-not $providerLoaded) {
        function Test-WtProviderAssemblyCompatibility {
            param(
                [Parameter(Mandatory = $true)]
                [string]$AssemblyPath
            )

            try {
                $null = [System.Reflection.AssemblyName]::GetAssemblyName($AssemblyPath)
                return $true
            }
            catch {
                return $false
            }
        }

        $moduleRoot = Split-Path -Path $PSScriptRoot -Parent
        $providerProjectDir = Join-Path -Path $moduleRoot -ChildPath 'PathUtils.WtProvider'
        $providerProjectPath = Join-Path -Path $providerProjectDir -ChildPath 'PathUtils.WtProvider.csproj'
        $providerSourcePath = Join-Path -Path $providerProjectDir -ChildPath 'WtProvider.cs'
        $providerLibDir = Join-Path -Path $PSScriptRoot -ChildPath 'lib'
        $packagedAssemblyPath = Join-Path -Path $providerLibDir -ChildPath 'PathUtils.WtProvider.dll'
        $sessionStamp = "pwsh-$($PSVersionTable.PSVersion)-pid-$PID"
        $providerOutputDir = Join-Path -Path $providerProjectDir -ChildPath (Join-Path -Path 'bin\sessions' -ChildPath $sessionStamp)
        $providerAssemblyPath = $packagedAssemblyPath

        if ((Test-Path -LiteralPath $packagedAssemblyPath) -and (Test-WtProviderAssemblyCompatibility -AssemblyPath $packagedAssemblyPath)) {
            $providerAssemblyPath = $packagedAssemblyPath
        }
        else {
            $providerAssemblyPath = $null
        }

        if (($null -eq $providerAssemblyPath) -and (Test-Path -LiteralPath $providerProjectPath)) {
            $shouldBuild = $true
            $providerAssemblyPath = Join-Path -Path $providerOutputDir -ChildPath 'PathUtils.WtProvider.dll'
            if ((Test-Path -LiteralPath $providerAssemblyPath) -and (Test-Path -LiteralPath $providerSourcePath)) {
                $sourceInfo = Get-Item -LiteralPath $providerSourcePath -ErrorAction Stop
                $assemblyInfo = Get-Item -LiteralPath $providerAssemblyPath -ErrorAction Stop
                $shouldBuild = $sourceInfo.LastWriteTimeUtc -gt $assemblyInfo.LastWriteTimeUtc

                if (-not $shouldBuild) {
                    if (-not (Test-WtProviderAssemblyCompatibility -AssemblyPath $providerAssemblyPath)) {
                        $shouldBuild = $true
                    }
                }
            }

            if ($shouldBuild) {
                $dotnetCmd = Get-Command dotnet -ErrorAction SilentlyContinue
                if ($null -eq $dotnetCmd) {
                    throw "dotnet SDK not found. Install .NET SDK or keep a prebuilt provider assembly."
                }

                & $dotnetCmd.Source build $providerProjectPath -c Release -nologo -o $providerOutputDir "-p:PowerShellHome=$PSHOME"
                if ($LASTEXITCODE -ne 0) {
                    throw "Failed to build provider project: $providerProjectPath"
                }
            }
        }
        elseif ($null -eq $providerAssemblyPath) {
            if (-not (Test-Path -LiteralPath $providerSourcePath)) {
                throw "Provider assembly not found at '$packagedAssemblyPath' and provider source file not found: $providerSourcePath"
            }

            $providerAssemblyPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath 'PathUtils.WtProvider.dll'
            $assemblyExists = Test-Path -LiteralPath $providerAssemblyPath
            if ($assemblyExists) {
                $sourceInfo = Get-Item -LiteralPath $providerSourcePath -ErrorAction Stop
                $assemblyInfo = Get-Item -LiteralPath $providerAssemblyPath -ErrorAction Stop
                $shouldBuild = $sourceInfo.LastWriteTimeUtc -gt $assemblyInfo.LastWriteTimeUtc
            }
            else {
                $shouldBuild = $true
            }

            if ($shouldBuild) {
                if (Test-Path -LiteralPath $providerAssemblyPath) {
                    Remove-Item -LiteralPath $providerAssemblyPath -Force
                }
                Add-Type -Path $providerSourcePath -OutputAssembly $providerAssemblyPath -ErrorAction Stop
            }
        }

        if ([string]::IsNullOrWhiteSpace($providerAssemblyPath) -or (-not (Test-Path -LiteralPath $providerAssemblyPath))) {
            throw "Provider assembly not found. Expected packaged assembly at '$packagedAssemblyPath' or build output at '$providerOutputDir'."
        }

        Import-Module -Name $providerAssemblyPath -Force -ErrorAction Stop
        $providerLoaded = $null -ne (Get-PSProvider -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $providerName })
        if (-not $providerLoaded) {
            throw "Failed to load provider module: $providerName"
        }
    }

    if (-not (Get-PSDrive -Name wt -ErrorAction SilentlyContinue)) {
        New-PSDrive -Name wt -PSProvider $providerName -Root '\' -Scope Global | Out-Null
    }
}

function global:Get-GitWorktreeRelativePath {
    [CmdletBinding()]
    param(
        [string]$BaseWorktreePath
    )

    $currentLocation = Get-Location

    if ($currentLocation.Provider.Name -eq 'WtProvider') {
        $providerPath = $currentLocation.ProviderPath.Trim('\', '/')
        $parts = if ($providerPath) { $providerPath -split '[\\/]' } else { @() }
        if ($parts.Count -gt 1) {
            return (($parts | Select-Object -Skip 1) -join [System.IO.Path]::DirectorySeparatorChar)
        }
        return $null
    }

    if ([string]::IsNullOrWhiteSpace($BaseWorktreePath)) {
        return $null
    }

    try {
        $baseResolved = (Resolve-Path -LiteralPath $BaseWorktreePath).Path
        $currentResolved = (Resolve-Path -LiteralPath $currentLocation.Path).Path
        if ($currentResolved -like "$baseResolved*") {
            $suffix = $currentResolved.Substring($baseResolved.Length).TrimStart('\', '/')
            if ($suffix) {
                return $suffix
            }
        }
    }
    catch {
        return $null
    }

    return $null
}

function global:Set-LocationEx {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true)]
        [string]$Path,

        [switch]$PassThru
    )

    if ($Path -match '^wt:[\\/]?(.*)$') {
        Register-GitWorktreeProvider

        $normalized = $Matches[1].Trim([char[]]@('\', '/'))
        $segments = if ($normalized) { $normalized -split '[\\/]' } else { @() }
        $explicitRelative = if ($segments.Count -gt 1) { ($segments | Select-Object -Skip 1) -join '\' } else { $null }

        $providerPath = if ([string]::IsNullOrWhiteSpace($normalized)) { 'wt:\' } else { "wt:\$normalized" }
        $providerItem = if ([string]::IsNullOrWhiteSpace($normalized)) {
            Get-ChildItem -LiteralPath 'wt:\' -ErrorAction SilentlyContinue | Where-Object { $_.IsMain } | Select-Object -First 1
        }
        else {
            Get-Item -LiteralPath $providerPath -ErrorAction SilentlyContinue
        }

        if ($null -eq $providerItem) {
            Write-Error "Worktree path not found: $providerPath"
            return
        }

        $targetRootPath = if ($providerItem.PSObject.Properties.Name -contains 'FullName') { $providerItem.FullName } else { $providerItem.Path }
        if ([string]::IsNullOrWhiteSpace($targetRootPath)) {
            Write-Error "Failed to resolve file system path for provider item: $providerPath"
            return
        }

        $targetPath = $targetRootPath
        if (-not $explicitRelative) {
            $relativeFromCurrent = Get-GitWorktreeRelativePath -BaseWorktreePath $targetRootPath
            if ($relativeFromCurrent) {
                $candidate = Join-Path -Path $targetRootPath -ChildPath $relativeFromCurrent
                if (Test-Path -LiteralPath $candidate) {
                    $targetPath = $candidate
                }
                else {
                    Write-Warning "Subdirectory not found in target worktree: $relativeFromCurrent"
                }
            }
        }

        Microsoft.PowerShell.Management\Set-Location -Path $targetPath -PassThru:$PassThru
        return
    }

    Microsoft.PowerShell.Management\Set-Location @PSBoundParameters
}

Register-ArgumentCompleter -CommandName Set-LocationEx, Set-Location, cd, Get-ChildItem, ls, dir -ParameterName Path -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    if ($wordToComplete -notlike 'wt:*') {
        return
    }

    Register-GitWorktreeProvider

    $prefix = if ($wordToComplete -match '^wt:[\\/]?(.*)') { $Matches[1] } else { '' }

    if (-not $prefix) {
        [System.Management.Automation.CompletionResult]::new(
            'wt:\',
            'wt:\',
            'ProviderContainer',
            'Main worktree'
        )
    }

    Get-ChildItem -LiteralPath 'wt:\' -ErrorAction SilentlyContinue | ForEach-Object {
        $name = $_.Name
        if ($name -like "$prefix*") {
            $completionText = "wt:\$name"
            $listText = $name
            $tooltip = if ($_.Branch) { "$($_.Branch) - $($_.CommitHash)" } else { $_.CommitHash }

            [System.Management.Automation.CompletionResult]::new(
                $completionText,
                $listText,
                'ProviderItem',
                $tooltip
            )
        }
    }
}

function script:Enable-WtCdAliasOverride {
    if (-not $script:WtCdAliasStateInitialized) {
        $existingCdAlias = Get-Alias -Name cd -Scope Global -ErrorAction SilentlyContinue
        $previousDefinition = if ($null -ne $existingCdAlias) { $existingCdAlias.Definition } else { $null }
        # Never persist our own override as the "original" alias target.
        $script:WtCdPreviousDefinition = if ($previousDefinition -eq 'Set-LocationEx') { 'Set-Location' } else { $previousDefinition }
        $script:WtCdAliasStateInitialized = $true
    }

    Set-Alias -Name cd -Value Set-LocationEx -Option AllScope -Scope Global
}

function script:Restore-WtCdAliasOverride {
    if (-not $script:WtCdAliasStateInitialized) {
        return
    }

    if ([string]::IsNullOrWhiteSpace($script:WtCdPreviousDefinition) -or $script:WtCdPreviousDefinition -eq 'Set-LocationEx') {
        Set-Alias -Name cd -Value Set-Location -Option AllScope -Scope Global
    }
    else {
        Set-Alias -Name cd -Value $script:WtCdPreviousDefinition -Option AllScope -Scope Global
    }

    $script:WtCdAliasStateInitialized = $false
    $script:WtCdPreviousDefinition = $null
}

function script:Register-WtOnRemoveHandler {
    if ($script:WtOnRemoveHandlerRegistered) {
        return
    }

    $module = $ExecutionContext.SessionState.Module
    if ($null -eq $module) {
        return
    }

    $previousOnRemove = $module.OnRemove
    $wtCdPreviousDefinition = $script:WtCdPreviousDefinition
    $module.OnRemove = {
        if ($null -ne $previousOnRemove) {
            & $previousOnRemove
        }

        $targetCdCommand = if ([string]::IsNullOrWhiteSpace($wtCdPreviousDefinition) -or $wtCdPreviousDefinition -eq 'Set-LocationEx') {
            'Set-Location'
        }
        else {
            $wtCdPreviousDefinition
        }

        Set-Alias -Name cd -Value $targetCdCommand -Option AllScope -Scope Global -ErrorAction SilentlyContinue
    }.GetNewClosure()

    $script:WtOnRemoveHandlerRegistered = $true
}

Register-GitWorktreeProvider
Enable-WtCdAliasOverride
Register-WtOnRemoveHandler