From 3ee60b41bee6b23a11821086e6dff4af4827d7eb Mon Sep 17 00:00:00 2001 From: Florian Lorenzen Date: Wed, 8 Jan 2025 20:17:13 +0100 Subject: [PATCH] Parse references at the end of a changelog as a separate part of a changelog. (#2779) * Parse references at the end of a changelog as a separate part of a changelog. * Test cases for changelog parsing with references in extra section --- src/app/Fake.Core.ReleaseNotes/Changelog.fs | 101 +++++++++++-- .../Fake.Core.ReleaseNotes.fs | 137 +++++++++++++++++- 2 files changed, 227 insertions(+), 11 deletions(-) diff --git a/src/app/Fake.Core.ReleaseNotes/Changelog.fs b/src/app/Fake.Core.ReleaseNotes/Changelog.fs index c63a0a06a80..b3fac548c94 100644 --- a/src/app/Fake.Core.ReleaseNotes/Changelog.fs +++ b/src/app/Fake.Core.ReleaseNotes/Changelog.fs @@ -108,6 +108,16 @@ module Changelog = | Security s -> sprintf "Security: %s" s.CleanedText | Custom (h, s) -> sprintf "%s: %s" h s.CleanedText + member x.ChangeText() = + match x with + | Added (changeText) + | Changed (changeText) + | Deprecated (changeText) + | Removed (changeText) + | Fixed (changeText) + | Security (changeText) + | Custom (_, changeText) -> changeText + /// Create a new change type changelog entry static member New(header: string, line: string) : Change = let text = @@ -267,6 +277,32 @@ module Changelog = assemblyVersion, nugetVersion + /// + /// A version or "Unreleased" + /// + type ReferenceVersion = + | SemVerRef of SemVerInfo + | UnreleasedRef + + override x.ToString() = + match x with + | SemVerRef (semVerInfo) -> semVerInfo.ToString() + | UnreleasedRef -> "Unreleased" + + /// + /// A reference from a version to a repository URL (e.g. a tag or a compare link) + /// + /// + /// [Unreleased]: https://github.com/user/MyCoolNewLib.git/compare/v0.1.0...HEAD + /// [0.1.0]: https://github.com/user/MyCoolNewLib.git/releases/tag/v0.1.0 + /// + type Reference = + { SemVer: ReferenceVersion + RepoUrl: Uri } + + override x.ToString() = + sprintf "[%s]: %s" (x.SemVer.ToString()) (x.RepoUrl.ToString()) + /// /// Holds data for a changelog file, which include changelog entries an other metadata /// @@ -283,17 +319,21 @@ module Changelog = /// The change log entries Entries: ChangelogEntry list + + /// The references to repository URLs + References: Reference list } /// the latest change log entry member x.LatestEntry = x.Entries |> Seq.head /// Create a new changelog record from given data - static member New(header, description, unreleased, entries) = + static member New(header, description, unreleased, entries, references) = { Header = header Description = description Unreleased = unreleased - Entries = entries } + Entries = entries + References = references } /// Promote an unreleased changelog entry to a released one member x.PromoteUnreleased(assemblyVersion: string, nugetVersion: string) : Changelog = @@ -311,7 +351,7 @@ module Changelog = ) let unreleased' = Some { Description = None; Changes = [] } - Changelog.New(x.Header, x.Description, unreleased', newEntry :: x.Entries) + Changelog.New(x.Header, x.Description, unreleased', newEntry :: x.Entries, x.References) /// Promote an unreleased changelog entry to a released one using version number member x.PromoteUnreleased(version: string) : Changelog = @@ -346,7 +386,10 @@ module Changelog = | "" -> "Changelog" | h -> h - (sprintf "# %s\n\n%s\n\n%s" header description entries) + let references = + x.References |> List.map (fun reference -> reference.ToString()) |> joinLines + + $"# {header}\n\n{description}\n\n{entries}\n\n{references}" |> fixMultipleNewlines |> String.trim @@ -358,8 +401,9 @@ module Changelog = /// the descriptive text for changelog /// the unreleased list of changelog entries /// the list of changelog entries - let createWithCustomHeader header description unreleased entries = - Changelog.New(header, description, unreleased, entries) + /// the list of references + let createWithCustomHeader header description unreleased entries references = + Changelog.New(header, description, unreleased, entries, references) /// /// Create a changelog with given data @@ -368,8 +412,9 @@ module Changelog = /// the descriptive text for changelog /// the unreleased list of changelog entries /// the list of changelog entries - let create description unreleased entries = - createWithCustomHeader "Changelog" description unreleased entries + /// the list of references + let create description unreleased entries references = + createWithCustomHeader "Changelog" description unreleased entries references /// /// Create a changelog with given entries and default values for other data including @@ -377,7 +422,7 @@ module Changelog = /// /// /// the list of changelog entries - let fromEntries entries = create None None entries + let fromEntries entries = create None None entries [] let internal isMainHeader line : bool = "# " <* line let internal isVersionHeader line : bool = "## " <* line @@ -538,7 +583,43 @@ module Changelog = | h :: _ -> h | _ -> "Changelog" - Changelog.New(header, description, unreleased, entries) + // Move references from last changelog entry into references. + let entriesWithoutReferences, references = + let referenceRegex = + $"""^\[(({nugetRegex.ToString()})|Unreleased)\]: +(http[s]://.+)$""" + |> String.getRegEx + + match entries with + | [] -> [], [] + | entries -> + let front, lastChange = entries |> List.splitAt (List.length entries - 1) + let last = List.head lastChange + + let refChanges, trueChanges = + last.Changes + |> List.partition (fun (change: Change) -> + referenceRegex.Match(change.ChangeText().CleanedText).Success) + + let references = + refChanges + |> List.map (fun change -> + let referenceMatch = referenceRegex.Match(change.ChangeText().CleanedText) + let version = referenceMatch.Groups[1].Value + let uri = referenceMatch.Groups[6].Value + + { SemVer = + if version = "Unreleased" then + UnreleasedRef + else + SemVerRef(SemVer.parse version) + RepoUrl = Uri(uri) }) + + let newLastEntry = + ChangelogEntry.New(last.AssemblyVersion, last.NuGetVersion, trueChanges) + + front @ [ newLastEntry ], references + + Changelog.New(header, description, unreleased, entriesWithoutReferences, references) /// /// Parses a Changelog text file and returns the latest changelog. diff --git a/src/test/Fake.Core.UnitTests/Fake.Core.ReleaseNotes.fs b/src/test/Fake.Core.UnitTests/Fake.Core.ReleaseNotes.fs index 5fe826d8df1..9a25bf72aac 100644 --- a/src/test/Fake.Core.UnitTests/Fake.Core.ReleaseNotes.fs +++ b/src/test/Fake.Core.UnitTests/Fake.Core.ReleaseNotes.fs @@ -2,6 +2,37 @@ module Fake.Core.ReleaseNotesTests open Fake.Core open Expecto +open System + +[] +let private changelogReleasesText = + """# 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.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Changed +- Foo 2 + +## [0.1.0-pre.2] - 2023-10-19 + +### Added +- Foo 1 + +## [0.1.0-pre.1] - 2023-10-11 + +### Added +- Foo 0""" + +[] +let private changelogReferencesText = + """[Unreleased]: https://github.com/bogus/Foo/compare/v0.1.0-pre.2...HEAD +[0.1.0-pre.2]: https://github.com/bogus/Foo/releases/tag/v0.1.0-pre.2 +[0.1.0-pre.1]: https://github.com/bogus/Foo/releases/tag/v0.1.0-pre.1""" [] let tests = @@ -245,4 +276,108 @@ let tests = checkPreRelease releaseNotesLines_case2 (Some "---RC-SNAPSHOT.12.9.1--.12") (Some "788") checkPreRelease releaseNotesLines_case3 (Some "---R-S.12.9.1--.12") (Some "meta") checkPreRelease releaseNotesLines_case4 (None) (Some "0.build.1-rc.10000aaa-kk-0.1") - checkPreRelease releaseNotesLines_case5 (Some "0A.is.legal") (None) ] ] + checkPreRelease releaseNotesLines_case5 (Some "0A.is.legal") (None) ] + + // https://keepachangelog.com + testList + "Changelog" + [ testCase "Test that we can parse changelog without references" + <| fun _ -> + let changelog = changelogReleasesText |> String.splitStr "\n" |> Changelog.parse + + Expect.isEmpty changelog.References "References not empty" + Expect.isSome changelog.Unreleased "Unreleased section empty" + Expect.hasLength changelog.Entries 2 "Wrong number of release entries parsed" + testCase "Test that we can parse changelog with references" + <| fun _ -> + let changelogText = changelogReleasesText + "\n\n" + changelogReferencesText + let changelog = changelogText |> String.splitStr "\n" |> Changelog.parse + + Expect.hasLength changelog.References 3 "Wrong number of references parsed" + + Expect.hasLength + (changelog.References + |> List.filter (fun r -> + match r.SemVer with + | Changelog.SemVerRef (_) -> true + | _ -> false)) + 2 + "Wrong number of released references parsed" + + Expect.hasLength + (changelog.References + |> List.filter (fun r -> + match r.SemVer with + | Changelog.SemVerRef (_) -> false + | _ -> true)) + 1 + "Wrong number of unreleased references parsed" + + Expect.hasLength changelog.References 3 "Wrong number of references parsed" + Expect.isSome changelog.Unreleased "Unreleased section empty" + Expect.hasLength changelog.Entries 2 "Wrong number of release entries parsed" + testCase "Test that references are not in the last changelog entry" + <| fun _ -> + let changelogText = changelogReleasesText + "\n\n" + changelogReferencesText + let changelog = changelogText |> String.splitStr "\n" |> Changelog.parse + let lastEntry = changelog.Entries |> List.last + let lastChanges = lastEntry.Changes + + Expect.isFalse + (lastChanges + |> List.exists (fun change -> + change.ChangeText().CleanedText.Contains("https://github.com/bogus/Foo/"))) + "URL of reference contained in change text" + testCase "Test that a release and reference can be added and correctly turned into a string" + <| fun _ -> + let changelogText = changelogReleasesText + "\n\n" + changelogReferencesText + let changelog = changelogText |> String.splitStr "\n" |> Changelog.parse + let versionText = "0.1.0-pre.3" + let semVerInfo = SemVer.parse versionText + + let newUnreleasedRef = + { Changelog.Reference.SemVer = Changelog.UnreleasedRef + Changelog.Reference.RepoUrl = Uri("https://github.com/bogus/Foo/compare/v0.1.0-pre.3...HEAD") } + + let releasedRefs = + changelog.References + |> List.filter (fun r -> + match r.SemVer with + | Changelog.SemVerRef (_) -> true + | _ -> false) + + let newReference = + { Changelog.Reference.SemVer = Changelog.SemVerRef(semVerInfo) + Changelog.Reference.RepoUrl = Uri("https://github.com/bogus/Foo/releases/tag/v0.1.0-pre.3") } + + let newFixed = + Changelog.Fixed( + { CleanedText = "Foo 3" + OriginalText = None } + ) + + let newReleaseEntry = + Changelog.ChangelogEntry.New( + "", + versionText, + Some(DateTime(2023, 11, 23)), + None, + [ newFixed ], + false + ) + + let changelogNew = + { changelog with + Entries = newReleaseEntry :: changelog.Entries + References = [ newUnreleasedRef; newReference ] @ releasedRefs } + + let expectedEnd = + """[Unreleased]: https://github.com/bogus/Foo/compare/v0.1.0-pre.3...HEAD +[0.1.0-pre.3]: https://github.com/bogus/Foo/releases/tag/v0.1.0-pre.3 +[0.1.0-pre.2]: https://github.com/bogus/Foo/releases/tag/v0.1.0-pre.2 +[0.1.0-pre.1]: https://github.com/bogus/Foo/releases/tag/v0.1.0-pre.1""" + + Expect.stringEnds + (changelogNew.ToString()) + expectedEnd + "Invalid references at end of changelog text" ] ]