tarakan/lib/tarakan/hosted_repositories/post_receive.ex
Maxfield Luke af6077b9c3
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s
Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
2026-07-29 04:43:40 -04:00

103 lines
2.8 KiB
Elixir

defmodule Tarakan.HostedRepositories.PostReceive do
@moduledoc """
Bookkeeping after a successful push, in place of git hooks.
Hosted repositories never execute hook scripts; this module is invoked by
the transport layer after `git receive-pack` exits successfully. It is
best-effort: the push has already succeeded at the git level, so failures
here are logged and never surfaced to the client.
"""
alias Tarakan.Audit
alias Tarakan.Git.Local
alias Tarakan.HostedRepositories
alias Tarakan.HostedRepositories.Storage
alias Tarakan.Repo
alias Tarakan.Repositories.Repository
alias Tarakan.RepositoryCode.Cache
require Logger
def run(%Repository{} = repository, scope) do
repository = Repo.get(Repository, repository.id)
if repository && HostedRepositories.hosted?(repository) do
dir = Storage.dir(repository)
repository
|> settle_head(dir)
|> record_push(dir)
Cache.delete_hosted(repository.id)
audit_push(repository, scope)
end
:ok
rescue
error ->
Logger.warning("post-receive bookkeeping failed: #{Exception.message(error)}")
:ok
end
# A fresh repository's HEAD may point at a branch that was never pushed
# (e.g. HEAD -> master while the client pushed main). When exactly one
# branch exists, adopt it so clones and the browser have a default.
defp settle_head(repository, dir) do
case Local.head_commit(dir) do
{:ok, %{branch: branch}} ->
persist_default_branch(repository, branch)
:empty ->
case Local.branches(dir) do
{:ok, [only_branch]} ->
_ = Local.run(dir, ["symbolic-ref", "HEAD", "refs/heads/#{only_branch}"])
persist_default_branch(repository, only_branch)
_other ->
repository
end
:miss ->
repository
end
end
defp persist_default_branch(%Repository{default_branch: branch} = repository, branch),
do: repository
defp persist_default_branch(repository, branch) do
repository
|> Ecto.Changeset.change(default_branch: branch)
|> Repo.update()
|> case do
{:ok, updated} -> updated
{:error, _changeset} -> repository
end
end
defp record_push(repository, _dir) do
disk_size =
case Storage.disk_size_bytes(repository) do
{:ok, bytes} -> bytes
_error -> repository.disk_size_bytes
end
repository
|> Ecto.Changeset.change(
pushed_at: DateTime.utc_now(:microsecond),
disk_size_bytes: disk_size
)
|> Repo.update()
|> case do
{:ok, updated} -> updated
{:error, _changeset} -> repository
end
end
defp audit_push(repository, scope) do
case Audit.record(scope, :repository_pushed, repository, %{}) do
{:ok, _event} -> :ok
{:error, reason} -> Logger.warning("push audit failed: #{inspect(reason)}")
end
end
end