lib/mix/tasks/bump.ex

defmodule Mix.Tasks.Bump do
  @moduledoc """
  Bump version number in mix.exs and create a new git tag.
  """

  @doc """
  Runs the task with the given arguments.
  """
  @spec run([binary]) :: :ok
  def run(args) do
    check_master_branch()
    {parsed, _args, _invalid} = OptionParser.parse(args, strict: [level: :string])
    level = parsed[:level]
    version = Mix.Project.config()[:version]

    with {:ok, new_version} <- bump(version, level),
         {:ok, _message} <- confirm_changes(new_version),
         {:ok, _message} <- update_version(new_version),
         {:ok, _message} <- commit_changes(new_version),
         {:ok, _message} <- push_changes(),
         {:ok, _message} <- create_tag(new_version),
         {:ok, _message} <- push_tag(new_version) do
      IO.puts("Bumped version from #{version} to #{new_version}")
      :ok
    else
      {:error, reason} -> IO.puts(reason)
    end
  end

  defp confirm_changes(new_version) do
    IO.puts("Bumping version from #{Mix.Project.config()[:version]} to #{new_version}")
    IO.puts("Continue? (y/n)")

    case IO.gets("") do
      "y\n" -> {:ok, new_version}
      _error -> {:error, "Aborted"}
    end
  end

  defp check_master_branch do
    IO.puts("Checking current branch")
    current_branch = System.cmd("git", ["branch", "--show-current"])

    case current_branch do
      {"master\n", _other} -> {:ok, current_branch}
      {_branch, _other} -> raise "Not on master branch"
    end
  end

  defp push_tag(new_version) do
    IO.puts("Pushing tag #{new_version}")
    system("git push origin #{new_version}")
    {:ok, new_version}
  end

  defp create_tag(new_version) do
    IO.puts("Creating tag #{new_version}")
    system("git tag -a #{new_version} -m \"Version #{new_version}\"")
    {:ok, new_version}
  end

  defp push_changes do
    IO.puts("Pushing changes")
    system("git push origin master")
    {:ok, "Pushed changes"}
  end

  defp commit_changes(new_version) do
    IO.puts("Committing changes")
    system("git add #{File.cwd!()}/mix.exs")
    system("git commit -m\"Bump version to #{new_version}\"")
    {:ok, "Committed changes"}
  end

  defp system(command) do
    system_result = System.cmd("sh", ["-c", command])

    case system_result do
      {_result, 0} -> :ok
      {0, _status} -> :ok
      {_result, _status} -> raise "Command failed: #{command}"
    end
  end

  defp update_version(new_version) do
    IO.puts("Updating version to #{new_version}")
    mix_exs = File.read!("mix.exs")

    new_mix_exs =
      Regex.replace(~r/version: \"\d+\.\d+\.\d+\"/, mix_exs, "version: \"#{new_version}\"")

    File.write!("mix.exs", new_mix_exs)
    {:ok, new_version}
  end

  defp bump(version, level) do
    case level do
      "major" -> {:ok, bump_major(version)}
      "minor" -> {:ok, bump_minor(version)}
      "patch" -> {:ok, bump_patch(version)}
      _error -> {:error, "Invalid bump level"}
    end
  end

  defp bump_major(version) do
    [major, _minor, _patch] = version |> String.split(".") |> Enum.map(&String.to_integer/1)
    [major + 1, 0, 0] |> Enum.join(".")
  end

  defp bump_minor(version) do
    [major, minor, _patch] = version |> String.split(".") |> Enum.map(&String.to_integer/1)
    [major, minor + 1, 0] |> Enum.join(".")
  end

  defp bump_patch(version) do
    [major, minor, patch] = version |> String.split(".") |> Enum.map(&String.to_integer/1)
    [major, minor, patch + 1] |> Enum.join(".")
  end
end