defmodule Mix.Tasks.Mod.Relocate do
use Mix.Task
@shortdoc "Relocate one or all modules in the current application"
defmodule Move do
defstruct module: nil, good_path: nil, cur_path: nil, split: nil
end
import Modkit.Cli
def run(argv) do
Mix.Task.run("app.config")
{opts, args} =
command(__MODULE__)
|> option(:prompt, :boolean,
alias: :p,
doc: "Prompt to move the files. Takes precedence over --force",
default: false
)
|> option(:force, :boolean,
alias: :f,
doc: "Move the files without confirmation",
default: false
)
|> argument(:module, required: false)
|> parse(argv)
project = Modkit.Config.current_project()
modules =
case args do
%{module: mod} -> [Module.concat([mod])]
_ -> Modkit.Mod.list(project)
end
do_run(project, modules, opts)
end
defp do_run(project, modules, opts) do
mount = Modkit.Config.mount(project)
print_mount(mount)
cwd = File.cwd!()
moves =
modules
|> Enum.reject(&is_protocol_impl?/1)
|> map_filter_ok(&build_move(&1, mount, cwd))
|> resolve_multis()
|> Enum.reject(&(in_good_path?(&1) or target_file_exists?(&1)))
dir_actions = compute_dirs(moves)
case dir_actions ++ moves do
[] ->
success_stop("nothing to do")
actions ->
run_actions(actions, opts)
end
end
defp run_actions(actions, opts) do
cond do
opts.prompt ->
print("Actions that would be executed:")
Enum.each(actions, &print_action/1)
if Mix.Shell.IO.yes?("Proceed with the moves") do
Enum.each(actions, &print_run_action/1)
else
print("cancelled")
end
opts.force ->
Enum.each(actions, &print_run_action/1)
:_ ->
print("Actions that would be executed:")
Enum.each(actions, &print_action/1)
end
:ok
end
defp print_action({:mkdir, dir}) do
print(["+dir ", cyan(dir)])
end
defp print_action(%Move{cur_path: from, good_path: to}) do
{bad_rest, good_rest, common} = deviate_path(from, to)
print([
"move ",
common,
"/",
magenta(bad_rest),
"\n -> ",
common,
"/",
green(good_rest)
])
end
defp deviate_path(from, to) do
deviate_path(Path.split(from), Path.split(to), [])
end
defp deviate_path([same | from], [same | to], acc) do
deviate_path(from, to, [same | acc])
end
defp deviate_path(from_rest, to_rest, acc) do
{Path.join(from_rest), Path.join(to_rest), Path.join(:lists.reverse(acc))}
end
defp print_run_action(action) do
print_action(action)
case run_action(action) do
:ok ->
print(" => ok")
{:error, reason} ->
reason = ensure_string(reason)
danger([" => ", reason])
abort(["action failed, other actions aborted"])
end
end
defp run_action(%Move{module: mod, split: ["Modkit", "Sample" | _]}) do
notice(" ! skipped moving of #{inspect(mod)}")
end
defp run_action(%Move{cur_path: from, good_path: to}) do
File.rename(from, to)
end
defp run_action({:mkdir, dir}) do
File.mkdir(dir)
end
defp is_protocol_impl?(module) do
{:__impl__, 1} in module.module_info(:exports)
end
defp build_move(module, mount, cwd) do
case Modkit.Mod.preferred_path(module, mount) do
{:ok, good_path} ->
{:ok,
%Move{
module: module,
good_path: good_path,
cur_path: Modkit.Mod.current_path(module, cwd),
split: Module.split(module)
}}
{:error, :no_mount_point} = err ->
notice("module #{inspect(module)} is not mounted")
err
end
end
# group modules per source file and keep only modules that we can move, i.e.
# they are alone in their source file, or they are a common prefix of all
# modules in the file
defp resolve_multis(moves) do
moves
|> Enum.group_by(& &1.cur_path)
|> Enum.flat_map(fn
{_file, [single]} -> [single]
{_file, multis} -> parent_mod_or_empty(multis)
end)
end
defp parent_mod_or_empty(moves) do
moves
|> Enum.find(fn %{split: split} ->
Enum.all?(moves, fn %{split: submodule} -> List.starts_with?(submodule, split) end)
end)
|> case do
nil ->
modules = Enum.map_join(moves, "\n", &" * #{inspect(&1.module)}")
warn("multiple modules defined in #{moves |> hd |> Map.get(:cur_path)}:\n#{modules}")
[]
mv ->
[mv]
end
end
defp in_good_path?(%Move{good_path: path, cur_path: path}), do: true
defp in_good_path?(_), do: false
defp target_file_exists?(%Move{good_path: path}) do
if File.regular?(path) do
warn("file #{path} already exists")
true
else
false
end
end
defp compute_dirs(moves) do
moves
|> Enum.map(fn %{good_path: path} -> Path.dirname(path) end)
|> Modkit.PathTool.list_create_dirs()
|> Enum.map(&{:mkdir, &1})
rescue
e in ArgumentError -> e |> Exception.message() |> abort()
end
def print_mount(mount) do
mount.points
|> Enum.map(fn %Modkit.Mount.Point{prefix: pref, path: path} ->
["mount ", cyan(inspect(pref)), " on ", cyan(path)]
end)
|> Enum.intersperse("\n")
|> print()
end
defp map_filter_ok(list, callback) do
list
|> Enum.map(callback)
|> Enum.filter(&(elem(&1, 0) == :ok))
|> Enum.map(&elem(&1, 1))
end
end