From b096692b9d4081f828cef0361642dc7d02e33a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Fri, 10 Apr 2026 15:00:34 +0200 Subject: [PATCH 1/3] Show mix hex.package diff commands in hex.outdated output Add a hint listing `mix hex.package diff PACKAGE FROM..TO` commands for each outdated dependency so users and agents can easily view diffs from the CLI without needing a browser. Closes hexpm/diff#108 --- lib/mix/tasks/hex.outdated.ex | 17 ++++++++++++++++- test/mix/tasks/hex.outdated_test.exs | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/mix/tasks/hex.outdated.ex b/lib/mix/tasks/hex.outdated.ex index c2b9a966..a4abd226 100644 --- a/lib/mix/tasks/hex.outdated.ex +++ b/lib/mix/tasks/hex.outdated.ex @@ -187,6 +187,7 @@ defmodule Mix.Tasks.Hex.Outdated 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) + diff_commands = Enum.map(versions, &build_diff_command/1) |> Enum.reject(&is_nil/1) if Enum.empty?(values) do Hex.Shell.info("No hex dependencies") @@ -196,7 +197,8 @@ 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]) + diff_commands_message = maybe_diff_commands_message(diff_commands) + Hex.Shell.info(["\n", base_message, diff_message, diff_commands_message]) outdated = outdated(versions) any_updatable? = any_possible_to_update?(outdated) @@ -328,6 +330,19 @@ defmodule Mix.Tasks.Hex.Outdated do diff_link(diff_links) end + defp build_diff_command([package, _dep_only, lock, latest, _requirements]) do + if Version.compare(lock, latest) == :lt do + "mix hex.package diff #{package} #{lock}..#{latest}" + end + end + + defp maybe_diff_commands_message([]), do: "" + + defp maybe_diff_commands_message(diff_commands) do + commands = Enum.map_join(diff_commands, "\n", &(" " <> &1)) + "\n\nTo view the diff of a specific update, run:\n" <> commands + end + defp diff_link(diff_links) do long_url = "https://diff.hex.pm/diffs?" <> Enum.join(diff_links, "&") diff --git a/test/mix/tasks/hex.outdated_test.exs b/test/mix/tasks/hex.outdated_test.exs index c98eff79..57fbee3b 100644 --- a/test/mix/tasks/hex.outdated_test.exs +++ b/test/mix/tasks/hex.outdated_test.exs @@ -668,6 +668,28 @@ defmodule Mix.Tasks.Hex.OutdatedTest do end) end + test "outdated shows diff commands for outdated packages" do + Mix.Project.push(OutdatedDeps.MixProject) + + in_tmp(fn -> + set_home_tmp() + Mix.Dep.Lock.write(%{bar: {:hex, :bar, "0.1.0"}, foo: {:hex, :foo, "0.1.0"}}) + + Mix.Task.run("deps.get") + flush() + + assert catch_throw(Mix.Task.run("hex.outdated", ["--all"])) == {:exit_code, 1} + + lines = flush() + output = Enum.map_join(lines, "\n", fn {_, _, [line]} -> line end) + + assert output =~ "To view the diff of a specific update, run:" + assert output =~ "mix hex.package diff ex_doc 0.0.1..0.1.0" + assert output =~ "mix hex.package diff foo 0.1.0..0.1.1" + refute output =~ "mix hex.package diff bar" + end) + end + defp extract_statuses(lines) do Enum.flat_map(lines, fn {_, _, [line]} -> ~r/Up-to-date|Update not possible|Update possible/ From c57b435c2ca54f8ec654847ad21c1f6c48730fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Fri, 10 Apr 2026 15:03:33 +0200 Subject: [PATCH 2/3] Add note about forcing dependency version in hex.outdated docs Mention that users can directly update the version in mix.exs to force a dependency to a specific version when mix deps.update does not update it due to transitive dependency conflicts. --- lib/mix/tasks/hex.outdated.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/mix/tasks/hex.outdated.ex b/lib/mix/tasks/hex.outdated.ex index a4abd226..22a7a831 100644 --- a/lib/mix/tasks/hex.outdated.ex +++ b/lib/mix/tasks/hex.outdated.ex @@ -33,7 +33,9 @@ defmodule Mix.Tasks.Hex.Outdated do at the project's current set of dependency requirements and what version they are locked to. When `mix deps.update` is called multiple packages may be updated that in turn update their own dependencies, which may cause the - package you want to update to not be able to update. + package you want to update to not be able to update. If you want to force + a dependency to be updated to a given version, you can directly update it + in your `mix.exs`. > In a project, this task must be invoked before any other tasks > that may load or start your application. Otherwise, you must From 75c55f64f9a1109125bf15bfa2d1f870d056dc92 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Apr 2026 07:53:34 +0000 Subject: [PATCH 3/3] Show a generic diff command hint in hex.outdated output Listing a `mix hex.package diff` command for every outdated package adds significant output. Show a single generic command hint (`mix hex.package diff APP FROM..TO`) instead. --- lib/mix/tasks/hex.outdated.ex | 18 +++++------------- test/mix/tasks/hex.outdated_test.exs | 8 +++----- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/lib/mix/tasks/hex.outdated.ex b/lib/mix/tasks/hex.outdated.ex index 22a7a831..a8d34ac1 100644 --- a/lib/mix/tasks/hex.outdated.ex +++ b/lib/mix/tasks/hex.outdated.ex @@ -189,7 +189,6 @@ defmodule Mix.Tasks.Hex.Outdated 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) - diff_commands = Enum.map(versions, &build_diff_command/1) |> Enum.reject(&is_nil/1) if Enum.empty?(values) do Hex.Shell.info("No hex dependencies") @@ -199,8 +198,8 @@ 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) - diff_commands_message = maybe_diff_commands_message(diff_commands) - Hex.Shell.info(["\n", base_message, diff_message, diff_commands_message]) + diff_command_message = maybe_diff_command_message(diff_links) + Hex.Shell.info(["\n", base_message, diff_message, diff_command_message]) outdated = outdated(versions) any_updatable? = any_possible_to_update?(outdated) @@ -332,17 +331,10 @@ defmodule Mix.Tasks.Hex.Outdated do diff_link(diff_links) end - defp build_diff_command([package, _dep_only, lock, latest, _requirements]) do - if Version.compare(lock, latest) == :lt do - "mix hex.package diff #{package} #{lock}..#{latest}" - end - end - - defp maybe_diff_commands_message([]), do: "" + defp maybe_diff_command_message([]), do: "" - defp maybe_diff_commands_message(diff_commands) do - commands = Enum.map_join(diff_commands, "\n", &(" " <> &1)) - "\n\nTo view the diff of a specific update, run:\n" <> commands + defp maybe_diff_command_message(_diff_links) do + "\n\nTo view the diff of a specific update, run `mix hex.package diff APP FROM..TO`." end defp diff_link(diff_links) do diff --git a/test/mix/tasks/hex.outdated_test.exs b/test/mix/tasks/hex.outdated_test.exs index 57fbee3b..e31ad577 100644 --- a/test/mix/tasks/hex.outdated_test.exs +++ b/test/mix/tasks/hex.outdated_test.exs @@ -668,7 +668,7 @@ defmodule Mix.Tasks.Hex.OutdatedTest do end) end - test "outdated shows diff commands for outdated packages" do + test "outdated shows a generic diff command hint when updates are available" do Mix.Project.push(OutdatedDeps.MixProject) in_tmp(fn -> @@ -683,10 +683,8 @@ defmodule Mix.Tasks.Hex.OutdatedTest do lines = flush() output = Enum.map_join(lines, "\n", fn {_, _, [line]} -> line end) - assert output =~ "To view the diff of a specific update, run:" - assert output =~ "mix hex.package diff ex_doc 0.0.1..0.1.0" - assert output =~ "mix hex.package diff foo 0.1.0..0.1.1" - refute output =~ "mix hex.package diff bar" + assert output =~ + "To view the diff of a specific update, run `mix hex.package diff APP FROM..TO`." end) end