defmodule GitGud.Imports.Provider do
@moduledoc """
Contract for a remote forge we can enumerate repositories from.
An adapter's only job is discovery: turn `(base_url, owner, token)`
into a flat list of remote repos. Everything after that — naming,
visibility, cloning, retries — is provider-agnostic and lives in
`GitGud.Imports`.
"""
@typedoc """
One remote repository, normalized across providers.
`clone_url` is always the http(s) URL; we authenticate with the
provider token over an askpass helper rather than embedding
credentials in the URL.
"""
@type remote_repo :: %{
name: String.t(),
full_name: String.t(),
clone_url: String.t(),
description: String.t() | nil,
default_branch: String.t() | nil,
private: boolean(),
fork: boolean(),
archived: boolean()
}
@type opts :: [base_url: String.t(), token: String.t() | nil]
@doc "Default API/base URL for the hosted flavour of this provider."
@callback default_base_url() :: String.t()
@doc "Human label for the UI."
@callback label() :: String.t()
@doc """
List every repository under `owner` (an organization/group or a user).
Adapters must paginate to exhaustion and return an error tuple rather
than a partial list.
"""
@callback list_repositories(owner :: String.t(), opts) ::
{:ok, [remote_repo]} | {:error, term()}
# Ordered — drives the order of the provider picker in the UI.
@adapters [
{"github", GitGud.Imports.Providers.GitHub},
{"gitlab", GitGud.Imports.Providers.GitLab},
{"git", GitGud.Imports.Providers.GitUrl}
]
@doc "Resolve a provider slug to its adapter module."
@spec adapter(String.t()) :: {:ok, module()} | {:error, :unknown_provider}
def adapter(provider) when is_binary(provider) do
case List.keyfind(@adapters, provider, 0) do
{^provider, mod} -> {:ok, mod}
nil -> {:error, :unknown_provider}
end
end
@doc "`[{label, slug}, ...]` for form selects."
def options do
Enum.map(@adapters, fn {slug, mod} -> {mod.label(), slug} end)
end
@doc "Default base URL for a provider slug, or `nil` if unknown."
def default_base_url(provider) do
case adapter(provider) do
{:ok, mod} -> mod.default_base_url()
_ -> nil
end
end
@doc """
Shared paginated-GET driver. Calls `fetch_page.(page)` until it returns
an empty list or `halt?.(response)` says the last page is in hand.
"""
def paginate(fetch_page, opts \\ []) do
max_pages = Keyword.get(opts, :max_pages, 100)
do_paginate(fetch_page, 1, max_pages, [])
end
defp do_paginate(_fetch_page, page, max_pages, acc) when page > max_pages,
do: {:ok, Enum.reverse(acc) |> List.flatten()}
defp do_paginate(fetch_page, page, max_pages, acc) do
case fetch_page.(page) do
{:ok, [], _more?} -> {:ok, Enum.reverse(acc) |> List.flatten()}
{:ok, items, false} -> {:ok, Enum.reverse([items | acc]) |> List.flatten()}
{:ok, items, true} -> do_paginate(fetch_page, page + 1, max_pages, [items | acc])
{:error, _} = err -> err
end
end
end
neiam /gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Packages
⭐
Log in to mark this repository.
3.1 KiB · text
History
6280797