diff --git a/lib/mix/tasks/hex.info.ex b/lib/mix/tasks/hex.info.ex index 93036d24..a71952f1 100644 --- a/lib/mix/tasks/hex.info.ex +++ b/lib/mix/tasks/hex.info.ex @@ -117,28 +117,73 @@ defmodule Mix.Tasks.Hex.Info do retirements = package["retirements"] || %{} Hex.Shell.info("Config: " <> package["configs"]["mix.exs"]) print_locked_package(locked_package) - Hex.Shell.info(["Releases: "] ++ format_releases(releases, Map.keys(retirements)) ++ ["\n"]) + + Hex.Shell.info(["\nRecent releases:\n" | format_releases(releases, Map.keys(retirements))]) + + print_downloads(package["downloads"]) print_meta(meta) end defp format_releases(releases, retirements) do {releases, rest} = Enum.split(releases, 8) - Enum.map(releases, &format_version(&1, retirements)) - |> Enum.intersperse([", "]) + releases + |> Enum.map(&[" ", format_version(&1, retirements), "\n"]) |> add_ellipsis(rest) end - defp format_version(%{"version" => version}, retirements) do + defp format_version(%{"version" => version} = release, retirements) do + date = format_release_date(release["inserted_at"]) + if version in retirements do - [:yellow, version, " (retired)", :reset] + [:yellow, version, date, " (retired)", :reset] else - [version] + [version, date] + end + end + + defp format_release_date(nil), do: "" + + defp format_release_date(date_string) do + case parse_date(date_string) do + {:ok, date} -> " (#{date})" + _ -> "" end end defp add_ellipsis(output, []), do: output - defp add_ellipsis(output, _rest), do: output ++ [", ..."] + defp add_ellipsis(output, _rest), do: output ++ [" ...\n"] + + defp print_downloads(nil), do: :ok + + defp print_downloads(downloads) do + parts = + Enum.reject( + [ + format_download_count("Yesterday", downloads["day"]), + format_download_count("Last 7 days", downloads["week"]), + format_download_count("All time", downloads["all"]) + ], + &is_nil/1 + ) + + if parts != [] do + Hex.Shell.info("Downloads:\n " <> Enum.join(parts, "\n ") <> "\n") + end + end + + defp format_download_count(_label, nil), do: nil + defp format_download_count(label, count), do: "#{label}: #{add_thousands_separators(count)}" + + defp add_thousands_separators(count, separator \\ " ") do + count + |> Integer.to_string() + |> String.reverse() + |> String.codepoints() + |> Enum.chunk_every(3) + |> Enum.join(separator) + |> String.reverse() + end defp print_meta(meta) do print_list(meta, "licenses") @@ -150,11 +195,14 @@ defmodule Mix.Tasks.Hex.Info do print_retirement(release) Hex.Shell.info("Config: " <> release["configs"]["mix.exs"]) + print_release_published_at(release["inserted_at"]) if release["has_docs"] do Hex.Shell.info("Documentation at: #{Hex.Utils.hexdocs_url(organization, package, version)}") end + print_release_downloads(release["downloads"]) + if requirements = release["requirements"] do Hex.Shell.info("Dependencies:") @@ -169,6 +217,28 @@ defmodule Mix.Tasks.Hex.Info do print_publisher(release) end + defp print_release_downloads(count) when is_integer(count) do + Hex.Shell.info("Downloads: #{add_thousands_separators(count)}") + end + + defp print_release_downloads(_), do: :ok + + defp print_release_published_at(nil), do: :ok + + defp print_release_published_at(date_string) do + case parse_date(date_string) do + {:ok, date} -> Hex.Shell.info("Released: #{date}") + _ -> :ok + end + end + + defp parse_date(date_string) do + case DateTime.from_iso8601(date_string) do + {:ok, datetime, _offset} -> {:ok, DateTime.to_date(datetime)} + _ -> Date.from_iso8601(date_string) + end + end + defp print_locked_package(nil), do: nil defp print_locked_package(locked_package) do diff --git a/test/mix/tasks/hex.info_test.exs b/test/mix/tasks/hex.info_test.exs index 0ee60861..e7e66463 100644 --- a/test/mix/tasks/hex.info_test.exs +++ b/test/mix/tasks/hex.info_test.exs @@ -17,7 +17,9 @@ defmodule Mix.Tasks.Hex.InfoTest do Mix.Tasks.Hex.Info.run(["ex_doc"]) assert_received {:mix_shell, :info, ["Some description\n"]} assert_received {:mix_shell, :info, ["Config: {:ex_doc, \"~> 0.1.0\"}"]} - assert_received {:mix_shell, :info, ["Releases: 0.1.0, 0.1.0-rc1, 0.0.1\n"]} + assert_received {:mix_shell, :info, ["\nRecent releases:\n" <> releases]} + today = Date.utc_today() + assert releases == " 0.1.0 (#{today})\n 0.1.0-rc1 (#{today})\n 0.0.1 (#{today})\n" assert catch_throw(Mix.Tasks.Hex.Info.run(["no_package"])) == {:exit_code, 1} assert_received {:mix_shell, :error, ["No package with name no_package"]} @@ -26,6 +28,52 @@ defmodule Mix.Tasks.Hex.InfoTest do assert_received {:mix_shell, :error, ["Package name is empty"]} end + test "package downloads" do + bypass = Bypass.open() + Hex.State.put(:api_url, "http://localhost:#{bypass.port}/api") + + today = Date.utc_today() + inserted_at = "#{today}T12:00:00Z" + + package_body = %{ + "name" => "ex_doc", + "meta" => %{ + "description" => "Some description", + "licenses" => ["GPL-2.0", "MIT", "Apache-2.0"], + "links" => %{"docs" => "http://docs", "repo" => "http://repo"} + }, + "configs" => %{"mix.exs" => "{:ex_doc, \"~> 0.1.0\"}"}, + "releases" => [ + %{"version" => "0.1.0", "inserted_at" => inserted_at}, + %{"version" => "0.1.0-rc1", "inserted_at" => inserted_at}, + %{"version" => "0.0.1", "inserted_at" => inserted_at} + ], + "retirements" => %{}, + "downloads" => %{ + "all" => 96_128_698, + "day" => 21_494, + "recent" => 1_421_136, + "week" => 124_095 + } + } + + Bypass.expect(bypass, "GET", "/api/packages/ex_doc", fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") + |> Plug.Conn.resp(200, Hex.Utils.safe_serialize_erlang(package_body)) + end) + + Mix.Tasks.Hex.Info.run(["ex_doc"]) + assert_received {:mix_shell, :info, ["Downloads:\n" <> downloads]} + + assert String.split(downloads, "\n") == [ + " Yesterday: 21 494", + " Last 7 days: 124 095", + " All time: 96 128 698", + "" + ] + end + test "locked package" do Mix.Project.push(Simple) @@ -38,7 +86,17 @@ defmodule Mix.Tasks.Hex.InfoTest do assert_received {:mix_shell, :info, ["Some description\n"]} assert_received {:mix_shell, :info, ["Locked version: 0.2.0"]} assert_received {:mix_shell, :info, ["Config: {:ecto, \"~> 3.3\"}"]} - assert_received {:mix_shell, :info, ["Releases: 3.3.2, 3.3.1, 0.2.1, 0.2.0\n"]} + assert_received {:mix_shell, :info, ["\nRecent releases:\n" <> releases]} + + today = Date.utc_today() + + assert String.split(releases, "\n") == [ + " 3.3.2 (#{today})", + " 3.3.1 (#{today})", + " 0.2.1 (#{today})", + " 0.2.0 (#{today})", + "" + ] end) after purge([ @@ -50,7 +108,9 @@ defmodule Mix.Tasks.Hex.InfoTest do test "package with retired release" do Mix.Tasks.Hex.Info.run(["tired"]) - assert_received {:mix_shell, :info, ["Releases: 0.2.0, 0.1.0 (retired)\n"]} + today = Date.utc_today() + assert_received {:mix_shell, :info, ["\nRecent releases:\n" <> releases]} + assert releases == " 0.2.0 (#{today})\n 0.1.0 (#{today}) (retired)\n" end test "package with --organization flag" do @@ -83,8 +143,46 @@ defmodule Mix.Tasks.Hex.InfoTest do assert_received {:mix_shell, :error, ["No release with name ex_doc 1.2.3"]} end + test "release downloads" do + bypass = Bypass.open() + Hex.State.put(:api_url, "http://localhost:#{bypass.port}/api") + + today = Date.utc_today() + inserted_at = "#{today}T12:00:00Z" + + release_body = %{ + "version" => "1.5.0-alpha.2", + "checksum" => "6dcaa0d9fdc22afe9b4d362f17f20844a85f121c50b6e9b9466ac04fe39f3665", + "inserted_at" => inserted_at, + "updated_at" => inserted_at, + "retirement" => nil, + "publisher" => nil, + "downloads" => 26_208, + "configs" => %{ + "erlang.mk" => "dep_jason = hex 1.5.0-alpha.2", + "mix.exs" => "{:jason, \"~\u003E 1.5.0-alpha.2\"}", + "rebar.config" => "{jason, \"1.5.0-alpha.2\"}" + } + } + + Bypass.expect(bypass, "GET", "/api/packages/ex_doc/releases/0.1.0", fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/vnd.hex+erlang") + |> Plug.Conn.resp(200, Hex.Utils.safe_serialize_erlang(release_body)) + end) + + Mix.Tasks.Hex.Info.run(["ex_doc", "0.1.0"]) + assert_received {:mix_shell, :info, ["Downloads: 26 208"]} + end + test "prints publisher info for releases" do Mix.Tasks.Hex.Info.run(["ex_doc", "0.0.1"]) assert_received {:mix_shell, :info, ["Published by: user (user@mail.com)"]} end + + test "prints release date for releases" do + Mix.Tasks.Hex.Info.run(["ex_doc", "0.0.1"]) + assert_received {:mix_shell, :info, ["Released: " <> date]} + assert date == "#{Date.utc_today()}" + end end