Skip to content

Commit

Permalink
feat: remove custom config file in favor of using CLI arguments to co…
Browse files Browse the repository at this point in the history
…nfigure `--github-repo` + make the generator more opinionated by only allowing Breaking Change, Feat and Fix to be included in the changelog
  • Loading branch information
MangelMaxime committed Nov 18, 2024
1 parent 8a56b29 commit cf8c5a3
Show file tree
Hide file tree
Showing 17 changed files with 207 additions and 261 deletions.
7 changes: 2 additions & 5 deletions src/Commands/Generate.fs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type GenerateCommand() =
result {
let! config = ConfigLoader.tryLoadConfig settings.Config
// Apply automatic resolution of remote config if needed
let! config = Verify.resolveRemoteConfig config
let! remoteConfig = Verify.resolveRemoteConfig settings

let! changelogInfo = Changelog.load settings
do! Verify.dirty settings
Expand All @@ -33,10 +33,7 @@ type GenerateCommand() =
| NoVersionBumpRequired -> Log.success "No version bump required."
| BumpRequired bumpInfo ->
let newChangelogContent =
Changelog.updateWithNewVersion
config.ChangelogGenConfig
bumpInfo
changelogInfo
Changelog.updateWithNewVersion remoteConfig bumpInfo changelogInfo

if settings.DryRun then
Log.info "Dry run enabled, not writing to file."
Expand Down
14 changes: 2 additions & 12 deletions src/ConfigLoader.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,14 @@ open EasyBuild.ChangelogGen.Types
type Config =
{
CommitParserConfig: CommitParserConfig
ChangelogGenConfig: ChangelogGenConfig
}

static member Decoder: Decoder<Config> =
Decode.object (fun get ->
{
CommitParserConfig = get.Required.Raw CommitParserConfig.decoder
ChangelogGenConfig =
get.Optional.Field "changelog-gen" ChangelogGenConfig.Decoder
|> Option.defaultValue ChangelogGenConfig.Default
}
{ CommitParserConfig = get.Required.Raw CommitParserConfig.decoder }
)

static member Default =
{
CommitParserConfig = CommitParserConfig.Default
ChangelogGenConfig = ChangelogGenConfig.Default
}
static member Default = { CommitParserConfig = CommitParserConfig.Default }

let tryLoadConfig (configFile: string option) : Result<Config, string> =
let configFile =
Expand Down
151 changes: 85 additions & 66 deletions src/Generate/Changelog.fs
Original file line number Diff line number Diff line change
Expand Up @@ -91,98 +91,117 @@ let tryFindAdditionalChangelogContent (text: string) : string list list =
let private capitalizeFirstLetter (text: string) =
(string text.[0]).ToUpper() + text.[1..]

let generateNewVersionSection (config: ChangelogGenConfig) (releaseContext: BumpInfo) =
let newVersionLines = ResizeArray<string>()
module Literals =

let appendLine (line: string) = newVersionLines.Add(line)
module Type =

let newLine () = newVersionLines.Add("")
[<Literal>]
let BREAKING_CHANGE = "breaking change"

appendLine ($"## %s{releaseContext.NewVersion.ToString()}")
newLine ()
[<Literal>]
let FEAT = "feat"

let groupMap =
config.Groups |> List.map (fun group -> group.Type, group) |> Map.ofList
[<Literal>]
let FIX = "fix"

let dedicatedBreakingChangeGroup =
match Map.tryFind "breaking change" groupMap with
| None -> false
| Some _ -> true
let (|BreakingChange|Feat|Fix|Other|) (commit: EasyBuild.CommitParser.Types.CommitMessage) =
if commit.BreakingChange then
BreakingChange
elif commit.Type = Literals.Type.FEAT then
Feat
elif commit.Type = Literals.Type.FIX then
Fix
else
Other

let rec groupCommits
(acc: Map<string, CommitForRelease list>)
(commits: CommitForRelease list)
=
type GroupedCommits =
{
BreakingChanges: CommitForRelease list
Feats: CommitForRelease list
Fixes: CommitForRelease list
}

let addOrUpdate
(key: string)
(value: CommitForRelease)
(map: Map<string, CommitForRelease list>)
=
match Map.tryFind key map with
| None -> map.Add(key, [ value ])
| Some commits -> map.Add(key, commits @ [ value ])
type Writer() =
let lines = ResizeArray<string>()

match commits with
| [] -> acc
| commit :: rest ->
match Map.tryFind commit.SemanticCommit.Type groupMap with
// This commit type is not to be emitted in the changelog
| None -> groupCommits acc rest
| Some groupInfo ->
let newAcc =
if commit.SemanticCommit.BreakingChange && dedicatedBreakingChangeGroup then
addOrUpdate "breaking change" commit acc
else
addOrUpdate groupInfo.Type commit acc
member _.AppendLine(line: string) = lines.Add(line)

member _.NewLine() = lines.Add("")

groupCommits newAcc rest
member _.ToText() = lines |> String.concat "\n"

let groupedCommits = groupCommits Map.empty releaseContext.CommitsForRelease
let private writeSection
(writer: Writer)
(label: string)
(githubRemote: GithubRemoteConfig)
(commits: CommitForRelease list)
=
if commits.Length > 0 then
writer.AppendLine $"### %s{label}"
writer.NewLine()

match config.Github with
| Some githubRemote ->
config.Groups
|> List.iter (fun groupInfo ->
match Map.tryFind groupInfo.Type groupedCommits with
| Some commits ->
appendLine ($"### %s{groupInfo.Group}")
newLine ()
for commit in commits do
let githubCommitUrl sha =
$"https://github.com/%s{githubRemote.Owner}/%s{githubRemote.Repository}/commit/%s{sha}"

for commit in commits do
let githubCommitUrl sha =
$"https://github.com/%s{githubRemote.Owner}/%s{githubRemote.Repository}/commit/%s{sha}"
let commitUrl = githubCommitUrl commit.OriginalCommit.Hash

let commitUrl = githubCommitUrl commit.OriginalCommit.Hash
let description = capitalizeFirstLetter commit.SemanticCommit.Description

let description = capitalizeFirstLetter commit.SemanticCommit.Description
$"* %s{description} ([%s{commit.OriginalCommit.AbbrevHash}](%s{commitUrl}))"
|> writer.AppendLine

$"* %s{description} ([%s{commit.OriginalCommit.AbbrevHash}](%s{commitUrl}))"
|> appendLine
let additionalChangelogContent =
tryFindAdditionalChangelogContent commit.SemanticCommit.Body

let additionalChangelogContent =
tryFindAdditionalChangelogContent commit.SemanticCommit.Body
for blockLines in additionalChangelogContent do
writer.NewLine()

for blockLines in additionalChangelogContent do
appendLine ""
for line in blockLines do
$" %s{line}" |> _.TrimEnd() |> writer.AppendLine

for line in blockLines do
$" %s{line}" |> _.TrimEnd() |> appendLine
writer.NewLine()

newLine ()
| None -> () // Can happen if there are no commits for this group
)
let generateNewVersionSection (githubRemote: GithubRemoteConfig) (releaseContext: BumpInfo) =
let writer = Writer()

writer.AppendLine $"## %s{releaseContext.NewVersion.ToString()}"
writer.NewLine()

let rec groupCommits (acc: GroupedCommits) (commits: CommitForRelease list) =

match commits with
| [] -> acc
| commit :: rest ->
match commit.SemanticCommit with
| BreakingChange ->
groupCommits { acc with BreakingChanges = commit :: acc.BreakingChanges } rest
| Feat -> groupCommits { acc with Feats = commit :: acc.Feats } rest
| Fix -> groupCommits { acc with Fixes = commit :: acc.Fixes } rest
// This commit type is not to be emitted in the changelog
| Other -> groupCommits acc rest

let groupedCommits =
groupCommits
{
BreakingChanges = []
Feats = []
Fixes = []
}
releaseContext.CommitsForRelease

| None -> failwith "Github remote not found"
writeSection writer "🏗️ Breaking changes" githubRemote groupedCommits.BreakingChanges
writeSection writer "🚀 Features" githubRemote groupedCommits.Feats
writeSection writer "🐞 Bug Fixes" githubRemote groupedCommits.Fixes

newVersionLines |> String.concat "\n"
writer.ToText()

let updateWithNewVersion
(config: ChangelogGenConfig)
(githubRemote: GithubRemoteConfig)
(releaseContext: BumpInfo)
(changelogInfo: ChangelogInfo)
=
let newVersionLines = generateNewVersionSection config releaseContext
let newVersionLines = generateNewVersionSection githubRemote releaseContext

let rec removeConsecutiveEmptyLines
(previousLineWasBlank: bool)
Expand Down
4 changes: 4 additions & 0 deletions src/Generate/Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ type GenerateSettings() =
[<Description("Run the command without writing to the changelog file, output the result in STDOUT instead")>]
member val DryRun: bool = false with get, set

[<CommandOption("--github-repo <REPO>")>]
[<Description("GitHub repository name in format 'owner/repo'")>]
member val GitHubRepo: string option = None with get, set

type CommitForRelease =
{
OriginalCommit: Git.Commit
Expand Down
54 changes: 24 additions & 30 deletions src/Generate/Verify.fs
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,29 @@ You can use the --allow-dirty option to allow a dirty repository."""
else
Ok()

let resolveRemoteConfig (config: ConfigLoader.Config) =
match config.ChangelogGenConfig.Github with
| Some _ -> Ok config
let resolveRemoteConfig (settings: GenerateSettings) =
match settings.GitHubRepo with
| Some githubRepo ->
let segments = githubRepo.Split('/') |> Array.toList

match segments with
| [ owner; repo ] ->
({
Owner = owner
Repository = repo
}
: Types.GithubRemoteConfig)
|> Ok
| _ ->
Error $"""Invalid format for --github-repo option, expected format is 'owner/repo'."""

| None ->
match Git.tryFindRemote () with
| Some remote ->
Ok
{ config with
ChangelogGenConfig.Github =
Some
{
Owner = remote.Owner
Repository = remote.Repository
}
}
| None ->
Error
"""Could not resolve the remote repository.
Automatic detection expected URL returned by `git config --get remote.origin.url` to be of the form 'https://hostname/owner/repo.git' or 'git@hostname:owner/repo.git'.
You can also provide the Github owner and repository in the configuration file:
```json
{
"changelog-gen": {
"github": {
"owner": "owner",
"repository": "repo"
}
}
}"""
| Ok remote ->
({
Owner = remote.Owner
Repository = remote.Repository
}
: Types.GithubRemoteConfig)
|> Ok
| Error error -> Error error
13 changes: 11 additions & 2 deletions src/Git.fs
Original file line number Diff line number Diff line change
Expand Up @@ -186,5 +186,14 @@ let tryFindRemote () =
let remoteUrl = remoteStdout.Trim()

match tryGetRemoteFromUrl remoteUrl with
| Some remote -> Some remote
| None -> tryGetRemoteFromSSH remoteUrl
| Some remote -> Ok remote
| None ->
match tryGetRemoteFromSSH remoteUrl with
| Some remote -> Ok remote
| None ->
Error
"""Could not resolve the remote repository.
Automatic detection expects URL returned by `git config --get remote.origin.url` to be of the form 'https://hostname/owner/repo.git' or 'git@hostname:owner/repo.git'.
You can use the --github-repo option to specify the repository manually."""
Loading

0 comments on commit cf8c5a3

Please sign in to comment.