3.7 KiB · text History 69753e1
defmodule GitGud.Imports.Import do
@moduledoc """
One bulk-import batch: "pull every repo under `remote_owner` on
`provider` into this namespace".
The provider token is stored AES-256-GCM encrypted (same scheme and
master key as `GitGud.Secrets`) only for as long as the batch has
work left to do — `GitGud.Imports.finish/1` clears it the moment the
import reaches a terminal state.
"""
use Ecto.Schema
import Ecto.Changeset
alias GitGud.Accounts.User
alias GitGud.Imports.ImportItem
alias GitGud.Organizations.Organization
@providers ~w(github gitlab git)
@statuses ~w(pending listing ready running completed failed canceled)
@visibility_modes ~w(mirror force)
@visibilities ~w(public internal private)
schema "repository_imports" do
field :provider, :string
field :base_url, :string
field :remote_owner, :string
field :remote_username, :string
field :visibility_mode, :string, default: "mirror"
field :default_visibility, :string, default: "private"
field :include_forks, :boolean, default: false
field :include_archived, :boolean, default: false
field :status, :string, default: "pending"
field :error, :string
field :token_ciphertext, :binary
field :token_iv, :binary
field :token_tag, :binary
field :token_key_id, :string
field :token_cleared_at, :utc_datetime
belongs_to :owner, User
belongs_to :organization, Organization
has_many :items, ImportItem, foreign_key: :repository_import_id
timestamps(type: :utc_datetime)
end
def providers, do: @providers
def statuses, do: @statuses
def visibility_modes, do: @visibility_modes
def terminal?(%__MODULE__{status: s}), do: s in ~w(completed failed canceled)
@doc """
Changeset for starting an import. `owner_id` / `organization_id` and
the encrypted token columns are set programmatically by the context,
never cast from user input.
"""
def create_changeset(import, attrs) do
import
|> cast(attrs, [
:provider,
:base_url,
:remote_owner,
:remote_username,
:visibility_mode,
:default_visibility,
:include_forks,
:include_archived
])
|> validate_required([:provider, :base_url, :remote_owner])
|> validate_inclusion(:provider, @providers)
|> validate_inclusion(:visibility_mode, @visibility_modes)
|> validate_inclusion(:default_visibility, @visibilities)
|> validate_length(:remote_owner, max: 500)
|> validate_remote_owner()
|> validate_base_url()
end
# For a forge, `remote_owner` is an org/user/group path. For a one-off
# `git` import it *is* the clone URL, which has a different shape.
defp validate_remote_owner(changeset) do
case get_field(changeset, :provider) do
"git" ->
validate_change(changeset, :remote_owner, fn :remote_owner, url ->
if http_url?(url), do: [], else: [remote_owner: "must be an http(s) git URL"]
end)
_ ->
validate_format(changeset, :remote_owner, ~r{\A[^\s/]+(/[^\s/]+)*\z},
message: "must be an owner or group path (no spaces)"
)
end
end
defp validate_base_url(changeset) do
validate_change(changeset, :base_url, fn :base_url, url ->
if http_url?(url), do: [], else: [base_url: "must be an http(s) URL"]
end)
end
defp http_url?(url) when is_binary(url) do
case URI.new(url) do
{:ok, %URI{scheme: s, host: h}} when s in ["http", "https"] -> is_binary(h) and h != ""
_ -> false
end
end
defp http_url?(_), do: false
def status_changeset(import, attrs) do
import
|> cast(attrs, [:status, :error])
|> validate_inclusion(:status, @statuses)
|> validate_length(:error, max: 2000)
end
end