lib/tasks/mix.ex

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

  A mix task should be configured as `{:mix_task, task_name, task_args}`,
  being `task_args` an optional configuration. See `#{__MODULE__}.new/1` for
  more information.

  For example:

  ```elixir
  config :git_hooks,
    hooks: [
      pre_commit: [
        {:mix_task, :test},
        {:mix_task, :format, ["--dry-run"]}
      ]
    ]
  ```

  See `https://hexdocs.pm/mix/Mix.Task.html#run/2` for reference.
  """

  defstruct [:task, args: [], result: nil]

  @typedoc """
  Represents a Mix task.
  """
  @type t :: %__MODULE__{
          task: Mix.Task.task_name(),
          args: [any]
        }

  @doc """
  Creates a new Mix task struct.

  This function expects a tuple or triple with `:mix_task`, the task name and
  the task args.

  ### Examples

      iex> #{__MODULE__}.new({:mix_task, :test, ["--failed"]})
      %#{__MODULE__}{task: :test, args: ["--failed"]}

  """
  @spec new({:mix_task, Mix.Task.task_name(), [any]} | Mix.Task.task_name()) :: __MODULE__.t()
  def new({:mix_task, task, args}) do
    %__MODULE__{
      task: task,
      args: args
    }
  end
end

defimpl GitHooks.Task, for: GitHooks.Tasks.Mix do
  alias GitHooks.Tasks.Mix, as: MixTask
  alias GitHooks.Printer

  # Mix tasks raise an error if they are valid, but determining if they are
  # success or not depends on the return of the task.
  # @default_success_results [0, :ok, nil, {:ok, []}, {:noop, []}]
  @default_success_results [0, :ok, nil]

  @success_results GitHooks.Config.extra_success_returns() ++ @default_success_results

  def run(%MixTask{task: :test, args: args} = mix_task, _opts) do
    args = ["test" | args] ++ ["--color"]

    {_, result} =
      System.cmd(
        "mix",
        args,
        into: IO.stream(:stdio, :line)
      )

    Map.put(mix_task, :result, result)
  end

  def run(%MixTask{task: task, args: args} = mix_task, _opts) do
    result = Mix.Task.run(task, args)

    Map.put(mix_task, :result, result)
  end

  def success?(%MixTask{result: result}) when result in @success_results, do: true
  def success?(%MixTask{result: _result}), do: false

  def print_result(%MixTask{task: task, result: result} = mix_task) do
    case result do
      result when result in @success_results ->
        Printer.success("`#{task}` was successful")

      _ ->
        Printer.error("mix task `#{task}` failed, return result: #{inspect(result)}")
    end

    mix_task
  end
end