defmodule GitGud.Syms do
@moduledoc """
"Syms" — stars, but with a set of symbols.
A user marks a repository with any of the instance's configured
symbols, several at once if they like. The set lives in config:
config :git_gud, syms: ["⭐", "🚀", "💔", "⁉️", "❤️"]
An empty list disables the feature: nothing renders, and marking is
refused. Dropping a symbol from the list only hides it — the rows stay
and reappear if it is added back, so an edit never destroys marks.
Everything read back through this module is filtered to the currently
configured set, so retired symbols stay invisible until then.
"""
import Ecto.Query, warn: false
alias GitGud.Accounts.User
alias GitGud.Repo
alias GitGud.Repositories.Repository
alias GitGud.Syms.RepositorySym
@default_symbols ["⭐", "🚀", "💔", "⁉️", "❤️"]
@doc """
The instance's symbol set, in configured order. Non-list or
non-binary entries are ignored rather than crashing a page render.
"""
def symbols do
case Application.get_env(:git_gud, :syms, @default_symbols) do
list when is_list(list) -> list |> Enum.filter(&is_binary/1) |> Enum.uniq()
_ -> []
end
end
@doc "False when the instance has configured no symbols."
def enabled?, do: symbols() != []
@doc "Is this one of the instance's current symbols?"
def known?(symbol), do: symbol in symbols()
@doc """
Add or remove one mark. Returns `{:ok, :marked | :unmarked}`, or
`{:error, :unknown_symbol}` for anything not currently configured —
which also covers the whole feature being disabled.
"""
def toggle(%User{id: uid}, %Repository{id: rid}, symbol) do
if known?(symbol) do
case Repo.get_by(RepositorySym, user_id: uid, repository_id: rid, symbol: symbol) do
nil ->
%RepositorySym{user_id: uid, repository_id: rid, symbol: symbol}
|> Ecto.Changeset.change()
|> Repo.insert()
|> case do
{:ok, _} -> {:ok, :marked}
err -> err
end
row ->
Repo.delete(row)
{:ok, :unmarked}
end
else
{:error, :unknown_symbol}
end
end
@doc "The symbols this user has put on this repo, configured ones only."
def marks(%User{id: uid}, %Repository{id: rid}) do
current = symbols()
from(s in RepositorySym,
where: s.user_id == ^uid and s.repository_id == ^rid,
select: s.symbol
)
|> Repo.all()
|> Enum.filter(&(&1 in current))
end
def marks(nil, _repo), do: []
@doc """
Counts per symbol for a repository, as `%{symbol => count}`. Only
configured symbols appear; symbols with no marks are absent.
"""
def counts(%Repository{id: rid}), do: counts_by(:repository_id, rid)
@doc "Counts per symbol across everything a user has marked."
def counts_for_user(%User{id: uid}), do: counts_by(:user_id, uid)
defp counts_by(field, id) do
current = symbols()
from(s in RepositorySym,
where: field(s, ^field) == ^id,
group_by: s.symbol,
select: {s.symbol, count(s.id)}
)
|> Repo.all()
|> Enum.filter(fn {sym, _} -> sym in current end)
|> Map.new()
end
@doc """
How many repositories the instance-wide listings rank. Config entry
`:syms_top_limit`, default 100.
"""
def top_limit do
case Application.get_env(:git_gud, :syms_top_limit, 100) do
n when is_integer(n) and n > 0 -> n
_ -> 100
end
end
@doc """
The instance's most-marked repositories, most marks first.
`viewer` is the user looking (or `nil`). Repositories they may not
read are excluded *in the query*, before the limit — filtering
afterwards would punch holes in the top N.
Each entry is `%{repository: repo, total: n, counts: %{symbol => n}}`.
"""
def top_repositories(viewer, limit \\ nil) do
limit = limit || top_limit()
current = symbols()
if current == [] do
[]
else
rows =
from(s in RepositorySym,
join: r in Repository,
on: r.id == s.repository_id,
where: s.symbol in ^current,
where: ^visibility_filter(viewer),
group_by: s.repository_id,
order_by: [desc: count(s.id), asc: s.repository_id],
limit: ^limit,
select: {s.repository_id, count(s.id)}
)
|> Repo.all()
decorate_ranking(rows, current)
end
end
@doc """
The most-marked repositories for one symbol. Same visibility rules as
`top_repositories/2`; `[]` for a symbol the instance doesn't configure.
"""
def top_for_symbol(symbol, viewer, limit \\ nil) do
limit = limit || top_limit()
if known?(symbol) do
rows =
from(s in RepositorySym,
join: r in Repository,
on: r.id == s.repository_id,
where: s.symbol == ^symbol,
where: ^visibility_filter(viewer),
group_by: s.repository_id,
order_by: [desc: count(s.id), asc: s.repository_id],
limit: ^limit,
select: {s.repository_id, count(s.id)}
)
|> Repo.all()
decorate_ranking(rows, symbols())
else
[]
end
end
@doc "Instance-wide count per symbol, across everything readable."
def global_counts(viewer) do
current = symbols()
from(s in RepositorySym,
join: r in Repository,
on: r.id == s.repository_id,
where: s.symbol in ^current,
where: ^visibility_filter(viewer),
group_by: s.symbol,
select: {s.symbol, count(s.id)}
)
|> Repo.all()
|> Map.new()
end
# Anonymous visitors see public repos; a signed-in user also sees
# internal ones and their own private repos. Mirrors the read rules
# used elsewhere — org membership and team grants are not yet
# reflected here, so a private org repo stays out of the ranking.
defp visibility_filter(%User{id: uid}) do
dynamic([_s, r], r.visibility in ["public", "internal"] or r.owner_id == ^uid)
end
defp visibility_filter(_), do: dynamic([_s, r], r.visibility == "public")
# One extra query for the per-symbol breakdown of the ranked repos,
# rather than one per row.
defp decorate_ranking([], _current), do: []
defp decorate_ranking(rows, current) do
ids = Enum.map(rows, &elem(&1, 0))
breakdown =
from(s in RepositorySym,
where: s.repository_id in ^ids and s.symbol in ^current,
group_by: [s.repository_id, s.symbol],
select: {s.repository_id, s.symbol, count(s.id)}
)
|> Repo.all()
|> Enum.group_by(&elem(&1, 0), fn {_, sym, n} -> {sym, n} end)
|> Map.new(fn {rid, pairs} -> {rid, Map.new(pairs)} end)
repos =
from(r in Repository, where: r.id in ^ids)
|> Repo.all()
|> Repo.preload([:owner, :organization])
|> Map.new(&{&1.id, &1})
for {rid, total} <- rows, repo = repos[rid] do
%{repository: repo, total: total, counts: Map.get(breakdown, rid, %{})}
end
end
@doc """
Repositories `user` marked with `symbol`, newest mark first.
Returns `[]` for a symbol the instance no longer configures. Callers
are responsible for visibility filtering — see
`GitGud.Repositories.can_read?/2` equivalents at the call site.
"""
def list_marked(%User{id: uid}, symbol) do
if known?(symbol) do
from(s in RepositorySym,
join: r in Repository,
on: r.id == s.repository_id,
where: s.user_id == ^uid and s.symbol == ^symbol,
order_by: [desc: s.inserted_at],
select: r
)
|> Repo.all()
|> Repo.preload([:owner, :organization])
else
[]
end
end
end
neiam /gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Packages
⭐
Log in to mark this repository.
7.5 KiB · text
History
34f99c6