-module(version_bump@branch).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/version_bump/branch.gleam").
-export([resolve/3, last_release/3, next_version/4]).
-export_type([branch_type/0, branch/0]).
-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(
" The branching / channel model (MVP).\n"
"\n"
" semantic-release resolves the configured branches against the branches that\n"
" actually exist in the repository, classifying each into one of three kinds:\n"
"\n"
" * `ReleaseBranch` — a normal release line (e.g. `main`, `master`,\n"
" `next`). The first such branch is the \"main\" branch.\n"
" * `MaintenanceBranch` — a branch whose name is a version range such as\n"
" `1.x`, `1.2.x` or `1.x.x`. Releases here are constrained to that range.\n"
" * `PrereleaseBranch` — a branch with a `prerelease` identifier (e.g.\n"
" `beta`, `alpha`). Releases here carry a `-id.N` prerelease suffix.\n"
"\n"
" This module is pure: it never shells out to git. The caller passes in the\n"
" list of branch names discovered in the repository (`git_branches`) and the\n"
" list of tags (`tags`); everything here is string / semver manipulation.\n"
).
-type branch_type() :: release_branch | maintenance_branch | prerelease_branch.
-type branch() :: {branch,
binary(),
branch_type(),
gleam@option:option(binary()),
gleam@option:option(binary()),
gleam@option:option(binary()),
boolean()}.
-file("src/version_bump/branch.gleam", 248).
?DOC(
" Parse a maintenance branch name into a range string.\n"
"\n"
" `1.x` / `1.x.x` -> `\">=1.0.0 <2.0.0\"`, `1.2.x` -> `\">=1.2.0 <1.3.0\"`.\n"
).
-spec parse_maintenance_name(binary()) -> {ok, binary()} | {error, nil}.
parse_maintenance_name(Name) ->
Major_only = case gleam@regexp:from_string(<<"^(\\d+)\\.x(\\.x)?$"/utf8>>) of
{ok, Re} ->
gleam@regexp:scan(Re, Name);
{error, _} ->
[]
end,
Major_minor = case gleam@regexp:from_string(
<<"^(\\d+)\\.(\\d+)\\.x$"/utf8>>
) of
{ok, Re@1} ->
gleam@regexp:scan(Re@1, Name);
{error, _} ->
[]
end,
case {Major_only, Major_minor} of
{[{match, _, [{some, Maj} | _]} | _], _} ->
case gleam_stdlib:parse_int(Maj) of
{ok, M} ->
{ok,
<<<<<<<<">="/utf8,
(erlang:integer_to_binary(M))/binary>>/binary,
".0.0 <"/utf8>>/binary,
(erlang:integer_to_binary(M + 1))/binary>>/binary,
".0.0"/utf8>>};
{error, _} ->
{error, nil}
end;
{_, [{match, _, [{some, Maj@1}, {some, Min}]} | _]} ->
case {gleam_stdlib:parse_int(Maj@1), gleam_stdlib:parse_int(Min)} of
{{ok, M@1}, {ok, N}} ->
{ok,
<<<<<<<<<<<<<<<<">="/utf8,
(erlang:integer_to_binary(
M@1
))/binary>>/binary,
"."/utf8>>/binary,
(erlang:integer_to_binary(N))/binary>>/binary,
".0 <"/utf8>>/binary,
(erlang:integer_to_binary(M@1))/binary>>/binary,
"."/utf8>>/binary,
(erlang:integer_to_binary(N + 1))/binary>>/binary,
".0"/utf8>>};
{_, _} ->
{error, nil}
end;
{_, _} ->
{error, nil}
end.
-file("src/version_bump/branch.gleam", 231).
?DOC(
" If `name` looks like a maintenance range (`N.x`, `N.x.x`, `N.N.x`), return\n"
" the normalised range string. An explicit configured `range` takes priority.\n"
).
-spec maintenance_range(binary(), gleam@option:option(binary())) -> gleam@option:option(binary()).
maintenance_range(Name, Configured) ->
case Configured of
{some, R} ->
{some, R};
none ->
case parse_maintenance_name(Name) of
{ok, Range} ->
{some, Range};
{error, _} ->
none
end
end.
-file("src/version_bump/branch.gleam", 177).
?DOC(" Classify a single configured branch into a resolved `Branch`.\n").
-spec classify(
version_bump@config:branch_config(),
gleam@option:option(binary())
) -> branch().
classify(Bc, Main_name) ->
Is_main = Main_name =:= {some, erlang:element(2, Bc)},
case erlang:element(4, Bc) of
{some, Id} ->
{branch,
erlang:element(2, Bc),
prerelease_branch,
gleam@option:'or'(erlang:element(3, Bc), {some, Id}),
{some, Id},
erlang:element(5, Bc),
false};
none ->
case maintenance_range(erlang:element(2, Bc), erlang:element(5, Bc)) of
{some, Range} ->
{branch,
erlang:element(2, Bc),
maintenance_branch,
gleam@option:'or'(
erlang:element(3, Bc),
{some, erlang:element(2, Bc)}
),
none,
{some, Range},
false};
none ->
{branch,
erlang:element(2, Bc),
release_branch,
erlang:element(3, Bc),
none,
erlang:element(5, Bc),
Is_main}
end
end.
-file("src/version_bump/branch.gleam", 217).
?DOC(
" The name of the first configured branch that is a plain release branch\n"
" (neither prerelease nor maintenance), i.e. the \"main\" branch.\n"
).
-spec first_main_name(list(version_bump@config:branch_config())) -> gleam@option:option(binary()).
first_main_name(Branches) ->
_pipe = Branches,
_pipe@1 = gleam@list:find(_pipe, fun(Bc) -> case erlang:element(4, Bc) of
{some, _} ->
false;
none ->
maintenance_range(
erlang:element(2, Bc),
erlang:element(5, Bc)
)
=:= none
end end),
_pipe@2 = gleam@result:map(
_pipe@1,
fun(Bc@1) -> erlang:element(2, Bc@1) end
),
gleam@option:from_result(_pipe@2).
-file("src/version_bump/branch.gleam", 59).
?DOC(
" Resolve the configured branches against the branches that exist in the\n"
" repository, returning the resolved `current` branch together with every\n"
" resolved branch.\n"
"\n"
" Each `BranchConfig` whose name appears in `git_branches` is classified into\n"
" a `Branch`. Configured branches that do not exist in the repository are\n"
" dropped (they cannot be released against). The `current` branch must be one\n"
" of the resolved branches, otherwise a `ConfigError` is returned.\n"
).
-spec resolve(version_bump@config:config(), list(binary()), binary()) -> {ok,
{branch(), list(branch())}} |
{error, version_bump@error:release_error()}.
resolve(Config, Git_branches, Current) ->
Existing = gleam@list:filter(
erlang:element(4, Config),
fun(Bc) -> gleam@list:contains(Git_branches, erlang:element(2, Bc)) end
),
Main_name = first_main_name(erlang:element(4, Config)),
Resolved = gleam@list:map(
Existing,
fun(Bc@1) -> classify(Bc@1, Main_name) end
),
case gleam@list:find(
Resolved,
fun(B) -> erlang:element(2, B) =:= Current end
) of
{ok, Branch} ->
{ok, {Branch, Resolved}};
{error, _} ->
{error,
{config_error,
<<<<"Current branch '"/utf8, Current/binary>>/binary,
"' is not a configured release branch"/utf8>>}}
end.
-file("src/version_bump/branch.gleam", 355).
?DOC(" The pair with the highest version per semver `compare`.\n").
-spec highest(list({binary(), version_bump@semver:version()})) -> gleam@option:option({binary(),
version_bump@semver:version()}).
highest(Candidates) ->
case Candidates of
[] ->
none;
[First | Rest] ->
{some,
gleam@list:fold(
Rest,
First,
fun(Acc, Cand) ->
case version_bump@semver:compare(
erlang:element(2, Cand),
erlang:element(2, Acc)
) of
gt ->
Cand;
_ ->
Acc
end
end
)}
end.
-file("src/version_bump/branch.gleam", 347).
-spec lt(version_bump@semver:version(), version_bump@semver:version()) -> boolean().
lt(A, B) ->
case version_bump@semver:compare(A, B) of
lt ->
true;
_ ->
false
end.
-file("src/version_bump/branch.gleam", 340).
-spec gte(version_bump@semver:version(), version_bump@semver:version()) -> boolean().
gte(A, B) ->
case version_bump@semver:compare(A, B) of
lt ->
false;
_ ->
true
end.
-file("src/version_bump/branch.gleam", 326).
?DOC(" Parse a `\">=lo <hi\"` range into its two bounds.\n").
-spec parse_range(binary()) -> {ok,
{version_bump@semver:version(), version_bump@semver:version()}} |
{error, nil}.
parse_range(R) ->
case gleam@string:split(gleam@string:trim(R), <<" "/utf8>>) of
[Lo, Hi] ->
Lo_str = gleam@string:replace(Lo, <<">="/utf8>>, <<""/utf8>>),
Hi_str = gleam@string:replace(Hi, <<"<"/utf8>>, <<""/utf8>>),
case {version_bump@semver:parse(Lo_str),
version_bump@semver:parse(Hi_str)} of
{{ok, Low}, {ok, High}} ->
{ok, {Low, High}};
{_, _} ->
{error, nil}
end;
_ ->
{error, nil}
end.
-file("src/version_bump/branch.gleam", 314).
?DOC(
" True when `version` falls inside a maintenance range string of the shape\n"
" `\">=A.B.C <D.E.F\"`. An absent range admits everything.\n"
).
-spec in_range(version_bump@semver:version(), gleam@option:option(binary())) -> boolean().
in_range(Version, Range) ->
case Range of
none ->
true;
{some, R} ->
case parse_range(R) of
{ok, {Low, High}} ->
gte(Version, Low) andalso lt(Version, High);
{error, _} ->
true
end
end.
-file("src/version_bump/branch.gleam", 293).
?DOC(" True when `version` belongs on `branch`.\n").
-spec version_matches_branch(version_bump@semver:version(), branch()) -> boolean().
version_matches_branch(Version, Branch) ->
case erlang:element(3, Branch) of
prerelease_branch ->
case {erlang:element(5, Branch), erlang:element(5, Version)} of
{{some, Id}, [First | _]} ->
First =:= Id;
{_, _} ->
false
end;
maintenance_branch ->
case erlang:element(5, Version) of
[] ->
in_range(Version, erlang:element(6, Branch));
_ ->
false
end;
release_branch ->
erlang:element(5, Version) =:= []
end.
-file("src/version_bump/branch.gleam", 382).
?DOC(
" Extract the version substring from a tag given the format's prefix/suffix.\n"
" Fails when the tag does not start with `prefix` and end with `suffix`.\n"
).
-spec extract_version_string(binary(), binary(), binary()) -> {ok, binary()} |
{error, nil}.
extract_version_string(Tag, Prefix, Suffix) ->
case {gleam_stdlib:string_starts_with(Tag, Prefix),
gleam_stdlib:string_ends_with(Tag, Suffix)} of
{true, true} ->
Without_prefix = gleam@string:drop_start(Tag, string:length(Prefix)),
Core = gleam@string:drop_end(Without_prefix, string:length(Suffix)),
case Core of
<<""/utf8>> ->
{error, nil};
_ ->
{ok, Core}
end;
{_, _} ->
{error, nil}
end.
-file("src/version_bump/branch.gleam", 373).
?DOC(
" Split a tag format like `\"v${version}\"` into its prefix and suffix around\n"
" the `${version}` placeholder. If the placeholder is missing, the whole\n"
" format is treated as a prefix (best effort).\n"
).
-spec tag_format_parts(binary()) -> {binary(), binary()}.
tag_format_parts(Tag_format) ->
case gleam@string:split_once(Tag_format, <<"${version}"/utf8>>) of
{ok, {Prefix, Suffix}} ->
{Prefix, Suffix};
{error, _} ->
{Tag_format, <<""/utf8>>}
end.
-file("src/version_bump/branch.gleam", 97).
?DOC(
" Pick the highest existing release on `branch` from a list of git tags.\n"
"\n"
" Tags are matched against `tag_format` (e.g. `\"v${version}\"`); the embedded\n"
" version is parsed as semver. Only versions compatible with the branch are\n"
" considered:\n"
"\n"
" * `PrereleaseBranch` — only versions whose first prerelease identifier is\n"
" the branch's prerelease id (e.g. `1.0.0-beta.2` on the `beta` branch).\n"
" * `MaintenanceBranch` — only non-prerelease versions inside the branch's\n"
" range (e.g. `1.x` admits `1.*.*`, `1.2.x` admits `1.2.*`).\n"
" * `ReleaseBranch` — only non-prerelease versions.\n"
"\n"
" Returns the highest matching version as a `LastRelease`, or `None` when no\n"
" tag matches.\n"
).
-spec last_release(list(binary()), branch(), binary()) -> gleam@option:option(version_bump@release:last_release()).
last_release(Tags, Branch, Tag_format) ->
{Prefix, Suffix} = tag_format_parts(Tag_format),
Candidates = begin
_pipe = Tags,
gleam@list:filter_map(
_pipe,
fun(Tag) -> case extract_version_string(Tag, Prefix, Suffix) of
{ok, Version_str} ->
case version_bump@semver:parse(Version_str) of
{ok, Version} ->
case version_matches_branch(Version, Branch) of
true ->
{ok, {Tag, Version}};
false ->
{error, nil}
end;
{error, _} ->
{error, nil}
end;
{error, _} ->
{error, nil}
end end
)
end,
case highest(Candidates) of
none ->
none;
{some, {Tag@1, Version@1}} ->
{some,
{last_release,
version_bump@semver:to_string(Version@1),
Tag@1,
<<""/utf8>>,
[erlang:element(4, Branch)]}}
end.
-file("src/version_bump/branch.gleam", 143).
?DOC(
" Compute the next version string for `branch`, given the last release and the\n"
" `ReleaseType` the commits warrant.\n"
"\n"
" With no previous release the first version is `1.0.0` (or `0.1.0` in\n"
" `InitialDevelopment` mode), with the branch's prerelease identifier appended\n"
" on a prerelease branch. Otherwise the last version is bumped by `rtype` — in\n"
" `InitialDevelopment` mode a breaking change is downshifted to a minor bump\n"
" while major is 0 (see `semver.effective_release_type`).\n"
).
-spec next_version(
gleam@option:option(version_bump@release:last_release()),
version_bump@semver:release_type(),
branch(),
version_bump@semver:versioning_mode()
) -> {ok, binary()} | {error, version_bump@error:release_error()}.
next_version(Last, Rtype, Branch, Mode) ->
case Last of
none ->
Base = case Mode of
initial_development ->
<<"0.1.0"/utf8>>;
stable ->
<<"1.0.0"/utf8>>
end,
case erlang:element(5, Branch) of
{some, Id} ->
{ok,
<<<<<<Base/binary, "-"/utf8>>/binary, Id/binary>>/binary,
".1"/utf8>>};
none ->
{ok, Base}
end;
{some, Release} ->
gleam@result:'try'(
version_bump@semver:parse(erlang:element(2, Release)),
fun(Version) ->
Effective = version_bump@semver:effective_release_type(
Version,
Rtype,
Mode
),
case erlang:element(5, Branch) of
{some, Id@1} ->
{ok,
version_bump@semver:to_string(
version_bump@semver:bump_with_prerelease(
Version,
Effective,
Id@1
)
)};
none ->
{ok,
version_bump@semver:to_string(
version_bump@semver:bump(Version, Effective)
)}
end
end
)
end.