lib/mix/tasks/graft.add.ex

defmodule Mix.Tasks.Graft.Add do
  @shortdoc "Clone repositories into the workspace"

  @moduledoc """
  Clone `owner/repo` repositories from GitHub into the workspace.

      mix graft.add owner1/repo1 owner2/repo2
      mix graft.add owner/repo --to-manifest
      mix graft.add owner/repo --root path/to/workspace

  Each repo is cloned to a directory named after the repository.
  If the directory already exists and has the correct remote origin,
  the clone is skipped (idempotent).

  ## Flags

    * `--to-manifest` — append the new repo to `graft.exs` as a sibling
    * `--root PATH`   — operate in a different workspace root
  """

  use Mix.Task

  alias Graft.Add

  @switches [to_manifest: :boolean, root: :string]

  @impl Mix.Task
  def run(argv) do
    case execute(argv) do
      {:ok, output} ->
        Mix.shell().info(output)

      {:error, message} ->
        Mix.shell().error(message)
        exit({:shutdown, 1})
    end
  end

  @spec execute([String.t()]) :: {:ok, String.t()} | {:error, String.t()}
  def execute(argv) do
    case OptionParser.parse!(argv, strict: @switches) do
      {opts, owner_repos} when owner_repos != [] ->
        root = opts[:root] || File.cwd!()
        results = Add.clone(owner_repos, root, to_manifest: opts[:to_manifest] || false)
        {:ok, format_results(results)}

      {_opts, []} ->
        {:error, usage()}
    end
  rescue
    e in OptionParser.ParseError ->
      {:error, "graft.add: #{Exception.message(e)}\n\n#{usage()}"}
  end

  ## ─── Formatting ─────────────────────────────────────────────────────

  defp format_results(results) do
    lines =
      Enum.map(results, fn {repo, result} ->
        case result do
          :ok -> "  #{check()} #{repo}"
          {:error, err} -> "  #{cross()} #{repo}: #{err.message}"
        end
      end)

    successes = Enum.count(results, fn {_, r} -> r == :ok end)
    failures = map_size(results) - successes

    header =
      case {successes, failures} do
        {s, 0} -> "Added #{s} repository(s):"
        {0, f} -> "Failed to add #{f} repository(s):"
        {s, f} -> "Added #{s}, failed #{f}:"
      end

    [header | lines]
    |> Enum.join("\n")
  end

  defp check, do: if(no_color?(), do: "[OK]", else: "✓")
  defp cross, do: if(no_color?(), do: "[ERR]", else: "✗")

  defp no_color? do
    System.get_env("NO_COLOR") in ["1", "true"] or System.get_env("TERM") == "dumb"
  end

  defp usage do
    """
    Usage: mix graft.add OWNER/REPO [OWNER/REPO ...] [options]

    Options:
      --to-manifest    Append cloned repos to graft.exs
      --root PATH      Workspace root (defaults to current directory)
    """
  end
end