defmodule TerminusDB.Branch do
@moduledoc """
Branch management API for TerminusDB.
Wraps the `/api/branch/{path}` endpoints. Branches are git-like pointers to
commits within a repository. Creating a branch forks from an existing branch
(default: `main`).
All functions require a `TerminusDB.Config` scoped to a database (via
`TerminusDB.Config.with_database/2`). The organization defaults to
`config.organization` and the repository to `config.repo` (default `local`),
both overridable per call.
## Quick start
config =
TerminusDB.Config.new(endpoint: "http://localhost:6363")
|> TerminusDB.Config.with_database("mydb")
# Create a branch (forks from main by default)
{:ok, _} = TerminusDB.Branch.create(config, "feature")
# Check it exists
true = TerminusDB.Branch.exists?(config, "feature")
# Delete it
{:ok, _} = TerminusDB.Branch.delete(config, "feature")
"""
alias TerminusDB.{Client, Config, Error}
alias TerminusDB.Client.Params
@type branch_opt ::
{:organization, String.t()}
| {:repo, String.t()}
| {:from, String.t()}
| {:author, String.t()}
| {:message, String.t()}
defp branch_path(config, branch_name, opts) do
repo = opts[:repo] || config.repo
"branch/#{Client.resource_path(config, opts)}/#{repo}/branch/#{branch_name}"
end
@doc """
Creates a new branch `branch_name` in the configured (or given) repository.
The branch is forked from the branch named in `:from` (default: `config.branch`,
which defaults to `"main"`).
## Options
- `:from` — the branch to fork from (default: `config.branch`).
- `:organization` — overrides `config.organization`.
- `:repo` — overrides `config.repo` (`local` or a remote name).
## 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, resp} = TerminusDB.Branch.create(config, "feature")
iex> resp["api:status"]
"api:success"
Fork from a specific branch:
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, resp} = TerminusDB.Branch.create(config, "dev", from: "main")
iex> resp["api:status"]
"api:success"
"""
@spec create(Config.t(), String.t(), [branch_opt()]) ::
{:ok, map()} | {:error, Error.t()}
def create(config, branch_name, opts \\ []) do
path = branch_path(config, branch_name, opts)
org = opts[:organization] || config.organization
repo = opts[:repo] || config.repo
origin_branch = opts[:from] || config.branch
body = %{
"origin" => "#{org}/#{config.database}/#{repo}/branch/#{origin_branch}"
}
Client.request(config, :post, path, json: body, area: :branch)
end
@doc """
Creates a branch, returning the response body or raising `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.Branch.create!(config, "feature")
%{"api:status" => "api:success"}
"""
@spec create!(Config.t(), String.t(), [branch_opt()]) :: map()
def create!(config, branch_name, opts \\ []) do
case create(config, branch_name, opts) do
{:ok, body} -> body
{:error, error} -> raise error
end
end
@doc """
Deletes the branch `branch_name`.
## Options
- `:organization` — overrides `config.organization`.
- `:repo` — overrides `config.repo`.
## 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, resp} = TerminusDB.Branch.delete(config, "feature")
iex> resp["api:status"]
"api:success"
"""
@spec delete(Config.t(), String.t(), [branch_opt()]) ::
{:ok, map() | nil} | {:error, Error.t()}
def delete(config, branch_name, opts \\ []) do
path = branch_path(config, branch_name, opts)
Client.request(config, :delete, path, area: :branch)
end
@doc """
Deletes a branch, returning the response body or raising `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.Branch.delete!(config, "feature")
%{"api:status" => "api:success"}
"""
@spec delete!(Config.t(), String.t(), [branch_opt()]) :: map() | nil
def delete!(config, branch_name, opts \\ []) do
case delete(config, branch_name, opts) do
{:ok, body} -> body
{:error, error} -> raise error
end
end
@doc """
Squashes the current branch HEAD into a single commit.
## Options
- `:author` — commit author.
- `:message` — commit message.
- `:organization` — overrides `config.organization`.
- `:repo` — overrides `config.repo`.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success", "api:commit" => "abc123"})} end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> {:ok, resp} = TerminusDB.Branch.squash(config, author: "admin", message: "squash")
iex> resp["api:commit"]
"abc123"
"""
@spec squash(Config.t(), [branch_opt()]) :: {:ok, map()} | {:error, Error.t()}
def squash(config, opts \\ []) do
path = squash_path(config, opts)
commit_info =
%{}
|> Params.maybe_put("author", opts[:author])
|> Params.maybe_put("message", opts[:message])
Client.request(config, :post, path, json: %{"commit_info" => commit_info}, area: :branch)
end
@doc """
Squashes the branch HEAD, or raises.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success", "api:commit" => "abc"})} end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> TerminusDB.Branch.squash!(config, author: "admin", message: "squash")
%{"api:commit" => "abc"}
"""
@spec squash!(Config.t(), [branch_opt()]) :: map()
def squash!(config, opts \\ []) do
case squash(config, opts) do
{:ok, body} -> body
{:error, error} -> raise error
end
end
@doc """
Hard-resets the branch HEAD to a specific commit.
## Options
- `:organization` — overrides `config.organization`.
- `:repo` — overrides `config.repo`.
## 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, resp} = TerminusDB.Branch.reset(config, "admin/mydb/local/commit/abc123")
iex> resp["api:status"]
"api:success"
"""
@spec reset(Config.t(), String.t(), [branch_opt()]) :: {:ok, map()} | {:error, Error.t()}
def reset(config, commit_descriptor, opts \\ []) do
path = reset_path(config, opts)
Client.request(config, :post, path,
json: %{"commit_descriptor" => commit_descriptor},
area: :branch
)
end
@doc """
Hard-resets the branch HEAD, or raises.
## 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.Branch.reset!(config, "admin/mydb/local/commit/abc")
%{"api:status" => "api:success"}
"""
@spec reset!(Config.t(), String.t(), [branch_opt()]) :: map()
def reset!(config, commit_descriptor, opts \\ []) do
case reset(config, commit_descriptor, opts) do
{:ok, body} -> body
{:error, error} -> raise error
end
end
defp squash_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
branch = config.branch
"squash/#{org}/#{db}/#{repo}/branch/#{branch}"
end
defp reset_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
branch = config.branch
"reset/#{org}/#{db}/#{repo}/branch/#{branch}"
end
@doc """
Returns `true` if the branch `branch_name` exists, `false` otherwise.
Checks the database's branch list via `GET /api/db/:org/:db?branches=true`.
A 404 on the database means it does not exist (so the branch cannot either);
any other non-success response raises `TerminusDB.Error`.
## Options
- `:organization` — overrides `config.organization`.
- `:repo` — overrides `config.repo`.
## Examples
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req ->
...> {req, Req.Response.new(status: 200, body: %{"branches" => ["main", "feature"], "path" => "admin/mydb"})}
...> end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> TerminusDB.Branch.exists?(config, "main")
true
iex> config = TerminusDB.Config.new(
...> endpoint: "http://localhost:6363",
...> adapter: fn req ->
...> {req, Req.Response.new(status: 200, body: %{"branches" => ["main"], "path" => "admin/mydb"})}
...> end
...> ) |> TerminusDB.Config.with_database("mydb")
iex> TerminusDB.Branch.exists?(config, "missing")
false
"""
@spec exists?(Config.t(), String.t(), [branch_opt()]) :: boolean()
def exists?(config, branch_name, opts \\ []) do
org = opts[:organization] || config.organization
db =
config.database ||
raise Error, reason: :http, message: "no database scoped in config"
case Client.request(config, :get, "db/#{org}/#{db}", params: [branches: true], area: :branch) do
{:ok, %{"branches" => branches}} ->
branch_name in branches
{:ok, _} ->
false
{:error, %Error{status: 404}} ->
false
{:error, error} ->
raise error
end
end
end