defmodule GitGud.Repositories.Workers.UpstreamSync do
@moduledoc """
Scheduled re-sync of repositories that opted into it
(`upstream_auto_sync`).
Two jobs, one module. Without arguments it is the cron entry: it walks
the opted-in repositories and enqueues one job per repo, so a slow or
unreachable upstream delays only itself. With `%{"repository_id" => id}`
it syncs that one repository.
Each repo syncs in its own `upstream_sync_mode`: a `"mirror"` repo is
force-updated as always, a `"reconcile"` repo only fast-forwards and
leaves divergence for a human. That distinction is what makes running
this unattended safe — see `GitGud.Repositories.sync_upstream/3`.
## Credentials
Scheduled syncs are **anonymous**. The import token is wiped the
moment a batch finishes and nothing else retains a credential, so a
private upstream can only be synced by hand, from repo settings, where
the operator supplies the token for that one fetch.
The seam for changing that is `credentials_for/1`: it is the single
place a scheduled sync obtains credentials, and it returns `nil`
today. Wiring per-repo stored credentials means implementing that
function (and the storage behind it) — every caller, error path and
the "needs credentials" reporting below already routes through it.
"""
use Oban.Worker, queue: :git_cache, max_attempts: 3
require Logger
alias GitGud.Repositories
alias GitGud.Repositories.Repository
@impl Oban.Worker
def perform(%Oban.Job{args: %{"repository_id" => id}}) do
case GitGud.Repo.get(Repository, id) do
nil -> :ok
repo -> sync(repo)
end
end
def perform(%Oban.Job{}) do
repos = Repositories.list_auto_sync_repositories()
Enum.each(repos, fn %Repository{id: id} ->
{:ok, _} = %{"repository_id" => id} |> __MODULE__.new() |> Oban.insert()
end)
{:ok, length(repos)}
end
@doc """
Credentials for an unattended sync of `repo`.
Always `nil` for now: nothing retains a credential past the import
that created the repository. Implementing stored per-repo credentials
starts here.
"""
def credentials_for(%Repository{}), do: nil
defp sync(%Repository{} = repo) do
case Repositories.sync_upstream(repo, credentials_for(repo)) do
{:ok, result} ->
_ = Repositories.clear_upstream_sync_error(result.repository)
:ok
{:error, reason} ->
message = describe(reason)
_ = Repositories.record_upstream_sync_error(repo, message)
Logger.warning("scheduled upstream sync failed for repo #{repo.id}: #{message}")
# Recorded, not retried: the next tick comes round soon enough,
# and a permanently private upstream would otherwise fail three
# times an hour forever.
:ok
end
end
defp describe({:git_fetch, out}) when is_binary(out) do
if needs_credentials?(out) do
"upstream needs credentials; scheduled syncs are anonymous — " <>
"fetch it by hand from the repository's settings"
else
out |> String.split("\n", trim: true) |> Enum.take(-2) |> Enum.join(" / ")
end
end
defp describe(:no_upstream), do: "no upstream URL"
defp describe(other), do: inspect(other)
# git's phrasing for "this remote wants a login" varies by transport
# and version; these are the ones that reach us with credentials
# deliberately absent.
defp needs_credentials?(out) do
out = String.downcase(out)
Enum.any?(
[
"authentication failed",
"could not read username",
"could not read password",
"terminal prompts disabled",
"403 forbidden",
"permission denied"
],
&String.contains?(out, &1)
)
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
3.6 KiB · text
History
b868907