Skip to content

Commit 6bc3cf7

Browse files
committed
Support filtering mix deps output
Makes it easier to report versions of specific dependencies by allowing users (or tools) to pass dependency names as arguments to `mix deps`. Warns when a dependency is not found, similar to `mix deps.unlock`. Proposal: https://groups.google.com/g/elixir-lang-core/c/5tlLZ1yu4rQ/m/g7Z8fNWiBwAJ
1 parent b60e424 commit 6bc3cf7

File tree

2 files changed

+149
-4
lines changed

2 files changed

+149
-4
lines changed

lib/mix/lib/mix/tasks/deps.ex

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,19 +186,30 @@ defmodule Mix.Tasks.Deps do
186186
187187
* `--all` - lists all dependencies, regardless of specified environment
188188
189+
## Filtering dependencies (since v1.20.0)
190+
191+
You can list specific dependencies by passing their names as arguments:
192+
193+
$ mix deps phoenix phoenix_live_view
194+
195+
This is particularly useful when you need to quickly check versions for
196+
bug reports or similar tasks.
189197
"""
190198

191199
@impl true
192200
def run(args) do
193201
Mix.Project.get!()
194-
{opts, _, _} = OptionParser.parse(args, switches: [all: :boolean])
202+
{opts, apps, _} = OptionParser.parse(args, switches: [all: :boolean])
195203
loaded_opts = if opts[:all], do: [], else: [env: Mix.env(), target: Mix.target()]
196204

205+
deps = Mix.Dep.Converger.converge(loaded_opts)
206+
apps = Enum.map(apps, &String.to_atom/1)
207+
208+
{deps, unknown} = sort_or_filter(deps, apps)
209+
197210
shell = Mix.shell()
198211

199-
Mix.Dep.Converger.converge(loaded_opts)
200-
|> Enum.sort_by(& &1.app)
201-
|> Enum.each(fn dep ->
212+
Enum.each(deps, fn dep ->
202213
%Mix.Dep{scm: scm, manager: manager} = dep
203214
dep = check_lock(dep)
204215
extra = if manager, do: " (#{manager})", else: ""
@@ -211,5 +222,20 @@ defmodule Mix.Tasks.Deps do
211222

212223
shell.info(" #{format_status(dep)}")
213224
end)
225+
226+
# Warnings are displayed at the end for visibility
227+
for app <- unknown do
228+
shell.error("warning: unknown dependency #{app}")
229+
end
230+
end
231+
232+
# Sort deps when showing all or preserve input order when filtering
233+
defp sort_or_filter(deps, []), do: {Enum.sort_by(deps, & &1.app), []}
234+
235+
defp sort_or_filter(deps, apps) do
236+
deps_map = Map.new(deps, fn dep -> {dep.app, dep} end)
237+
unknown = Enum.reject(apps, &Map.has_key?(deps_map, &1))
238+
239+
{Enum.map(apps -- unknown, &deps_map[&1]), unknown}
214240
end
215241
end

lib/mix/test/mix/tasks/deps_test.exs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,63 @@ defmodule Mix.Tasks.DepsTest do
100100
end)
101101
end
102102

103+
test "filters dependencies by name" do
104+
in_fixture("deps_status", fn ->
105+
Mix.Project.push(DepsApp)
106+
107+
Mix.Tasks.Deps.run(["ok"])
108+
109+
assert_received {:mix_shell, :info, ["* ok (https://github.com/elixir-lang/ok.git) (mix)"]}
110+
refute_received {:mix_shell, :info, ["* invalidvsn" <> _]}
111+
refute_received {:mix_shell, :info, ["* invalidapp" <> _]}
112+
refute_received {:mix_shell, :info, ["* noappfile" <> _]}
113+
refute_received {:mix_shell, :info, ["* nosemver" <> _]}
114+
end)
115+
end
116+
117+
test "warns when filtering for unknown dependencies" do
118+
in_fixture("deps_status", fn ->
119+
Mix.Project.push(DepsApp)
120+
121+
Mix.Tasks.Deps.run(["ok", "unknowndep"])
122+
123+
assert_received {:mix_shell, :info, ["* ok (https://github.com/elixir-lang/ok.git) (mix)"]}
124+
assert_received {:mix_shell, :error, ["warning: unknown dependency unknowndep"]}
125+
end)
126+
end
127+
128+
test "filters dependencies preserving argument order" do
129+
in_fixture("deps_status", fn ->
130+
Mix.Project.push(DepsApp)
131+
132+
Mix.Tasks.Deps.run(["nosemver", "unknowndep2", "ok", "unknowndep1", "invalidapp"])
133+
134+
assert_output_order([
135+
{:info, "nosemver"},
136+
{:info, "ok"},
137+
{:info, "invalidapp"},
138+
{:error, "unknowndep2"},
139+
{:error, "unknowndep1"}
140+
])
141+
end)
142+
end
143+
144+
test "lists all dependencies in alphabetical order when no filter is given" do
145+
in_fixture("deps_status", fn ->
146+
Mix.Project.push(DepsApp)
147+
148+
Mix.Tasks.Deps.run([])
149+
150+
assert_output_order([
151+
{:info, "invalidapp"},
152+
{:info, "invalidvsn"},
153+
{:info, "noappfile"},
154+
{:info, "nosemver"},
155+
{:info, "ok"}
156+
])
157+
end)
158+
end
159+
103160
test "prints list of dependencies and their status, including req mismatches and custom apps" do
104161
in_fixture("deps_status", fn ->
105162
Mix.Project.push(ReqDepsApp)
@@ -977,4 +1034,66 @@ defmodule Mix.Tasks.DepsTest do
9771034
assert File.exists?("deps/ok")
9781035
end)
9791036
end
1037+
1038+
## Helpers
1039+
1040+
defp assert_output_order(expected) do
1041+
output = receive_shell_messages()
1042+
1043+
# Find the index of each expected output line
1044+
indices =
1045+
Enum.map(expected, fn
1046+
{:info, dep} ->
1047+
Enum.find_index(output, fn
1048+
{:info, line} -> line =~ ~r/^\* #{dep} /
1049+
_ -> nil
1050+
end)
1051+
1052+
{:error, dep} ->
1053+
Enum.find_index(output, fn
1054+
{:error, line} -> line =~ ~r/^warning: .* #{dep}/
1055+
_ -> nil
1056+
end)
1057+
end)
1058+
1059+
if not Enum.all?(indices) do
1060+
flunk("""
1061+
Expected output not found!
1062+
1063+
Expected:
1064+
#{inspect(expected)}
1065+
1066+
Output:
1067+
#{inspect(output)}
1068+
""")
1069+
end
1070+
1071+
if not strictly_increasing?(indices) do
1072+
flunk("""
1073+
Output not in expected order!
1074+
1075+
Expected:
1076+
#{inspect(expected)}
1077+
1078+
Output:
1079+
#{inspect(output)}
1080+
""")
1081+
end
1082+
end
1083+
1084+
defp receive_shell_messages(acc \\ []) do
1085+
receive do
1086+
{:mix_shell, level, [line]} when level in [:info, :error] ->
1087+
receive_shell_messages([{level, line} | acc])
1088+
after
1089+
0 -> Enum.reverse(acc)
1090+
end
1091+
end
1092+
1093+
defp strictly_increasing?([]), do: true
1094+
defp strictly_increasing?([_]), do: true
1095+
1096+
defp strictly_increasing?([a, b | rest]) do
1097+
a < b and strictly_increasing?([b | rest])
1098+
end
9801099
end

0 commit comments

Comments
 (0)