@@ -129,7 +129,7 @@
defp build_capability_advertisement do
lines = [
PktLine.encode("version 2"),
PktLine.encode("ls-refs"),
PktLine.encode("ls-refs=unborn"),
PktLine.encode("fetch=shallow"),
PktLine.encode("server-option"),
PktLine.flush()
@@ -177,39 +177,126 @@
end
# -- ls-refs Command --
#
# Per Documentation/technical/protocol-v2.txt:
#
# output = *ref flush-pkt
# ref = PKT-LINE(obj-id-or-unborn SP refname *(SP ref-attribute) LF)
# obj-id-or-unborn = (obj-id | "unborn")
# ref-attribute = (symref | peeled)
# symref = "symref-target:" symref-target
# peeled = "peeled:" obj-id
defp handle_ls_refs(repo, args) do
prefixes =
args
|> Enum.filter(&String.starts_with?(&1, "ref-prefix "))
|> Enum.map(&String.trim_leading(&1, "ref-prefix "))
@type ref_entry :: %{
name: String.t(),
sha: String.t() | :unborn,
symref_target: String.t() | nil
}
refs = list_refs(repo, prefixes)
defp handle_ls_refs(repo, args) do
opts = %{
prefixes: extract_ref_prefixes(args),
symrefs?: "symrefs" in args,
peel?: "peel" in args,
unborn?: "unborn" in args
}
lines =
Enum.map(refs, fn {ref, sha} ->
PktLine.encode("#{sha} #{ref}")
end)
repo
|> collect_ref_entries(opts)
|> filter_by_prefixes(opts.prefixes)
|> Enum.map(&encode_ref_entry(repo, &1, opts))
IO.iodata_to_binary(lines ++ [PktLine.flush()])
end
defp extract_ref_prefixes(args) do
args
|> Enum.filter(&String.starts_with?(&1, "ref-prefix "))
|> Enum.map(&String.trim_leading(&1, "ref-prefix "))
end
# HEAD (if present) is always the first entry; the remaining heads and
# tags follow in lexicographic order. Callers filter by prefix after.
defp collect_ref_entries(repo, opts) do
head = head_entry(repo, opts)
heads_and_tags =
(list_refs_safe(repo, "refs/heads/") ++ list_refs_safe(repo, "refs/tags/"))
|> Enum.sort_by(fn {name, _sha} -> name end)
|> Enum.map(fn {name, sha} -> %{name: name, sha: sha, symref_target: nil} end)
List.wrap(head) ++ heads_and_tags
end
# Build the advertisement entry for HEAD. Returns `nil` if HEAD is not
# set at all. For an unborn HEAD (HEAD → refs/heads/X but X doesn't
# exist), returns an entry only when the client asked for `unborn`.
defp head_entry(repo, opts) do
case Ref.get_head(repo) do
{:ok, "ref: " <> target} ->
case Ref.get(repo, target) do
{:ok, sha} -> %{name: "HEAD", sha: sha, symref_target: target}
{:error, _} when opts.unborn? -> %{name: "HEAD", sha: :unborn, symref_target: target}
defp list_refs(repo, []) do
list_all_refs(repo)
{:error, _} -> nil
end
{:ok, sha} ->
if sha?(sha), do: %{name: "HEAD", sha: sha, symref_target: nil}
{:error, _} ->
nil
end
end
defp list_refs(repo, prefixes) do
all_refs = list_all_refs(repo)
defp sha?(s), do: is_binary(s) and byte_size(s) == 40 and String.match?(s, ~r/\A[0-9a-f]{40}\z/)
Enum.filter(all_refs, fn {ref, _sha} ->
Enum.any?(prefixes, &String.starts_with?(ref, &1))
defp filter_by_prefixes(entries, []), do: entries
defp filter_by_prefixes(entries, prefixes) do
Enum.filter(entries, fn %{name: name} ->
Enum.any?(prefixes, &String.starts_with?(name, &1))
end)
end
defp encode_ref_entry(repo, %{name: name, sha: sha, symref_target: symref_target}, opts) do
head = if sha == :unborn, do: "unborn", else: sha
attrs =
[]
|> maybe_prepend(opts.symrefs? and symref_target != nil, "symref-target:#{symref_target}")
|> maybe_prepend_peeled(opts.peel?, repo, sha)
|> Enum.reverse()
PktLine.encode(Enum.join([head, name | attrs], " "))
end
defp list_all_refs(repo) do
heads = list_refs_safe(repo, "refs/heads/")
tags = list_refs_safe(repo, "refs/tags/")
(heads ++ tags) |> Enum.sort_by(fn {ref, _sha} -> ref end)
defp maybe_prepend(list, false, _value), do: list
defp maybe_prepend(list, true, value), do: [value | list]
defp maybe_prepend_peeled(list, false, _repo, _sha), do: list
defp maybe_prepend_peeled(list, true, _repo, :unborn), do: list
defp maybe_prepend_peeled(list, true, repo, sha) do
case peel_tag(repo, sha) do
{:ok, peeled} when peeled != sha -> ["peeled:#{peeled}" | list]
_ -> list
end
end
# Recursively peel a tag chain to the underlying non-tag object.
# Returns the original sha unchanged if it isn't a tag.
@peel_max_depth 10
defp peel_tag(repo, sha), do: peel_tag(repo, sha, @peel_max_depth)
defp peel_tag(_repo, sha, 0), do: {:ok, sha}
defp peel_tag(repo, sha, budget) do
case ObjectResolver.read(repo, sha) do
{:ok, %Tag{object: target}} -> peel_tag(repo, target, budget - 1)
{:ok, _} -> {:ok, sha}
{:error, _} = err -> err
end
end
defp list_refs_safe(repo, prefix) do