From 9f154871c40595e4086bdc44df7061408c618a46 Mon Sep 17 00:00:00 2001 From: Tyler Witt Date: Thu, 23 Jan 2025 21:04:12 +0900 Subject: [PATCH 1/6] Refactor hex.outdated This unifies outdated dependency processing. All behaviors should be the same, except for one edge case. If there is only one dependency, the display will be the same as `single`. I'm not sure if that's an issue or not. Assuming that the current UI is not meant to be programmatically accessed it should be fine. --- lib/mix/tasks/hex.outdated.ex | 163 ++++++++++++++++------------------ 1 file changed, 77 insertions(+), 86 deletions(-) diff --git a/lib/mix/tasks/hex.outdated.ex b/lib/mix/tasks/hex.outdated.ex index 8feea485..49b5f1c2 100644 --- a/lib/mix/tasks/hex.outdated.ex +++ b/lib/mix/tasks/hex.outdated.ex @@ -48,7 +48,7 @@ defmodule Mix.Tasks.Hex.Outdated do """ @behaviour Hex.Mix.TaskDescription - @switches [all: :boolean, pre: :boolean, within_requirements: :boolean, sort: :string] + @switches [all: :boolean, pre: :boolean, within_requirements: :boolean, sort: :string, json: :boolean] @impl true def run(args) do @@ -61,22 +61,11 @@ defmodule Mix.Tasks.Hex.Outdated do lock |> Hex.Mix.packages_from_lock() - |> Hex.Registry.Server.prefetch() + |> Registry.prefetch() - case args do - [app] -> - single(lock, app, opts) - - [] -> - all(lock, opts) - - _ -> - Mix.raise(""" - Invalid arguments, expected: - - mix hex.outdated [APP] - """) - end + lock + |> process_lockfile(args, opts) + |> display_outdated(opts) end @impl true @@ -87,25 +76,39 @@ defmodule Mix.Tasks.Hex.Outdated do ] end - defp single(lock, app, opts) do - app = String.to_atom(app) + defp process_lockfile(lock, args, opts) do deps = Hex.Mix.top_level_deps() - {repo, package, current} = - case Hex.Utils.lock(lock[app]) do - %{repo: repo, name: package, version: version} -> - {repo, package, version} + dep_names = requested_dep_names(deps, lock, args, opts) - nil -> - Mix.raise("Dependency #{app} not locked as a Hex package") - end + dep_names + |> Enum.sort() + |> get_versions(deps, lock, opts[:pre]) + end - latest = latest_version(repo, package, current, opts[:pre]) - outdated? = Version.compare(current, latest) == :lt - lock_requirements = get_requirements_from_lock(app, lock) - deps_requirements = get_requirements_from_deps(app, deps) - requirements = deps_requirements ++ lock_requirements + defp requested_dep_names(_deps, lock, [app], _opts) do + app = String.to_atom(app) + + if is_nil(Hex.Utils.lock(lock[app])) do + Mix.raise("Dependency #{app} not locked as a Hex package") + end + [app] + end + + defp requested_dep_names(deps, lock, [], opts) do + if opts[:all], do: Map.keys(lock), else: Map.keys(deps) + end + + defp requested_dep_names(_deps, _lock, _args, _opts) do + Mix.raise(""" + Invalid arguments, expected: + + mix hex.outdated [APP] + """) + end + + defp display_outdated([{package, current, latest, requirements, outdated?}], _opts) do if outdated? do [ "There is newer version of the dependency available ", @@ -131,6 +134,40 @@ defmodule Mix.Tasks.Hex.Outdated do if outdated?, do: Mix.Tasks.Hex.set_exit_code(1) end + defp display_outdated(versions, opts) do + values = versions |> Enum.map(&format_all_row/1) |> maybe_sort_by(opts[:sort]) + + diff_links = Enum.map(versions, &build_diff_link/1) |> Enum.reject(&is_nil/1) + + if Enum.empty?(values) do + Hex.Shell.info("No hex dependencies") + else + header = ["Dependency", "Current", "Latest", "Status"] + Mix.Tasks.Hex.print_table(header, values) + + base_message = "Run `mix hex.outdated APP` to see requirements for a specific dependency." + diff_message = maybe_diff_message(diff_links) + Hex.Shell.info(["\n", base_message, diff_message]) + + any_outdated? = any_outdated?(versions) + req_met? = any_req_matches?(versions) + + cond do + any_outdated? && opts[:within_requirements] && req_met? -> + Mix.Tasks.Hex.set_exit_code(1) + + any_outdated? && opts[:within_requirements] && not req_met? -> + nil + + any_outdated? -> + Mix.Tasks.Hex.set_exit_code(1) + + true -> + nil + end + end + end + defp get_requirements_from_lock(app, lock) do Enum.flat_map(lock, fn {source, lock} -> case Hex.Utils.lock(lock) do @@ -167,48 +204,6 @@ defmodule Mix.Tasks.Hex.Outdated do [[:bright, source], [req_color, req || ""], [req_color, up_to_date?]] end - defp all(lock, opts) do - deps = Hex.Mix.top_level_deps() - dep_names = if opts[:all], do: Map.keys(lock), else: Map.keys(deps) - - versions = - dep_names - |> Enum.sort() - |> get_versions(deps, lock, opts[:pre]) - - values = versions |> Enum.map(&format_all_row/1) |> maybe_sort_by(opts[:sort]) - - diff_links = Enum.map(versions, &build_diff_link/1) |> Enum.reject(&is_nil/1) - - if Enum.empty?(values) do - Hex.Shell.info("No hex dependencies") - else - header = ["Dependency", "Current", "Latest", "Status"] - Mix.Tasks.Hex.print_table(header, values) - - base_message = "Run `mix hex.outdated APP` to see requirements for a specific dependency." - diff_message = maybe_diff_message(diff_links) - Hex.Shell.info(["\n", base_message, diff_message]) - - any_outdated? = any_outdated?(versions) - req_met? = any_req_matches?(versions) - - cond do - any_outdated? && opts[:within_requirements] && req_met? -> - Mix.Tasks.Hex.set_exit_code(1) - - any_outdated? && opts[:within_requirements] && not req_met? -> - nil - - any_outdated? -> - Mix.Tasks.Hex.set_exit_code(1) - - true -> - nil - end - end - end - defp maybe_sort_by(values, "status") do status_order = %{ "Up-to-date" => 1, @@ -234,11 +229,11 @@ defmodule Mix.Tasks.Hex.Outdated do lock_requirements = get_requirements_from_lock(name, lock) deps_requirements = get_requirements_from_deps(name, deps) - requirements = - (deps_requirements ++ lock_requirements) - |> Enum.map(fn [_, req_version] -> req_version end) + outdated? = Version.compare(lock_version, latest_version) == :lt + + requirements = deps_requirements ++ lock_requirements - [[Atom.to_string(name), lock_version, latest_version, requirements]] + [{Atom.to_string(name), lock_version, latest_version, requirements, outdated?}] _ -> [] @@ -265,8 +260,7 @@ defmodule Mix.Tasks.Hex.Outdated do List.last(versions) end - defp format_all_row([package, lock, latest, requirements]) do - outdated? = Version.compare(lock, latest) == :lt + defp format_all_row({package, lock, latest, requirements, outdated?}) do latest_color = if outdated?, do: :red, else: :green req_matches? = req_matches?(requirements, latest) @@ -285,9 +279,8 @@ defmodule Mix.Tasks.Hex.Outdated do ] end - defp build_diff_link([package, lock, latest, requirements]) do - outdated? = Version.compare(lock, latest) == :lt - req_matches? = Enum.all?(requirements, &version_match?(latest, &1)) + defp build_diff_link({package, lock, latest, requirements, outdated?}) do + req_matches? = req_matches?(requirements, latest) case {outdated?, req_matches?} do {true, true} -> "diffs[]=#{package}:#{lock}:#{latest}" @@ -299,9 +292,7 @@ defmodule Mix.Tasks.Hex.Outdated do defp version_match?(version, req), do: Version.match?(version, req) defp any_outdated?(versions) do - Enum.any?(versions, fn [_package, lock, latest, _requirements] -> - Version.compare(lock, latest) == :lt - end) + Enum.any?(versions, fn {_package, _lock, _latest, _requirements, outdated?} -> outdated? end) end defp maybe_diff_message([]), do: "" @@ -329,12 +320,12 @@ defmodule Mix.Tasks.Hex.Outdated do end defp any_req_matches?(versions) do - Enum.any?(versions, fn [_package, _lock, latest, requirements] -> + Enum.any?(versions, fn {_package, _lock, latest, requirements, _outdated?} -> req_matches?(requirements, latest) end) end defp req_matches?(requirements, latest) do - Enum.all?(requirements, &version_match?(latest, &1)) + Enum.all?(requirements, fn [_source, req_version] -> version_match?(latest, req_version) end) end end From 1b0e6034b520e2ccccc03d48ab85fee53c852890 Mon Sep 17 00:00:00 2001 From: Tyler Witt Date: Thu, 23 Jan 2025 21:24:54 +0900 Subject: [PATCH 2/6] Add support for outputting JSON Printed tables are hard to use programmatically. This change will enable the ability to output the task as serialized json. --- lib/mix/tasks/hex.outdated.ex | 25 +++++++++++++++++++++++-- mix.exs | 1 + mix.lock | 1 + 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/mix/tasks/hex.outdated.ex b/lib/mix/tasks/hex.outdated.ex index 49b5f1c2..773b4bff 100644 --- a/lib/mix/tasks/hex.outdated.ex +++ b/lib/mix/tasks/hex.outdated.ex @@ -108,7 +108,18 @@ defmodule Mix.Tasks.Hex.Outdated do """) end - defp display_outdated([{package, current, latest, requirements, outdated?}], _opts) do + defp display_outdated(versions, opts) do + if opts[:json] do + versions + |> Enum.map(&cast_version_map/1) + |> Jason.encode!() + |> Hex.Shell.info() + else + display_table(versions, opts) + end + end + + defp display_table([{package, current, latest, requirements, outdated?}], _opts) do if outdated? do [ "There is newer version of the dependency available ", @@ -134,7 +145,7 @@ defmodule Mix.Tasks.Hex.Outdated do if outdated?, do: Mix.Tasks.Hex.set_exit_code(1) end - defp display_outdated(versions, opts) do + defp display_table(versions, opts) do values = versions |> Enum.map(&format_all_row/1) |> maybe_sort_by(opts[:sort]) diff_links = Enum.map(versions, &build_diff_link/1) |> Enum.reject(&is_nil/1) @@ -328,4 +339,14 @@ defmodule Mix.Tasks.Hex.Outdated do defp req_matches?(requirements, latest) do Enum.all?(requirements, fn [_source, req_version] -> version_match?(latest, req_version) end) end + + defp cast_version_map({package, current, latest, requirements, outdated?}) do + %{ + package: package, + lock_version: current, + latest_version: latest, + requirements: Enum.map(requirements, fn [_source, req_version] -> req_version end), + outdated: outdated? + } + end end diff --git a/mix.exs b/mix.exs index dc206a73..4c74d1d9 100644 --- a/mix.exs +++ b/mix.exs @@ -29,6 +29,7 @@ defmodule Hex.MixProject do [ {:bypass, "~> 1.0.0"}, {:cowboy, "~> 2.7.0"}, + {:jason, "~> 1.0"}, {:mime, "~> 1.0"}, {:plug, "~> 1.9.0"}, {:plug_cowboy, "~> 2.1.0"}, diff --git a/mix.lock b/mix.lock index 0d45864f..e3c077dd 100644 --- a/mix.lock +++ b/mix.lock @@ -2,6 +2,7 @@ "bypass": {:hex, :bypass, "1.0.0", "b78b3dcb832a71aca5259c1a704b2e14b55fd4e1327ff942598b4e7d1a7ad83d", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}], "hexpm", "5a1dc855dfcc86160458c7a70d25f65d498bd8012bd4c06a8d3baa368dda3c45"}, "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"}, "cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "plug": {:hex, :plug, "1.9.0", "8d7c4e26962283ff9f8f3347bd73838e2413fbc38b7bb5467d5924f68f3a5a4a", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "9902eda2c52ada2a096434682e99a2493f5d06a94d6ac6bcfff9805f952350f1"}, "plug_cowboy": {:hex, :plug_cowboy, "2.1.3", "38999a3e85e39f0e6bdfdf820761abac61edde1632cfebbacc445cdcb6ae1333", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "056f41f814dbb38ea44613e0f613b3b2b2f2c6afce64126e252837669eba84db"}, From 85ee213e03afd15ca1f33527e850d01f6caa8a74 Mon Sep 17 00:00:00 2001 From: Tyler Witt Date: Thu, 23 Jan 2025 22:44:19 +0900 Subject: [PATCH 3/6] Revert changing display based on dependency count Showing the previous `single` view any time there was one dependency broke a test, so I reverted the change and now pass args to be able to detect if a single app was requested or not. --- lib/mix/tasks/hex.outdated.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/mix/tasks/hex.outdated.ex b/lib/mix/tasks/hex.outdated.ex index 773b4bff..761837ad 100644 --- a/lib/mix/tasks/hex.outdated.ex +++ b/lib/mix/tasks/hex.outdated.ex @@ -65,7 +65,7 @@ defmodule Mix.Tasks.Hex.Outdated do lock |> process_lockfile(args, opts) - |> display_outdated(opts) + |> display_outdated(args, opts) end @impl true @@ -108,18 +108,18 @@ defmodule Mix.Tasks.Hex.Outdated do """) end - defp display_outdated(versions, opts) do + defp display_outdated(versions, args, opts) do if opts[:json] do versions |> Enum.map(&cast_version_map/1) |> Jason.encode!() |> Hex.Shell.info() else - display_table(versions, opts) + display_table(versions, args, opts) end end - defp display_table([{package, current, latest, requirements, outdated?}], _opts) do + defp display_table([{package, current, latest, requirements, outdated?}], [_app], _opts) do if outdated? do [ "There is newer version of the dependency available ", @@ -145,7 +145,7 @@ defmodule Mix.Tasks.Hex.Outdated do if outdated?, do: Mix.Tasks.Hex.set_exit_code(1) end - defp display_table(versions, opts) do + defp display_table(versions, _args, opts) do values = versions |> Enum.map(&format_all_row/1) |> maybe_sort_by(opts[:sort]) diff_links = Enum.map(versions, &build_diff_link/1) |> Enum.reject(&is_nil/1) From 665b52fc4f630fa71c88d0e52e0f400c1f7830f0 Mon Sep 17 00:00:00 2001 From: Tyler Witt Date: Thu, 23 Jan 2025 22:46:34 +0900 Subject: [PATCH 4/6] Fix warning --- lib/mix/tasks/hex.outdated.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/tasks/hex.outdated.ex b/lib/mix/tasks/hex.outdated.ex index 761837ad..0332136d 100644 --- a/lib/mix/tasks/hex.outdated.ex +++ b/lib/mix/tasks/hex.outdated.ex @@ -119,7 +119,7 @@ defmodule Mix.Tasks.Hex.Outdated do end end - defp display_table([{package, current, latest, requirements, outdated?}], [_app], _opts) do + defp display_table([{_package, current, latest, requirements, outdated?}], [_app], _opts) do if outdated? do [ "There is newer version of the dependency available ", From 3067f2db4904e8e91e4243ff685ce5f0aa4b925d Mon Sep 17 00:00:00 2001 From: Tyler Witt Date: Thu, 23 Jan 2025 23:03:43 +0900 Subject: [PATCH 5/6] Add test --- lib/mix/tasks/hex.outdated.ex | 35 +++++++++++++----------- test/mix/tasks/hex.outdated_test.exs | 40 ++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/lib/mix/tasks/hex.outdated.ex b/lib/mix/tasks/hex.outdated.ex index 0332136d..1c3fea4b 100644 --- a/lib/mix/tasks/hex.outdated.ex +++ b/lib/mix/tasks/hex.outdated.ex @@ -66,6 +66,7 @@ defmodule Mix.Tasks.Hex.Outdated do lock |> process_lockfile(args, opts) |> display_outdated(args, opts) + |> set_exit_status(opts) end @impl true @@ -117,6 +118,8 @@ defmodule Mix.Tasks.Hex.Outdated do else display_table(versions, args, opts) end + + versions end defp display_table([{_package, current, latest, requirements, outdated?}], [_app], _opts) do @@ -141,8 +144,6 @@ defmodule Mix.Tasks.Hex.Outdated do message = "Up-to-date indicates if the requirement matches the latest version." Hex.Shell.info(["\n", message]) - - if outdated?, do: Mix.Tasks.Hex.set_exit_code(1) end defp display_table(versions, _args, opts) do @@ -159,23 +160,25 @@ defmodule Mix.Tasks.Hex.Outdated do base_message = "Run `mix hex.outdated APP` to see requirements for a specific dependency." diff_message = maybe_diff_message(diff_links) Hex.Shell.info(["\n", base_message, diff_message]) + end + end - any_outdated? = any_outdated?(versions) - req_met? = any_req_matches?(versions) + defp set_exit_status(versions, opts) do + any_outdated? = any_outdated?(versions) + req_met? = any_req_matches?(versions) - cond do - any_outdated? && opts[:within_requirements] && req_met? -> - Mix.Tasks.Hex.set_exit_code(1) + cond do + any_outdated? && opts[:within_requirements] && req_met? -> + Mix.Tasks.Hex.set_exit_code(1) - any_outdated? && opts[:within_requirements] && not req_met? -> - nil + any_outdated? && opts[:within_requirements] && not req_met? -> + nil - any_outdated? -> - Mix.Tasks.Hex.set_exit_code(1) + any_outdated? -> + Mix.Tasks.Hex.set_exit_code(1) - true -> - nil - end + true -> + nil end end @@ -345,7 +348,9 @@ defmodule Mix.Tasks.Hex.Outdated do package: package, lock_version: current, latest_version: latest, - requirements: Enum.map(requirements, fn [_source, req_version] -> req_version end), + requirements: Enum.map(requirements, fn [source, req_version] -> + %{source: source, requirement: req_version, up_to_date: version_match?(latest, req_version)} + end), outdated: outdated? } end diff --git a/test/mix/tasks/hex.outdated_test.exs b/test/mix/tasks/hex.outdated_test.exs index 7b75e772..147894d6 100644 --- a/test/mix/tasks/hex.outdated_test.exs +++ b/test/mix/tasks/hex.outdated_test.exs @@ -445,6 +445,46 @@ defmodule Mix.Tasks.Hex.OutdatedTest do end) end + test "outdated app --json" do + Mix.Project.push(OutdatedApp.MixProject) + + in_tmp(fn -> + set_home_tmp() + Mix.Dep.Lock.write(%{ex_doc: {:hex, :ex_doc, "0.0.1"}}) + + Mix.Task.run("deps.get") + flush() + + assert catch_throw(Mix.Task.run("hex.outdated", ["ex_doc", "--json"])) == {:exit_code, 1} + + msg = Jason.encode!([%{ + outdated: true, + requirements: [ + %{ + source: "mix.exs", + requirement: ">= 0.0.0", + up_to_date: true + }, + %{ + source: "ecto", + requirement: "~> 0.0.1", + up_to_date: false + }, + %{ + source: "postgrex", + requirement: "0.0.1", + up_to_date: false + } + ], + package: "ex_doc", + lock_version: "0.0.1", + latest_version: "0.1.0" + }]) + + assert_received {:mix_shell, :info, [^msg]} + end) + end + test "not outdated app" do Mix.Project.push(NotOutdatedApp.MixProject) From 1b669894be8c11f06190a47b219e973a2bd96bf9 Mon Sep 17 00:00:00 2001 From: Tyler Witt Date: Thu, 23 Jan 2025 23:08:23 +0900 Subject: [PATCH 6/6] Update hex.outdated.ex --- lib/mix/tasks/hex.outdated.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/tasks/hex.outdated.ex b/lib/mix/tasks/hex.outdated.ex index 1c3fea4b..e1fb5629 100644 --- a/lib/mix/tasks/hex.outdated.ex +++ b/lib/mix/tasks/hex.outdated.ex @@ -66,7 +66,7 @@ defmodule Mix.Tasks.Hex.Outdated do lock |> process_lockfile(args, opts) |> display_outdated(args, opts) - |> set_exit_status(opts) + |> set_exit_code(opts) end @impl true @@ -163,7 +163,7 @@ defmodule Mix.Tasks.Hex.Outdated do end end - defp set_exit_status(versions, opts) do + defp set_exit_code(versions, opts) do any_outdated? = any_outdated?(versions) req_met? = any_req_matches?(versions)