Skip to content

Commit

Permalink
improvement: add Igniter.apply_and_fetch_dependencies/1 and Igniter.h…
Browse files Browse the repository at this point in the history
…as_changes?/1 (#28)

---------

Co-authored-by: Zach Daniel <zach@zachdaniel.dev>
  • Loading branch information
ibarakaiev and zachdaniel authored Jul 1, 2024
1 parent 99ed483 commit 05f52a9
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 151 deletions.
225 changes: 166 additions & 59 deletions lib/igniter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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")

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()

space_padding =
content_lines
|> Enum.map(&elem(&1, 1))
|> Enum.max()
|> to_string()
|> String.length()

diffish_looking_text =
Enum.map_join(content_lines, "\n", fn {line, line_number_minus_one} ->
line_number = line_number_minus_one + 1

"#{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)}
:dry_run_with_no_changes

sources ->
if "--dry-run" in argv || "--yes" not in argv do
Mix.shell().info("\n#{title}: Proposed changes:\n")

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()

space_padding =
content_lines
|> Enum.map(&elem(&1, 1))
|> Enum.max()
|> to_string()
|> String.length()

diffish_looking_text =
Enum.map_join(content_lines, "\n", fn {line, line_number_minus_one} ->
line_number = line_number_minus_one + 1

"#{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
#{diff}
""")
end
end)
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_changes
:dry_run_with_no_changes
end

if igniter.warnings != [] do
Expand Down Expand Up @@ -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),
Expand Down
117 changes: 25 additions & 92 deletions lib/igniter/util/install.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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} =
Expand Down Expand Up @@ -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

dependency_add_result =
Igniter.do_or_dry_run(igniter, argv,
title: "Fetching Dependency",
quiet_on_no_changes?: true,
confirmation_message: confirmation_message
)

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()
igniter = Igniter.apply_and_fetch_dependencies(igniter, cancel_on_abort?: true)

"mix.exs"
|> File.read!()
|> Code.eval_string([], file: Path.expand("mix.exs"))
desired_tasks = Enum.map(install_list, &"#{&1}.install")

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
Expand Down

0 comments on commit 05f52a9

Please sign in to comment.