lib/tasks/file.ex

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

  A file should be configured as `{:file, file_path, opts}`, being `opts` an
  optional configuration. The file should be readable and have execution
  permissions.

  See `#{__MODULE__}.new/1` for more information.

  For example:

  ```elixir
  config :git_hooks,
    hooks: [
      pre_commit: [
        {:file, "opt/scripts/checks", include_hook_args: true}
      ]
    ]
  ```
  """

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

  defstruct [:file_path, :args, :env, :git_hook_type, result: nil]

  @doc """
  Creates a new `file` struct.

  This function expects a tuple or triple with `:file`, the file path 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({:file, :test, env: [{"var", "test"}], include_hook_args: true}, :pre_commit, ["commit message"])
      %#{__MODULE__}{file_path: :test, args: ["commit message"], env: [{"var", "test"}], git_hook_type: :pre_commit}

      iex> #{__MODULE__}.new({:file, :test, include_hook_args: false}, :pre_commit, ["commit message"])
      %#{__MODULE__}{file_path: :test, args: [], env: [], git_hook_type: :pre_commit}

  """
  @spec new(
          {:file, path :: String.t(), [any]},
          GitHooks.git_hook_type(),
          GitHooks.git_hook_args()
        ) ::
          __MODULE__.t()
  def new({:file, script_file, opts}, git_hook_type, git_hook_args) when is_list(opts) do
    args =
      if Keyword.get(opts, :include_hook_args, false) do
        git_hook_args
      else
        []
      end

    %__MODULE__{
      file_path: script_file,
      args: args,
      git_hook_type: git_hook_type,
      env: Keyword.get(opts, :env, [])
    }
  end
end

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

  def run(
        %File{file_path: script_file, env: env, args: args, git_hook_type: git_hook_type} = file,
        _opts
      ) do
    result =
      script_file
      |> Path.absname()
      |> System.cmd(
        args,
        into: Config.io_stream(git_hook_type),
        env: env
      )

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

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

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

    file
  end

  def print_result(%File{file_path: file_path, result: {_result, _code}} = file) do
    Printer.error("`#{file_path}` execution failed")

    file
  end
end