Skip to main content

lib/terminus_db/merge.ex

defmodule TerminusDB.Merge do
  @moduledoc """
  Branch merge API for TerminusDB.

  Wraps the `/api/rebase` endpoint to merge (rebase) a source branch into a target
  branch. TerminusDB uses a rebase model: the source branch's commits are
  replayed on top of the target branch, creating a linear history.

  ## Quick start

      config =
        TerminusDB.Config.new(endpoint: "http://localhost:6363")
        |> TerminusDB.Config.with_database("mydb")

      # Merge `feature` into `main`
      {:ok, result} = TerminusDB.Merge.merge(config,
        source_branch: "feature",
        target_branch: "main"
      )

  """

  alias TerminusDB.{Client, Config, Error}

  @type merge_opt ::
          {:source_branch, String.t()}
          | {:target_branch, String.t()}
          | {:organization, String.t()}
          | {:repo, String.t()}
          | {:author, String.t()}
          | {:message, String.t()}

  defp build_resource(config, branch, opts) do
    org = opts[:organization] || config.organization
    db = config.database || raise Error, reason: :http, message: "no database scoped in config"
    repo = opts[:repo] || config.repo
    "#{org}/#{db}/#{repo}/branch/#{branch}"
  end

  defp rebase_path(config, opts) do
    org = opts[:organization] || config.organization
    db = config.database || raise Error, reason: :http, message: "no database scoped in config"
    repo = opts[:repo] || config.repo
    target = opts[:target_branch] || config.branch
    "rebase/#{org}/#{db}/#{repo}/branch/#{target}"
  end

  defp rebase_body(config, opts) do
    source_branch = Keyword.fetch!(opts, :source_branch)
    source = build_resource(config, source_branch, opts)
    author = opts[:author] || "admin"
    message = opts[:message] || "rebase #{source_branch}"

    %{
      "author" => author,
      "message" => message,
      "rebase_from" => source
    }
  end

  @doc """
  Merges (rebases) the `source_branch` into the `target_branch`.

  TerminusDB replays the source branch's commits on top of the target branch.
  If there are conflicts, the operation returns an error describing them.

  ## Options

  - `:source_branch` (required) - the branch to merge from.
  - `:target_branch` - the branch to merge into (default: `config.branch`).
  - `:organization` - overrides `config.organization`.
  - `:repo` - overrides `config.repo`.
  - `:author` - commit author for the merge commit.
  - `:message` - commit message for the merge commit.

  ## Examples

      iex> config = TerminusDB.Config.new(
      ...>   endpoint: "http://localhost:6363",
      ...>   adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success"})} end
      ...> ) |> TerminusDB.Config.with_database("mydb")
      iex> {:ok, result} = TerminusDB.Merge.merge(config,
      ...>   source_branch: "feature",
      ...>   target_branch: "main"
      ...> )
      iex> result["api:status"]
      "api:success"

  """
  @spec merge(Config.t(), [merge_opt()]) :: {:ok, map()} | {:error, Error.t()}
  def merge(config, opts \\ []) do
    path = rebase_path(config, opts)
    body = rebase_body(config, opts)
    Client.request(config, :post, path, json: body, area: :merge)
  end

  @doc """
  Merges branches, or raises `TerminusDB.Error`.

  ## Examples

      iex> config = TerminusDB.Config.new(
      ...>   endpoint: "http://localhost:6363",
      ...>   adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success"})} end
      ...> ) |> TerminusDB.Config.with_database("mydb")
      iex> TerminusDB.Merge.merge!(config, source_branch: "feature", target_branch: "main")
      %{"api:status" => "api:success"}

  """
  @spec merge!(Config.t(), [merge_opt()]) :: map()
  def merge!(config, opts \\ []) do
    case merge(config, opts) do
      {:ok, body} -> body
      {:error, error} -> raise error
    end
  end
end