defmodule GitGud.Imports.Workers.CloneRepo do
@moduledoc """
Imports one `ImportItem`: mirror-clone the remote and create the local
repository row.
One job per item, so an unreachable or oversized remote fails on its
own rather than aborting the batch. Retries cover transient network
failures; a name collision is a permanent condition, so it resolves to
`skipped` on the first attempt instead of burning retries.
"""
use Oban.Worker, queue: :imports, max_attempts: 3
require Logger
alias GitGud.Imports
alias GitGud.Imports.ImportItem
@impl Oban.Worker
def perform(%Oban.Job{args: %{"item_id" => item_id}, attempt: attempt, max_attempts: max}) do
item = Imports.get_item!(item_id)
import_row = item.import
cond do
ImportItem.done?(item) ->
:ok
import_row.status == "canceled" ->
_ = Imports.mark_item(item, "skipped", %{error: "canceled"})
:ok
true ->
item
|> Imports.mark_item("cloning")
|> run(import_row, attempt, max)
end
end
defp run(item, import_row, attempt, max) do
case Imports.import_item(item, import_row) do
{:ok, repository} ->
_ = Imports.mark_item(item, "imported", %{repository_id: repository.id, error: nil})
settle(import_row)
:ok
{:skip, reason} ->
_ = Imports.mark_item(item, "skipped", %{error: reason})
settle(import_row)
:ok
{:error, reason} ->
message = describe(reason)
if attempt >= max do
_ = Imports.mark_item(item, "failed", %{error: message})
settle(import_row)
:ok
else
# Back to `pending` so a retry doesn't leave the row reading
# "cloning" during the backoff window.
_ = Imports.mark_item(item, "pending", %{error: message})
Logger.warning("import item #{item.id} failed (attempt #{attempt}/#{max}): #{message}")
{:error, message}
end
end
end
# Re-read the batch so `settle/1` sees this item's committed status.
defp settle(import_row) do
case Imports.get_import(import_row.id) do
nil -> :ok
fresh -> Imports.settle(fresh)
end
end
defp describe(%Ecto.Changeset{} = cs) do
cs
|> Ecto.Changeset.traverse_errors(fn {msg, opts} ->
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), "") |> to_string()
end)
end)
|> Enum.map_join("; ", fn {field, msgs} -> "#{field} #{Enum.join(msgs, ", ")}" end)
|> String.slice(0, 2000)
end
defp describe({:git_init, {:git_clone, out}}), do: "clone failed: " <> tail(out)
defp describe({:git_clone, out}), do: "clone failed: " <> tail(out)
defp describe({:visibility_too_open, org_vis}), do: "org visibility is #{org_vis}"
defp describe(:forbidden), do: "not permitted to create repositories here"
defp describe(other), do: other |> inspect() |> String.slice(0, 2000)
# git's failure reason is on the last lines; keep the message readable.
defp tail(out) when is_binary(out) do
out
|> String.split("\n", trim: true)
|> Enum.take(-4)
|> Enum.join(" / ")
|> String.slice(0, 1000)
end
defp tail(other), do: inspect(other)
@doc "Build a job for one import item."
def new_job(%{item_id: item_id}) do
__MODULE__.new(%{"item_id" => item_id})
end
end
neiam /gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Packages
⭐
Log in to mark this repository.
3.3 KiB · text
History
6280797