From ed0b129ff5b15640748b5125362b4a00648f412b Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Mon, 30 Mar 2026 07:49:54 -0500 Subject: [PATCH 1/6] Add download stats and release dates to mix hex.info This adds download counts and release dates (provided by `hex.pm/api/packages/[package_name]`) to the `mix hex.info [package_name]` and `mix hex.info [package_name] [version]` commands. Download counts for the overall package info include yesterday, last 7 days, and all-time. Showing the download numbers for the current version (like the Hex.pm website does) would require a second HTTP request, which seemed iffy. I also wasn't clear on what the "recent" key in the downloads meant (it looks like it might be the last 8 releases), so I left that out of the download output. Resolves #1128 --- lib/mix/tasks/hex.info.ex | 60 ++++++++++++++++++++++++++++++-- test/mix/tasks/hex.info_test.exs | 25 +++++++++++-- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/lib/mix/tasks/hex.info.ex b/lib/mix/tasks/hex.info.ex index 93036d24..4d113312 100644 --- a/lib/mix/tasks/hex.info.ex +++ b/lib/mix/tasks/hex.info.ex @@ -118,6 +118,7 @@ defmodule Mix.Tasks.Hex.Info do Hex.Shell.info("Config: " <> package["configs"]["mix.exs"]) print_locked_package(locked_package) Hex.Shell.info(["Releases: "] ++ format_releases(releases, Map.keys(retirements)) ++ ["\n"]) + print_downloads(package["downloads"]) print_meta(meta) end @@ -129,17 +130,49 @@ defmodule Mix.Tasks.Hex.Info do |> 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 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}: #{count}" + defp print_meta(meta) do print_list(meta, "licenses") print_dict(meta, "links") @@ -150,11 +183,16 @@ 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 + if downloads = release["downloads"] do + Hex.Shell.info("Downloads: #{downloads}") + end + if requirements = release["requirements"] do Hex.Shell.info("Dependencies:") @@ -169,6 +207,22 @@ defmodule Mix.Tasks.Hex.Info do print_publisher(release) end + 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..de418265 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, ["Releases: " <> releases]} + today = Date.utc_today() + assert releases == "0.1.0 (#{today}), 0.1.0-rc1 (#{today}), 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"]} @@ -38,7 +40,16 @@ 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, ["Releases: " <> releases]} + + + today = Date.utc_today() + assert String.split(releases, ", ") == [ + "3.3.2 (#{today})", + "3.3.1 (#{today})", + "0.2.1 (#{today})", + "0.2.0 (#{today})\n" + ] end) after purge([ @@ -50,7 +61,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, ["Releases: " <> releases]} + assert releases == "0.2.0 (#{today}), 0.1.0 (#{today}) (retired)\n" end test "package with --organization flag" do @@ -87,4 +100,10 @@ defmodule Mix.Tasks.Hex.InfoTest 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 From 44791128190e7a32c29515bcc024b7031c121400 Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Thu, 9 Apr 2026 07:45:58 -0500 Subject: [PATCH 2/6] Fix formatting per @ericmj --- lib/mix/tasks/hex.info.ex | 36 +++++++++++++++++++++++++------- test/mix/tasks/hex.info_test.exs | 27 ++++++++++++++---------- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/lib/mix/tasks/hex.info.ex b/lib/mix/tasks/hex.info.ex index 4d113312..bea8cdd4 100644 --- a/lib/mix/tasks/hex.info.ex +++ b/lib/mix/tasks/hex.info.ex @@ -117,7 +117,12 @@ 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( + ["Recent releases:\n"] ++ format_releases(releases, Map.keys(retirements)) ++ ["\n"] + ) + + dbg(package["downloads"]) print_downloads(package["downloads"]) print_meta(meta) end @@ -125,16 +130,21 @@ defmodule Mix.Tasks.Hex.Info do defp format_releases(releases, retirements) do {releases, rest} = Enum.split(releases, 8) - Enum.map(releases, &format_version(&1, retirements)) - |> Enum.intersperse([", "]) + releases + |> Enum.map(fn release -> + release + |> format_version(retirements) + |> Enum.join(" ") + end) |> add_ellipsis(rest) + |> Enum.map(&" #{&1}\n") end defp format_version(%{"version" => version} = release, retirements) do date = format_release_date(release["inserted_at"]) if version in retirements do - [:yellow, version, date, " (retired)", :reset] + [:yellow, version, date, "(retired)", :reset] else [version, date] end @@ -144,13 +154,13 @@ defmodule Mix.Tasks.Hex.Info do defp format_release_date(date_string) do case parse_date(date_string) do - {:ok, date} -> " (#{date})" + {: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 ++ ["..."] defp print_downloads(nil), do: :ok @@ -171,7 +181,17 @@ defmodule Mix.Tasks.Hex.Info do end defp format_download_count(_label, nil), do: nil - defp format_download_count(label, count), do: "#{label}: #{count}" + 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") @@ -190,7 +210,7 @@ defmodule Mix.Tasks.Hex.Info do end if downloads = release["downloads"] do - Hex.Shell.info("Downloads: #{downloads}") + Hex.Shell.info("Downloads: #{add_thousands_separators(downloads)}") end if requirements = release["requirements"] do diff --git a/test/mix/tasks/hex.info_test.exs b/test/mix/tasks/hex.info_test.exs index de418265..6b5c2234 100644 --- a/test/mix/tasks/hex.info_test.exs +++ b/test/mix/tasks/hex.info_test.exs @@ -17,9 +17,12 @@ 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: " <> releases]} + assert_received {:mix_shell, :info, ["Recent releases:\n" <> releases]} + assert_received {:mix_shell, :info, ["Downloads:\n" <> downloads]} today = Date.utc_today() - assert releases == "0.1.0 (#{today}), 0.1.0-rc1 (#{today}), 0.0.1 (#{today})\n" + assert releases == " 0.1.0 (#{today})\n 0.1.0-rc1 (#{today})\n 0.0.1 (#{today})\n\n" + + assert downloads == "Yesterday: 123\nLast 7 days: 12 345\nAll time: 123 456\n\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"]} @@ -40,15 +43,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: " <> releases]} - + assert_received {:mix_shell, :info, ["Recent releases:\n" <> releases]} today = Date.utc_today() - assert String.split(releases, ", ") == [ - "3.3.2 (#{today})", - "3.3.1 (#{today})", - "0.2.1 (#{today})", - "0.2.0 (#{today})\n" + + assert String.split(releases, "\n") == [ + " 3.3.2 (#{today})", + " 3.3.1 (#{today})", + " 0.2.1 (#{today})", + " 0.2.0 (#{today})", + "", + "" ] end) after @@ -62,8 +67,8 @@ defmodule Mix.Tasks.Hex.InfoTest do test "package with retired release" do Mix.Tasks.Hex.Info.run(["tired"]) today = Date.utc_today() - assert_received {:mix_shell, :info, ["Releases: " <> releases]} - assert releases == "0.2.0 (#{today}), 0.1.0 (#{today}) (retired)\n" + assert_received {:mix_shell, :info, ["Recent releases:\n" <> releases]} + assert releases == " 0.2.0 (#{today})\n yellow 0.1.0 (#{today}) (retired) reset\n\n" end test "package with --organization flag" do From 32b91d2a594cac16259120768a8c8691e5976fd8 Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Thu, 9 Apr 2026 08:52:04 -0500 Subject: [PATCH 3/6] Remove accidentally committed dbg --- lib/mix/tasks/hex.info.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/mix/tasks/hex.info.ex b/lib/mix/tasks/hex.info.ex index bea8cdd4..68657aa3 100644 --- a/lib/mix/tasks/hex.info.ex +++ b/lib/mix/tasks/hex.info.ex @@ -122,7 +122,6 @@ defmodule Mix.Tasks.Hex.Info do ["Recent releases:\n"] ++ format_releases(releases, Map.keys(retirements)) ++ ["\n"] ) - dbg(package["downloads"]) print_downloads(package["downloads"]) print_meta(meta) end From 67f6180f84987782d969c6b2af7fbc2fe177fc4a Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Sat, 11 Apr 2026 18:10:46 -0500 Subject: [PATCH 4/6] Add downloads tests --- lib/mix/tasks/hex.info.ex | 8 +++- test/mix/tasks/hex.info_test.exs | 80 ++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/lib/mix/tasks/hex.info.ex b/lib/mix/tasks/hex.info.ex index 68657aa3..3c452a53 100644 --- a/lib/mix/tasks/hex.info.ex +++ b/lib/mix/tasks/hex.info.ex @@ -208,8 +208,12 @@ defmodule Mix.Tasks.Hex.Info do Hex.Shell.info("Documentation at: #{Hex.Utils.hexdocs_url(organization, package, version)}") end - if downloads = release["downloads"] do - Hex.Shell.info("Downloads: #{add_thousands_separators(downloads)}") + case release["downloads"] do + download_count when is_integer(download_count) -> + Hex.Shell.info("Downloads: #{add_thousands_separators(download_count)}") + + _ -> + :ok end if requirements = release["requirements"] do diff --git a/test/mix/tasks/hex.info_test.exs b/test/mix/tasks/hex.info_test.exs index 6b5c2234..1ea25367 100644 --- a/test/mix/tasks/hex.info_test.exs +++ b/test/mix/tasks/hex.info_test.exs @@ -18,12 +18,9 @@ defmodule Mix.Tasks.Hex.InfoTest do assert_received {:mix_shell, :info, ["Some description\n"]} assert_received {:mix_shell, :info, ["Config: {:ex_doc, \"~> 0.1.0\"}"]} assert_received {:mix_shell, :info, ["Recent releases:\n" <> releases]} - assert_received {:mix_shell, :info, ["Downloads:\n" <> downloads]} today = Date.utc_today() assert releases == " 0.1.0 (#{today})\n 0.1.0-rc1 (#{today})\n 0.0.1 (#{today})\n\n" - assert downloads == "Yesterday: 123\nLast 7 days: 12 345\nAll time: 123 456\n\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"]} @@ -31,6 +28,51 @@ 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) @@ -101,6 +143,38 @@ 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)"]} From 2f0c4c8a565fe6eb40f5bb7b1a0a9a23411cf35a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Sun, 12 Apr 2026 20:11:35 -0700 Subject: [PATCH 5/6] Preserve ANSI color for retired releases and fix test fixture Restore iodata pipeline in format_releases so :yellow/:reset atoms reach Hex.Shell rather than being stringified by Enum.join. Fix "configs": atom-key in the release downloads test fixture that made release["configs"]["mix.exs"] return nil. --- lib/mix/tasks/hex.info.ex | 27 +++++++++++---------------- test/mix/tasks/hex.info_test.exs | 7 ++++--- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/lib/mix/tasks/hex.info.ex b/lib/mix/tasks/hex.info.ex index 3c452a53..654f7e75 100644 --- a/lib/mix/tasks/hex.info.ex +++ b/lib/mix/tasks/hex.info.ex @@ -130,20 +130,15 @@ defmodule Mix.Tasks.Hex.Info do {releases, rest} = Enum.split(releases, 8) releases - |> Enum.map(fn release -> - release - |> format_version(retirements) - |> Enum.join(" ") - end) + |> Enum.map(&[" ", format_version(&1, retirements), "\n"]) |> add_ellipsis(rest) - |> Enum.map(&" #{&1}\n") end defp format_version(%{"version" => version} = release, retirements) do date = format_release_date(release["inserted_at"]) if version in retirements do - [:yellow, version, date, "(retired)", :reset] + [:yellow, version, date, " (retired)", :reset] else [version, date] end @@ -153,13 +148,13 @@ defmodule Mix.Tasks.Hex.Info do defp format_release_date(date_string) do case parse_date(date_string) do - {:ok, date} -> "(#{date})" + {: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 @@ -208,13 +203,7 @@ defmodule Mix.Tasks.Hex.Info do Hex.Shell.info("Documentation at: #{Hex.Utils.hexdocs_url(organization, package, version)}") end - case release["downloads"] do - download_count when is_integer(download_count) -> - Hex.Shell.info("Downloads: #{add_thousands_separators(download_count)}") - - _ -> - :ok - end + print_release_downloads(release["downloads"]) if requirements = release["requirements"] do Hex.Shell.info("Dependencies:") @@ -230,6 +219,12 @@ 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 diff --git a/test/mix/tasks/hex.info_test.exs b/test/mix/tasks/hex.info_test.exs index 1ea25367..11d40b93 100644 --- a/test/mix/tasks/hex.info_test.exs +++ b/test/mix/tasks/hex.info_test.exs @@ -54,7 +54,7 @@ defmodule Mix.Tasks.Hex.InfoTest do "day" => 21_494, "recent" => 1_421_136, "week" => 124_095 - }, + } } Bypass.expect(bypass, "GET", "/api/packages/ex_doc", fn conn -> @@ -65,6 +65,7 @@ defmodule Mix.Tasks.Hex.InfoTest do 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", @@ -110,7 +111,7 @@ defmodule Mix.Tasks.Hex.InfoTest do Mix.Tasks.Hex.Info.run(["tired"]) today = Date.utc_today() assert_received {:mix_shell, :info, ["Recent releases:\n" <> releases]} - assert releases == " 0.2.0 (#{today})\n yellow 0.1.0 (#{today}) (retired) reset\n\n" + assert releases == " 0.2.0 (#{today})\n 0.1.0 (#{today}) (retired)\n\n" end test "package with --organization flag" do @@ -158,7 +159,7 @@ defmodule Mix.Tasks.Hex.InfoTest do "retirement" => nil, "publisher" => nil, "downloads" => 26_208, - "configs": %{ + "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\"}" From 0f84c90e146a5cc5664411ff02177914a2879c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Sun, 12 Apr 2026 20:15:44 -0700 Subject: [PATCH 6/6] Fix Recent releases spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the extra trailing "\n" in the Recent releases info call — each entry already ends with a newline and IO.puts adds another, which produced two blank lines. Add a leading newline so there's a blank line between the Config/Locked version block and the release list. --- lib/mix/tasks/hex.info.ex | 4 +--- test/mix/tasks/hex.info_test.exs | 11 +++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/mix/tasks/hex.info.ex b/lib/mix/tasks/hex.info.ex index 654f7e75..a71952f1 100644 --- a/lib/mix/tasks/hex.info.ex +++ b/lib/mix/tasks/hex.info.ex @@ -118,9 +118,7 @@ defmodule Mix.Tasks.Hex.Info do Hex.Shell.info("Config: " <> package["configs"]["mix.exs"]) print_locked_package(locked_package) - Hex.Shell.info( - ["Recent releases:\n"] ++ 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) diff --git a/test/mix/tasks/hex.info_test.exs b/test/mix/tasks/hex.info_test.exs index 11d40b93..e7e66463 100644 --- a/test/mix/tasks/hex.info_test.exs +++ b/test/mix/tasks/hex.info_test.exs @@ -17,9 +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, ["Recent releases:\n" <> releases]} + 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\n" + 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"]} @@ -86,7 +86,7 @@ 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, ["Recent releases:\n" <> releases]} + assert_received {:mix_shell, :info, ["\nRecent releases:\n" <> releases]} today = Date.utc_today() @@ -95,7 +95,6 @@ defmodule Mix.Tasks.Hex.InfoTest do " 3.3.1 (#{today})", " 0.2.1 (#{today})", " 0.2.0 (#{today})", - "", "" ] end) @@ -110,8 +109,8 @@ defmodule Mix.Tasks.Hex.InfoTest do test "package with retired release" do Mix.Tasks.Hex.Info.run(["tired"]) today = Date.utc_today() - assert_received {:mix_shell, :info, ["Recent releases:\n" <> releases]} - assert releases == " 0.2.0 (#{today})\n 0.1.0 (#{today}) (retired)\n\n" + 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