cdup.psm1

Set-StrictMode -Version Latest

function Get-CdUpCurrentDirectory {
    $location = Get-Location

    if ($location.Provider.Name -ne 'FileSystem') {
        throw "cdup only supports FileSystem locations. Current provider: '$($location.Provider.Name)'."
    }

    return Get-Item -LiteralPath $location.Path
}

function Resolve-CdUpTarget {
    [CmdletBinding(DefaultParameterSetName = 'Levels')]
    param(
        [Parameter(ParameterSetName = 'Levels', Position = 0)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$Levels = 1,

        [Parameter(ParameterSetName = 'NamedAncestor', Position = 0, Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

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

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

    $current = Get-CdUpCurrentDirectory

    switch ($PSCmdlet.ParameterSetName) {
        'Levels' {
            $target = $current

            for ($index = 0; $index -lt $Levels; $index++) {
                if ($null -eq $target.Parent) {
                    break
                }

                $target = $target.Parent
            }

            return $target.FullName
        }

        'NamedAncestor' {
            $candidate = $current

            while ($null -ne $candidate) {
                if ($candidate.Name -ieq $Name) {
                    return $candidate.FullName
                }

                $candidate = $candidate.Parent
            }

            throw "No matching ancestor named '$Name' was found from '$($current.FullName)'."
        }

        'GitRoot' {
            $candidate = $current

            while ($null -ne $candidate) {
                $gitPath = Join-Path -Path $candidate.FullName -ChildPath '.git'

                if (Test-Path -LiteralPath $gitPath) {
                    return $candidate.FullName
                }

                $candidate = $candidate.Parent
            }

            throw "No Git repository root was found from '$($current.FullName)'."
        }

        'Root' {
            $target = $current

            while ($null -ne $target.Parent) {
                $target = $target.Parent
            }

            return $target.FullName
        }

        default {
            throw "Unsupported parameter set '$($PSCmdlet.ParameterSetName)'."
        }
    }
}

function Set-LocationUp {
    <#
    .SYNOPSIS
    Moves to an ancestor directory quickly.

    .DESCRIPTION
    Set-LocationUp moves the current FileSystem location by level count, ancestor name,
    Git repository root, or filesystem root.

    .PARAMETER Levels
    Number of parent directories to traverse upward. This is the default mode.

    .PARAMETER Name
    The name of the current directory or an ancestor directory to jump to.

    .PARAMETER GitRoot
    Jump to the closest ancestor containing a .git marker.

    .PARAMETER Root
    Jump to the filesystem root for the current path.

    .EXAMPLE
    cdup 3

    Moves up three directory levels.

    .EXAMPLE
    cdup src

    Jumps to the nearest ancestor directory named src.

    .EXAMPLE
    cdup -GitRoot

    Jumps to the current repository root.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Levels', SupportsShouldProcess)]
    param(
        [Parameter(ParameterSetName = 'Levels', Position = 0)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$Levels = 1,

        [Parameter(ParameterSetName = 'NamedAncestor', Position = 0, Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

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

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

    $target = Resolve-CdUpTarget @PSBoundParameters

    if ($PSCmdlet.ShouldProcess($target, 'Set current location')) {
        Set-Location -LiteralPath $target
    }
}

Set-Alias -Name cdup -Value Set-LocationUp
Set-Alias -Name up -Value Set-LocationUp

Export-ModuleMember -Function Set-LocationUp -Alias cdup, up