diff --git a/lib/mix/lib/mix/tasks/app.tree.ex b/lib/mix/lib/mix/tasks/app.tree.ex index d2ec3feb373..ffe3a33e13c 100644 --- a/lib/mix/lib/mix/tasks/app.tree.ex +++ b/lib/mix/lib/mix/tasks/app.tree.ex @@ -13,7 +13,7 @@ defmodule Mix.Tasks.App.Tree do ## Command line options - * `--exclude` - exclude dependencies which you do not want to see printed. + * `--exclude` - exclude applications which you do not want to see printed. `kernel`, `stdlib` and `compiler` are always excluded from the tree. * `--pretty` - use Unicode codepoints for formatting the tree. @@ -41,11 +41,7 @@ defmodule Mix.Tasks.App.Tree do Mix.Utils.print_tree([{:normal, app}], fn {type, app} -> load(app) - if app in excluded do - false - else - {"#{app}#{type(type)}", children_for(app)} - end + {"#{app}#{type(type)}", children_for(app, excluded)} end, opts) end @@ -53,12 +49,13 @@ defmodule Mix.Tasks.App.Tree do case Application.load(app) do :ok -> :ok {:error, {:already_loaded, ^app}} -> :ok + _ -> Mix.raise("could not find application #{app}") end end - defp children_for(app) do - apps = Application.spec(app, :applications) - included_apps = Application.spec(app, :included_applications) + defp children_for(app, excluded) do + apps = Application.spec(app, :applications) -- excluded + included_apps = Application.spec(app, :included_applications) -- excluded Enum.map(apps, &{:normal, &1}) ++ Enum.map(included_apps, &{:included, &1}) end diff --git a/lib/mix/lib/mix/tasks/deps.tree.ex b/lib/mix/lib/mix/tasks/deps.tree.ex new file mode 100644 index 00000000000..7326b66daf0 --- /dev/null +++ b/lib/mix/lib/mix/tasks/deps.tree.ex @@ -0,0 +1,76 @@ +defmodule Mix.Tasks.Deps.Tree do + use Mix.Task + + @shortdoc "Prints the dependency tree" + + @moduledoc """ + Prints the dependency tree. + + mix deps.tree + + If no dependency is given, it uses the tree defined in the `mix.exs` file. + + ## Command line options + + * `--only` - the enviroment to show dependencies for + + * `--exclude` - exclude dependencies which you do not want to see printed. + + * `--pretty` - use Unicode codepoints for formatting the tree. + Defaults to true except on Windows. + + """ + @switches [only: :string, exclude: :keep, pretty: :boolean] + + @spec run(OptionParser.argv) :: :ok + def run(args) do + Mix.Project.get! + {opts, args, _} = OptionParser.parse(args, switches: @switches) + + deps_opts = if only = opts[:only], do: [env: :"#{only}"], else: [] + deps = Mix.Dep.loaded(deps_opts) + excluded = Keyword.get_values(opts, :exclude) |> Enum.map(&String.to_atom/1) + top_level = Enum.filter(deps, & &1.top_level) + + root = + case args do + [] -> + Mix.Project.config[:app] || Mix.raise("no application given and none found in mix.exs file") + [app] -> + app = String.to_atom(app) + find_dep(deps, app) || Mix.raise("could not find dependency #{app}") + end + + Mix.Utils.print_tree([root], fn + %Mix.Dep{app: app} = dep -> + deps = + # Do not show dependencies if they were + # already show at the top level + if not dep.top_level && find_dep(top_level, app) do + [] + else + find_dep(deps, app).deps + end + {format_dep(dep), exclude(deps, excluded)} + app -> + {Atom.to_string(app), exclude(top_level, excluded)} + end, opts) + end + + defp exclude(deps, excluded) do + Enum.reject deps, & &1.app in excluded + end + + defp format_dep(%{app: app, scm: scm, requirement: requirement, opts: opts}) do + override = if opts[:override], do: "#{IO.ANSI.bright} *override*#{IO.ANSI.normal}", else: "" + "#{app}#{requirement(requirement)} (#{scm.format(opts)})#{override}" + end + + defp requirement(nil), do: "" + defp requirement(%Regex{} = regex), do: " #{inspect regex}" + defp requirement(binary) when is_binary(binary), do: " #{binary}" + + defp find_dep(deps, app) do + Enum.find(deps, & &1.app == app) + end +end diff --git a/lib/mix/lib/mix/utils.ex b/lib/mix/lib/mix/utils.ex index cea52feba71..f5e181bb6d9 100644 --- a/lib/mix/lib/mix/utils.ex +++ b/lib/mix/lib/mix/utils.ex @@ -143,7 +143,7 @@ defmodule Mix.Utils do must either return `{printed, children}` tuple or `false` if the given node must not be printed. """ - @spec print_tree([term], (term -> {String.t, [term]} | false), Keyword.t) :: :ok + @spec print_tree([term], (term -> {String.t, [term]}), Keyword.t) :: :ok def print_tree(nodes, callback, opts \\ []) do pretty = Keyword.get(opts, :pretty, elem(:os.type, 0) != :win32) print_tree(nodes, [], pretty, callback) @@ -151,13 +151,9 @@ defmodule Mix.Utils do defp print_tree([], _depth, _pretty, _callback), do: :ok defp print_tree([node | nodes], depth, pretty, callback) do - case callback.(node) do - {print, children} -> - Mix.shell.info("#{depth(pretty, depth)}#{prefix(pretty, depth, nodes)}#{print}") - print_tree(children, [(nodes != []) | depth], pretty, callback) - false -> - :ok - end + {print, children} = callback.(node) + Mix.shell.info("#{depth(pretty, depth)}#{prefix(pretty, depth, nodes)}#{print}") + print_tree(children, [(nodes != []) | depth], pretty, callback) print_tree(nodes, depth, pretty, callback) end diff --git a/lib/mix/test/mix/tasks/app.tree_test.exs b/lib/mix/test/mix/tasks/app.tree_test.exs index 8b65c096795..d7b5f31d4a5 100644 --- a/lib/mix/test/mix/tasks/app.tree_test.exs +++ b/lib/mix/test/mix/tasks/app.tree_test.exs @@ -34,6 +34,10 @@ defmodule Mix.Tasks.App.TreeTest do Mix.Project.push AppDepsSample in_tmp context.test, fn -> + assert_raise Mix.Error, "could not find application app_deps_sample", fn -> + Mix.Tasks.App.Tree.run(["--pretty", "app_deps_sample"]) + end + load_apps() Mix.Tasks.App.Tree.run(["--pretty", "app_deps_sample"]) @@ -54,7 +58,7 @@ defmodule Mix.Tasks.App.TreeTest do assert_received {:mix_shell, :info, ["test"]} assert_received {:mix_shell, :info, ["└── app_deps_sample"]} - assert_received {:mix_shell, :info, [" ├── app_deps2_sample"]} + assert_received {:mix_shell, :info, [" └── app_deps2_sample"]} refute_received {:mix_shell, :info, [" │ └── app_deps4_sample (included)"]} refute_received {:mix_shell, :info, [" └── app_deps3_sample"]} end @@ -64,6 +68,6 @@ defmodule Mix.Tasks.App.TreeTest do :ok = :application.load({:application, :app_deps4_sample, [vsn: '1.0.0', env: []]}) :ok = :application.load({:application, :app_deps3_sample, [vsn: '1.0.0', env: []]}) :ok = :application.load({:application, :app_deps2_sample, [vsn: '1.0.0', env: [], included_applications: [:app_deps4_sample]]}) - :ok = :application.load({:application, :app_deps_sample, [vsn: '1.0.0', env: [], applications: [:app_deps2_sample, :app_deps3_sample]]}) + :ok = :application.load({:application, :app_deps_sample, [vsn: '1.0.0', env: [], applications: [:app_deps2_sample, :app_deps3_sample]]}) end end diff --git a/lib/mix/test/mix/tasks/deps.tree_test.exs b/lib/mix/test/mix/tasks/deps.tree_test.exs new file mode 100644 index 00000000000..2bbe72a53cf --- /dev/null +++ b/lib/mix/test/mix/tasks/deps.tree_test.exs @@ -0,0 +1,98 @@ +Code.require_file "../../test_helper.exs", __DIR__ + +defmodule Mix.Tasks.Deps.TreeTest do + use MixTest.Case + + defmodule ConvergedDepsApp do + def project do + [ + app: :sample, + version: "0.1.0", + deps: [ + {:deps_on_git_repo, "0.2.0", git: fixture_path("deps_on_git_repo")}, + {:git_repo, ">= 0.1.0", git: MixTest.Case.fixture_path("git_repo")} + ] + ] + end + end + + defmodule OverridenDepsApp do + def project do + [ + app: :sample, + version: "0.1.0", + deps: [ + {:deps_on_git_repo, ~r"0.2.0", git: fixture_path("deps_on_git_repo"), only: :test}, + {:git_repo, git: MixTest.Case.fixture_path("git_repo"), override: true} + ] + ] + end + end + + test "shows the dependency tree", context do + Mix.Project.push ConvergedDepsApp + + in_tmp context.test, fn -> + Mix.Tasks.Deps.Tree.run(["--pretty"]) + assert_received {:mix_shell, :info, ["sample"]} + assert_received {:mix_shell, :info, ["├── git_repo >= 0.1.0 (" <> _]} + assert_received {:mix_shell, :info, ["└── deps_on_git_repo 0.2.0 (" <> _]} + refute_received {:mix_shell, :info, [" └── git_repo (" <> _]} + + Mix.Tasks.Deps.Get.run([]) + Mix.Tasks.Deps.Tree.run(["--pretty"]) + assert_received {:mix_shell, :info, ["sample"]} + assert_received {:mix_shell, :info, ["├── git_repo >= 0.1.0 (" <> _]} + assert_received {:mix_shell, :info, ["└── deps_on_git_repo 0.2.0 (" <> _]} + assert_received {:mix_shell, :info, [" └── git_repo (" <> _]} + end + end + + test "shows the given dependency", context do + Mix.Project.push ConvergedDepsApp + + in_tmp context.test, fn -> + assert_raise Mix.Error, "could not find dependency unknown", fn -> + Mix.Tasks.Deps.Tree.run(["--pretty", "unknown"]) + end + + Mix.Tasks.Deps.Tree.run(["--pretty", "deps_on_git_repo"]) + assert_received {:mix_shell, :info, ["deps_on_git_repo 0.2.0 (" <> _]} + refute_received {:mix_shell, :info, ["└── git_repo (" <> _]} + end + end + + test "shows overriden deps", context do + Mix.Project.push OverridenDepsApp + + in_tmp context.test, fn -> + Mix.Tasks.Deps.Tree.run(["--pretty"]) + assert_received {:mix_shell, :info, ["sample"]} + assert_received {:mix_shell, :info, ["├── git_repo (" <> msg]} + assert_received {:mix_shell, :info, ["└── deps_on_git_repo ~r/0.2.0/ (" <> _]} + assert msg =~ "*override*" + end + end + + test "excludes the given deps", context do + Mix.Project.push OverridenDepsApp + + in_tmp context.test, fn -> + Mix.Tasks.Deps.Tree.run(["--pretty", "--exclude", "deps_on_git_repo"]) + assert_received {:mix_shell, :info, ["sample"]} + assert_received {:mix_shell, :info, ["└── git_repo (" <> _]} + refute_received {:mix_shell, :info, ["└── deps_on_git_repo ~r/0.2.0/ (" <> _]} + end + end + + test "shows a particular environment", context do + Mix.Project.push OverridenDepsApp + + in_tmp context.test, fn -> + Mix.Tasks.Deps.Tree.run(["--pretty", "--only", "prod"]) + assert_received {:mix_shell, :info, ["sample"]} + assert_received {:mix_shell, :info, ["└── git_repo (" <> _]} + refute_received {:mix_shell, :info, ["└── deps_on_git_repo ~r/0.2.0/ (" <> _]} + end + end +end