-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 {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 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, <<<<<>/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.