lib/tasks/cmd.ex

defmodule GitHooks.Tasks.Cmd do
  @moduledoc """
  Represents a command that will be executed as a git hook task.

  A command should be configured as `{:cmd, command, opts}`, being `opts` an
  optional configuration.

  For example:

  ```elixir
  config :git_hooks,
    hooks: [
      pre_commit: [
        {:cmd, "ls -lisa", include_hook_args: true}
      ]
    ]
  ```
  """

  @typedoc """
  Represents a `command` to be executed.
  """
  @type t :: %__MODULE__{
          original_command: String.t(),
          command: String.t(),
          args: [any],
          env: [{String.t(), String.t()}],
          git_hook_type: atom,
          result: term
        }

  defstruct [:original_command, :command, :args, :env, :git_hook_type, result: nil]

  @doc """
  Creates a new `cmd` struct that will execute a command.

  This function expects a tuple or triple with `:cmd`, the command to execute
  and the opts.

  ### Options

  * `include_hook_args`: Whether the git options will be passed as argument when
  executing the file. You will need to check [which arguments are being sent by each git hook](https://git-scm.com/docs/githooks).
  * `env`: The environment variables that will be set in the execution context of the file.

  ### Examples

      iex> #{__MODULE__}.new({:cmd, "ls -l", env: [{"var", "test"}], include_hook_args: true}, :pre_commit, ["commit message"])
      %#{__MODULE__}{command: "ls", original_command: "ls -l", args: ["-l", "commit message"], env: [{"var", "test"}], git_hook_type: :pre_commit}

      iex> #{__MODULE__}.new({:cmd, "ls", include_hook_args: false}, :pre_commit, ["commit message"])
      %#{__MODULE__}{command: "ls", original_command: "ls", args: [], env: [], git_hook_type: :pre_commit}

  """
  @spec new(
          {:cmd, command :: String.t(), [any]},
          GitHooks.git_hook_type(),
          GitHooks.git_hook_args()
        ) ::
          __MODULE__.t()
  def new({:cmd, original_command, opts}, git_hook_type, git_hook_args) when is_list(opts) do
    [base_command | args] = String.split(original_command, " ")

    command_args =
      if Keyword.get(opts, :include_hook_args, false) do
        Enum.concat(args, git_hook_args)
      else
        args
      end

    %__MODULE__{
      original_command: original_command,
      command: base_command,
      args: command_args,
      env: Keyword.get(opts, :env, []),
      git_hook_type: git_hook_type
    }
  end
end

defimpl GitHooks.Task, for: GitHooks.Tasks.Cmd do
  alias GitHooks.Config
  alias GitHooks.Tasks.Cmd
  alias GitHooks.Printer

  def run(%Cmd{command: command, args: args, env: env, git_hook_type: git_hook_type} = cmd, _opts) do
    result =
      System.cmd(
        command,
        args,
        into: Config.io_stream(git_hook_type),
        env: env
      )

    Map.put(cmd, :result, result)
  rescue
    error ->
      Map.put(cmd, :result, {"Execution failed: #{inspect(error)}", 1})
  end

  def success?(%Cmd{result: {_result, 0}}), do: true
  def success?(%Cmd{result: _}), do: false

  def print_result(%Cmd{original_command: original_command, result: {_result, 0}} = file) do
    Printer.success("`#{original_command}` was successful")

    file
  end

  def print_result(
        %Cmd{
          git_hook_type: git_hook_type,
          original_command: original_command,
          result: {_result, _code}
        } = file
      ) do
    # if !Config.verbose?(git_hook_type), do: IO.puts(result)

    Printer.error("`#{git_hook_type}`: `#{original_command}` execution failed")

    file
  end
end