lib/git_ops/changelog.ex

defmodule GitOps.Changelog do
  @moduledoc """
  Functions for writing commits to the changelog, and initializing it.
  """

  alias GitOps.Commit
  alias GitOps.Config

  @spec write(String.t(), [Commit.t()], String.t(), String.t(), Keyword.t()) :: String.t()
  def write(path, commits, last_version, current_version, opts \\ []) do
    original_file_contents = File.read!(path)

    [head | rest] = String.split(original_file_contents, "<!-- changelog -->")

    config_types = Config.types()

    breaking_changes = Enum.filter(commits, &Commit.breaking?/1)

    breaking_changes_contents =
      if Enum.empty?(breaking_changes) do
        []
      else
        [
          "### Breaking Changes:\n\n",
          Enum.map_join(breaking_changes, "\n\n", &Commit.format/1)
        ]
      end

    contents_to_insert =
      commits
      |> Enum.reject(&Map.get(&1, :breaking?))
      |> Enum.group_by(fn commit ->
        String.downcase(commit.type)
      end)
      |> Stream.filter(fn {group, _commits} ->
        Map.has_key?(config_types, group) && !config_types[group][:hidden?]
      end)
      |> Enum.map(fn {group, commits} ->
        formatted_commits = Enum.map_join(commits, "\n\n", &Commit.format/1)

        ["\n\n### ", config_types[group][:header] || group, ":\n\n", formatted_commits]
      end)

    repository_url = Config.repository_url()

    today = Date.utc_today()
    date = ["(", Date.to_iso8601(today), ")"]

    version_header =
      if repository_url do
        trimmed_url = String.trim_trailing(repository_url, "/")
        compare_link = compare_link(trimmed_url, last_version, current_version)

        ["## [", current_version, "](", compare_link, ") ", date]
      else
        ["## ", current_version, " ", date]
      end

    new_message =
      IO.iodata_to_binary([
        version_header,
        "\n",
        breaking_changes_contents,
        "\n\n",
        contents_to_insert
      ])

    new_contents =
      IO.iodata_to_binary([
        String.trim(head),
        "\n\n<!-- changelog -->\n\n",
        new_message,
        rest
      ])

    unless opts[:dry_run] do
      File.write!(path, new_contents)
    end

    String.trim(new_message)
  end

  @spec initialize(String.t(), Keyword.t()) :: :ok
  def initialize(path, opts \\ []) do
    contents = """
    # Change Log

    All notable changes to this project will be documented in this file.
    See [Conventional Commits](Https://conventionalcommits.org) for commit guidelines.

    <!-- changelog -->
    """

    if File.exists?(path) do
      raise "\nFile already exists: #{path}. Please remove it to initialize."
    end

    unless opts[:dry_run] do
      File.write!(path, String.trim_leading(contents))
    end

    :ok
  end

  defp compare_link(url, last_version, current_version) do
    [
      url,
      "/compare/",
      Config.prefix(),
      last_version,
      "...",
      current_version
    ]
  end
end