Scripts/Public/Remove-GitBranches.ps1

function Remove-GitBranches {
    [CmdletBinding()]
    param(
        [switch]$Help,
        [switch]$DeleteCurrent,
        [switch]$Yes
    )

    if ($Help) {
        Write-Host "`nRemove-GitBranches:" -ForegroundColor Cyan
        Write-Host "Safely delete local branches that no longer exist on the remote." -ForegroundColor White
        Write-Host "`nUSAGE:" -ForegroundColor Yellow
        Write-Host " Remove-GitBranches [-DeleteCurrent] [-Yes] [-Help]" -ForegroundColor White
        Write-Host "`nOPTIONS:" -ForegroundColor Yellow
        Write-Host " -DeleteCurrent Also delete the current branch (if it has no remote)." -ForegroundColor White
        Write-Host " -Yes Skip confirmation prompts." -ForegroundColor White
        Write-Host " -Help Show this help." -ForegroundColor White
        return
    }

    Invoke-GitCultureInvariant -args @('rev-parse', '--is-inside-work-tree') *> $null
    if ($LASTEXITCODE -ne 0) {
        Write-Host "Not inside a Git repository" -ForegroundColor Red
        return
    }

    # Ensure UTF-8 during execution of the command
    $originalEncodingOfUser = [Console]::OutputEncoding
    [Console]::OutputEncoding = [System.Text.UTF8Encoding]::UTF8

    try {
        Invoke-GitCultureInvariant -args @('fetch','-p')
        if ($LASTEXITCODE -ne 0) {
            Write-Host "Failed to fetch from the remote; aborting without deleting anything." -ForegroundColor Red
            return
        }

        $currentBranch = "$(Invoke-GitCultureInvariant -args @('symbolic-ref','--short','-q','HEAD'))".Trim()

        function Get-DefaultBranch {
            if (branchExists 'master') { return 'master' }
            if (branchExists 'main') { return 'main' }
            $remoteHead = Invoke-GitCultureInvariant -args @('symbolic-ref','-q','--short','refs/remotes/origin/HEAD') |
            ForEach-Object { $_.Trim() }
            if ($LASTEXITCODE -eq 0 -and $remoteHead) {
                $candidate = ($remoteHead -replace '^origin/','')
                # Only treat it as the default if the branch actually exists locally;
                # otherwise switching to it would silently DWIM-create it from the remote.
                if (branchExists $candidate) { return $candidate }
            }
            return $null
        }

        $defaultBranch = Get-DefaultBranch

        if ($currentBranch -and -not (hasRemote $currentBranch) -and $defaultBranch -and $currentBranch -ne $defaultBranch -and -not $DeleteCurrent) {
            if ($Yes) {
                $DeleteCurrent = $true
            } else {
                $resp = Read-Host "Current local branch '$currentBranch' has no remote; delete this and switch to '$defaultBranch'? (Y/n)"
                $DeleteCurrent = (-not $resp.ToLower().StartsWith('n'))
            }
        }

        if ($DeleteCurrent -and -not $defaultBranch) {
            Write-Host "There exists no default branch ('main'/'master' or origin/HEAD) to switch to after deleting current branch." -ForegroundColor Red
            return
        }

        # Collect refs without parsing `git branch` output
        $localBranches = Invoke-GitCultureInvariant -args @('for-each-ref','--format=%(refname:short)','refs/heads')
        $remoteBranches = Invoke-GitCultureInvariant -args @('for-each-ref','--format=%(refname:lstrip=3)','refs/remotes') |
        Where-Object { $_ -ne 'HEAD' }

        $remoteSet = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
        $remoteBranches | ForEach-Object { [void]$remoteSet.Add($_) }

        $branchesToDelete = $localBranches | Where-Object {
            ($_ -ne 'master' -and $_ -ne 'main') -and
            ($_ -ne $defaultBranch) -and
            ($_ -ne $currentBranch -or $DeleteCurrent) -and
            (-not $remoteSet.Contains($_))
        }

        if (-not $branchesToDelete) {
            Write-Host "No local branches to delete" -ForegroundColor Cyan
            return
        }

        Write-Host "The following local Git branches will be deleted:" -ForegroundColor Yellow
        $branchesToDelete | ForEach-Object { Write-Host " - $_" -ForegroundColor Magenta }

        if (-not $Yes) {
            $resp = Read-Host "Delete these local branches? (Y/n)"
            if ($resp.ToLower().StartsWith('n')) {
                Write-Host "No local branches were deleted" -ForegroundColor Cyan
                return
            }
        }

        if ($DeleteCurrent -and $currentBranch -ne $defaultBranch) {
            Invoke-GitCultureInvariant -args @('switch',$defaultBranch) *>$null
            if ($LASTEXITCODE -ne 0) {
                Invoke-GitCultureInvariant -args @('checkout',$defaultBranch) *>$null
                if ($LASTEXITCODE -ne 0) {
                    Write-Host "Failed to switch to '$defaultBranch'; no branches were deleted." -ForegroundColor Red
                    return
                }
            }
        }

        $deleteFailed = $false
        foreach ($branch in $branchesToDelete) {
            Invoke-GitCultureInvariant -args @('branch','-D',$branch)
            if ($LASTEXITCODE -ne 0) { $deleteFailed = $true }
        }

        if ($deleteFailed) {
            Write-Host "Some local branches could not be deleted" -ForegroundColor Red
            return
        }

        Write-Host "The local branches were deleted successfully" -ForegroundColor Green
    }
    finally {
        [Console]::OutputEncoding = $originalEncodingOfUser
    }
}