src/rebar3_hex_cut.erl

%% @doc `rebar3 hex cut' - Publish with git tag and version bump features 
%%
%% Increments tags and publishes.
%%
%% Incrementing and tagging are both optional features of this provider. 
%%
%% By default you'll be prompted what type of increment to make to the version or it can be supplied as an argument to
%% the command `-i', `--increment' switch. 
%%
%% `cut' will also optionally create version bump commit, create a tag with a name corresponding to the new version, and
%%  prompt you if you'd like to push the tag up to your git repository. 
%%
%% 
%% Below is a full example of `cut' in action : 
%%
%% ```
%% rebar3 hex cut
%% ===> Analyzing applications...
%% ===> Verifying dependencies...
%% Select semver increment or other (Current 0.1.3):
%% 1) patch
%% 2) minor
%% 3) major
%% 4) other
%% [1-4] > 1
%% Create 'v0.1.4' commit? ("Y")>
%% Push master to origin master? ("N")>
%% Local Password:
%% Publishing myapp 0.1.4 to hexpm:myrepo
%% Description: My Application
%% Dependencies:
%%   relx ~>4.5.0
%% Included files:
%%   LICENSE
%%   README.md
%%   rebar.config
%%   src/myapp.app.src
%%   src/myapp.erl
%% Licenses: Apache-2.0
%% Links:
%%   github: https://github.com/myname/myapp
%% Build tools: rebar3
%% Before publishing, please read Hex CoC: https://hex.pm/policies/codeofconduct
%% Proceed (with warnings)? ("Y")>
%% ===> Published myapp 0.1.4
%% ===> Analyzing applications...
%% ===> Compiling myapp
%% ===> Running edoc for myapp
%% ===> Running ex_doc for myapp
%% ===> Published docs for myapp 0.1.4
%% Create new git tag v0.1.4? ("Y")>
%% ===> Creating new tag v0.1.4...
%% Push new tag to origin? ("Y")>
%% ===> Pushing new tag v0.1.4...
%% '''
%% 
%% <ul>
%%  <li>`--repo' - Specify the repository to work with. This option is required when 
%%      you have multiple repositories configured, including organizations. The argument must 
%%      be a fully qualified repository name (e.g, `hexpm', `hexpm:my_org', `my_own_hexpm').
%%      Defaults to `hexpm'.
%%   </li>
%%   <li>`-i', `--increment' - Specify the type of version increment to perform without being prompted at runtime. 
%%       Supported types are : 
%%          <ul>
%%              <li><b>major</b></li>
%%              <li><b>minor</b></li>
%%              <li><b>patch</b></li>
%%          </ul>
%%  </li>
%% </ul>
-module(rebar3_hex_cut).

-export([init/1,
         do/1,
         format_error/1]).

-include("rebar3_hex.hrl").

-define(PROVIDER, cut).
-define(DEPS, [{default, lock}]).

%% ===================================================================
%% Public API
%% ===================================================================

%% @private
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
    Provider = providers:create([
                                {name, ?PROVIDER},
                                {module, ?MODULE},
                                {namespace, hex},
                                {bare, true},
                                {deps, ?DEPS},
                                {example, "rebar3 hex cut"},
                                {short_desc, "Increment version number and publish package"},
                                {desc, ""},
                                {opts, [{increment, $i, "increment", {string, undefined},
                                         "Type of semver increment: major, minor or patch"},
                                        rebar3_hex:repo_opt()]}
                                ]),
    State1 = rebar_state:add_provider(State, Provider),
    {ok, State1}.


%% @private
-spec do(rebar_state:t()) -> {ok, rebar_state:t()}.
do(State) ->
    case rebar3_hex:task_state(State) of
        {ok, Task} ->
            handle_task(Task);
        {error, Reason} ->
            ?RAISE(Reason)
    end.

%% default path - maybe tag, maybe commit, maybe push commit,  publish, maybe push tag

handle_task(#{args := Args} = Task) ->
    #{repo := Repo, state := State, apps := Apps} = Task,
    Selected = rebar3_hex_io:select_apps(Apps),
    lists:foreach(fun(App) -> cut(State, Repo, App, Args) end, Selected),
    {ok, State}.

%% @private
-spec format_error(any()) -> iolist().
format_error({no_write_key, RepoName}) ->
    io_lib:format("No api key with permissions to write to the repository ~ts was found.", [RepoName]);
format_error({bad_increment, Type}) ->
    io_lib:format("Increment must be major, minor or patch. ~s is not valid.", [Type]);
format_error({invalid_semver, Version}) ->
    Err = "non-semantic version number \"~ts\" found",
    io_lib:format(Err, [Version]);
format_error(Reason) ->
    rebar3_hex_error:format_error(Reason).

%% ===================================================================
%% Public API
%% ===================================================================

cut(State, Repo, App, #{} = Args) ->
    {Version, ResolvedVersion} = version_info(State, App),
    MaybeType = maps:get(increment, Args, undefined),
    Type = get_increment(MaybeType, ResolvedVersion),
    NewVersion = increment_version(Type, ResolvedVersion),
    AppSrcFile = rebar_app_info:app_file_src(App),

    case Version of
        _Git when Version =:= git orelse Version =:= "git" ->
            create_tag(NewVersion),

            case try_publish(State, Repo, App, Args) of
                {ok, _State} ->
                    case rebar3_hex_io:ask("Push new tag to origin?", boolean, "Y") of
                        true ->
                            maybe_push_tag(Version),
                            {ok, State};
                        false ->
                            {ok, State}
                    end;
                _ ->
                    delete_tag(NewVersion),
                   {ok, State}
            end;
        _ ->
            Spec = rebar3_hex_file:update_app_src(App, NewVersion),
            NewAppSrcFile = io_lib:format("~tp.\n", [Spec]),
            ok = rebar_file_utils:write_file_if_contents_differ(AppSrcFile, NewAppSrcFile),
            ask_commit_and_push(NewVersion),
            case try_publish(State, Repo, rebar_app_info:original_vsn(App, NewVersion), Args) of
                {ok, _} ->
                    maybe_create_and_push_tag(NewVersion),
                    {ok, State};
                {error, publish_failed} ->
                    delete_tag(NewVersion),
                    {ok, State}
            end

    end.

maybe_create_and_push_tag(Version) ->
    PromptStr = io_lib:format("Create new git tag v~s?", [Version]),
    case rebar3_hex_io:ask(PromptStr, boolean, "Y") of
        true ->
            create_tag(Version),
            maybe_push_tag(Version);
        false ->
            ok
    end.

create_tag(Version) ->
    rebar_api:info("Creating new tag v~s...", [Version]),
    rebar_utils:sh(io_lib:format("git tag v~s", [Version]), []).

maybe_push_tag(Version) ->
    case rebar3_hex_io:ask("Push new tag to origin?", boolean, "Y") of
        true ->
            push_tag(Version);
        false ->
            ok
    end.

push_tag(Version) ->
    rebar_api:info("Pushing new tag v~s...", [Version]),
    rebar_utils:sh(io_lib:format("git push origin v~s", [Version]), []).

delete_tag(Version) ->
    rebar_api:info("Deleting new tag v~s...", [Version]),
    rebar_utils:sh(io_lib:format("git tag -d v~s", [Version]), []).

try_publish(State, Repo, App, Args) ->
 try rebar3_hex_publish:publish(State, Repo, App, Args) of
    {ok, _} ->
        {ok, State}
    catch
        error:{error, {rebar3_hex_publish, Err}} ->
        ErrStr  = rebar3_hex_publish:format_error(Err),
        rebar_api:error(ErrStr, []),
        {error, publish_failed}
  end.

increment_version(Type, VersionStr) when is_list(VersionStr) orelse is_binary(VersionStr) ->
    Parsed = parse_version(VersionStr),
    increment_version(Type, Parsed);

increment_version(Type, Version) ->
    rebar3_hex_version:format(rebar3_hex_version:increment(Type, Version)).

get_increment(undefined, Version) ->
    rebar3_hex_io:say("Select semver increment or other (Current ~s):", [Version]),
    rebar3_hex_io:say("1) patch", []),
    rebar3_hex_io:say("2) minor", []),
    rebar3_hex_io:say("3) major", []),
    rebar3_hex_io:say("4) other", []),
    case rebar3_hex_io:ask("[1-4] ", number) of
        4 ->
            rebar3_hex_io:ask("New Version ", string);
        TypeInt ->
            case int_to_increment(TypeInt) of
                error ->
                    rebar_api:error("Invalid number given, try again~n", []),
                    get_increment(undefined, Version);
                Type ->
                    Type
            end
    end;

get_increment(Type, _) -> normalize_increment(Type).

parse_version(Version) ->
    case rebar3_hex_version:parse(Version) of
        {ok, Parsed} ->
            Parsed;
        _Err ->
         ?RAISE({invalid_semver, Version})
    end.

int_to_increment(1) -> patch;
int_to_increment(2) -> minor;
int_to_increment(3) -> major;
int_to_increment(_) -> error.

version_info(State, App) ->
    Version = rebar_app_info:original_vsn(App),
    Resolved = rebar3_hex_app:vcs_vsn(State, App),
    {Version, Resolved}.

normalize_increment("patch") -> patch;
normalize_increment("minor") -> minor;
normalize_increment("major") -> major;
normalize_increment(_) -> error.

ask_commit_and_push(NewVersion) ->
    case rebar3_hex_io:ask(io_lib:format("Create 'v~s' commit?", [NewVersion]), boolean, "Y") of
        true ->
            rebar_utils:sh(io_lib:format("git commit -a -m 'v~s'", [NewVersion]), []),
            case rebar3_hex_io:ask("Push master to origin master?", boolean, "N") of
                true ->
                    rebar_utils:sh("git push origin master:master", []);
                false ->
                    ok
            end;
        false ->
            ok
    end.