Skip to content

Commit

Permalink
Add mix hex.package diff APP VERSION (hexpm#819)
Browse files Browse the repository at this point in the history
Co-authored-by: Ryan SIU <[email protected]>
Co-authored-by: xinz <[email protected]>
  • Loading branch information
xinz and RyanSiu1995 authored Nov 10, 2020
1 parent 8989524 commit b9fe8c3
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 60 deletions.
136 changes: 94 additions & 42 deletions lib/mix/tasks/hex.package.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ defmodule Mix.Tasks.Hex.Package do
You can pipe the fetched tarball to stdout by setting `--output -`.
## Diff package versions
mix hex.package diff APP VERSION
This command compares the project's dependency `APP` against
the target package version, unpacking the target version into
temporary directory and running a diff command.
## Fetch and diff package contents between versions
mix hex.package diff PACKAGE VERSION1 VERSION2
Expand Down Expand Up @@ -93,16 +101,17 @@ defmodule Mix.Tasks.Hex.Package do
fetch(repo(opts), package, version, unpack, output)

["diff", package, version1, version2] ->
diff(repo(opts), package, "#{version1}..#{version2}")
diff(repo(opts), package, parse_version!(version1, version2))

["diff", package, version_range] ->
diff(repo(opts), package, version_range)
["diff", package, version] ->
diff(repo(opts), package, parse_version!(version))

_ ->
Mix.raise("""
Invalid arguments, expected one of:
mix hex.package fetch PACKAGE VERSION [--unpack]
mix hex.package diff APP VERSION
mix hex.package diff PACKAGE VERSION1 VERSION2
mix hex.package diff PACKAGE VERSION1..VERSION2
""")
Expand All @@ -113,8 +122,9 @@ defmodule Mix.Tasks.Hex.Package do
def tasks() do
[
{"fetch PACKAGE VERSION [--unpack]", "Fetch the package"},
{"diff PACKAGE VERSION1 VERSION2", "Fetch and diff package contents between versions"},
{"diff PACKAGE VERSION1..VERSION2", "Fetch and diff package contents between versions"}
{"diff APP VERSION", "Diff dependency against version"},
{"diff PACKAGE VERSION1 VERSION2", "Diff package versions"},
{"diff PACKAGE VERSION1..VERSION2", "Diff package versions"}
]
end

Expand All @@ -137,15 +147,15 @@ defmodule Mix.Tasks.Hex.Package do
Hex.Registry.Server.prefetch([{repo, package}])

tarball = fetch_tarball!(repo, package, version)
if !is_nil(output), do: File.mkdir_p!(output)
if output, do: File.mkdir_p!(output)

abs_name = Path.absname("#{package}-#{version}")

{abs_path, tar_path} =
if is_nil(output) do
{abs_name, "#{abs_name}.tar"}
else
if output do
{output, Path.join(output, "#{package}-#{version}.tar")}
else
{abs_name, "#{abs_name}.tar"}
end

File.write!(tar_path, tarball)
Expand Down Expand Up @@ -208,64 +218,106 @@ defmodule Mix.Tasks.Hex.Package do
end
end

defp diff(repo, package, version_range) do
Hex.Registry.Server.open()
Hex.Registry.Server.prefetch([{repo, package}])
defp diff(repo, app, version) when is_binary(version) do
Hex.Mix.check_deps()

{path_lock, package} =
case Map.get(Mix.Dep.Lock.read(), String.to_atom(app)) do
nil ->
Mix.raise(
"Cannot find the app \"#{app}\" in \"mix.lock\" file, " <>
"please ensure it has been specified in \"mix.exs\" and run \"mix deps.get\""
)

{version1, version2} = parse_version_range!(version_range)
lock ->
path = Path.join(Mix.Project.deps_path(), app)
package = Hex.Utils.lock(lock).name
{path, package}
end

path = tmp_path("#{package}-#{version}-")

try do
fetch_and_unpack!(repo, package, [{path, version}])
code = run_diff_path!(path_lock, path)
Mix.Tasks.Hex.set_exit_code(code)
after
File.rm_rf!(path)
end
end

defp diff(repo, package, {version1, version2}) do
path1 = tmp_path("#{package}-#{version1}-")
path2 = tmp_path("#{package}-#{version2}-")

try do
tarball1 = fetch_tarball!(repo, package, version1)
tarball2 = fetch_tarball!(repo, package, version2)

%{inner_checksum: inner_checksum, outer_checksum: outer_checksum} =
Hex.Tar.unpack!({:binary, tarball1}, path1)
fetch_and_unpack!(repo, package, [{path1, version1}, {path2, version2}])
code = run_diff_path!(path1, path2)
Mix.Tasks.Hex.set_exit_code(code)
after
File.rm_rf!(path1)
File.rm_rf!(path2)
end
end

verify_inner_checksum!(repo, package, version1, inner_checksum)
verify_outer_checksum!(repo, package, version1, outer_checksum)
defp fetch_and_unpack!(repo, package, versions) do
Hex.Registry.Server.open()
Hex.Registry.Server.prefetch([{repo, package}])

%{inner_checksum: inner_checksum, outer_checksum: outer_checksum} =
Hex.Tar.unpack!({:binary, tarball2}, path2)
try do
Enum.each(versions, fn {path, version} ->
tarball = fetch_tarball!(repo, package, version)

verify_inner_checksum!(repo, package, version2, inner_checksum)
verify_outer_checksum!(repo, package, version2, outer_checksum)
%{inner_checksum: inner_checksum, outer_checksum: outer_checksum} =
Hex.Tar.unpack!({:binary, tarball}, path)

verify_inner_checksum!(repo, package, version, inner_checksum)
verify_outer_checksum!(repo, package, version, outer_checksum)
end)
after
Hex.Registry.Server.close()
end
end

cmd =
Hex.State.fetch!(:diff_command)
|> String.replace("__PATH1__", path1)
|> String.replace("__PATH2__", path2)
defp run_diff_path!(path1, path2) do
cmd =
Hex.State.fetch!(:diff_command)
|> String.replace("__PATH1__", escape_and_quote_path(path1))
|> String.replace("__PATH2__", escape_and_quote_path(path2))

code = Mix.shell().cmd(cmd)
Mix.Tasks.Hex.set_exit_code(code)
after
File.rm_rf!(path1)
File.rm_rf!(path2)
end
Mix.shell().cmd(cmd)
end

defp escape_and_quote_path(path) do
escaped = String.replace(path, "\"", "\\\"")
~s("#{escaped}")
end

defp tmp_path(prefix) do
random_string = Base.encode16(:crypto.strong_rand_bytes(4))
Path.join(System.tmp_dir!(), prefix <> random_string)
end

defp parse_version_range!(string) do
defp parse_version!(string) do
case String.split(string, "..", trim: true) do
[version1, version2] ->
version1 = Hex.Version.parse!(version1)
version2 = Hex.Version.parse!(version2)
{to_string(version1), to_string(version2)}
parse_two_versions!(version1, version2)

_ ->
Mix.raise(
"Expected version range to be in format `VERSION1..VERSION2`, got: `#{inspect(string)}`"
)
[version] ->
version |> Hex.Version.parse!() |> to_string()
end
end

defp parse_version!(version1, version2) do
parse_two_versions!(version1, version2)
end

defp parse_two_versions!(version1, version2) do
version1 = Hex.Version.parse!(version1)
version2 = Hex.Version.parse!(version2)
{to_string(version1), to_string(version2)}
end

defp repo(opts) do
repo = Keyword.get(opts, :repo, "hexpm")

Expand Down
15 changes: 15 additions & 0 deletions test/fixtures/diff/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule ReleaseDeps.MixProject do
def project do
[
app: :release_b,
description: "bar",
version: "0.0.2",
deps: [
{:ex_doc, "0.0.1"}
],
package: [
licenses: ["MIT"]
]
]
end
end
91 changes: 75 additions & 16 deletions test/mix/tasks/hex.package_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
defmodule Mix.Tasks.Hex.PackageTest do
use HexTest.IntegrationCase

defp in_diff_fixture(fun) do
in_fixture("diff", fn ->
Mix.Project.push(ReleaseDeps.MixProject)
Mix.Dep.Lock.write(%{ex_doc: {:hex, :ex_doc, "0.0.1"}})
Hex.State.put(:home, File.cwd!())
Mix.Task.run("deps.get")
fun.()
end)
end

test "fetch: success" do
in_tmp(fn ->
cwd = File.cwd!()
Expand Down Expand Up @@ -84,8 +94,53 @@ defmodule Mix.Tasks.Hex.PackageTest do
end
end

test "diff: success" do
in_tmp(fn ->
test "diff: success with version number" do
in_diff_fixture(fn ->
Hex.State.put(:diff_command, "git diff --no-index --no-color __PATH1__ __PATH2__")

assert catch_throw(Mix.Tasks.Hex.Package.run(["diff", "ex_doc", "0.1.0"])) ==
{:exit_code, 1}

assert_received {:mix_shell, :run, [out]}
assert out =~ ~s(-{<<"version">>,<<"0.0.1">>}.)
assert out =~ ~s(+{<<"version">>,<<"0.1.0">>}.)
end)
after
purge([ReleaseDeps.MixProject])
end

test "diff: outdated lockfile with single version number" do
msg = "Can't continue due to errors on dependencies"

in_diff_fixture(fn ->
assert_raise Mix.Error, msg, fn ->
Mix.Dep.Lock.write(%{
ok: {:ex_doc, "https://github.com/elixir-lang/ex_doc.git", "abcdefghi", []}
})

Mix.Tasks.Hex.Package.run(["diff", "ex_doc", "1.0.0"])
end
end)
after
purge([ReleaseDeps.MixProject])
end

test "diff: not having target package with single version number" do
msg =
"Cannot find the app \"tesla\" in \"mix.lock\" file, " <>
"please ensure it has been specified in \"mix.exs\" and run \"mix deps.get\""

in_diff_fixture(fn ->
assert_raise Mix.Error, msg, fn ->
Mix.Tasks.Hex.Package.run(["diff", "tesla", "1.0.0"])
end
end)
after
purge([ReleaseDeps.MixProject])
end

test "diff: success with version range" do
in_diff_fixture(fn ->
Hex.State.put(:diff_command, "git diff --no-index --no-color __PATH1__ __PATH2__")

assert catch_throw(Mix.Tasks.Hex.Package.run(["diff", "ex_doc", "0.0.1..0.1.0"])) ==
Expand All @@ -95,10 +150,12 @@ defmodule Mix.Tasks.Hex.PackageTest do
assert out =~ ~s(-{<<"version">>,<<"0.0.1">>}.)
assert out =~ ~s(+{<<"version">>,<<"0.1.0">>}.)
end)
after
purge([ReleaseDeps.MixProject])
end

test "diff: success (variant args)" do
in_tmp(fn ->
in_diff_fixture(fn ->
Hex.State.put(:diff_command, "git diff --no-index --no-color __PATH1__ __PATH2__")

assert catch_throw(Mix.Tasks.Hex.Package.run(["diff", "ex_doc", "0.0.1", "0.1.0"])) ==
Expand All @@ -108,10 +165,12 @@ defmodule Mix.Tasks.Hex.PackageTest do
assert out =~ ~s(-{<<"version">>,<<"0.0.1">>}.)
assert out =~ ~s(+{<<"version">>,<<"0.1.0">>}.)
end)
after
purge([ReleaseDeps.MixProject])
end

test "diff: custom diff command" do
in_tmp(fn ->
in_diff_fixture(fn ->
Hex.State.put(:diff_command, "ls __PATH1__ __PATH2__")

assert catch_throw(Mix.Tasks.Hex.Package.run(["diff", "ex_doc", "0.0.1..0.1.0"])) ==
Expand All @@ -120,27 +179,27 @@ defmodule Mix.Tasks.Hex.PackageTest do
assert_received {:mix_shell, :run, [out]}
assert out =~ "hex_metadata.config\nmix.exs"
end)
end

test "diff: bad version range" do
msg = "Expected version range to be in format `VERSION1..VERSION2`, got: `\"1.0.0..\"`"

assert_raise Mix.Error, msg, fn ->
Mix.Tasks.Hex.Package.run(["diff", "ex_doc", "1.0.0.."])
end
after
purge([ReleaseDeps.MixProject])
end

test "diff: package not found" do
Hex.State.put(:shell_process, self())

assert_raise Mix.Error, ~r"Request failed \(404\)", fn ->
Mix.Tasks.Hex.Package.run(["diff", "bad", "1.0.0..1.1.0"])
end
in_diff_fixture(fn ->
assert_raise Mix.Error, ~r"Request failed \(404\)", fn ->
Mix.Tasks.Hex.Package.run(["diff", "bad", "1.0.0..1.1.0"])
end

in_tmp(fn ->
assert_raise Mix.Error, ~r"Request failed \(404\)", fn ->
Mix.Tasks.Hex.Package.run(["diff", "ex_doc", "0.0.1..2.0.0"])
end

assert_raise Mix.Error, ~r"Request failed \(404\)", fn ->
Mix.Tasks.Hex.Package.run(["diff", "ex_doc", "2.0.0"])
end
end)
after
purge([ReleaseDeps.MixProject])
end
end
4 changes: 2 additions & 2 deletions test/mix/tasks/hex.repo_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ defmodule Mix.Tasks.Hex.RepoTest do
in_tmp(fn ->
Hex.State.put(:config_home, File.cwd!())

assert_raise Mix.Error, "Config does not contain repo reponame", fn ->
Mix.Tasks.Hex.Repo.run(["show", "reponame"])
assert_raise Mix.Error, "Config does not contain repo non-existant-reponame", fn ->
Mix.Tasks.Hex.Repo.run(["show", "non-existant-reponame"])
end
end)
end
Expand Down

0 comments on commit b9fe8c3

Please sign in to comment.