-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=<pretty_format>` 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} ->
<<Ref/binary, "..HEAD"/utf8>>;
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"
" `<src>:<dst>` 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 -- <paths>`).\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
).