Remove-Branch.ps1

function Remove-Branch {
    <#
    .SYNOPSIS
        Delete an explicitly named local or remote git branch.
    .DESCRIPTION
        Deletes a local branch with git branch -d by default. Git refuses branches
        that are not fully merged into their upstream (or HEAD without an upstream).
        -Force uses -D for local deletion only; it never suppresses confirmation
        or overrides git's protection of branches checked out in any worktree.
 
        -Remote deletes only refs/heads/<Name> on the selected configured remote,
        using its configured push URLs. It does not delete a local branch or just
        a remote-tracking ref. No upstream or remote-prefix inference is performed.
        Configured mirror pushes and automatic tag following are disabled.
        Git may accept deletion of an already absent remote ref as a no-op.
        -WhatIf and declined confirmation never execute a deletion or push.
    .PARAMETER Name
        Exact branch name, optionally prefixed with refs/heads/. Wildcards,
        revision expressions, other ref namespaces and option-like names are
        rejected. Accepts pipeline strings or objects with Name, BranchName or Branch.
    .PARAMETER Path
        Literal directory inside the repository. Defaults to the current location.
        Accepts object properties Path, RepositoryPath or RepoPath. Bare repositories
        are also supported. The caller's location is unchanged.
    .PARAMETER Force
        Permit deletion of an unmerged local branch. Cannot be combined with
        -Remote. Confirmation and worktree protections remain in effect.
    .PARAMETER Remote
        Delete the branch from a configured remote instead of the local repository.
    .PARAMETER RemoteName
        Configured remote name to use with -Remote. Defaults to origin. URLs and
        filesystem paths are not accepted in place of a configured remote name.
    .OUTPUTS
        None. Git failures are reported as PowerShell errors.
    .EXAMPLE
        Remove-Branch -Name feature/finished -WhatIf
        Preview deletion of a local branch.
    .EXAMPLE
        Remove-Branch -Name feature/abandoned -Force -Confirm:$false
        Delete an unmerged local branch without prompting.
    .EXAMPLE
        Remove-Branch -Name feature/finished -Remote -RemoteName upstream
        Confirm deletion of that branch on the configured upstream remote.
    .EXAMPLE
        'feature/one', 'feature/two' | Remove-Branch -Path ../project
        Confirm deletion of each named local branch in another repository.
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High', DefaultParameterSetName = 'Local')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('BranchName', 'Branch')]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Position = 1, ValueFromPipelineByPropertyName)]
        [Alias('RepositoryPath', 'RepoPath')]
        [string]$Path,

        [Parameter(ParameterSetName = 'Local')]
        [switch]$Force,

        [Parameter(Mandatory, ParameterSetName = 'Remote')]
        [switch]$Remote,

        [Parameter(ParameterSetName = 'Remote')]
        [ValidateNotNullOrEmpty()]
        [string]$RemoteName = 'origin'
    )

    process {
        $branchName = $Name
        if ($branchName.StartsWith('refs/heads/', [StringComparison]::Ordinal)) {
            $branchName = $branchName.Substring('refs/heads/'.Length)
        } elseif ($branchName.StartsWith('refs/', [StringComparison]::Ordinal)) {
            $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
                [ArgumentException]::new('Only branch names or refs/heads/ references are accepted.'),
                'GitBranchNameInvalid', [System.Management.Automation.ErrorCategory]::InvalidArgument, $Name))
            return
        }
        if (-not $branchName -or $branchName.StartsWith('-') -or $branchName -ceq 'HEAD') {
            $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
                [ArgumentException]::new("Invalid branch name: '$Name'."),
                'GitBranchNameInvalid', [System.Management.Automation.ErrorCategory]::InvalidArgument, $Name))
            return
        }
        if ($PSCmdlet.ParameterSetName -eq 'Remote' -and -not $Remote) {
            $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
                [ArgumentException]::new('Remote deletion requires -Remote; omit remote parameters for local deletion.'),
                'GitRemoteRequired', [System.Management.Automation.ErrorCategory]::InvalidArgument, $RemoteName))
            return
        }

        # A full ref cannot be parsed as an option or expand @{-1} like --branch.
        $reference = "refs/heads/$branchName"
        $validated = Invoke-Git -Path $Path -AllowBare -Arguments @('check-ref-format', $reference)
        if ($null -eq $validated) { return }
        $repositoryPath = $validated.RepositoryPath

        if ($Remote) {
            $remotes = Invoke-Git -Path $repositoryPath -AllowBare -Arguments @('remote')
            if ($null -eq $remotes) { return }
            if ($RemoteName.StartsWith('-') -or $RemoteName -cnotin ($remotes.StandardOutput -split '\r?\n')) {
                $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
                    [ArgumentException]::new("Configured remote '$RemoteName' was not found in '$repositoryPath'."),
                    'GitRemoteNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $RemoteName))
                return
            }
            $target = "$reference on remote '$RemoteName' in '$repositoryPath'"
            $action = 'Delete remote git branch'
            $arguments = @(
                '-c', "remote.$RemoteName.mirror=false",
                'push', '--delete', '--no-follow-tags', '--recurse-submodules=no', '--', $RemoteName, $reference
            )
        } else {
            $target = "$reference in '$repositoryPath'"
            $action = if ($Force) { 'Delete local git branch (allow unmerged)' } else { 'Delete merged local git branch' }
            $deleteOption = if ($Force) { '-D' } else { '-d' }
            $arguments = @('branch', $deleteOption, '--', $branchName)
        }

        if ($PSCmdlet.ShouldProcess($target, $action)) {
            $result = Invoke-Git -Path $repositoryPath -AllowBare -Arguments $arguments
            if ($null -eq $result) { return }
            if ($result.StandardOutput) { Write-Verbose $result.StandardOutput.TrimEnd() }
        }
    }
}