Skip to content

Commit

Permalink
Add mix hex.package diff VERSION1..VERSION2 (hexpm#698)
Browse files Browse the repository at this point in the history
  • Loading branch information
wojtekmach authored Jun 5, 2019
1 parent 92e8616 commit 41005c6
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 63 deletions.
16 changes: 12 additions & 4 deletions lib/hex/scm.ex
Original file line number Diff line number Diff line change
Expand Up @@ -313,17 +313,25 @@ defmodule Hex.SCM do
end)
end

defp fetch(repo, package, version, path, etag) do
@doc false
def fetch(repo, package, version, path, etag) do
if Hex.State.fetch!(:offline) do
{:ok, :offline}
else
case Hex.Repo.get_tarball(repo, package, version, etag) do
{:ok, {200, body, headers}} ->
etag = headers['etag']
etag = if etag, do: List.to_string(etag)
File.mkdir_p!(Path.dirname(path))
File.write!(path, body)
{:ok, :new, etag}

case path do
:memory ->
{:ok, :new, body, etag}

_ when is_binary(path) ->
File.mkdir_p!(Path.dirname(path))
File.write!(path, body)
{:ok, :new, etag}
end

{:ok, {304, _body, _headers}} ->
{:ok, :cached}
Expand Down
5 changes: 5 additions & 0 deletions lib/hex/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ defmodule Hex.State do
repos_key: %{
env: ["HEX_REPOS_KEY"],
config: [:repos_key]
},
diff_command: %{
env: ["HEX_DIFF_COMMAND"],
config: [:diff_command],
default: Mix.Tasks.Hex.Package.default_diff_command()
}
}

Expand Down
3 changes: 2 additions & 1 deletion lib/mix/tasks/hex.owner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ defmodule Mix.Tasks.Hex.Owner do
def tasks() do
[
{"add PACKAGE EMAIL_OR_USERNAME", "Adds an owner to package"},
{"transfer PACKAGE EMAIL_OR_USERNAME", "Transfers ownership of a package to another user or organization"},
{"transfer PACKAGE EMAIL_OR_USERNAME",
"Transfers ownership of a package to another user or organization"},
{"remove PACKAGE EMAIL_OR_USERNAME", "Removes an owner from package"},
{"list PACKAGE", "List all owners of a given package"},
{"packages", "List all packages owned by the current user"}
Expand Down
151 changes: 125 additions & 26 deletions lib/mix/tasks/hex.package.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,70 @@ defmodule Mix.Tasks.Hex.Package do

@default_repo "hexpm"

@shortdoc "Fetches Hex packages"
@shortdoc "Fetches or diffs packages"

@default_diff_command "git diff --no-index __PATH1__ __PATH2__"

@doc false
def default_diff_command(), do: @default_diff_command()

@moduledoc """
Fetches (and unpacks) package tarballs. This can be useful for auditing of
packages.
Fetches or diffs packages.
## Fetch package
Downloads a package tarball to the current directory.
Fetch a package tarball to the current directory.
mix hex.package fetch PACKAGE VERSION [--unpack]
## Fetch and diff package contents between versions
mix hex.package diff PACKAGE VERSION1..VERSION2
This command fetches package tarballs for both versions,
unpacks them into temporary directories and runs a diff
command. Afterwards, the temporary directories are automatically
deleted.
Note, similarly to when tarballs are fetched with `mix deps.get`,
a `hex_metadata.config` is placed in each unpacked directory.
This file contains package's metadata as Erlang terms and so
we can additionally see the diff of that.
The exit code of the task is that of the underlying diff command.
### Diff command
The diff command can be customized by setting `diff_command`
configuration option, see `mix help config` for more information.
The default diff command is:
#{@default_diff_command}
The `__PATH1__` and `__PATH2__` placeholders will be interpolated with
paths to directories of unpacked tarballs for each version.
Many diff commands supports coloured output but becase we execute
the command in non-interactive mode, they'd usually be disabled.
On Unix systems you can pipe the output to more commands, for example:
`mix hex.package diff decimal 1.0.0..1.1.0 | colordiff | less -R`
Here, the output of `mix hex.package diff` is piped to the `colordiff`
utility to adds colours, which in turn is piped to `less -R` which
"pages" it. (`-R` preserves escape codes which allows colours to work.)
mix hex.package fetch PACKAGE VERSION [--unpack]
Another option is to configure the diff command itself. For example, to
force Git to always colour the output we can set the `--color=always` option:
mix hex.config diff_command "git diff --color=always --no-index __PATH1__ __PATH2__"
mix hex.package diff decimal 1.0.0..1.1.0
## Command line options
* `--unpack` - Unpacks the tarball after downloading it
* `--unpack` - Unpacks the tarball after fetching it
"""
@behaviour Hex.Mix.TaskDescription

Expand All @@ -31,51 +80,101 @@ defmodule Mix.Tasks.Hex.Package do

case args do
["fetch", package, version] ->
fetch_tarball(package, version, unpack)
fetch(@default_repo, package, version, unpack)

["diff", package, version_range] ->
diff(@default_repo, package, version_range)

_ ->
Mix.raise("""
Invalid arguments, expected one of:
mix hex.package fetch PACKAGE VERSION [--unpack]
mix hex.package diff PACKAGE VERSION1..VERSION2
""")
end
end

@impl true
def tasks() do
[
{"fetch PACKAGE VERSION [--unpack]", "Fetch the package"}
{"fetch PACKAGE VERSION [--unpack]", "Fetch the package"},
{"diff PACKAGE VERSION1..VERSION2", "Fetch and diff package contents between versions"}
]
end

defp fetch_tarball(package, version, unpack) do
case Hex.Repo.get_tarball(@default_repo, package, version, nil) do
{:ok, {200, tar_body, _headers}} ->
abs_path = Path.absname("#{package}-#{version}")
tar_path = "#{abs_path}.tar"
File.write!(tar_path, tar_body)

message =
if unpack do
unpack_tarball!(tar_path, abs_path)
"#{package} v#{version} extracted to #{abs_path}"
else
"#{package} v#{version} downloaded to #{tar_path}"
end
defp fetch(repo, package, version, unpack?) do
tarball = fetch_tarball!(repo, package, version)
abs_path = Path.absname("#{package}-#{version}")
tar_path = "#{abs_path}.tar"
File.write!(tar_path, tarball)

message =
if unpack? do
unpack_tarball!(tar_path, abs_path)
"#{package} v#{version} extracted to #{abs_path}"
else
"#{package} v#{version} downloaded to #{tar_path}"
end

Hex.Shell.info(message)
end

Hex.Shell.info(message)
defp fetch_tarball!(repo, package, version) do
etag = nil

{:ok, {code, _body, _headers}} ->
Hex.Shell.error("Request failed (#{code})")
case Hex.SCM.fetch(repo, package, version, :memory, etag) do
{:ok, :new, tarball, _etag} ->
tarball

{:error, reason} ->
Hex.Shell.error("Request failed (#{inspect(reason)})")
Mix.raise(reason)
end
end

defp unpack_tarball!(tar_path, dest_path) do
Hex.unpack_tar!(tar_path, dest_path)
File.rm!(tar_path)
end

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

try do
tarball1 = fetch_tarball!(repo, package, to_string(version1))
tarball2 = fetch_tarball!(repo, package, to_string(version2))
Hex.unpack_tar!({:binary, tarball1}, path1)
Hex.unpack_tar!({:binary, tarball2}, path2)

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

code = Mix.shell().cmd(cmd)
Mix.Tasks.Hex.set_exit_code(code)
after
File.rm_rf!(path1)
File.rm_rf!(path2)
end
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
case String.split(string, "..", trim: true) do
[version1, version2] ->
{Hex.Version.parse!(version1), Hex.Version.parse!(version2)}

_ ->
Mix.raise(
"Expected version range to be in format `VERSION1..VERSION2`, got: `#{inspect(string)}`"
)
end
end
end
5 changes: 4 additions & 1 deletion lib/mix/tasks/hex.publish.ex
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ defmodule Mix.Tasks.Hex.Publish do
Hex.Shell.info("")
Hex.Shell.info(" [1] Yourself")

numbers = Stream.map(Stream.iterate(2, & &1 + 1), &Integer.to_string/1)
numbers = Stream.map(Stream.iterate(2, &(&1 + 1)), &Integer.to_string/1)
organizations = Map.new(Stream.zip(numbers, organizations))

Enum.each(organizations, fn {ix, organization} ->
Expand Down Expand Up @@ -328,8 +328,10 @@ defmodule Mix.Tasks.Hex.Publish do
case Hex.API.Package.get("hexpm", build.meta.name) do
{:ok, {200, _body, _headers}} ->
true

{:ok, {404, _body, _headers}} ->
false

other ->
Hex.Utils.print_error_result(other)
true
Expand All @@ -340,6 +342,7 @@ defmodule Mix.Tasks.Hex.Publish do
case Hex.API.User.me(auth) do
{:ok, {200, body, _header}} ->
Enum.map(body["organizations"], & &1["name"])

other ->
Hex.Utils.print_error_result(other)
[]
Expand Down
9 changes: 8 additions & 1 deletion test/hex/api_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,14 @@ defmodule Hex.APITest do
Hex.API.Package.Owner.get("hexpm", "orange", auth)

assert {:ok, {status, _, _}} =
Hex.API.Package.Owner.add("hexpm", "orange", "[email protected]", "full", false, auth)
Hex.API.Package.Owner.add(
"hexpm",
"orange",
"[email protected]",
"full",
false,
auth
)

assert status in 200..299

Expand Down
17 changes: 14 additions & 3 deletions test/mix/tasks/hex.organization_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ defmodule Mix.Tasks.Hex.OrganizationTest do
in_tmp(fn ->
Hex.State.put(:home, File.cwd!())

auth = Hexpm.new_user("orgkeygenuser", "[email protected]", "password", "orgkeygenuser")
auth =
Hexpm.new_user("orgkeygenuser", "[email protected]", "password", "orgkeygenuser")

Hexpm.new_repo("orgkeygenrepo", auth)
Mix.Tasks.Hex.update_keys(auth[:"$write_key"], auth[:"$read_key"])

Expand All @@ -141,7 +143,9 @@ defmodule Mix.Tasks.Hex.OrganizationTest do
in_tmp(fn ->
Hex.State.put(:home, File.cwd!())

auth = Hexpm.new_user("orgkeylistuser", "[email protected]", "password", "orgkeylistuser")
auth =
Hexpm.new_user("orgkeylistuser", "[email protected]", "password", "orgkeylistuser")

Hexpm.new_repo("orgkeylistrepo", auth)
Mix.Tasks.Hex.update_keys(auth[:"$write_key"], auth[:"$read_key"])

Expand All @@ -161,7 +165,14 @@ defmodule Mix.Tasks.Hex.OrganizationTest do
in_tmp(fn ->
Hex.State.put(:home, File.cwd!())

auth = Hexpm.new_user("orgkeyrevokeyuser", "[email protected]", "password", "orgkeyrevokeuser1")
auth =
Hexpm.new_user(
"orgkeyrevokeyuser",
"[email protected]",
"password",
"orgkeyrevokeuser1"
)

Hexpm.new_repo("orgkeyrevokerepo", auth)
Hexpm.new_organization_key("orgkeyrevokerepo", "orgkeyrevokerepo2", auth)
Mix.Tasks.Hex.update_keys(auth[:"$write_key"], auth[:"$read_key"])
Expand Down
Loading

0 comments on commit 41005c6

Please sign in to comment.