Public/WorktreeMaintenance.ps1

function Invoke-GitWorktreeMaintenance {
    <#
    .SYNOPSIS
    Run a git worktree maintenance command and capture its output.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$Arguments
    )

    $output = & git @Arguments 2>&1
    $exitCode = $LASTEXITCODE

    [PSCustomObject]@{
        PSTypeName = 'GitWorktreeCommandResult'
        ExitCode   = $exitCode
        Messages   = [string[]]($output | ForEach-Object { $_.ToString() })
    }
}

function Remove-StaleWorktree {
    <#
    .SYNOPSIS
    Prune stale git worktree administrative entries.
    .DESCRIPTION
    Runs `git worktree prune --verbose` for the current repository to remove
    stale administrative entries left behind after worktree directories were
    deleted manually. Use -DryRun to report what would be pruned without making
    changes. When -WhatIf is used, the command runs git's dry-run mode so git can
    report the stale entries while preserving the repository.
    .PARAMETER DryRun
    Run `git worktree prune --dry-run --verbose` and leave stale entries in place.
    .PARAMETER Expire
    Optional value passed through to git worktree prune's --expire option.
    .EXAMPLE
    Remove-StaleWorktree
    Removes stale worktree metadata for deleted worktree directories.
    .EXAMPLE
    Remove-StaleWorktree -DryRun
    Shows the stale worktree metadata git would remove without changing it.
    .EXAMPLE
    Remove-StaleWorktree -Expire now
    Prunes stale worktree metadata using git's `--expire now` option.
    #>

    [OutputType('WorktreeMaintenanceResult')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [switch]$DryRun,

        [string]$Expire
    )

    $repository = git rev-parse --show-toplevel 2>$null
    if ($LASTEXITCODE -ne 0 -or -not $repository) {
        Write-Error 'Not in a git repository.'
        return
    }

    $effectiveDryRun = $DryRun.IsPresent
    if (-not $effectiveDryRun) {
        if (-not $PSCmdlet.ShouldProcess($repository, 'Prune stale worktree administrative entries')) {
            if (-not $WhatIfPreference) {
                return
            }
            $effectiveDryRun = $true
        }
    }

    $arguments = @('worktree', 'prune', '--verbose')
    if ($effectiveDryRun) {
        $arguments += '--dry-run'
    }
    if ($Expire) {
        $arguments += @('--expire', $Expire)
    }

    $gitResult = Invoke-GitWorktreeMaintenance -Arguments $arguments
    if ($gitResult.ExitCode -ne 0) {
        $message = if ($gitResult.Messages) { $gitResult.Messages -join [Environment]::NewLine } else { 'No output.' }
        Write-Error "git worktree prune failed (exit $($gitResult.ExitCode)): $message"
        return
    }

    [PSCustomObject]@{
        PSTypeName = 'WorktreeMaintenanceResult'
        Command    = 'prune'
        Repository = $repository
        Paths      = @()
        DryRun     = $effectiveDryRun
        Expire     = $Expire
        ExitCode   = $gitResult.ExitCode
        Messages   = $gitResult.Messages
    }
}

function Repair-Worktree {
    <#
    .SYNOPSIS
    Repair git worktree links after a repository or worktree move.
    .DESCRIPTION
    Runs `git worktree repair` for the current repository. Pass one or more
    -Path values to forward them to git as worktree paths to repair, matching
    git's `git worktree repair <paths...>` form.
    .PARAMETER Path
    Optional worktree path values to pass to `git worktree repair`.
    .EXAMPLE
    Repair-Worktree
    Repairs worktree links for the current repository.
    .EXAMPLE
    Repair-Worktree -Path ..\moved-worktree
    Repairs the links for a moved worktree directory.
    #>

    [OutputType('WorktreeMaintenanceResult')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$Path
    )

    begin {
        $paths = [System.Collections.Generic.List[string]]::new()
    }

    process {
        foreach ($item in $Path) {
            if ($item) {
                $paths.Add($item)
            }
        }
    }

    end {
        $repository = git rev-parse --show-toplevel 2>$null
        if ($LASTEXITCODE -ne 0 -or -not $repository) {
            Write-Error 'Not in a git repository.'
            return
        }

        $target = if ($paths.Count -gt 0) { $paths -join ', ' } else { $repository }
        if (-not $PSCmdlet.ShouldProcess($target, 'Repair git worktree links')) {
            return
        }

        $arguments = @('worktree', 'repair')
        $arguments += $paths.ToArray()

        $gitResult = Invoke-GitWorktreeMaintenance -Arguments $arguments
        if ($gitResult.ExitCode -ne 0) {
            $message = if ($gitResult.Messages) { $gitResult.Messages -join [Environment]::NewLine } else { 'No output.' }
            Write-Error "git worktree repair failed (exit $($gitResult.ExitCode)): $message"
            return
        }

        [PSCustomObject]@{
            PSTypeName = 'WorktreeMaintenanceResult'
            Command    = 'repair'
            Repository = $repository
            Paths      = [string[]]$paths.ToArray()
            DryRun     = $false
            Expire     = $null
            ExitCode   = $gitResult.ExitCode
            Messages   = $gitResult.Messages
        }
    }
}