KeepAChangelog.psm1
|
Set-StrictMode -Version Latest # Source: src/public/Convert-ChangelogReleaseNotesToTagMessage.ps1 function Add-ChangelogTagMessageBlankLine { [CmdletBinding()] param( [Parameter(Mandatory)] [object]$LineList, [Parameter(Mandatory)] [bool]$AllowBlankLine ) if (-not $AllowBlankLine -or $LineList.Count -eq 0) { return $false } $LineList.Add('') return $false } function Get-ChangelogTagMessageLine { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$TrimmedLine ) if ($TrimmedLine -match '^###\s+(?<value>.+)$') { return $Matches.value } if ($TrimmedLine -match '^(?:-|\*)\s+(?<value>.+)$') { return $Matches.value } return $TrimmedLine } function Remove-ChangelogTagMessageTrailingBlanks { [CmdletBinding()] param( [Parameter(Mandatory)] [object]$LineList ) while ($LineList.Count -gt 0 -and [string]::IsNullOrWhiteSpace($LineList[$LineList.Count - 1])) { $LineList.RemoveAt($LineList.Count - 1) } } function Convert-ChangelogReleaseNotesToTagMessage { [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyString()] [string]$ReleaseNotes ) $lineList = [System.Collections.Generic.List[string]]::new() $allowBlankLine = $false foreach ($line in $ReleaseNotes -split '\r?\n') { $trimmedLine = $line.Trim() if ([string]::IsNullOrWhiteSpace($trimmedLine)) { $allowBlankLine = Add-ChangelogTagMessageBlankLine -LineList $lineList -AllowBlankLine $allowBlankLine continue } $lineList.Add((Get-ChangelogTagMessageLine -TrimmedLine $trimmedLine)) $allowBlankLine = $true } Remove-ChangelogTagMessageTrailingBlanks -LineList $lineList return ($lineList -join "`n").Trim() } # Source: src/public/Get-KeepAChangelogVersion.ps1 function Get-KeepAChangelogVersion { [CmdletBinding()] [OutputType([string])] param() return Get-KeepAChangelogModuleVersion } # Source: src/public/Initialize-KeepAChangelogFile.ps1 function Initialize-KeepAChangelogFile { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Position = 0)] [string]$Path = 'CHANGELOG.md', [Parameter(Mandatory)] [string]$RepositoryUrl, [string]$PreviousReleaseReference, [string[]]$SectionHeading = @('Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security'), [switch]$Force ) Assert-KeepAChangelogInitialization -Path $Path -RepositoryUrl $RepositoryUrl -Force $Force.IsPresent $normalizedRepositoryUrl = $RepositoryUrl.TrimEnd('/') $template = New-KeepAChangelogTemplate ` -RepositoryUrl $normalizedRepositoryUrl ` -PreviousReleaseReference $PreviousReleaseReference ` -SectionHeading $SectionHeading if (-not $PSCmdlet.ShouldProcess($Path, 'Initialize Keep a Changelog template')) { return } Set-Content -LiteralPath $Path -Value $template -Encoding utf8 return [pscustomobject]@{ Path = $Path RepositoryUrl = $normalizedRepositoryUrl PreviousReleaseReference = if ([string]::IsNullOrWhiteSpace($PreviousReleaseReference)) { $null } else { $PreviousReleaseReference } } } # Source: src/public/Move-UnreleasedChangelog.ps1 function Move-UnreleasedChangelog { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Position = 0)] [string]$Path = 'CHANGELOG.md', [Parameter(Mandatory)] [string]$Version, [string]$Date, [string]$RepositoryUrl ) if ([string]::IsNullOrWhiteSpace($Version)) { throw 'Version is required.' } if (-not (Test-Path -LiteralPath $Path)) { throw "Could not find CHANGELOG file at '$Path'." } $releaseDate = Resolve-KeepAChangelogReleaseDate -Date $Date $release = @{ Version = $Version Date = $releaseDate Tag = $Version } $text = Get-Content -LiteralPath $Path -Raw $result = Resolve-KeepAChangelogReleaseData -Text $text -Release $release -RepositoryUrl $RepositoryUrl $result | Add-Member -NotePropertyName KeepAChangelogVersion -NotePropertyValue (Get-KeepAChangelogModuleVersion) -Force $targetVersion = $result.Release.Version if (-not $PSCmdlet.ShouldProcess($Path, "Promote [Unreleased] to [$targetVersion]")) { return $result } Set-Content -LiteralPath $Path -Value $result.UpdatedText -Encoding utf8 return $result } # Source: src/public/Test-KeepAChangelogFile.ps1 function Test-KeepAChangelogFile { [CmdletBinding()] param( [Parameter(Position = 0)] [string]$Path = 'CHANGELOG.md', [switch]$ThrowOnError ) if (-not (Test-Path -LiteralPath $Path)) { throw "Could not find CHANGELOG file at '$Path'." } $text = Get-Content -LiteralPath $Path -Raw $result = Get-KeepAChangelogValidationResult -Text $text $result | Add-Member -NotePropertyName Path -NotePropertyValue $Path -Force if ($ThrowOnError -and -not $result.IsValid) { throw ($result.Errors -join ' ') } return $result } # Source: src/private/initialize/Assert-KeepAChangelogInitialization.ps1 function Assert-KeepAChangelogInitialization { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path, [Parameter(Mandatory)] [string]$RepositoryUrl, [Parameter(Mandatory)] [bool]$Force ) if ([string]::IsNullOrWhiteSpace($RepositoryUrl)) { throw 'RepositoryUrl is required.' } if ((Test-Path -LiteralPath $Path) -and -not $Force) { throw "File already exists at '$Path'. Use -Force to overwrite it." } } # Source: src/private/initialize/Get-KeepAChangelogFooterLine.ps1 function Get-KeepAChangelogFooterLine { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$RepositoryUrl, [string]$PreviousReleaseReference ) if ([string]::IsNullOrWhiteSpace($PreviousReleaseReference)) { return $null } return "[Unreleased]: $RepositoryUrl/compare/$PreviousReleaseReference...HEAD" } # Source: src/private/initialize/Get-KeepAChangelogHeadingLines.ps1 function Get-KeepAChangelogHeadingLines { [CmdletBinding()] param( [Parameter(Mandatory)] [string[]]$SectionHeading ) return @( foreach ($heading in $SectionHeading) { "### $heading" '' } ) } # Source: src/private/initialize/New-KeepAChangelogTemplate.ps1 function New-KeepAChangelogTemplate { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$RepositoryUrl, [string]$PreviousReleaseReference, [Parameter(Mandatory)] [string[]]$SectionHeading ) $footerLine = Get-KeepAChangelogFooterLine -RepositoryUrl $RepositoryUrl -PreviousReleaseReference $PreviousReleaseReference $lineList = @( '# Changelog' '' 'All notable changes to this project will be documented in this file.' '' 'The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),' 'and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).' '' '## [Unreleased]' '' (Get-KeepAChangelogHeadingLines -SectionHeading $SectionHeading) $footerLine ) | Where-Object { $null -ne $_ } return (($lineList -join "`n").TrimEnd() + "`n") } # Source: src/private/release/Assert-KeepAChangelogRelease.ps1 function Assert-KeepAChangelogRelease { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$Release ) $requiredKeyList = @('Version', 'Date', 'Tag') foreach ($requiredKey in $requiredKeyList) { if (-not $Release.ContainsKey($requiredKey)) { throw "Release.$requiredKey is required." } if ([string]::IsNullOrWhiteSpace([string]$Release[$requiredKey])) { throw "Release.$requiredKey is required." } } return [pscustomobject]@{ Version = [string]$Release.Version Date = [string]$Release.Date Tag = [string]$Release.Tag } } # Source: src/private/release/Assert-KeepAChangelogReleaseDateOrder.ps1 function Get-KeepAChangelogLatestReleaseDate { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Body ) $latestReleaseDate = $null foreach ($match in [regex]::Matches($Body, '(?m)^## \[[^\]]+\] - (?<date>\d{4}-\d{2}-\d{2})$')) { $releaseDate = [datetime]::ParseExact( $match.Groups['date'].Value, 'yyyy-MM-dd', [System.Globalization.CultureInfo]::InvariantCulture ) if ($null -eq $latestReleaseDate -or $releaseDate -gt $latestReleaseDate) { $latestReleaseDate = $releaseDate } } if ($null -eq $latestReleaseDate) { return $null } return $latestReleaseDate.ToString('yyyy-MM-dd', [System.Globalization.CultureInfo]::InvariantCulture) } function Assert-KeepAChangelogReleaseDateOrder { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Body, [Parameter(Mandatory)] [pscustomobject]$Release ) $latestExistingReleaseDate = Get-KeepAChangelogLatestReleaseDate -Body $Body if ([string]::IsNullOrWhiteSpace($latestExistingReleaseDate)) { return } $releaseDate = [datetime]::ParseExact( $Release.Date, 'yyyy-MM-dd', [System.Globalization.CultureInfo]::InvariantCulture ) $latestReleaseDate = [datetime]::ParseExact( $latestExistingReleaseDate, 'yyyy-MM-dd', [System.Globalization.CultureInfo]::InvariantCulture ) if ($releaseDate -lt $latestReleaseDate) { throw "Release.Date '$($Release.Date)' cannot be earlier than latest existing release date '$latestExistingReleaseDate'." } } # Source: src/private/release/Get-ChangelogReleaseNotesBody.ps1 function Get-ChangelogReleaseNotesBody { [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyString()] [string]$Body ) $trimmedBody = $Body.Trim() if ([string]::IsNullOrWhiteSpace($trimmedBody)) { return '' } if ($trimmedBody -notmatch '(?m)^[ \t]*###\s+') { return $trimmedBody } $sectionPattern = '(?ms)^(?<heading>[ \t]*###\s+[^\r\n]+)\r?\n(?<content>.*?)(?=^[ \t]*###\s+|\z)' $sectionMatchList = [regex]::Matches($trimmedBody, $sectionPattern) $sectionTextList = [System.Collections.Generic.List[string]]::new() foreach ($sectionMatch in $sectionMatchList) { $heading = $sectionMatch.Groups['heading'].Value.TrimEnd() $content = $sectionMatch.Groups['content'].Value.Trim() if ([string]::IsNullOrWhiteSpace($content)) { continue } $sectionTextList.Add("$heading`n`n$content") } return ($sectionTextList -join "`n`n").Trim() } # Source: src/private/release/Get-ClearedUnreleasedBody.ps1 function Get-ClearedUnreleasedBody { [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyString()] [string]$Body ) $headingList = @( [regex]::Matches($Body, '(?m)^[ \t]*###\s+[^\r\n]+') | ForEach-Object { $_.Value.TrimEnd() } ) if (-not $headingList) { return '' } return ($headingList -join "`n`n").TrimEnd() } # Source: src/private/release/Get-KeepAChangelogPreviousReleaseReference.ps1 function Get-ChangelogReleaseTargetReference { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Link ) $compareMatch = [regex]::Match($Link, '/compare/.+\.\.\.(?<target>.+)$') if ($compareMatch.Success) { return $compareMatch.Groups['target'].Value } $tagMatch = [regex]::Match($Link, '/releases/tag/(?<target>.+)$') if ($tagMatch.Success) { return $tagMatch.Groups['target'].Value } return $null } function Get-KeepAChangelogPreviousReleaseReference { [CmdletBinding()] param( [AllowEmptyString()] [string]$Footer, [Parameter(Mandatory)] [pscustomobject]$Validation, [Parameter(Mandatory)] [pscustomobject]$Release ) $previousReleaseReference = $Validation.PreviousReleaseReference if ([string]::IsNullOrWhiteSpace($previousReleaseReference)) { return $null } if ($previousReleaseReference -ne $Release.Tag) { return $previousReleaseReference } $referenceLinkData = Get-ChangelogReferenceLinkData -Footer $Footer foreach ($releaseVersion in $Validation.ReleaseVersions) { if (-not $referenceLinkData.LinkMap.Contains($releaseVersion)) { continue } $releaseTargetReference = Get-ChangelogReleaseTargetReference -Link $referenceLinkData.LinkMap[$releaseVersion] if (-not [string]::IsNullOrWhiteSpace($releaseTargetReference) -and $releaseTargetReference -ne $Release.Tag) { return $releaseTargetReference } } return $null } # Source: src/private/release/Get-UnreleasedSectionMatch.ps1 function Get-UnreleasedSectionMatch { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Text ) $pattern = '(?ms)^##\s+\[Unreleased\]\s*\r?\n(?<body>.*?)(?=^##\s+\[|\z)' $match = [regex]::Match($Text, $pattern) if (-not $match.Success) { throw 'Could not find ## [Unreleased] section in CHANGELOG.md.' } return $match } # Source: src/private/release/Get-UpdatedChangelogReferenceFooter.ps1 function Get-ChangelogReferenceLinkData { [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyString()] [string]$Footer ) $orderedLabelList = [System.Collections.Generic.List[string]]::new() $linkMap = [ordered]@{} foreach ($referenceMatch in [regex]::Matches($Footer, '(?m)^\[(?<label>[^\]]+)\]:\s*(?<url>\S.*?)\s*$')) { $label = $referenceMatch.Groups['label'].Value $url = $referenceMatch.Groups['url'].Value.Trim() if (-not $linkMap.Contains($label)) { $orderedLabelList.Add($label) } $linkMap[$label] = $url } return [pscustomobject]@{ OrderedLabelList = $orderedLabelList LinkMap = $linkMap } } function Get-NormalizedChangelogRepositoryUrl { [CmdletBinding()] param( [string]$RepositoryUrl, [Parameter(Mandatory)] [string]$UnreleasedCompareLinkPrefix ) if ([string]::IsNullOrWhiteSpace($RepositoryUrl)) { return ($UnreleasedCompareLinkPrefix -replace '/compare/$', '') } return $RepositoryUrl.TrimEnd('/') } function Get-ChangelogReleaseLink { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$RepositoryUrl, [Parameter(Mandatory)] [string]$UnreleasedCompareLinkPrefix, [AllowEmptyString()] [string]$PreviousReleaseReference, [Parameter(Mandatory)] [string]$ReleaseTag ) if ([string]::IsNullOrWhiteSpace($PreviousReleaseReference)) { return "$RepositoryUrl/releases/tag/$ReleaseTag" } return "$UnreleasedCompareLinkPrefix$PreviousReleaseReference...$ReleaseTag" } function Add-ChangelogReferenceLabel { [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.Generic.List[string]]$OrderedLabelList, [Parameter(Mandatory)] [hashtable]$LinkMap, [Parameter(Mandatory)] [string]$Label ) if ($LinkMap.Contains($Label)) { return } $OrderedLabelList.Insert($OrderedLabelList.IndexOf('Unreleased') + 1, $Label) } function Get-UpdatedChangelogReferenceFooter { [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyString()] [string]$Footer, [Parameter(Mandatory)] [pscustomobject]$Release, [Parameter(Mandatory)] [pscustomobject]$Context ) $referenceLinkData = Get-ChangelogReferenceLinkData -Footer $Footer $orderedLabelList = $referenceLinkData.OrderedLabelList $linkMap = $referenceLinkData.LinkMap if (-not $linkMap.Contains('Unreleased')) { $orderedLabelList.Add('Unreleased') } $normalizedRepositoryUrl = Get-NormalizedChangelogRepositoryUrl ` -RepositoryUrl $Context.RepositoryUrl ` -UnreleasedCompareLinkPrefix $Context.UnreleasedCompareLinkPrefix $updatedUnreleasedLink = "$($Context.UnreleasedCompareLinkPrefix)$($Release.Tag)...HEAD" $newReleaseLink = Get-ChangelogReleaseLink ` -RepositoryUrl $normalizedRepositoryUrl ` -UnreleasedCompareLinkPrefix $Context.UnreleasedCompareLinkPrefix ` -PreviousReleaseReference $Context.PreviousReleaseReference ` -ReleaseTag $Release.Tag $linkMap['Unreleased'] = $updatedUnreleasedLink Add-ChangelogReferenceLabel -OrderedLabelList $orderedLabelList -LinkMap $linkMap -Label $Release.Version $linkMap[$Release.Version] = $newReleaseLink $footerLineList = foreach ($label in $orderedLabelList) { "[$label]: $($linkMap[$label])" } return [pscustomobject]@{ Footer = ($footerLineList -join "`n").TrimEnd() UpdatedUnreleasedLink = $updatedUnreleasedLink NewReleaseCompareLink = $newReleaseLink NewReleaseLink = $newReleaseLink } } # Source: src/private/release/Resolve-KeepAChangelogReleaseData.ps1 function Assert-KeepAChangelogValidation { [CmdletBinding()] param( [Parameter(Mandatory)] [pscustomobject]$Validation ) if (-not $Validation.IsValid) { throw "CHANGELOG.md is not valid. $($Validation.Errors -join ' ')" } } function Get-KeepAChangelogRepositoryContext { [CmdletBinding()] param( [string]$RepositoryUrl, [AllowEmptyString()] [string]$Footer, [Parameter(Mandatory)] [pscustomobject]$Release, [Parameter(Mandatory)] [pscustomobject]$Validation ) $normalizedRepositoryUrl = if ([string]::IsNullOrWhiteSpace($RepositoryUrl)) { $null } else { $RepositoryUrl.TrimEnd('/') } $unreleasedCompareLinkPrefix = $Validation.UnreleasedCompareLinkPrefix if ([string]::IsNullOrWhiteSpace($unreleasedCompareLinkPrefix)) { if ([string]::IsNullOrWhiteSpace($normalizedRepositoryUrl)) { throw 'RepositoryUrl is required for the first release when CHANGELOG.md has no [Unreleased] compare link.' } $unreleasedCompareLinkPrefix = "$normalizedRepositoryUrl/compare/" } if ([string]::IsNullOrWhiteSpace($normalizedRepositoryUrl)) { $normalizedRepositoryUrl = $unreleasedCompareLinkPrefix -replace '/compare/$', '' } $previousReleaseReference = Get-KeepAChangelogPreviousReleaseReference ` -Footer $Footer ` -Validation $Validation ` -Release $Release return [pscustomobject]@{ RepositoryUrl = $normalizedRepositoryUrl UnreleasedCompareLinkPrefix = $unreleasedCompareLinkPrefix PreviousReleaseReference = $previousReleaseReference } } function Get-KeepAChangelogSectionText { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Heading, [AllowEmptyString()] [string]$Body ) if ([string]::IsNullOrWhiteSpace($Body)) { return $Heading } return "$Heading`n`n$Body" } function Get-UpdatedChangelogBodyText { [CmdletBinding()] param( [Parameter(Mandatory)] [pscustomobject]$Parts, [Parameter(Mandatory)] [System.Text.RegularExpressions.Match]$UnreleasedSectionMatch, [Parameter(Mandatory)] [string]$UnreleasedSection, [Parameter(Mandatory)] [string]$ReleaseSection ) $beforeUnreleased = $Parts.Body.Substring(0, $UnreleasedSectionMatch.Index).TrimEnd() $afterUnreleasedStart = $UnreleasedSectionMatch.Index + $UnreleasedSectionMatch.Length $afterUnreleased = $Parts.Body.Substring($afterUnreleasedStart).TrimStart("`r", "`n") return (@( $beforeUnreleased $UnreleasedSection $ReleaseSection $afterUnreleased ) | Where-Object {-not [string]::IsNullOrWhiteSpace($_)} | ForEach-Object {$_.TrimEnd()} | Join-String -Separator "`n`n").TrimEnd() } function Resolve-KeepAChangelogReleaseData { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Text, [Parameter(Mandatory)] [hashtable]$Release, [string]$RepositoryUrl ) $normalizedRelease = Assert-KeepAChangelogRelease -Release $Release $validation = Get-KeepAChangelogValidationResult -Text $Text Assert-KeepAChangelogValidation -Validation $validation $parts = Split-KeepAChangelogText -Text $Text Assert-KeepAChangelogReleaseDateOrder -Body $parts.Body -Release $normalizedRelease $unreleasedSectionMatch = Get-UnreleasedSectionMatch -Text $parts.Body $repositoryContext = Get-KeepAChangelogRepositoryContext ` -RepositoryUrl $RepositoryUrl ` -Footer $parts.Footer ` -Release $normalizedRelease ` -Validation $validation $unreleasedBody = $unreleasedSectionMatch.Groups['body'].Value.Trim() $releaseNotesBody = Get-ChangelogReleaseNotesBody -Body $unreleasedBody $clearedUnreleasedBody = Get-ClearedUnreleasedBody -Body $unreleasedBody $unreleasedSection = Get-KeepAChangelogSectionText -Heading '## [Unreleased]' -Body $clearedUnreleasedBody $newReleaseSection = Get-KeepAChangelogSectionText -Heading "## [$($normalizedRelease.Version)] - $($normalizedRelease.Date)" -Body $releaseNotesBody $updatedBody = Get-UpdatedChangelogBodyText ` -Parts $parts ` -UnreleasedSectionMatch $unreleasedSectionMatch ` -UnreleasedSection $unreleasedSection ` -ReleaseSection $newReleaseSection $updatedFooterData = Get-UpdatedChangelogReferenceFooter ` -Footer $parts.Footer ` -Release $normalizedRelease ` -Context $repositoryContext $updatedText = ($updatedBody + "`n`n" + $updatedFooterData.Footer).TrimEnd() + "`n" return [pscustomobject]@{ Release = $normalizedRelease UnreleasedCompareLinkPrefix = $repositoryContext.UnreleasedCompareLinkPrefix PreviousReleaseReference = $repositoryContext.PreviousReleaseReference UnreleasedBody = $unreleasedBody ReleaseNotesBody = $releaseNotesBody ClearedUnreleasedBody = $clearedUnreleasedBody NewReleaseSection = $newReleaseSection UpdatedUnreleasedLink = $updatedFooterData.UpdatedUnreleasedLink NewReleaseCompareLink = $updatedFooterData.NewReleaseCompareLink NewReleaseLink = $updatedFooterData.NewReleaseLink TagMessageText = Convert-ChangelogReleaseNotesToTagMessage -ReleaseNotes $releaseNotesBody UpdatedText = $updatedText } } # Source: src/private/release/Resolve-KeepAChangelogReleaseDate.ps1 function Resolve-KeepAChangelogReleaseDate { [CmdletBinding()] param( [AllowEmptyString()] [string]$Date ) if ([string]::IsNullOrWhiteSpace($Date)) { return (Get-Date).ToString('yyyy-MM-dd', [System.Globalization.CultureInfo]::InvariantCulture) } $parsedDate = [datetime]::MinValue $isValidDate = [datetime]::TryParseExact( $Date, 'yyyy-MM-dd', [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None, [ref]$parsedDate ) if (-not $isValidDate) { throw "Date must use yyyy-MM-dd format. Received: '$Date'." } return $parsedDate.ToString('yyyy-MM-dd', [System.Globalization.CultureInfo]::InvariantCulture) } # Source: src/private/shared/Get-KeepAChangelogModuleVersion.ps1 function Get-KeepAChangelogModuleVersion { [CmdletBinding()] param() return $ExecutionContext.SessionState.Module.Version.ToString() } # Source: src/private/shared/Split-KeepAChangelogText.ps1 function Split-KeepAChangelogText { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Text ) $footerMatch = [regex]::Match($Text, '(?ms)(?<footer>(?:^\[[^\]]+\]:[^\r\n]*(?:\r?\n|$))+)\s*\z') if ($footerMatch.Success) { return [pscustomobject]@{ Body = $Text.Substring(0, $footerMatch.Index).TrimEnd() Footer = $footerMatch.Groups['footer'].Value.TrimEnd() } } return [pscustomobject]@{ Body = $Text.TrimEnd() Footer = '' } } # Source: src/private/validation/Add-KeepAChangelogDuplicateReferenceLinkError.ps1 function Add-KeepAChangelogDuplicateReferenceLinkError { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$ReferenceLabelCountMap, [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.Generic.List[string]]$ErrorList ) foreach ($label in $ReferenceLabelCountMap.Keys) { if ($ReferenceLabelCountMap[$label] -le 1) { continue } $ErrorList.Add("Reference link [$label] is duplicated.") } } # Source: src/private/validation/Get-KeepAChangelogReferenceLabelCountMap.ps1 function Get-KeepAChangelogReferenceLabelCountMap { [CmdletBinding()] param( [AllowEmptyString()] [string]$Footer ) $referenceLabelCountMap = @{} if ([string]::IsNullOrWhiteSpace($Footer)) { return $referenceLabelCountMap } foreach ($referenceMatch in [regex]::Matches($Footer, '(?m)^\[(?<label>[^\]]+)\]:')) { $label = $referenceMatch.Groups['label'].Value if (-not $referenceLabelCountMap.ContainsKey($label)) { $referenceLabelCountMap[$label] = 0 } $referenceLabelCountMap[$label]++ } return $referenceLabelCountMap } # Source: src/private/validation/Get-KeepAChangelogReleaseVersionList.ps1 function Get-KeepAChangelogReleaseVersionList { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Body, [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.Generic.List[string]]$ErrorList ) $releaseVersionList = [System.Collections.Generic.List[string]]::new() foreach ($releaseHeadingLine in [regex]::Matches($Body, '(?m)^##\s+\[(?<label>[^\]]+)\].*$')) { $label = $releaseHeadingLine.Groups['label'].Value if ($label -eq 'Unreleased') { continue } $line = $releaseHeadingLine.Value.TrimEnd() if ($line -notmatch '^## \[[^\]]+\] - \d{4}-\d{2}-\d{2}$') { $ErrorList.Add("Release section '$line' must use '## [<version>] - <date>' format.") continue } if ($releaseVersionList.Contains($label)) { $ErrorList.Add("Release section [$label] is duplicated.") continue } $releaseVersionList.Add($label) } return ,$releaseVersionList } # Source: src/private/validation/Get-KeepAChangelogUnreleasedCompareLinkData.ps1 function Get-KeepAChangelogUnreleasedCompareLinkData { [CmdletBinding()] param( [AllowEmptyString()] [string]$Footer, [switch]$HasReferenceFooter, [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.Generic.List[string]]$ErrorList ) $result = [pscustomobject]@{ UnreleasedCompareLinkPrefix = $null PreviousReleaseReference = $null } if (-not $HasReferenceFooter) { return $result } $pattern = '(?m)^\[Unreleased\]:\s*(?<prefix>\S+/compare/)(?<from>.+?)\.\.\.HEAD\s*$' $matchList = [regex]::Matches($Footer, $pattern) if ($matchList.Count -eq 0) { $ErrorList.Add('Could not find an [Unreleased] compare link in CHANGELOG.md.') return $result } if ($matchList.Count -gt 1) { $ErrorList.Add('CHANGELOG.md must contain exactly one [Unreleased] compare link.') } $match = $matchList[0] return [pscustomobject]@{ UnreleasedCompareLinkPrefix = $match.Groups['prefix'].Value PreviousReleaseReference = $match.Groups['from'].Value } } # Source: src/private/validation/Get-KeepAChangelogValidationResult.ps1 function Get-KeepAChangelogValidationResult { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Text ) $errorList = [System.Collections.Generic.List[string]]::new() $parts = Split-KeepAChangelogText -Text $Text if (-not (Test-KeepAChangelogHasUnreleasedSection -Body $parts.Body)) { $errorList.Add('Could not find ## [Unreleased] section in CHANGELOG.md.') } $releaseVersionList = Get-KeepAChangelogReleaseVersionList ` -Body $parts.Body ` -ErrorList $errorList $hasReferenceFooter = -not [string]::IsNullOrWhiteSpace($parts.Footer) if (-not $hasReferenceFooter -and $releaseVersionList.Count -gt 0) { $errorList.Add('CHANGELOG.md must end with reference links once releases exist.') } $referenceLabelCountMap = Get-KeepAChangelogReferenceLabelCountMap -Footer $parts.Footer Add-KeepAChangelogDuplicateReferenceLinkError ` -ReferenceLabelCountMap $referenceLabelCountMap ` -ErrorList $errorList $unreleasedCompareLinkData = Get-KeepAChangelogUnreleasedCompareLinkData ` -Footer $parts.Footer ` -HasReferenceFooter:$hasReferenceFooter ` -ErrorList $errorList return [pscustomobject]@{ IsValid = ($errorList.Count -eq 0) Errors = @($errorList.ToArray()) UnreleasedCompareLinkPrefix = $unreleasedCompareLinkData.UnreleasedCompareLinkPrefix PreviousReleaseReference = $unreleasedCompareLinkData.PreviousReleaseReference ReleaseVersions = @($releaseVersionList.ToArray()) } } # Source: src/private/validation/Get-UnreleasedCompareLinkMatch.ps1 function Get-UnreleasedCompareLinkMatch { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Text ) $pattern = '(?m)^\[Unreleased\]:\s*(?<prefix>\S+/compare/)(?<from>.+?)\.\.\.HEAD\s*$' $match = [regex]::Match($Text, $pattern) if (-not $match.Success) { throw 'Could not find an [Unreleased] compare link in CHANGELOG.md.' } return $match } # Source: src/private/validation/Test-KeepAChangelogHasUnreleasedSection.ps1 function Test-KeepAChangelogHasUnreleasedSection { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Body ) return $Body -match '(?ms)^##\s+\[Unreleased\]\s*\r?\n' } |