3.3 KiB · text History 69753e1
defmodule GitGud.Imports.Providers.GitLab do
@moduledoc """
Repository discovery against gitlab.com or a self-hosted GitLab.
`base_url` is the **instance** root (`https://gitlab.com`); we append
`/api/v4` ourselves.
An owner may be a group path — including a nested one like
`acme/platform` — or a user. Group listing is tried first with
`include_subgroups=true` so a whole tree comes across in one pass;
a 404 falls back to the user endpoint.
"""
@behaviour GitGud.Imports.Provider
alias GitGud.Imports.Provider
@per_page 100
@impl true
def label, do: "GitLab"
@impl true
def default_base_url, do: "https://gitlab.com"
@impl true
def list_repositories(owner, opts) do
base = Keyword.fetch!(opts, :base_url) |> String.trim_trailing("/")
token = Keyword.get(opts, :token)
api = base <> "/api/v4"
# Group and user paths may be nested; the whole path is one path
# segment as far as the API is concerned, so encode the slashes.
id = URI.encode(owner, &URI.char_unreserved?/1)
case fetch_all("#{api}/groups/#{id}/projects", token, include_subgroups: true) do
{:error, :not_found} -> fetch_all("#{api}/users/#{id}/projects", token, [])
other -> other
end
end
defp fetch_all(url, token, extra_params) do
Provider.paginate(fn page ->
request(url, token, [page: page, per_page: @per_page] ++ extra_params)
end)
end
defp request(url, token, params) do
req =
Req.new(
url: url,
params: params,
headers: headers(token),
receive_timeout: 30_000,
retry: :safe_transient
)
|> Req.merge(Application.get_env(:git_gud, :imports_req_options, []))
case Req.get(req) do
{:ok, %Req.Response{status: 200, body: body} = resp} when is_list(body) ->
{:ok, Enum.map(body, &normalize/1), more?(resp, body)}
{:ok, %Req.Response{status: 404}} ->
{:error, :not_found}
{:ok, %Req.Response{status: status}} when status in [401, 403] ->
{:error, {:unauthorized, status}}
{:ok, %Req.Response{status: status, body: body}} ->
{:error, {:http_error, status, message(body)}}
{:error, reason} ->
{:error, {:transport, reason}}
end
end
# GitLab advertises the next page in `x-next-page` (blank on the last
# page). Keyset-paginated instances omit it, so fall back to a full
# page meaning "probably more".
defp more?(resp, body) do
case Req.Response.get_header(resp, "x-next-page") do
[next | _] -> String.trim(next) != ""
[] -> length(body) == @per_page
end
end
defp headers(nil), do: [{"user-agent", "gitgud-importer"}]
defp headers(token), do: [{"private-token", token} | headers(nil)]
defp normalize(project) do
%{
name: project["path"] || project["name"],
full_name: project["path_with_namespace"] || project["path"],
clone_url: project["http_url_to_repo"],
description: project["description"],
default_branch: project["default_branch"],
private: project["visibility"] != "public",
fork: is_map(project["forked_from_project"]),
archived: !!project["archived"]
}
end
defp message(%{"message" => m}) when is_binary(m), do: m
defp message(%{"error" => m}) when is_binary(m), do: m
defp message(_), do: nil
end