Skip to main content

src/version_bump@note.erl

-module(version_bump@note).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/version_bump/note.gleam").
-export([generate/3]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " Generate Markdown release notes from parsed conventional commits.\n"
    "\n"
    " Commits are grouped into sections by type (Features, Bug Fixes,\n"
    " Performance) plus a dedicated \"BREAKING CHANGES\" section that lists the\n"
    " breaking-change note text for any breaking commit. Empty sections are\n"
    " omitted. All functions here are PURE — given the same input they always\n"
    " produce the same Markdown string.\n"
).

-file("src/version_bump/note.gleam", 118).
?DOC(
    " Extract the text of the first `BREAKING CHANGE`/`BREAKING CHANGES` note,\n"
    " or \"\" when there is no such note.\n"
).
-spec breaking_text(list(version_bump@commit_parser:commit_note())) -> binary().
breaking_text(Notes) ->
    Found = gleam@list:find(
        Notes,
        fun(N) ->
            Title = string:uppercase(erlang:element(2, N)),
            (Title =:= <<"BREAKING CHANGE"/utf8>>) orelse (Title =:= <<"BREAKING CHANGES"/utf8>>)
        end
    ),
    case Found of
        {ok, Note} ->
            erlang:element(3, Note);

        {error, _} ->
            <<""/utf8>>
    end.

-file("src/version_bump/note.gleam", 103).
?DOC(" Render a single breaking-change bullet using the note text when present.\n").
-spec breaking_line(version_bump@commit_parser:conventional_commit()) -> binary().
breaking_line(C) ->
    Text = breaking_text(erlang:element(7, C)),
    Body = case Text of
        <<""/utf8>> ->
            erlang:element(5, C);

        _ ->
            Text
    end,
    Scope_prefix = case erlang:element(4, C) of
        {some, S} ->
            <<<<"**"/utf8, S/binary>>/binary, ":** "/utf8>>;

        none ->
            <<""/utf8>>
    end,
    <<<<"* "/utf8, Scope_prefix/binary>>/binary, Body/binary>>.

-file("src/version_bump/note.gleam", 91).
?DOC(
    " Build the BREAKING CHANGES section from any breaking commits. The breaking\n"
    " note text is preferred; if a breaking commit has no `BREAKING CHANGE` note,\n"
    " its subject is used as a fallback. Returns `Error(Nil)` when none break.\n"
).
-spec breaking_section(list(version_bump@commit_parser:conventional_commit())) -> {ok,
        binary()} |
    {error, nil}.
breaking_section(Commits) ->
    Breaking = gleam@list:filter(Commits, fun(C) -> erlang:element(6, C) end),
    case Breaking of
        [] ->
            {error, nil};

        _ ->
            Lines = gleam@list:map(Breaking, fun breaking_line/1),
            {ok,
                <<"### BREAKING CHANGES\n\n"/utf8,
                    (gleam@string:join(Lines, <<"\n"/utf8>>))/binary>>}
    end.

-file("src/version_bump/note.gleam", 44).
?DOC(" Keep only commits whose conventional type matches `type_name`.\n").
-spec filter_type(
    list(version_bump@commit_parser:conventional_commit()),
    binary()
) -> list(version_bump@commit_parser:conventional_commit()).
filter_type(Commits, Type_name) ->
    gleam@list:filter(Commits, fun(C) -> case erlang:element(3, C) of
                {some, T} ->
                    T =:= Type_name;

                none ->
                    false
            end end).

-file("src/version_bump/note.gleam", 75).
?DOC(
    " Render a single commit as a Markdown bullet:\n"
    "   `* **scope:** subject (shorthash)`\n"
    " When there is no scope the `**scope:**` prefix is omitted:\n"
    "   `* subject (shorthash)`\n"
).
-spec commit_line(version_bump@commit_parser:conventional_commit()) -> binary().
commit_line(C) ->
    Scope_prefix = case erlang:element(4, C) of
        {some, S} ->
            <<<<"**"/utf8, S/binary>>/binary, ":** "/utf8>>;

        none ->
            <<""/utf8>>
    end,
    Short = erlang:element(3, erlang:element(2, C)),
    Hash_suffix = case Short of
        <<""/utf8>> ->
            <<""/utf8>>;

        _ ->
            <<<<" ("/utf8, Short/binary>>/binary, ")"/utf8>>
    end,
    <<<<<<"* "/utf8, Scope_prefix/binary>>/binary,
            (erlang:element(5, C))/binary>>/binary,
        Hash_suffix/binary>>.

-file("src/version_bump/note.gleam", 58).
?DOC(
    " Build a non-breaking section (Features / Bug Fixes / Performance). Returns\n"
    " `Error(Nil)` when there are no commits so the caller can omit it.\n"
).
-spec section(binary(), list(version_bump@commit_parser:conventional_commit())) -> {ok,
        binary()} |
    {error, nil}.
section(Heading, Commits) ->
    case Commits of
        [] ->
            {error, nil};

        _ ->
            Lines = gleam@list:map(Commits, fun commit_line/1),
            {ok,
                <<<<Heading/binary, "\n\n"/utf8>>/binary,
                    (gleam@string:join(Lines, <<"\n"/utf8>>))/binary>>}
    end.

-file("src/version_bump/note.gleam", 19).
?DOC(
    " Generate the full Markdown release notes for the given commits and version.\n"
    "\n"
    " `repo_url` is currently accepted for future link generation; the output is\n"
    " stable regardless of whether it is provided. Sections that have no matching\n"
    " commits are omitted entirely.\n"
).
-spec generate(
    list(version_bump@commit_parser:conventional_commit()),
    binary(),
    gleam@option:option(binary())
) -> binary().
generate(Commits, Version, Repo_url) ->
    _ = Repo_url,
    Sections = begin
        _pipe = [section(
                <<"### Features"/utf8>>,
                filter_type(Commits, <<"feat"/utf8>>)
            ),
            section(
                <<"### Bug Fixes"/utf8>>,
                filter_type(Commits, <<"fix"/utf8>>)
            ),
            section(
                <<"### Performance"/utf8>>,
                filter_type(Commits, <<"perf"/utf8>>)
            ),
            breaking_section(Commits)],
        gleam@list:filter_map(_pipe, fun(S) -> S end)
    end,
    Heading = <<"## "/utf8, Version/binary>>,
    case Sections of
        [] ->
            Heading;

        _ ->
            <<<<Heading/binary, "\n\n"/utf8>>/binary,
                (gleam@string:join(Sections, <<"\n\n"/utf8>>))/binary>>
    end.