Skip to content

Commit

Permalink
Add mix deps.tree
Browse files Browse the repository at this point in the history
  • Loading branch information
José Valim committed Feb 4, 2016
1 parent 0db3a62 commit 8ff3275
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 19 deletions.
15 changes: 6 additions & 9 deletions lib/mix/lib/mix/tasks/app.tree.ex
Expand Up @@ -13,7 +13,7 @@ defmodule Mix.Tasks.App.Tree do
## Command line options ## 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. `kernel`, `stdlib` and `compiler` are always excluded from the tree.
* `--pretty` - use Unicode codepoints for formatting the tree. * `--pretty` - use Unicode codepoints for formatting the tree.
Expand Down Expand Up @@ -41,24 +41,21 @@ defmodule Mix.Tasks.App.Tree do


Mix.Utils.print_tree([{:normal, app}], fn {type, app} -> Mix.Utils.print_tree([{:normal, app}], fn {type, app} ->
load(app) load(app)
if app in excluded do {"#{app}#{type(type)}", children_for(app, excluded)}
false
else
{"#{app}#{type(type)}", children_for(app)}
end
end, opts) end, opts)
end end


defp load(app) do defp load(app) do
case Application.load(app) do case Application.load(app) do
:ok -> :ok :ok -> :ok
{:error, {:already_loaded, ^app}} -> :ok {:error, {:already_loaded, ^app}} -> :ok
_ -> Mix.raise("could not find application #{app}")
end end
end end


defp children_for(app) do defp children_for(app, excluded) do
apps = Application.spec(app, :applications) apps = Application.spec(app, :applications) -- excluded
included_apps = Application.spec(app, :included_applications) included_apps = Application.spec(app, :included_applications) -- excluded
Enum.map(apps, &{:normal, &1}) ++ Enum.map(included_apps, &{:included, &1}) Enum.map(apps, &{:normal, &1}) ++ Enum.map(included_apps, &{:included, &1})
end end


Expand Down
76 changes: 76 additions & 0 deletions 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
12 changes: 4 additions & 8 deletions lib/mix/lib/mix/utils.ex
Expand Up @@ -143,21 +143,17 @@ defmodule Mix.Utils do
must either return `{printed, children}` tuple or must either return `{printed, children}` tuple or
`false` if the given node must not be printed. `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 def print_tree(nodes, callback, opts \\ []) do
pretty = Keyword.get(opts, :pretty, elem(:os.type, 0) != :win32) pretty = Keyword.get(opts, :pretty, elem(:os.type, 0) != :win32)
print_tree(nodes, [], pretty, callback) print_tree(nodes, [], pretty, callback)
end end


defp print_tree([], _depth, _pretty, _callback), do: :ok defp print_tree([], _depth, _pretty, _callback), do: :ok
defp print_tree([node | nodes], depth, pretty, callback) do defp print_tree([node | nodes], depth, pretty, callback) do
case callback.(node) do {print, children} = callback.(node)
{print, children} -> Mix.shell.info("#{depth(pretty, depth)}#{prefix(pretty, depth, nodes)}#{print}")
Mix.shell.info("#{depth(pretty, depth)}#{prefix(pretty, depth, nodes)}#{print}") print_tree(children, [(nodes != []) | depth], pretty, callback)
print_tree(children, [(nodes != []) | depth], pretty, callback)
false ->
:ok
end
print_tree(nodes, depth, pretty, callback) print_tree(nodes, depth, pretty, callback)
end end


Expand Down
8 changes: 6 additions & 2 deletions lib/mix/test/mix/tasks/app.tree_test.exs
Expand Up @@ -34,6 +34,10 @@ defmodule Mix.Tasks.App.TreeTest do
Mix.Project.push AppDepsSample Mix.Project.push AppDepsSample


in_tmp context.test, fn -> 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() load_apps()
Mix.Tasks.App.Tree.run(["--pretty", "app_deps_sample"]) Mix.Tasks.App.Tree.run(["--pretty", "app_deps_sample"])


Expand All @@ -54,7 +58,7 @@ defmodule Mix.Tasks.App.TreeTest do


assert_received {:mix_shell, :info, ["test"]} assert_received {:mix_shell, :info, ["test"]}
assert_received {:mix_shell, :info, ["└── app_deps_sample"]} 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_deps4_sample (included)"]}
refute_received {:mix_shell, :info, [" └── app_deps3_sample"]} refute_received {:mix_shell, :info, [" └── app_deps3_sample"]}
end end
Expand All @@ -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_deps4_sample, [vsn: '1.0.0', env: []]})
:ok = :application.load({:application, :app_deps3_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_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
end end
98 changes: 98 additions & 0 deletions 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

0 comments on commit 8ff3275

Please sign in to comment.