Skip to content

Commit

Permalink
Parse references at the end of a changelog as a separate part of a ch…
Browse files Browse the repository at this point in the history
…angelog. (fsprojects#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
  • Loading branch information
florenzen authored Jan 8, 2025
1 parent 4d5de20 commit 3ee60b4
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 11 deletions.
101 changes: 91 additions & 10 deletions src/app/Fake.Core.ReleaseNotes/Changelog.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -267,6 +277,32 @@ module Changelog =

assemblyVersion, nugetVersion

/// <summary>
/// A version or "Unreleased"
/// </summary>
type ReferenceVersion =
| SemVerRef of SemVerInfo
| UnreleasedRef

override x.ToString() =
match x with
| SemVerRef (semVerInfo) -> semVerInfo.ToString()
| UnreleasedRef -> "Unreleased"

/// <summary>
/// A reference from a version to a repository URL (e.g. a tag or a compare link)
/// </summary>
/// <code>
/// [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
/// </code>
type Reference =
{ SemVer: ReferenceVersion
RepoUrl: Uri }

override x.ToString() =
sprintf "[%s]: %s" (x.SemVer.ToString()) (x.RepoUrl.ToString())

/// <summary>
/// Holds data for a changelog file, which include changelog entries an other metadata
/// </summary>
Expand All @@ -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 =
Expand All @@ -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 =
Expand Down Expand Up @@ -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

Expand All @@ -358,8 +401,9 @@ module Changelog =
/// <param name="description">the descriptive text for changelog</param>
/// <param name="unreleased">the unreleased list of changelog entries</param>
/// <param name="entries">the list of changelog entries</param>
let createWithCustomHeader header description unreleased entries =
Changelog.New(header, description, unreleased, entries)
/// <param name="references">the list of references</param>
let createWithCustomHeader header description unreleased entries references =
Changelog.New(header, description, unreleased, entries, references)

/// <summary>
/// Create a changelog with given data
Expand All @@ -368,16 +412,17 @@ module Changelog =
/// <param name="description">the descriptive text for changelog </param>
/// <param name="unreleased">the unreleased list of changelog entries</param>
/// <param name="entries">the list of changelog entries</param>
let create description unreleased entries =
createWithCustomHeader "Changelog" description unreleased entries
/// <param name="references">the list of references</param>
let create description unreleased entries references =
createWithCustomHeader "Changelog" description unreleased entries references

/// <summary>
/// Create a changelog with given entries and default values for other data including
/// header and description.
/// </summary>
///
/// <param name="entries">the list of changelog entries</param>
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
Expand Down Expand Up @@ -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)

/// <summary>
/// Parses a Changelog text file and returns the latest changelog.
Expand Down
137 changes: 136 additions & 1 deletion src/test/Fake.Core.UnitTests/Fake.Core.ReleaseNotes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@ module Fake.Core.ReleaseNotesTests

open Fake.Core
open Expecto
open System

[<Literal>]
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"""

[<Literal>]
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"""

[<Tests>]
let tests =
Expand Down Expand Up @@ -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" ] ]

0 comments on commit 3ee60b4

Please sign in to comment.