defmodule Mix.Tasks.Git.Test.Install do
@pre_commit_contents """
#!/bin/sh
MIX_ENV=test exec mix git.test
"""
@shortdoc "Installs a git pre-commit hook that runs `mix git.test`"
@moduledoc """
Installs a git pre-commit hook to ensure `mix git.test` passes before allowing a commit.
The actual contents of the hook are incredibly simple:
```sh
#{@pre_commit_contents}
```
The hook will be installed as `.git/hooks/pre-commit`. If that file already
exists, it will **not** be overwritten; instead, this task will report
whether the existing hook appears to contain `mix git.test` or not.
"""
use Mix.Task
import Bitwise
@doc false
def run([] = _args) do
File.cwd!()
|> git_test_install()
end
def run(_) do
Mix.raise("git.test.install does not accept arguments")
end
@doc false
def git_test_install(cwd) do
path = get_hooks_path(cwd)
install(path, "pre-commit", @pre_commit_contents)
end
defp get_hooks_path(cwd) do
hooks = [cwd, ".git", "hooks"] |> Path.join()
cond do
!File.exists?(hooks) ->
Mix.raise("Path does not exist: #{hooks}")
!File.dir?(hooks) ->
Mix.raise("Not a directory: #{hooks}")
true ->
hooks
end
end
defp install(hooks_path, file_name, contents) do
path = Path.join(hooks_path, file_name)
case File.lstat(path) do
{:error, :enoent} ->
File.write!(path, contents)
make_executable(path)
puts(:light_green, "* Created: #{path}")
{:ok, %File.Stat{type: :regular}} ->
check_contents(path, contents)
{:ok, %File.Stat{}} ->
puts(:light_red, "* Not a regular file: #{path}")
end
end
defp puts(colour, text) do
ansi = apply(IO.ANSI, colour, [])
IO.puts([ansi, text, IO.ANSI.reset()])
end
defp make_executable(path) do
mode = File.stat!(path).mode
[
mode,
# owner executable
0o100,
# group executable if group readable
if(mode &&& 0o040, do: 0o010, else: 0),
# other executable if other readable
if(mode &&& 0o004, do: 0o001, else: 0)
]
|> Enum.reduce(&Bitwise.|||/2)
|> then(&File.chmod!(path, &1))
end
defp check_contents(path, expected) do
contents = File.read!(path)
cond do
contents == expected ->
make_executable(path)
puts(:light_green, "* Up-to-date: #{path}")
contents =~ "mix git.test" ->
puts(:light_yellow, "* File exists: #{path}")
puts(
:light_yellow,
"* The existing hook does appear to mention `mix git.test`, " <>
"but this task cannot verify if it is set up correctly."
)
true ->
puts(:light_red, "* File exists: #{path}")
puts(:light_red, "* The existing hook does not appear to run `mix git.test`.")
end
end
end