improvement: add Igniter.apply_and_fetch_dependencies/1 and Igniter.has_changes?/1 (#28)

---------

Co-authored-by: Zach Daniel <zach@zachdaniel.dev>
This commit is contained in:
Igor Barakaiev 2024-07-01 22:29:51 +03:00 committed by GitHub
parent 99ed483629
commit 05f52a9413
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 186 additions and 146 deletions

View file

@ -369,13 +369,118 @@ defmodule Igniter do
|> format(path)
end
@doc """
Applies the current changes to the `mix.exs` in the Igniter and fetches dependencies.
Returns the remaining changes in the Igniter if successful.
## Options
* `:error_on_abort?` - If `true`, raises an error if the user aborts the operation. Returns the original igniter if not.
"""
def apply_and_fetch_dependencies(igniter, opts \\ []) do
if has_changes?(igniter, "mix.exs") do
case Igniter.do_or_dry_run(igniter, ["--dry-run"],
title: "Preview",
paths: ["mix.exs"]
) do
:issues ->
raise "Exiting due to issues found while previewing changes."
_ ->
message =
if opts[:error_on_abort?] do
"""
Before continuing, we need to first apply the changes and install dependencies. Would you like to do so now?
If not, the task will be aborted.
"""
else
"""
We would first like to first apply the changes and install dependencies. Would you like to do so now?
If not, the task will continue, but some nested installation steps may not be performed.
"""
end
proceed? =
Mix.shell().yes?(message)
if proceed? do
:changes_made = Igniter.do_or_dry_run(igniter, ["--yes"], title: "Applying changes")
Mix.shell().info("running mix deps.get")
case Mix.shell().cmd("mix deps.get") do
0 ->
Mix.Project.clear_deps_cache()
Mix.Project.pop()
"mix.exs"
|> File.read!()
|> Code.eval_string([], file: Path.expand("mix.exs"))
Mix.Dep.clear_cached()
Mix.Project.clear_deps_cache()
Mix.Task.run("deps.compile")
Mix.Task.reenable("compile")
Mix.Task.run("compile")
exit_code ->
Mix.shell().info("""
mix deps.get returned exited with code: `#{exit_code}`
""")
end
Map.update!(igniter, :rewrite, fn rewrite ->
Rewrite.drop(rewrite, ["mix.exs"])
end)
else
if opts[:error_on_abort?] do
raise "Aborted by the user."
else
igniter
end
end
end
else
igniter
end
end
@doc """
Returns whether the current Igniter has pending changes.
"""
def has_changes?(igniter, paths \\ nil) do
paths =
if paths do
Enum.map(paths, &Path.relative_to_cwd/1)
end
igniter.rewrite
|> Rewrite.sources()
|> then(fn sources ->
if paths do
sources
|> Enum.filter(&(&1.path in paths))
else
sources
end
end)
|> Enum.any?(fn source ->
Rewrite.Source.from?(source, :string) || Rewrite.Source.updated?(source)
end)
end
@doc """
Executes or dry-runs a given Igniter.
This takes `argv` to parameterize it as it is generally invoked from a mix task.
"""
def do_or_dry_run(igniter, argv, opts \\ []) do
igniter = prepare_for_write(igniter)
igniter = prepare_for_write(igniter, opts)
title = opts[:title] || "Igniter"
sources =
@ -409,66 +514,60 @@ defmodule Igniter do
case igniter do
%{issues: []} ->
result_of_dry_run =
sources
|> Enum.filter(fn source ->
Rewrite.Source.updated?(source)
end)
|> case do
[] ->
unless opts[:quiet_on_no_changes?] || "--yes" in argv do
Mix.shell().info("\n#{title}: No proposed changes!\n")
end
if has_changes?(igniter) do
if "--dry-run" in argv || "--yes" not in argv do
Mix.shell().info("\n#{title}: Proposed changes:\n")
:dry_run_with_no_changes
Enum.each(sources, fn source ->
if Rewrite.Source.from?(source, :string) do
content_lines =
source
|> Rewrite.Source.get(:content)
|> String.split("\n")
|> Enum.with_index()
sources ->
if "--dry-run" in argv || "--yes" not in argv do
Mix.shell().info("\n#{title}: Proposed changes:\n")
space_padding =
content_lines
|> Enum.map(&elem(&1, 1))
|> Enum.max()
|> to_string()
|> String.length()
Enum.each(sources, fn source ->
if Rewrite.Source.from?(source, :string) do
content_lines =
source
|> Rewrite.Source.get(:content)
|> String.split("\n")
|> Enum.with_index()
diffish_looking_text =
Enum.map_join(content_lines, "\n", fn {line, line_number_minus_one} ->
line_number = line_number_minus_one + 1
space_padding =
content_lines
|> Enum.map(&elem(&1, 1))
|> Enum.max()
|> to_string()
|> String.length()
"#{String.pad_trailing(to_string(line_number), space_padding)} #{IO.ANSI.yellow()}| #{IO.ANSI.green()}#{line}#{IO.ANSI.reset()}"
end)
diffish_looking_text =
Enum.map_join(content_lines, "\n", fn {line, line_number_minus_one} ->
line_number = line_number_minus_one + 1
if String.trim(diffish_looking_text) != "" do
Mix.shell().info("""
Create: #{Rewrite.Source.get(source, :path)}
"#{String.pad_trailing(to_string(line_number), space_padding)} #{IO.ANSI.yellow()}| #{IO.ANSI.green()}#{line}#{IO.ANSI.reset()}"
end)
if String.trim(diffish_looking_text) != "" do
Mix.shell().info("""
Create: #{Rewrite.Source.get(source, :path)}
#{diffish_looking_text}
""")
end
else
diff = Rewrite.Source.diff(source) |> IO.iodata_to_binary()
if String.trim(diff) != "" do
Mix.shell().info("""
Update: #{Rewrite.Source.get(source, :path)}
#{diff}
""")
end
#{diffish_looking_text}
""")
end
end)
end
else
diff = Rewrite.Source.diff(source) |> IO.iodata_to_binary()
:dry_run_with_changes
if String.trim(diff) != "" do
Mix.shell().info("""
Update: #{Rewrite.Source.get(source, :path)}
#{diff}
""")
end
end
end)
end
:dry_run_with_changes
else
unless opts[:quiet_on_no_changes?] || "--yes" in argv do
Mix.shell().info("\n#{title}: No proposed changes!\n")
end
:dry_run_with_no_changes
end
if igniter.warnings != [] do
@ -860,7 +959,15 @@ defmodule Igniter do
end
@doc false
def prepare_for_write(igniter) do
def prepare_for_write(igniter, opts \\ []) do
igniter =
if opts[:paths] do
all_paths = Rewrite.paths(igniter.rewrite)
%{igniter | rewrite: Rewrite.drop(igniter.rewrite, all_paths -- opts[:paths])}
else
igniter
end
%{
igniter
| issues: Enum.uniq(igniter.issues),

View file

@ -24,9 +24,6 @@ defmodule Igniter.Util.Install do
Application.ensure_all_started(:req)
{options, _unprocessed_argv} =
OptionParser.parse!(argv, @info)
igniter = Igniter.new()
{igniter, install_list} =
@ -57,98 +54,34 @@ defmodule Igniter.Util.Install do
end
end)
confirmation_message =
unless options[:dry_run] do
"Dependencies changes must go into effect before individual installers can be run. Proceed with changes?"
end
igniter = Igniter.apply_and_fetch_dependencies(igniter, cancel_on_abort?: true)
dependency_add_result =
Igniter.do_or_dry_run(igniter, argv,
title: "Fetching Dependency",
quiet_on_no_changes?: true,
confirmation_message: confirmation_message
)
desired_tasks = Enum.map(install_list, &"#{&1}.install")
if dependency_add_result == :issues do
raise "Exiting due to issues found while fetching dependency"
end
if dependency_add_result == :dry_run_with_changes do
install_dep_now? =
Mix.shell().yes?("""
Cannot run any associated installers for the requested packages without
commiting changes and fetching dependencies.
Would you like to do so now? The remaining steps will be displayed as a dry run.
""")
if install_dep_now? do
Igniter.do_or_dry_run(igniter, (argv ++ ["--yes"]) -- ["--dry-run"],
title: "Fetching Dependency",
quiet_on_no_changes?: true
)
end
end
if dependency_add_result == :changes_aborted do
Mix.shell().info("\nChanges aborted by user request.")
else
Mix.shell().info("running mix deps.get")
case Mix.shell().cmd("mix deps.get") do
0 ->
Mix.Project.clear_deps_cache()
Mix.Project.pop()
"mix.exs"
|> File.read!()
|> Code.eval_string([], file: Path.expand("mix.exs"))
Mix.Dep.clear_cached()
Mix.Project.clear_deps_cache()
Mix.Task.run("deps.compile")
Mix.Task.reenable("compile")
Mix.Task.run("compile")
exit_code ->
Mix.shell().info("""
mix deps.get returned exited with code: `#{exit_code}`
""")
end
igniter =
Igniter.new()
|> Igniter.assign(%{manually_installed: install_list})
desired_tasks = Enum.map(install_list, &"#{&1}.install")
igniter_tasks =
Mix.Task.load_all()
|> Stream.map(fn item ->
Code.ensure_compiled!(item)
item
end)
|> Stream.filter(&implements_behaviour?(&1, Igniter.Mix.Task))
|> Enum.filter(&(Mix.Task.task_name(&1) in desired_tasks))
Igniter.Util.Options.validate!(
argv,
%{
schema: @info[:switches],
aliases: @info[:aliases],
composes: desired_tasks
},
"igniter.install"
)
igniter_tasks
|> Enum.reduce(igniter, fn task, igniter ->
Igniter.compose_task(igniter, task, argv)
igniter_tasks =
Mix.Task.load_all()
|> Stream.map(fn item ->
Code.ensure_compiled!(item)
item
end)
|> Igniter.do_or_dry_run(argv)
end
|> Stream.filter(&implements_behaviour?(&1, Igniter.Mix.Task))
|> Enum.filter(&(Mix.Task.task_name(&1) in desired_tasks))
Igniter.Util.Options.validate!(
argv,
%{
schema: @info[:switches],
aliases: @info[:aliases],
composes: desired_tasks
},
"igniter.install"
)
igniter_tasks
|> Enum.reduce(igniter, fn task, igniter ->
Igniter.compose_task(igniter, task, argv)
end)
|> Igniter.do_or_dry_run(argv)
:ok
end