diff --git a/lib/mix/tasks/hex.outdated.ex b/lib/mix/tasks/hex.outdated.ex index 8feea485..e1fb5629 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,12 @@ 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(args, opts) + |> set_exit_code(opts) end @impl true @@ -87,25 +77,52 @@ 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 + + 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 - 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 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, args, opts) + end + + versions + end + defp display_table([{_package, current, latest, requirements, outdated?}], [_app], _opts) do if outdated? do [ "There is newer version of the dependency available ", @@ -127,8 +144,42 @@ defmodule Mix.Tasks.Hex.Outdated do message = "Up-to-date indicates if the requirement matches the latest version." Hex.Shell.info(["\n", message]) + end + + 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) - if outdated?, do: Mix.Tasks.Hex.set_exit_code(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]) + end + end + + defp set_exit_code(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) + + any_outdated? && opts[:within_requirements] && not req_met? -> + nil + + any_outdated? -> + Mix.Tasks.Hex.set_exit_code(1) + + true -> + nil + end end defp get_requirements_from_lock(app, lock) do @@ -167,48 +218,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 +243,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 - [[Atom.to_string(name), lock_version, latest_version, requirements]] + requirements = deps_requirements ++ lock_requirements + + [{Atom.to_string(name), lock_version, latest_version, requirements, outdated?}] _ -> [] @@ -265,8 +274,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 +293,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 +306,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 +334,24 @@ 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 + + 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] -> + %{source: source, requirement: req_version, up_to_date: version_match?(latest, 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"}, 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)