Private/Container/Get-NAVGitContainerName.ps1

function Get-NAVGitContainerName {
    <#
    .SYNOPSIS
        Derives the default container name from the project name: '_' and other invalid characters become '-',
        truncated to 15 characters (NetBIOS host name limit), no leading/trailing '-'.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [string] $ProjectName
    )

    $name = $ProjectName -replace '[^A-Za-z0-9-]', '-'
    $name = $name.Trim('-')
    if ($name.Length -gt 15) {
        $name = $name.Substring(0, 15).TrimEnd('-')
    }
    return $name
}

function Get-NAVGitBranchName {
    <#
    .SYNOPSIS
        Returns the current Git branch of the repository at Path, or $null (no git, not a repository, detached HEAD).
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [string] $Path
    )

    if (-not (Get-Command git -CommandType Application -ErrorAction SilentlyContinue)) {
        return $null
    }
    $branch = & git -C $Path branch --show-current 2>$null
    if ($LASTEXITCODE -ne 0 -or -not $branch) {
        return $null
    }
    return ([string]$branch).Trim()
}

function Resolve-NAVGitContainerName {
    <#
    .SYNOPSIS
        Resolves containerName from the user settings and navgit.json. Each may be a name, { "fixed": "<name>" } or
        { "branch": { "prefix": "<key>", "length": <n> } } (prefix optional). The user setting wins. A branch rule
        applies only on a ticket branch (starting with <prefix>-<number>, or any issue key such as MIS-1234 without a
        prefix) and yields its first <n> characters; otherwise the next value is used: the user setting, then
        navgit.json, then the name derived from projectName.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        $UserValue,

        $RepoValue,

        $ProjectName,

        [Parameter(Mandatory)]
        [string] $RepositoryPath,

        [string] $SettingsPath = 'settings.json',

        [string] $ConfigPath = 'navgit.json',

        # For tests; by default the branch is read with git when a branch rule needs it.
        [AllowEmptyString()]
        [string] $BranchName
    )

    $layers = @(@(
            @{ Value = $UserValue; Source = $SettingsPath }
            @{ Value = $RepoValue; Source = $ConfigPath }
        ) | Where-Object { $null -ne $_.Value -and -not ($_.Value -is [string] -and $_.Value -eq '') })

    $branchRead = $PSBoundParameters.ContainsKey('BranchName')
    foreach ($layer in $layers) {
        $value = $layer.Value
        $source = $layer.Source

        if ($value -is [string]) {
            return (Assert-NAVGitFixedContainerName -Name $value -Source $source -Setting 'containerName')
        }
        if ($value -isnot [hashtable]) {
            throw "${source}: 'containerName' must be a name, { `"fixed`": `"<name>`" } or { `"branch`": { `"prefix`": `"<key>`", `"length`": <number> } }."
        }
        $forms = @(@('fixed', 'branch') | Where-Object { $value.ContainsKey($_) })
        if ($forms.Count -ne 1) {
            throw "${source}: 'containerName' must contain exactly one of 'fixed' or 'branch' (found: $(if ($forms.Count) { $forms -join ', ' } else { 'none' }))."
        }
        if ($forms[0] -eq 'fixed') {
            if ($value.fixed -isnot [string]) {
                throw "${source}: 'containerName.fixed' must be a string."
            }
            return (Assert-NAVGitFixedContainerName -Name $value.fixed -Source $source -Setting 'containerName.fixed')
        }

        $rule = $value.branch
        $validLength = $rule -is [hashtable] -and ($rule.length -is [int] -or $rule.length -is [long]) -and
            $rule.length -ge 1 -and $rule.length -le 15
        if (-not $validLength) {
            throw "${source}: 'containerName.branch' must be { `"length`": <1-15> }, optionally with `"prefix`": `"<key>`"."
        }
        foreach ($key in $rule.Keys) {
            if ($key -notin @('prefix', 'length')) { Write-Warning "${source}: unknown setting 'containerName.branch.$key' is ignored." }
        }
        # Ticket branches start with an issue key: project key, '-', number (MIS-1234-short-text).
        $ticketPattern = '^[A-Z][A-Z0-9]*-\d'
        $caseSensitive = $true
        if ($null -ne $rule.prefix -and -not ($rule.prefix -is [string] -and $rule.prefix -eq '')) {
            # The '-' is added here, so "MIS" and "MIS-" mean the same and "MIS" does not match MISC-....
            $issueKey = if ($rule.prefix -is [string]) { $rule.prefix.TrimEnd('-') } else { $null }
            if ($issueKey -notmatch '^[A-Za-z][A-Za-z0-9]*$') {
                throw "${source}: 'containerName.branch.prefix' ('$($rule.prefix)') must be an issue key such as 'MIS': letters and digits, starting with a letter."
            }
            $ticketPattern = "^$issueKey-\d"
            $caseSensitive = $false
        }

        if (-not $branchRead) {
            $BranchName = Get-NAVGitBranchName -Path $RepositoryPath
            $branchRead = $true
            if (-not $BranchName) {
                Write-NAVGitLog "Could not read the Git branch of '$RepositoryPath' (no git, or a detached HEAD); containerName.branch in '$source' is not used." -Level Warning
            }
        }
        $isTicket = if ($caseSensitive) { $BranchName -cmatch $ticketPattern } else { $BranchName -match $ticketPattern }
        if ($isTicket) {
            $start = $BranchName.Substring(0, [Math]::Min([int]$rule.length, $BranchName.Length))
            $name = Get-NAVGitContainerName -ProjectName $start
            if ($name) {
                Write-NAVGitLog "Container name '$name' from branch '$BranchName' ($source)." -Level Detail
                return $name
            }
        }
        elseif ($BranchName) {
            Write-NAVGitLog "Branch '$BranchName' is not a ticket branch; containerName.branch in '$source' is not used." -Level Detail
        }
    }

    if ($ProjectName -is [string] -and $ProjectName) {
        return (Get-NAVGitContainerName -ProjectName $ProjectName)
    }
    return $null
}

function Assert-NAVGitFixedContainerName {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string] $Name,

        [string] $Source,

        [string] $Setting
    )

    if ($Name -notmatch '^[A-Za-z0-9][A-Za-z0-9-]{0,14}$') {
        throw "${Source}: '$Setting' ('$Name') must be 1-15 characters (letters, digits, -) and start with a letter or digit."
    }
    return $Name
}