src/KeepAChangelog.Core.ps1

Set-StrictMode -Version Latest

function Resolve-KeepAChangelogPath {
    [CmdletBinding()]
    [OutputType([string])]
    param()

    Join-Path (Get-Location) 'CHANGELOG.md'
}

function Read-KeepAChangelogSections {
    [CmdletBinding()]
    [OutputType([object[]])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $Path
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        throw "Changelog not found: $Path"
    }

    $content = Get-Content -LiteralPath $Path -Raw
    $headerPattern = '(?m)^## \[(?<Name>[^\]]+)\](?<Suffix>(?: - .+)?)\r?$'
    $headerMatches = [System.Text.RegularExpressions.Regex]::Matches($content, $headerPattern)
    $sections = [System.Collections.Generic.List[object]]::new()

    for ($index = 0; $index -lt $headerMatches.Count; $index++) {
        $headerMatch = $headerMatches[$index]
        $bodyStartIndex = $headerMatch.Index + $headerMatch.Length
        $bodyEndIndex = $content.Length

        if ($index + 1 -lt $headerMatches.Count) {
            $bodyEndIndex = $headerMatches[$index + 1].Index
        }

        $rawBody = $content.Substring($bodyStartIndex, $bodyEndIndex - $bodyStartIndex).TrimStart("`r", "`n")
        $footerMatch = [System.Text.RegularExpressions.Regex]::Match($rawBody, '(?m)^---\s*\r?$')
        if ($footerMatch.Success) {
            $rawBody = $rawBody.Substring(0, $footerMatch.Index)
        }

        $sections.Add([pscustomobject]@{
                Version = $headerMatch.Groups['Name'].Value
                Heading = $headerMatch.Value.TrimEnd("`r", "`n")
                Body = $rawBody.TrimEnd("`r", "`n")
            })
    }

    $sections.ToArray()
}

function Find-KeepAChangelogSection {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $Version
    )

    $section = Read-KeepAChangelogSections -Path $Path |
        Where-Object { $_.Version -eq $Version } |
        Select-Object -First 1

    if (-not $section) {
        throw "Changelog entry not found for version: $Version"
    }

    $section
}

function ConvertFrom-ReleaseTagToVersion {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $ReleaseTag
    )

    $normalizedTag = $ReleaseTag -replace '^refs/tags/', ''
    $match = [System.Text.RegularExpressions.Regex]::Match($normalizedTag, '^v(?<Version>.+)$')
    if (-not $match.Success) {
        throw "Release tag must use the form v<version>: $ReleaseTag"
    }

    $match.Groups['Version'].Value
}

function Get-KeepAChangelogSection {
    [CmdletBinding(DefaultParameterSetName = 'List')]
    [OutputType([object[]])]
    [OutputType([object])]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $Path = (Resolve-KeepAChangelogPath),

        [Parameter(Mandatory, ParameterSetName = 'ByVersion')]
        [ValidateNotNullOrEmpty()]
        [string] $Version
    )

    if ($PSCmdlet.ParameterSetName -eq 'List') {
        Read-KeepAChangelogSections -Path $Path
    }
    else {
        Find-KeepAChangelogSection -Path $Path -Version $Version
    }
}

function Get-KeepAChangelogEntry {
    [CmdletBinding(DefaultParameterSetName = 'ByVersion')]
    [OutputType([string])]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $Path = (Resolve-KeepAChangelogPath),

        [Parameter(Mandatory, ParameterSetName = 'ByVersion')]
        [ValidateNotNullOrEmpty()]
        [string] $Version,

        [Parameter(Mandatory, ParameterSetName = 'ByReleaseTag')]
        [ValidateNotNullOrEmpty()]
        [string] $ReleaseTag
    )

    $resolvedVersion = if ($PSCmdlet.ParameterSetName -eq 'ByReleaseTag') {
        ConvertFrom-ReleaseTagToVersion -ReleaseTag $ReleaseTag
    }
    else {
        $Version
    }

    (Find-KeepAChangelogSection -Path $Path -Version $resolvedVersion).Body
}

function Assert-KeepAChangelogReleaseMetadata {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $Path = (Resolve-KeepAChangelogPath),

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $Version,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $ReleaseTag
    )

    Find-KeepAChangelogSection -Path $Path -Version $Version | Out-Null

    if ($PSBoundParameters.ContainsKey('ReleaseTag')) {
        $tagVersion = ConvertFrom-ReleaseTagToVersion -ReleaseTag $ReleaseTag
        if ($tagVersion -ne $Version) {
            throw "Release tag version does not match manifest version. Tag: $tagVersion, Manifest: $Version"
        }
    }
}