-module(version_bump@git). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/version_bump/git.gleam"). -export([parse_log/1, log_since/2, get_tags/1, current_branch/1, head_sha/1, list_branches/1, create_tag/3, push/3, stage/2, commit/4, get_remote_url/2]). -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( " Git access by shelling out to the `git` executable via `shellout`.\n" "\n" " `parse_log` is the only PURE function here — it decodes a custom\n" " `--pretty` format that delimits fields with the ASCII unit separator\n" " (`\\u{1f}`) and records with the ASCII record separator (`\\u{1e}`). Those\n" " control characters never appear in normal commit text, so the parse is\n" " unambiguous without any quoting/escaping.\n" ). -file("src/version_bump/git.gleam", 45). ?DOC( " Parse a single commit record. Returns `Error(Nil)` for blank/short records\n" " so `filter_map` can drop them cleanly.\n" ). -spec parse_record(binary()) -> {ok, version_bump@commit_parser:commit()} | {error, nil}. parse_record(Record) -> case gleam@string:trim(Record) of <<""/utf8>> -> {error, nil}; _ -> case gleam@string:split(Record, <<"\x{1f}"/utf8>>) of [Hash, Short_hash, Subject, Body, Author_name, Author_email, Date] -> {ok, {commit, gleam@string:trim(Hash), gleam@string:trim(Short_hash), Subject, gleam@string:trim(Body), Author_name, Author_email, gleam@string:trim(Date)}}; _ -> {error, nil} end end. -file("src/version_bump/git.gleam", 37). ?DOC( " Parse the raw output of `git log --pretty=` into commits.\n" "\n" " PURE: no IO. Splits on the record separator, then each record on the unit\n" " separator. Records with the wrong number of fields (including the empty\n" " trailing record produced by the final record separator) are skipped.\n" ). -spec parse_log(binary()) -> list(version_bump@commit_parser:commit()). parse_log(Raw) -> _pipe = Raw, _pipe@1 = gleam@string:split(_pipe, <<"\x{1e}"/utf8>>), gleam@list:filter_map(_pipe@1, fun parse_record/1). -file("src/version_bump/git.gleam", 214). ?DOC(" Run a `git` subcommand in `cwd`, mapping the failure tuple to a `GitError`.\n"). -spec run(binary(), list(binary())) -> {ok, binary()} | {error, version_bump@error:release_error()}. run(Cwd, Args) -> _pipe = shellout:command(<<"git"/utf8>>, Args, Cwd, []), gleam@result:map_error( _pipe, fun(Failure) -> {Code, Message} = Failure, {git_error, <<<<<<<<<<"`git "/utf8, (gleam@string:join(Args, <<" "/utf8>>))/binary>>/binary, "` failed (exit "/utf8>>/binary, (erlang:integer_to_binary(Code))/binary>>/binary, "): "/utf8>>/binary, (gleam@string:trim(Message))/binary>>} end ). -file("src/version_bump/git.gleam", 70). ?DOC( " Run `git log [from..HEAD]` with our pretty format and parse the result.\n" "\n" " When `from` is `None` the entire history reachable from `HEAD` is returned;\n" " otherwise only commits in the `from..HEAD` range (i.e. reachable from HEAD\n" " but not from `from`).\n" ). -spec log_since(binary(), gleam@option:option(binary())) -> {ok, list(version_bump@commit_parser:commit())} | {error, version_bump@error:release_error()}. log_since(Cwd, From) -> Range = case From of {some, Ref} -> <>; none -> <<"HEAD"/utf8>> end, gleam@result:map( run( Cwd, [<<"log"/utf8>>, <<"--pretty="/utf8, "%H\x{1f}%h\x{1f}%s\x{1f}%b\x{1f}%an\x{1f}%ae\x{1f}%cI\x{1e}"/utf8>>, Range] ), fun(Raw) -> parse_log(Raw) end ). -file("src/version_bump/git.gleam", 230). ?DOC(" Split text into lines, dropping any that are empty after trimming.\n"). -spec nonempty_lines(binary()) -> list(binary()). nonempty_lines(Raw) -> _pipe = Raw, _pipe@1 = gleam@string:split(_pipe, <<"\n"/utf8>>), _pipe@2 = gleam@list:map(_pipe@1, fun gleam@string:trim/1), gleam@list:filter(_pipe@2, fun(Line) -> Line /= <<""/utf8>> end). -file("src/version_bump/git.gleam", 83). ?DOC(" List all tags in the repository.\n"). -spec get_tags(binary()) -> {ok, list(binary())} | {error, version_bump@error:release_error()}. get_tags(Cwd) -> gleam@result:map( run(Cwd, [<<"tag"/utf8>>]), fun(Raw) -> nonempty_lines(Raw) end ). -file("src/version_bump/git.gleam", 89). ?DOC(" The name of the currently checked-out branch.\n"). -spec current_branch(binary()) -> {ok, binary()} | {error, version_bump@error:release_error()}. current_branch(Cwd) -> gleam@result:map( run( Cwd, [<<"rev-parse"/utf8>>, <<"--abbrev-ref"/utf8>>, <<"HEAD"/utf8>>] ), fun(Raw) -> gleam@string:trim(Raw) end ). -file("src/version_bump/git.gleam", 95). ?DOC(" The full SHA of `HEAD`.\n"). -spec head_sha(binary()) -> {ok, binary()} | {error, version_bump@error:release_error()}. head_sha(Cwd) -> gleam@result:map( run(Cwd, [<<"rev-parse"/utf8>>, <<"HEAD"/utf8>>]), fun(Raw) -> gleam@string:trim(Raw) end ). -file("src/version_bump/git.gleam", 102). ?DOC( " List local and remote-tracking branch names, with the leading `origin/`\n" " (or any other remote prefix is preserved as-is) and decorations stripped.\n" ). -spec list_branches(binary()) -> {ok, list(binary())} | {error, version_bump@error:release_error()}. list_branches(Cwd) -> gleam@result:map( run( Cwd, [<<"branch"/utf8>>, <<"--all"/utf8>>, <<"--format=%(refname:short)"/utf8>>] ), fun(Raw) -> _pipe = Raw, _pipe@1 = nonempty_lines(_pipe), gleam@list:filter( _pipe@1, fun(Name) -> not gleam_stdlib:contains_string(Name, <<" -> "/utf8>>) end ) end ). -file("src/version_bump/git.gleam", 120). ?DOC( " Create an annotated tag at `HEAD`. The committer identity is set per-command\n" " (an annotated tag records a tagger, which git refuses to invent) so this works\n" " on a bare CI runner with no `user.name`/`user.email` configured — matching how\n" " the `git` plugin's commit sets its identity.\n" ). -spec create_tag(binary(), binary(), binary()) -> {ok, nil} | {error, version_bump@error:release_error()}. create_tag(Cwd, Tag, Message) -> gleam@result:map( run( Cwd, [<<"-c"/utf8>>, <<"user.name=version_bump"/utf8>>, <<"-c"/utf8>>, <<"user.email=version_bump@users.noreply.github.com"/utf8>>, <<"tag"/utf8>>, <<"-a"/utf8>>, Tag, <<"-m"/utf8>>, Message] ), fun(_) -> nil end ). -file("src/version_bump/git.gleam", 143). ?DOC( " Push a single ref to a remote. `ref` may be a tag name, a branch name, or a\n" " `:` refspec such as `HEAD:main`.\n" ). -spec push(binary(), binary(), binary()) -> {ok, nil} | {error, version_bump@error:release_error()}. push(Cwd, Remote, Ref) -> gleam@result:map( run(Cwd, [<<"push"/utf8>>, Remote, Ref]), fun(_) -> nil end ). -file("src/version_bump/git.gleam", 153). ?DOC(" Stage the given paths (`git add -- `).\n"). -spec stage(binary(), list(binary())) -> {ok, nil} | {error, version_bump@error:release_error()}. stage(Cwd, Paths) -> gleam@result:map( run(Cwd, lists:append([<<"add"/utf8>>, <<"--"/utf8>>], Paths)), fun(_) -> nil end ). -file("src/version_bump/git.gleam", 190). ?DOC( " True when there are staged changes. `git diff --staged --quiet` exits 0 when\n" " the index matches HEAD (nothing staged) and non-zero when it differs.\n" ). -spec has_staged_changes(binary()) -> boolean(). has_staged_changes(Cwd) -> case shellout:command( <<"git"/utf8>>, [<<"diff"/utf8>>, <<"--staged"/utf8>>, <<"--quiet"/utf8>>], Cwd, [] ) of {ok, _} -> false; {error, _} -> true end. -file("src/version_bump/git.gleam", 163). ?DOC( " Commit the staged changes with `message`, attributing the commit to the given\n" " identity (set per-command so a release works even when git's `user.name`/\n" " `user.email` aren't configured, e.g. on a fresh CI runner). When nothing is\n" " staged this is a successful no-op, so a release with no file changes to commit\n" " doesn't fail.\n" ). -spec commit(binary(), binary(), binary(), binary()) -> {ok, nil} | {error, version_bump@error:release_error()}. commit(Cwd, Message, Committer_name, Committer_email) -> case has_staged_changes(Cwd) of false -> {ok, nil}; true -> gleam@result:map( run( Cwd, [<<"-c"/utf8>>, <<"user.name="/utf8, Committer_name/binary>>, <<"-c"/utf8>>, <<"user.email="/utf8, Committer_email/binary>>, <<"commit"/utf8>>, <<"-m"/utf8>>, Message] ), fun(_) -> nil end ) end. -file("src/version_bump/git.gleam", 205). ?DOC(" Resolve the fetch URL configured for a remote.\n"). -spec get_remote_url(binary(), binary()) -> {ok, binary()} | {error, version_bump@error:release_error()}. get_remote_url(Cwd, Remote) -> gleam@result:map( run(Cwd, [<<"remote"/utf8>>, <<"get-url"/utf8>>, Remote]), fun(Raw) -> gleam@string:trim(Raw) end ).