defmodule GitGudWeb.BadgeCatalog do
@moduledoc """
Every badge a repository has, and how to embed each one: the shared
answer behind the badge sheet's "This repository now"
(`GitGudWeb.RepoLive.Badges`) and the API that `gg badges` reads
(`GitGudWeb.Api.Mobile.BadgeController`), so the page and the CLI
never disagree about what a README block says.
A badge is named by a **key**: the kind for most (`ci`, `release`),
`workflow:<name>` for a workflow, `package:<format>:<name>` for a
package. `embed/3` turns a key plus the display options into its
image URLs, link, alt text and the Markdown and HTML that show it.
"""
use GitGudWeb, :verified_routes
alias GitGud.Badges
alias GitGud.Badges.Spec
alias GitGud.Repositories.Repository
alias GitGud.Repositories.Storage
# The plain kinds, in display order. Workflows and packages come from
# the repository.
@kinds ~w(ci ci-duration last-commit coverage release license language syms issues pulls
commits contributors mirror)
# What a README block starts with, of the badges the repository has.
@readme_defaults ~w(ci coverage release license)
@groups [
{"status", "Status"},
{"activity", "Activity"},
{"project", "Project"},
{"mark", "Mark"}
]
@kind_groups %{
"ci" => "status",
"ci-duration" => "status",
"coverage" => "status",
"workflow" => "status",
"last-commit" => "activity",
"commits" => "activity",
"contributors" => "activity",
"issues" => "activity",
"pulls" => "activity",
"release" => "project",
"license" => "project",
"language" => "project",
"syms" => "project",
"mirror" => "project",
"package" => "project",
"mark" => "mark"
}
@kind_labels %{
"ci" => "CI",
"last-commit" => "Last commit",
"coverage" => "Coverage",
"workflow" => "Workflow",
"package" => "Package",
"issues" => "Issues",
"pulls" => "Pulls",
"commits" => "Commits",
"contributors" => "Contributors",
"ci-duration" => "CI duration",
"release" => "Release",
"license" => "License",
"mirror" => "Mirror",
"language" => "Language",
"syms" => "Syms",
"mark" => "Mark"
}
def kinds, do: @kinds
def groups, do: @groups
def kind_labels, do: @kind_labels
def readme_defaults, do: @readme_defaults
@typedoc "One badge the repository has, with what it says right now."
@type item :: %{
key: String.t(),
kind: String.t(),
title: String.t(),
hint: String.t(),
group: String.t(),
content: GitGud.Badges.Svg.content(),
index: non_neg_integer()
}
@doc """
Every badge `repo` has, grouped (status, activity, project, mark) and
in display order within each group. A mirror badge only for a
repository that follows an upstream (`Badges.mirror?/1`); a workflow badge for each
workflow that has run on the default branch; a package badge for each
package.
"""
@spec items(Repository.t()) :: [item()]
def items(%Repository{} = repo) do
branch = repo.default_branch || "main"
plain =
for kind <- plain_kinds(repo) do
item(kind, Badges.content(repo, kind), @kind_labels[kind], hint(kind, branch))
end
workflows =
for name <- Badges.workflow_names(repo) do
item("workflow:" <> name, Badges.workflow(repo, name), name, "Newest run on #{branch}")
end
packages =
for {registry, name} <- Badges.packages(repo) do
item(
"package:#{registry}:#{name}",
Badges.package(repo, registry, name),
name,
"#{registry} package"
)
end
mark = [item("mark", :mark, "GG", "The mark on its own")]
(plain ++ workflows ++ packages ++ mark)
|> Enum.with_index()
|> Enum.map(fn {item, i} -> Map.put(item, :index, i) end)
|> Enum.sort_by(&group_rank(&1.group))
end
# A mirror badge only for a repository that really follows an upstream,
# not one imported once and developed here since (`Badges.mirror?/1`).
defp plain_kinds(repo), do: if(Badges.mirror?(repo), do: @kinds, else: @kinds -- ["mirror"])
defp item(key, content, title, hint) do
{kind, _name, _registry} = split_key(key)
%{
key: key,
kind: kind,
content: content,
title: title,
hint: hint,
group: Map.fetch!(@kind_groups, kind)
}
end
defp group_rank(group), do: Enum.find_index(@groups, &(elem(&1, 0) == group))
defp hint("ci", branch), do: "Newest push to #{branch}"
defp hint("ci-duration", _), do: "How long that push's CI took"
defp hint("coverage", _), do: "From gitgud-coverage"
defp hint("last-commit", branch), do: "Age of #{branch}'s head"
defp hint("commits", branch), do: "History of #{branch}"
defp hint("contributors", _), do: "Everyone who authored a commit"
defp hint("issues", _), do: "Open issues"
defp hint("pulls", _), do: "Open pull requests"
defp hint("release", _), do: "Newest tag"
defp hint("license", _), do: "Detected from the license file"
defp hint("language", _), do: "Largest share of the code"
defp hint("syms", _), do: "What people marked it with"
defp hint("mirror", _), do: "Since the last upstream sync"
@doc "`{kind, name, registry}` for a key."
@spec split_key(String.t()) :: {String.t(), String.t() | nil, String.t() | nil}
def split_key("workflow:" <> name), do: {"workflow", name, nil}
def split_key("package:" <> rest) do
case String.split(rest, ":", parts: 2) do
[registry, name] -> {"package", name, registry}
_ -> {"package", rest, nil}
end
end
def split_key(kind), do: {kind, nil, nil}
@doc "The badge `key` names, drawn with `options`' style, corners, scale and GG."
@spec spec(Spec.t(), String.t(), String.t(), String.t()) :: Spec.t()
def spec(%Spec{} = options, key, theme, format) do
{kind, name, registry} = split_key(key)
%{options | kind: kind, name: name, registry: registry, theme: theme, format: format}
end
@typedoc "Everything one badge needs to be embedded."
@type embed :: %{
path: String.t(),
url: String.t(),
dark_url: String.t(),
light_url: String.t(),
link: String.t(),
alt: String.t(),
markdown: String.t(),
html: String.t()
}
@doc """
The URLs, link, alt text, Markdown and HTML for the badge `spec`
describes. The HTML picks the `dark` or `light` image by the reader's
setting, for formats (PNG) and sites that can't follow it themselves.
"""
@spec embed(Repository.t(), String.t(), Spec.t()) :: embed()
def embed(%Repository{} = repo, handle, %Spec{} = spec) do
path = image_path(handle, repo, spec)
url = absolute(path)
dark = absolute(image_path(handle, repo, %{spec | theme: GitGud.Themes.system_dark()}))
light = absolute(image_path(handle, repo, %{spec | theme: GitGud.Themes.system_light()}))
link = absolute(link_path(handle, repo, spec.kind))
alt = alt(spec)
%{
path: path,
url: url,
dark_url: dark,
light_url: light,
link: link,
alt: alt,
markdown: "[](#{link})",
html: """
<a href="#{link}">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="#{dark}">
<img alt="#{alt}" src="#{light}">
</picture>
</a>\
"""
}
end
@doc "The repository's handle as its URLs spell it."
def handle(%Repository{} = repo), do: Storage.repo_handle(repo)
@doc "The image path below the app's root for `spec`."
def image_path(handle, repo, %Spec{} = spec) do
segments = spec |> Spec.to_path() |> String.split("/")
~p"/r/#{handle}/#{repo.name}/badge/#{segments}"
end
@doc "Where a badge of `kind` links to."
def link_path(handle, repo, kind) when kind in ["ci", "coverage", "workflow", "ci-duration"],
do: ~p"/r/#{handle}/#{repo.name}/actions"
def link_path(handle, repo, "issues"), do: ~p"/r/#{handle}/#{repo.name}/issues"
def link_path(handle, repo, "pulls"), do: ~p"/r/#{handle}/#{repo.name}/pulls"
def link_path(handle, repo, "package"), do: ~p"/r/#{handle}/#{repo.name}/packages"
def link_path(handle, repo, "language"), do: ~p"/r/#{handle}/#{repo.name}/stats/languages"
def link_path(handle, repo, "contributors"),
do: ~p"/r/#{handle}/#{repo.name}/stats/committers"
def link_path(handle, %{license_file: file} = repo, "license") when is_binary(file),
do:
~p"/r/#{handle}/#{repo.name}/blob/#{repo.default_branch || "main"}/#{String.split(file, "/")}"
def link_path(handle, repo, kind) when kind in ["last-commit", "commits"],
do: ~p"/r/#{handle}/#{repo.name}/commits/#{repo.default_branch || "main"}"
def link_path(handle, repo, _kind), do: ~p"/r/#{handle}/#{repo.name}"
defp absolute(path), do: GitGudWeb.Endpoint.url() <> path
@doc "The image's alt text."
def alt(%Spec{kind: "ci"}), do: "CI status"
def alt(%Spec{kind: "last-commit"}), do: "last commit"
def alt(%Spec{kind: "coverage"}), do: "coverage"
def alt(%Spec{kind: "workflow", name: name}), do: "#{name} status"
# The format too: a package published as both deb and rpm has a badge
# for each, and they shouldn't read the same.
def alt(%Spec{kind: "package", name: name, registry: registry}),
do: "install #{name} (#{registry})"
def alt(%Spec{kind: "issues"}), do: "open issues"
def alt(%Spec{kind: "pulls"}), do: "open pull requests"
def alt(%Spec{kind: "commits"}), do: "commits"
def alt(%Spec{kind: "contributors"}), do: "contributors"
def alt(%Spec{kind: "ci-duration"}), do: "CI duration"
def alt(%Spec{kind: "release"}), do: "latest release"
def alt(%Spec{kind: "license"}), do: "license"
def alt(%Spec{kind: "mirror"}), do: "mirror sync"
def alt(%Spec{kind: "language"}), do: "top language"
def alt(%Spec{kind: "syms"}), do: "syms"
def alt(%Spec{kind: "mark"}), do: "GitGud"
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
9.7 KiB · text
History
b868907