Current section
Files
Jump to
Current section
Files
lib/mix/tasks/spec.index.ex
defmodule Mix.Tasks.Spec.Index do
use Mix.Task
@shortdoc "Generate or refresh index.md for a spec-hub"
@moduledoc """
Usage:
mix spec.index --hub ../lang-spec-hub
Scans `<hub>/requests/<project>/<id>` and writes `<hub>/index.md` grouped by project
and sorted by updated time from request.json/ack.json or file mtimes.
"""
@impl true
def run(argv) do
{opts, _, _} = OptionParser.parse(argv, switches: [hub: :string])
hub = Keyword.get(opts, :hub) || Mix.raise("Missing --hub")
req_root = Path.join(hub, "requests")
projects = Path.wildcard(Path.join(req_root, "*")) |> Enum.filter(&File.dir?/1)
sections =
Enum.map(projects, fn proj_dir ->
project = Path.basename(proj_dir)
rows = entries_for_project(proj_dir, project)
["## ", project, "\n\n", rows, "\n"]
end)
|> IO.iodata_to_binary()
content =
[
"# Spec Hub Index\n\n",
"Generated at ",
DateTime.utc_now() |> DateTime.to_iso8601(),
"\n\n",
sections
]
|> IO.iodata_to_binary()
File.write!(Path.join(hub, "index.md"), content)
Mix.shell().info("Updated #{Path.join(hub, "index.md")}")
end
defp entries_for_project(dir, project) do
ids =
Path.wildcard(Path.join(dir, "*")) |> Enum.filter(&File.dir?/1) |> Enum.map(&Path.basename/1)
items = Enum.map(ids, fn id -> {id, info_for(Path.join(dir, id))} end)
items = Enum.sort_by(items, fn {_id, info} -> info.updated end, {:desc, DateTime})
[
table_header(),
Enum.map(items, fn {id, info} -> table_row(project, id, info) end)
]
end
defp info_for(path) do
req = maybe_decode(Path.join(path, "request.json")) || %{}
ack = maybe_decode(Path.join(path, "ack.json")) || %{}
statuses =
Path.wildcard(Path.join(path, "*.status"))
|> Enum.map(&Path.basename/1)
|> Enum.map(&String.trim_trailing(&1, ".status"))
updated =
["ack.json", "request.json", "thread.md"]
|> Enum.map(&Path.join(path, &1))
|> Enum.filter(&File.exists?/1)
|> Enum.map(&mtime!/1)
|> Enum.max_by(& &1, fn -> DateTime.from_unix!(0) end)
%{title: req["title"], statuses: statuses, request: req, ack: ack, updated: updated}
end
defp table_header do
[
"| Status | ID | Title | Updated | Links |\n",
"|:------:|:----|:------|:--------|:------|\n"
]
end
defp table_row(project, id, %{title: title, statuses: statuses, updated: updated}) do
updated_str =
case updated do
%DateTime{} = dt -> dt |> DateTime.truncate(:second) |> DateTime.to_iso8601()
_ -> ""
end
req_link = ["[request](requests/", project, "/", id, "/request.json)"]
thread_link = ["[thread](requests/", project, "/", id, "/thread.md)"]
links = [thread_link, " Β· ", req_link]
[
"| ",
status_badges(statuses),
" | ",
id,
" | ",
title || "(no title)",
" | ",
(updated_str == "" && "") || updated_str,
" | ",
links,
" |\n"
]
end
defp status_badges([]), do: badge("proposed")
defp status_badges(list) when is_list(list) do
list
|> Enum.map(&badge/1)
|> Enum.join(" ")
end
defp badge("proposed"), do: "π‘ proposed"
defp badge("accepted"), do: "π’ accepted"
defp badge("in_progress"), do: "π΅ in_progress"
defp badge("done"), do: "β
done"
defp badge("rejected"), do: "β rejected"
defp badge("blocked"), do: "π₯ blocked"
defp badge(other) when is_binary(other), do: other
defp maybe_decode(path) do
if File.exists?(path) do
try do
Jason.decode!(File.read!(path))
rescue
_ -> nil
end
else
nil
end
end
defp mtime!(path) do
{:ok, stat} = File.stat(path)
stat.mtime
|> NaiveDateTime.to_erl()
|> NaiveDateTime.from_erl!()
|> DateTime.from_naive!("Etc/UTC")
end
end