From 49ca50e56f4ada2d5ad4d7198cc2bfef2681894d Mon Sep 17 00:00:00 2001 From: qgadrian Date: Tue, 1 Feb 2022 18:35:08 +0100 Subject: [PATCH 01/17] Use git to resolve the correct hooks path --- lib/git.ex | 25 ++++---------- lib/git/path.ex | 52 ++++++++++++++++++++++++++++++ lib/mix/tasks/git_hooks/install.ex | 41 ++++++++--------------- test/git/path_test.exs | 11 +++++++ 4 files changed, 83 insertions(+), 46 deletions(-) create mode 100644 lib/git/path.ex create mode 100644 test/git/path_test.exs diff --git a/lib/git.ex b/lib/git.ex index 7fab35b..5cd49c8 100644 --- a/lib/git.ex +++ b/lib/git.ex @@ -1,8 +1,6 @@ defmodule GitHooks.Git do @moduledoc false - alias Mix.Project - @doc false @spec resolve_git_path() :: any def resolve_git_path do @@ -10,28 +8,17 @@ defmodule GitHooks.Git do |> Application.get_env(:git_path) |> case do nil -> - path = Path.join(Project.deps_path(), "/../.git") - - if File.dir?(path) do - path - else - resolve_git_submodule_path(path) - end + {path, 0} = System.cmd("git", ["rev-parse", "--git-path", "hooks"]) + String.replace(path, "\n", "") custom_path -> custom_path end end - @spec resolve_git_submodule_path(String.t()) :: any - defp resolve_git_submodule_path(git_path) do - with {:ok, contents} <- File.read(git_path), - %{"dir" => submodule_dir} <- Regex.named_captures(~r/^gitdir:\s+(?.*)$/, contents) do - Project.deps_path() - |> Path.join("/../" <> submodule_dir) - else - _error -> - raise "Error resolving git submodule path '#{git_path}'" - end + @spec git_hooks_path_for(path :: String.t()) :: String.t() + def git_hooks_path_for(path) do + __MODULE__.resolve_git_path() + |> Path.join("/#{path}") end end diff --git a/lib/git/path.ex b/lib/git/path.ex new file mode 100644 index 0000000..e4a5c46 --- /dev/null +++ b/lib/git/path.ex @@ -0,0 +1,52 @@ +defmodule GitHooks.Git.Path do + @moduledoc false + + alias GitHooks.Git + + @doc false + @spec resolve_git_hooks_path() :: any + def resolve_git_hooks_path do + :git_hooks + |> Application.get_env(:git_path) + |> case do + nil -> resolve_git_hooks_path_based_on_git_version() + custom_path -> custom_path + end + end + + @spec git_hooks_path_for(path :: String.t()) :: String.t() + def git_hooks_path_for(path) do + __MODULE__.resolve_git_hooks_path() + |> Path.join("/#{path}") + |> String.replace(~r/\/+/, "/") + end + + # + # Private functions + # + + # https://stackoverflow.com/questions/10848191/git-submodule-commit-hooks + # + # Git 2.10+ + # `git rev-parse --git-path hooks` + # Pre Git 2.10+ + # `git rev-parse --git-dir /hooks` + # + # This will support as well changes on the default /hooks path + # git config core.hooksPath .githooks/ + + @spec resolve_git_hooks_path_based_on_git_version() :: String.t() + defp resolve_git_hooks_path_based_on_git_version() do + Git.git_version() + |> Version.compare(Version.parse!("2.10.0")) + |> case do + :lt -> + {path, 0} = System.cmd("git", ["rev-parse", "--git-dir", "hooks"]) + String.replace(path, "\n", "") + + _gt_or_eq -> + {path, 0} = System.cmd("git", ["rev-parse", "--git-path", "hooks"]) + String.replace(path, "\n", "") + end + end +end diff --git a/lib/mix/tasks/git_hooks/install.ex b/lib/mix/tasks/git_hooks/install.ex index 2c69c2b..547bf2e 100644 --- a/lib/mix/tasks/git_hooks/install.ex +++ b/lib/mix/tasks/git_hooks/install.ex @@ -21,7 +21,7 @@ defmodule Mix.Tasks.GitHooks.Install do use Mix.Task alias GitHooks.Config - alias GitHooks.Git + alias GitHooks.Git.Path, as: GitPath alias GitHooks.Printer @impl true @@ -55,9 +55,7 @@ defmodule Mix.Tasks.GitHooks.Install do case File.read(template_file) do {:ok, body} -> - target_file_path = - Git.resolve_git_path() - |> Path.join("/hooks/#{git_hook_atom_as_kebab_string}") + target_file_path = GitPath.git_hooks_path_for(git_hook_atom_as_kebab_string) target_file_body = body @@ -85,13 +83,9 @@ defmodule Mix.Tasks.GitHooks.Install do @spec backup_current_hook(String.t(), Keyword.t()) :: {:error, atom} | {:ok, non_neg_integer()} defp backup_current_hook(git_hook_to_backup, opts) do - source_file_path = - Git.resolve_git_path() - |> Path.join("/hooks/#{git_hook_to_backup}") + source_file_path = GitPath.git_hooks_path_for(git_hook_to_backup) - target_file_path = - Git.resolve_git_path() - |> Path.join("/hooks/#{git_hook_to_backup}.pre_git_hooks_backup") + target_file_path = GitPath.git_hooks_path_for("/#{git_hook_to_backup}.pre_git_hooks_backup") unless opts[:quiet] || !Config.verbose?() do Printer.info("Backing up git hook file `#{source_file_path}` to `#{target_file_path}`") @@ -104,8 +98,8 @@ defmodule Mix.Tasks.GitHooks.Install do defp track_configured_hooks do git_hooks = Config.git_hooks() |> Enum.join(" ") - Git.resolve_git_path() - |> Path.join("/hooks/git_hooks.db") + "/git_hooks.db" + |> GitPath.git_hooks_path_for() |> write_backup(git_hooks) end @@ -123,8 +117,8 @@ defmodule Mix.Tasks.GitHooks.Install do @spec ensure_hooks_folder_exists() :: any defp ensure_hooks_folder_exists do - Git.resolve_git_path() - |> Path.join("/hooks") + "/" + |> GitPath.git_hooks_path_for() |> File.mkdir_p() end @@ -134,8 +128,8 @@ defmodule Mix.Tasks.GitHooks.Install do Config.git_hooks() |> Enum.map(&Atom.to_string/1) - Git.resolve_git_path() - |> Path.join("/git_hooks.db") + "/git_hooks.db" + |> GitPath.git_hooks_path_for() |> File.read() |> case do {:ok, file} -> @@ -149,8 +143,8 @@ defmodule Mix.Tasks.GitHooks.Install do "Remove old git hook `#{git_hook_atom_as_kebab_string}` and restore backup" ) - Git.resolve_git_path() - |> Path.join("/hooks/#{git_hook_atom_as_kebab_string}") + git_hook_atom_as_kebab_string + |> GitPath.git_hooks_path_for() |> File.rm() restore_backup(git_hook_atom_as_kebab_string) @@ -165,16 +159,9 @@ defmodule Mix.Tasks.GitHooks.Install do @spec restore_backup(String.t()) :: any defp restore_backup(git_hook_atom_as_kebab_string) do backup_path = - Path.join( - Git.resolve_git_path(), - "/hooks/#{git_hook_atom_as_kebab_string}.pre_git_hooks_backup" - ) + GitPath.git_hooks_path_for("/#{git_hook_atom_as_kebab_string}.pre_git_hooks_backup") - restore_path = - Path.join( - Git.resolve_git_path(), - "/#{git_hook_atom_as_kebab_string}" - ) + restore_path = GitPath.git_hooks_path_for(git_hook_atom_as_kebab_string) case File.rename(backup_path, restore_path) do :ok -> :ok diff --git a/test/git/path_test.exs b/test/git/path_test.exs new file mode 100644 index 0000000..46a86b3 --- /dev/null +++ b/test/git/path_test.exs @@ -0,0 +1,11 @@ +defmodule GitHooks.Git.PathTest do + use ExUnit.Case + + alias GitHooks.Git.Path + + describe "git_hooks_path_for/1" do + test "appends the path to the hooks folder" do + assert Path.git_hooks_path_for("/testing") == ".git/hooks/testing" + end + end +end From c5c09608ee98f5ee50b8195f3670c8e3f8ad6e3c Mon Sep 17 00:00:00 2001 From: qgadrian Date: Tue, 1 Feb 2022 18:36:07 +0100 Subject: [PATCH 02/17] Add function to return current git version --- lib/git.ex | 22 +++++----------------- lib/git/path.ex | 10 ++++------ 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/lib/git.ex b/lib/git.ex index 5cd49c8..9500579 100644 --- a/lib/git.ex +++ b/lib/git.ex @@ -1,24 +1,12 @@ defmodule GitHooks.Git do @moduledoc false - @doc false - @spec resolve_git_path() :: any - def resolve_git_path do - :git_hooks - |> Application.get_env(:git_path) - |> case do - nil -> - {path, 0} = System.cmd("git", ["rev-parse", "--git-path", "hooks"]) - String.replace(path, "\n", "") + @spec git_version() :: Version.t() + def git_version() do + {full_git_version, 0} = System.cmd("git", ["--version"]) - custom_path -> - custom_path - end - end + [version] = Regex.run(~r/\d\.\d+\.\d+/, full_git_version) - @spec git_hooks_path_for(path :: String.t()) :: String.t() - def git_hooks_path_for(path) do - __MODULE__.resolve_git_path() - |> Path.join("/#{path}") + Version.parse!(version) end end diff --git a/lib/git/path.ex b/lib/git/path.ex index e4a5c46..c3f1a1e 100644 --- a/lib/git/path.ex +++ b/lib/git/path.ex @@ -27,13 +27,11 @@ defmodule GitHooks.Git.Path do # https://stackoverflow.com/questions/10848191/git-submodule-commit-hooks # - # Git 2.10+ - # `git rev-parse --git-path hooks` - # Pre Git 2.10+ - # `git rev-parse --git-dir /hooks` + # For git >= 2.10+ => `git rev-parse --git-path hooks` + # For git < 2.10+ => `git rev-parse --git-dir /hooks` # - # This will support as well changes on the default /hooks path - # git config core.hooksPath .githooks/ + # This will support as well changes on the default /hooks path: + # `git config core.hooksPath .myCustomGithooks/` @spec resolve_git_hooks_path_based_on_git_version() :: String.t() defp resolve_git_hooks_path_based_on_git_version() do From 78c6cf1ad8e22d84a02aca02f2b5f3297bb92475 Mon Sep 17 00:00:00 2001 From: qgadrian Date: Tue, 1 Feb 2022 18:44:09 +0100 Subject: [PATCH 03/17] Update README --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ed61d1e..45cf3d5 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Main features are: * [Manual installation](#manual-installation) * [Configuration](#configuration) * [Mix path](#mix-path) - * [Git path](#git-path) + * [Git submodules](#git-submodules) * [Troubleshooting in docker containers](#troubleshooting-in-docker-containers) * [Auto install](#auto-install) * [Hook configuration](#hook-configuration) @@ -103,11 +103,19 @@ config :git_hooks, mix_path: "docker-compose exec mix", ``` -### Git path +### Git submodules -This library expects `git` to be installed in the `.git` directory relative to your project root, or when using git submodules, the root of the superproject. If you want to provide a specific path to a custom git directory, it can be done using the `git_path` configuration. +This library supports git submodules, just add your `git_hooks` configuration to +any of the submodules projects. -The follow example would run the hooks within a git submodule: +Setting a custom _git hooks_ config path is also supported: + +``` +git config core.hooksPath .myCustomGithooks/ +``` + +If for any reason you want to override the folder of the _git hooks_ path you +can add the following configuration: ```elixir config :git_hooks, From b164fa1ee155de3d892fb5826e6655f15a493d02 Mon Sep 17 00:00:00 2001 From: qgadrian Date: Tue, 1 Feb 2022 18:45:37 +0100 Subject: [PATCH 04/17] Add pre-release for `v0.6.6` --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 468655a..ac18a5b 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule GitHooks.MixProject do use Mix.Project @source_url "https://github.com/qgadrian/elixir_git_hooks" - @version "0.6.5" + @version "0.6.6-pre" def project do [ From 7da69d8a5ba7b9738ec5a0dae188dcfa72f94a20 Mon Sep 17 00:00:00 2001 From: qgadrian Date: Mon, 7 Feb 2022 17:47:12 +0100 Subject: [PATCH 05/17] Update README.md --- README.md | 51 ++++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 45cf3d5..5aac60d 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ [![Build Status](https://travis-ci.org/qgadrian/elixir_git_hooks.svg?branch=master)](https://travis-ci.org/qgadrian/elixir_git_hooks.svg?branch=master) [![Inline docs](http://inch-ci.org/github/qgadrian/elixir_git_hooks.svg)](http://inch-ci.org/github/qgadrian/elixir_git_hooks) -# GitHooks +# GitHooks 🪝 Configure [git hooks](https://git-scm.com/docs/githooks) in your Elixir projects. -Main features are: +Main features: * **Simplicity**: Automatic or manually install the configured git hook actions. * **Flexibility**: You choose what to use to define the git hooks actions: @@ -30,13 +30,13 @@ Main features are: * [Automatic installation](#automatic-installation) * [Manual installation](#manual-installation) * [Configuration](#configuration) + * [Auto install](#auto-install) + * [Hook configuration](#hook-configuration) * [Mix path](#mix-path) * [Git submodules](#git-submodules) * [Troubleshooting in docker containers](#troubleshooting-in-docker-containers) - * [Auto install](#auto-install) - * [Hook configuration](#hook-configuration) * [Example config](#example-config) - * [Type of tasks](#type-of-tasks) + * [Task types](#task-types) * [Mix task](#mix-task) * [Command](#command) * [Executable file](#executable-file) @@ -91,9 +91,26 @@ mix git_hooks.install ## Configuration +### Auto install + +To disable the automatic install of the git hooks set the configuration key `auto_install` to +`false`. + +### Hook configuration + +One or more git hooks can be configured, those hooks will be the ones +[installed](#installation) in your git project. + +Currently there are supported two configuration options: + + * **tasks**: A list of the commands that will be executed when running a git hook. [See types of tasks](#type-of-tasks) for more info. + * **verbose**: If true, the output of the mix tasks will be visible. This can be configured globally or per git hook. + * **branches**: Allow or forbid the hook configuration to run (or not) in certain branches using `whitelist` or `blacklist` configuration (see example below). You can use regular expressions to match a branch name. + + ### Mix path -This library expects `elixir` to be installed in your system and the `mix` binary to be available. If you want to provide an specific path to run the `mix` executable, it can be done using the `mix_path` configuration. +This library expects `elixir` to be installed in your system and the `mix` binary to be available. If you want to provide a specific path to run the `mix` executable, it can be done using the `mix_path` configuration. The following example would run the hooks on a docker container: @@ -124,25 +141,9 @@ config :git_hooks, #### Troubleshooting in docker containers -The `mix_path` configuration can be use to run mix hooks on a Docker container. +The `mix_path` configuration can be used to run mix hooks on a Docker container. If you have a TTY error running mix in a Docker container use `docker exec --tty $(docker-compose ps -q web) mix` as the `mix_path`. See this [issue](https://github.com/qgadrian/elixir_git_hooks/issues/82) as reference. -### Auto install - -To disable the automatic install of the git hooks set the configuration key `auto_install` to -`false`. - -### Hook configuration - -One or more git hooks can be configured, those hooks will be the ones -[installed](#installation) in your git project. - -Currently there are supported two configuration options: - - * **tasks**: A list of the commands that will be executed when running a git hook. [See types of tasks](#type-of-tasks) for more info. - * **verbose**: If true, the output of the mix tasks will be visible. This can be configured globally or per git hook. - * **branches**: Allow or forbid the hook configuration to run (or not) in certain branches using `whitelist` or `blacklist` configuration (see example below).You can use regular expressions to match a branch name. - ### Example config In `config/config.exs` @@ -177,7 +178,7 @@ if Mix.env() == :dev do end ``` -### Type of tasks +### Task types > For more information, check the [module > documentation](https://hexdocs.pm/git_hooks) for each of the different @@ -305,6 +306,6 @@ mix git_hooks.run all ## Copyright and License -Copyright © 2021 Adrián Quintás +Copyright © 2022 Adrián Quintás Source code is released under [the MIT license](./LICENSE). From 224bf77d4a5e5e86e75130a81f1a2638e5c4d375 Mon Sep 17 00:00:00 2001 From: qgadrian Date: Mon, 7 Feb 2022 18:06:25 +0100 Subject: [PATCH 06/17] Don't log any error on non VCS folder The error message is useless and confusing. When this project is running under a path that not under VCS, no branch whitelist/blacklist would apply. This is a first step to reduce noise, in the future the branch filtering could use parent's git folder. --- lib/config/branch_config.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/config/branch_config.ex b/lib/config/branch_config.ex index c197784..a50d876 100644 --- a/lib/config/branch_config.ex +++ b/lib/config/branch_config.ex @@ -10,7 +10,7 @@ defmodule GitHooks.Config.BranchConfig do def current_branch do current_branch_fn = Application.get_env(:git_hooks, :current_branch_fn, fn -> - System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]) + System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"], stderr_to_stdout: true) end) current_branch_fn.() From d5c962298aa048269d85e92589e692e6dde29d41 Mon Sep 17 00:00:00 2001 From: "Michael J. Cohen" Date: Mon, 7 Feb 2022 18:33:29 +0100 Subject: [PATCH 07/17] Add config to set a path where to run the hooks --- lib/git/path.ex | 7 +++++++ lib/mix/tasks/git_hooks/install.ex | 2 ++ priv/hook_template | 2 ++ 3 files changed, 11 insertions(+) diff --git a/lib/git/path.ex b/lib/git/path.ex index c3f1a1e..c53b89f 100644 --- a/lib/git/path.ex +++ b/lib/git/path.ex @@ -14,6 +14,13 @@ defmodule GitHooks.Git.Path do end end + @doc false + def resolve_app_path do + git_dir = Application.get_env(:git_hooks, :git_path, &resolve_git_hooks_path/0) + repo_dir = Path.dirname(git_dir) + Path.relative_to(File.cwd!(), repo_dir) + end + @spec git_hooks_path_for(path :: String.t()) :: String.t() def git_hooks_path_for(path) do __MODULE__.resolve_git_hooks_path() diff --git a/lib/mix/tasks/git_hooks/install.ex b/lib/mix/tasks/git_hooks/install.ex index 547bf2e..4c3a81f 100644 --- a/lib/mix/tasks/git_hooks/install.ex +++ b/lib/mix/tasks/git_hooks/install.ex @@ -43,6 +43,7 @@ defmodule Mix.Tasks.GitHooks.Install do Printer.info("Installing git hooks...") mix_path = Config.mix_path() + project_path = GitPath.resolve_app_path() ensure_hooks_folder_exists() clean_missing_hooks() @@ -61,6 +62,7 @@ defmodule Mix.Tasks.GitHooks.Install do body |> String.replace("$git_hook", git_hook_atom_as_string) |> String.replace("$mix_path", mix_path) + |> String.replace("$project_path", project_path) unless opts[:quiet] || !Config.verbose?() do Printer.info( diff --git a/priv/hook_template b/priv/hook_template index c05557e..3f012b3 100644 --- a/priv/hook_template +++ b/priv/hook_template @@ -1,5 +1,7 @@ #!/bin/sh +[ "$project_path" != "" ] && cd $project_path + $mix_path git_hooks.run $git_hook "$@" [ $? -ne 0 ] && exit 1 exit 0 From 28a5c16ece5a86aedb352d9d3bde8e5cd4b483b9 Mon Sep 17 00:00:00 2001 From: qgadrian Date: Mon, 7 Feb 2022 18:59:33 +0100 Subject: [PATCH 08/17] Support `git_path` and `git_hooks_path` configs --- README.md | 34 ++++++++++++++++++++++++++++++++-- lib/git/path.ex | 17 ++++++++++++----- test/git/path_test.exs | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5aac60d..672387c 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ Main features: * [Hook configuration](#hook-configuration) * [Mix path](#mix-path) * [Git submodules](#git-submodules) + * [Custom git paths](#custom-git-paths) + * [Git path](#git-path) + * [Git hooks path](#git-hooks-path) * [Troubleshooting in docker containers](#troubleshooting-in-docker-containers) * [Example config](#example-config) * [Task types](#task-types) @@ -131,14 +134,41 @@ Setting a custom _git hooks_ config path is also supported: git config core.hooksPath .myCustomGithooks/ ``` -If for any reason you want to override the folder of the _git hooks_ path you +### Custom git paths + +By default this library expects your Elixir project to be the root of the git +repository. If this is the case, you might need to configure custom paths based +on your folders relative paths. + +#### Git path + +If you need to override the folder of the _git_ path you can add the following configuration: ```elixir config :git_hooks, - git_path: "../.git/modules/submodule-repo" + git_path: "../.git" ``` +This is useful if the root of your project is not managed directly by the VCS +but the parent. + +If you set the `git_path`, the git hooks path will expect to be inside +the `hooks` folder of the provided path configuration. In the example above, `../.git/hooks`. + +#### Git hooks path + +If you need to override the folder of the _git hooks_ path you +can add the following configuration: + +```elixir +config :git_hooks, + git_hooks_path: "../.git/hooks" +``` + +This is useful if the root of your project is not managed directly by the VCS +but the parent and you are using a custom path for your git hooks. + #### Troubleshooting in docker containers The `mix_path` configuration can be used to run mix hooks on a Docker container. diff --git a/lib/git/path.ex b/lib/git/path.ex index c53b89f..b1911a5 100644 --- a/lib/git/path.ex +++ b/lib/git/path.ex @@ -6,11 +6,18 @@ defmodule GitHooks.Git.Path do @doc false @spec resolve_git_hooks_path() :: any def resolve_git_hooks_path do - :git_hooks - |> Application.get_env(:git_path) - |> case do - nil -> resolve_git_hooks_path_based_on_git_version() - custom_path -> custom_path + git_path_config = Application.get_env(:git_hooks, :git_path, nil) + git_hooks_path_config = Application.get_env(:git_hooks, :git_hooks_path, nil) + + case {git_hooks_path_config, git_path_config} do + {nil, nil} -> + resolve_git_hooks_path_based_on_git_version() + + {nil, git_path_config} -> + "#{git_path_config}/hooks" + + {git_hooks_path_config, _} -> + git_hooks_path_config end end diff --git a/test/git/path_test.exs b/test/git/path_test.exs index 46a86b3..db2e40f 100644 --- a/test/git/path_test.exs +++ b/test/git/path_test.exs @@ -8,4 +8,36 @@ defmodule GitHooks.Git.PathTest do assert Path.git_hooks_path_for("/testing") == ".git/hooks/testing" end end + + describe "resolve_git_hooks_path/0" do + test "returns the configuration for git_hooks_path" do + Application.put_env(:git_hooks, :git_hooks_path, "./custom-hooks-path") + + assert Path.resolve_git_hooks_path() == "./custom-hooks-path" + + Application.delete_env(:git_hooks, :git_hooks_path) + end + + test "returns the hooks path based on the git_path configuration" do + Application.put_env(:git_hooks, :git_path, "./custom-git-path") + + assert Path.resolve_git_hooks_path() == "./custom-git-path/hooks" + + Application.delete_env(:git_hooks, :git_path) + end + + test "prioritizes the git_hooks_path config over the git_path" do + Application.put_env(:git_hooks, :git_hooks_path, "./prioritized-custom-hooks-path") + Application.put_env(:git_hooks, :git_path, "./custom-git-path") + + assert Path.resolve_git_hooks_path() == "./prioritized-custom-hooks-path" + + Application.delete_env(:git_hooks, :git_hooks_path) + Application.delete_env(:git_hooks, :git_path) + end + + test "returns the git path when there is no other configuration" do + assert Path.resolve_git_hooks_path() == ".git/hooks" + end + end end From 8810bf0742ad54f91328d21030bbbacc821593bf Mon Sep 17 00:00:00 2001 From: qgadrian Date: Mon, 7 Feb 2022 19:12:34 +0100 Subject: [PATCH 09/17] Update README.md --- README.md | 38 +++++++++++++++++++------------------- lib/git/path.ex | 1 + 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 672387c..e4f7312 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,12 @@ Main features: * [Configuration](#configuration) * [Auto install](#auto-install) * [Hook configuration](#hook-configuration) - * [Mix path](#mix-path) * [Git submodules](#git-submodules) - * [Custom git paths](#custom-git-paths) + * [Custom paths](#custom-paths) * [Git path](#git-path) * [Git hooks path](#git-hooks-path) - * [Troubleshooting in docker containers](#troubleshooting-in-docker-containers) + * [Mix path](#mix-path) + * [Troubleshooting in docker containers](#troubleshooting-in-docker-containers) * [Example config](#example-config) * [Task types](#task-types) * [Mix task](#mix-task) @@ -111,18 +111,6 @@ Currently there are supported two configuration options: * **branches**: Allow or forbid the hook configuration to run (or not) in certain branches using `whitelist` or `blacklist` configuration (see example below). You can use regular expressions to match a branch name. -### Mix path - -This library expects `elixir` to be installed in your system and the `mix` binary to be available. If you want to provide a specific path to run the `mix` executable, it can be done using the `mix_path` configuration. - -The following example would run the hooks on a docker container: - -```elixir -config :git_hooks, - auto_install: false, - mix_path: "docker-compose exec mix", -``` - ### Git submodules This library supports git submodules, just add your `git_hooks` configuration to @@ -134,7 +122,7 @@ Setting a custom _git hooks_ config path is also supported: git config core.hooksPath .myCustomGithooks/ ``` -### Custom git paths +### Custom paths By default this library expects your Elixir project to be the root of the git repository. If this is the case, you might need to configure custom paths based @@ -142,7 +130,7 @@ on your folders relative paths. #### Git path -If you need to override the folder of the _git_ path you +If you need to override the folder of the _git path_ you can add the following configuration: ```elixir @@ -158,7 +146,7 @@ the `hooks` folder of the provided path configuration. In the example above, `.. #### Git hooks path -If you need to override the folder of the _git hooks_ path you +If you need to override the folder of the _git hooks path_ you can add the following configuration: ```elixir @@ -169,7 +157,19 @@ config :git_hooks, This is useful if the root of your project is not managed directly by the VCS but the parent and you are using a custom path for your git hooks. -#### Troubleshooting in docker containers +#### Mix path + +This library expects `elixir` to be installed in your system and the `mix` binary to be available. If you want to provide a specific path to run the `mix` executable, it can be done using the `mix_path` configuration. + +The following example would run the hooks on a docker container: + +```elixir +config :git_hooks, + auto_install: false, + mix_path: "docker-compose exec mix", +``` + +##### Troubleshooting in docker containers The `mix_path` configuration can be used to run mix hooks on a Docker container. If you have a TTY error running mix in a Docker container use `docker exec --tty $(docker-compose ps -q web) mix` as the `mix_path`. See this [issue](https://github.com/qgadrian/elixir_git_hooks/issues/82) as reference. diff --git a/lib/git/path.ex b/lib/git/path.ex index b1911a5..610f391 100644 --- a/lib/git/path.ex +++ b/lib/git/path.ex @@ -25,6 +25,7 @@ defmodule GitHooks.Git.Path do def resolve_app_path do git_dir = Application.get_env(:git_hooks, :git_path, &resolve_git_hooks_path/0) repo_dir = Path.dirname(git_dir) + Path.relative_to(File.cwd!(), repo_dir) end From 37875d4448b3d5a7092cf6aecdc135885b8fe092 Mon Sep 17 00:00:00 2001 From: qgadrian Date: Mon, 7 Feb 2022 20:16:05 +0100 Subject: [PATCH 10/17] Resolve app path based on git path --- lib/git.ex | 2 +- lib/git/path.ex | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/git.ex b/lib/git.ex index 9500579..5ede837 100644 --- a/lib/git.ex +++ b/lib/git.ex @@ -2,7 +2,7 @@ defmodule GitHooks.Git do @moduledoc false @spec git_version() :: Version.t() - def git_version() do + def git_version do {full_git_version, 0} = System.cmd("git", ["--version"]) [version] = Regex.run(~r/\d\.\d+\.\d+/, full_git_version) diff --git a/lib/git/path.ex b/lib/git/path.ex index 610f391..35636d4 100644 --- a/lib/git/path.ex +++ b/lib/git/path.ex @@ -11,7 +11,7 @@ defmodule GitHooks.Git.Path do case {git_hooks_path_config, git_path_config} do {nil, nil} -> - resolve_git_hooks_path_based_on_git_version() + resolve_git_path_based_on_git_version("hooks") {nil, git_path_config} -> "#{git_path_config}/hooks" @@ -23,7 +23,8 @@ defmodule GitHooks.Git.Path do @doc false def resolve_app_path do - git_dir = Application.get_env(:git_hooks, :git_path, &resolve_git_hooks_path/0) + git_path = resolve_git_path_based_on_git_version() + git_dir = Application.get_env(:git_hooks, :git_path, git_path) repo_dir = Path.dirname(git_dir) Path.relative_to(File.cwd!(), repo_dir) @@ -48,17 +49,17 @@ defmodule GitHooks.Git.Path do # This will support as well changes on the default /hooks path: # `git config core.hooksPath .myCustomGithooks/` - @spec resolve_git_hooks_path_based_on_git_version() :: String.t() - defp resolve_git_hooks_path_based_on_git_version() do + @spec resolve_git_path_based_on_git_version(dir :: String.t()) :: String.t() + defp resolve_git_path_based_on_git_version(dir \\ "") do Git.git_version() |> Version.compare(Version.parse!("2.10.0")) |> case do :lt -> - {path, 0} = System.cmd("git", ["rev-parse", "--git-dir", "hooks"]) + {path, 0} = System.cmd("git", ["rev-parse", "--git-dir", dir]) String.replace(path, "\n", "") _gt_or_eq -> - {path, 0} = System.cmd("git", ["rev-parse", "--git-path", "hooks"]) + {path, 0} = System.cmd("git", ["rev-parse", "--git-path", dir]) String.replace(path, "\n", "") end end From 7d72f3a46aa72d1a3096549a7cd95ce28171d998 Mon Sep 17 00:00:00 2001 From: qgadrian Date: Mon, 7 Feb 2022 20:16:34 +0100 Subject: [PATCH 11/17] Introduce _dry-run_ install option This is useful to skip the hooks installation and debug the output. Needed for testing the install module. --- lib/mix/tasks/git_hooks/install.ex | 80 +++++++++++++++++------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/lib/mix/tasks/git_hooks/install.ex b/lib/mix/tasks/git_hooks/install.ex index 4c3a81f..b288196 100644 --- a/lib/mix/tasks/git_hooks/install.ex +++ b/lib/mix/tasks/git_hooks/install.ex @@ -15,7 +15,6 @@ defmodule Mix.Tasks.GitHooks.Install do ```elixir mix git_hooks.install` ``` - """ use Mix.Task @@ -28,7 +27,10 @@ defmodule Mix.Tasks.GitHooks.Install do @spec run(Keyword.t()) :: :ok def run(args) do {opts, _other_args, _} = - OptionParser.parse(args, switches: [quiet: :boolean], aliases: [q: :quiet]) + OptionParser.parse(args, + switches: [quiet: :boolean, dry_run: :boolean], + aliases: [q: :quiet] + ) install(opts) end @@ -49,38 +51,48 @@ defmodule Mix.Tasks.GitHooks.Install do clean_missing_hooks() track_configured_hooks() - Config.git_hooks() - |> Enum.each(fn git_hook -> - git_hook_atom_as_string = Atom.to_string(git_hook) - git_hook_atom_as_kebab_string = Recase.to_kebab(git_hook_atom_as_string) - - case File.read(template_file) do - {:ok, body} -> - target_file_path = GitPath.git_hooks_path_for(git_hook_atom_as_kebab_string) - - target_file_body = - body - |> String.replace("$git_hook", git_hook_atom_as_string) - |> String.replace("$mix_path", mix_path) - |> String.replace("$project_path", project_path) - - unless opts[:quiet] || !Config.verbose?() do - Printer.info( - "Writing git hook for `#{git_hook_atom_as_string}` to `#{target_file_path}`" - ) - end - - backup_current_hook(git_hook_atom_as_kebab_string, opts) - - File.write(target_file_path, target_file_body) - File.chmod(target_file_path, 0o755) - - {:error, reason} -> - reason |> inspect() |> Printer.error() - end - end) - - :ok + git_hooks_configs = Config.git_hooks() + + install_result = + Enum.map(git_hooks_configs, fn git_hook -> + git_hook_atom_as_string = Atom.to_string(git_hook) + git_hook_atom_as_kebab_string = Recase.to_kebab(git_hook_atom_as_string) + + case File.read(template_file) do + {:ok, body} -> + target_file_path = GitPath.git_hooks_path_for(git_hook_atom_as_kebab_string) + + target_file_body = + body + |> String.replace("$git_hook", git_hook_atom_as_string) + |> String.replace("$mix_path", mix_path) + |> String.replace("$project_path", project_path) + + unless opts[:quiet] || !Config.verbose?() do + Printer.info( + "Writing git hook for `#{git_hook_atom_as_string}` to `#{target_file_path}`" + ) + end + + backup_current_hook(git_hook_atom_as_kebab_string, opts) + + if opts[:dry_run] do + {git_hook, target_file_body} + else + File.write(target_file_path, target_file_body) + File.chmod(target_file_path, 0o755) + end + + {:error, reason} -> + reason |> inspect() |> Printer.error() + end + end) + + if opts[:dry_run] do + install_result + else + :ok + end end @spec backup_current_hook(String.t(), Keyword.t()) :: {:error, atom} | {:ok, non_neg_integer()} From 19b7970b61d3282f2fdd86c434f6afd800780489 Mon Sep 17 00:00:00 2001 From: qgadrian Date: Mon, 7 Feb 2022 20:17:15 +0100 Subject: [PATCH 12/17] Add first tests to install module --- test/mix/tasks/git_hooks/install_test.exs | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 test/mix/tasks/git_hooks/install_test.exs diff --git a/test/mix/tasks/git_hooks/install_test.exs b/test/mix/tasks/git_hooks/install_test.exs new file mode 100644 index 0000000..d652218 --- /dev/null +++ b/test/mix/tasks/git_hooks/install_test.exs @@ -0,0 +1,44 @@ +defmodule Mix.Tasks.InstallTest do + @moduledoc false + + use ExUnit.Case, async: false + use GitHooks.TestSupport.ConfigCase + + alias Mix.Tasks.GitHooks.Install + alias GitHooks.Git.Path, as: GitPath + + @tag capture_log: true + + describe "run/1" do + test "replaces the hook template with config values" do + put_git_hook_config( + [:pre_commit, :pre_push], + tasks: {:cmd, "check"} + ) + + hooks_file = Install.run(["--dry-run", "--quiet"]) + + assert hooks_file == [ + pre_commit: expect_hook_template("pre_commit"), + pre_push: expect_hook_template("pre_push") + ] + end + end + + # + # Private functions + # + + defp expect_hook_template(git_hook) do + app_path = GitPath.resolve_app_path() + + ~s(#!/bin/sh + +[ "#{app_path}" != "" ] && cd #{app_path} + +mix git_hooks.run #{git_hook} "$@" +[ $? -ne 0 ] && exit 1 +exit 0 +) + end +end From af3c728a71240817eafa970b98202a3e513fd31f Mon Sep 17 00:00:00 2001 From: qgadrian Date: Mon, 7 Feb 2022 20:28:42 +0100 Subject: [PATCH 13/17] Remove `git_path` config The config is no longer needed as the git path will be resolved automatically for the repo. --- README.md | 42 ++---------------------------------------- lib/git/path.ex | 22 +++++----------------- test/git/path_test.exs | 28 +--------------------------- 3 files changed, 8 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index e4f7312..26cca12 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,7 @@ Main features: * [Auto install](#auto-install) * [Hook configuration](#hook-configuration) * [Git submodules](#git-submodules) - * [Custom paths](#custom-paths) - * [Git path](#git-path) - * [Git hooks path](#git-hooks-path) - * [Mix path](#mix-path) + * [Custom mix path](#custom-mix-path) * [Troubleshooting in docker containers](#troubleshooting-in-docker-containers) * [Example config](#example-config) * [Task types](#task-types) @@ -122,42 +119,7 @@ Setting a custom _git hooks_ config path is also supported: git config core.hooksPath .myCustomGithooks/ ``` -### Custom paths - -By default this library expects your Elixir project to be the root of the git -repository. If this is the case, you might need to configure custom paths based -on your folders relative paths. - -#### Git path - -If you need to override the folder of the _git path_ you -can add the following configuration: - -```elixir -config :git_hooks, - git_path: "../.git" -``` - -This is useful if the root of your project is not managed directly by the VCS -but the parent. - -If you set the `git_path`, the git hooks path will expect to be inside -the `hooks` folder of the provided path configuration. In the example above, `../.git/hooks`. - -#### Git hooks path - -If you need to override the folder of the _git hooks path_ you -can add the following configuration: - -```elixir -config :git_hooks, - git_hooks_path: "../.git/hooks" -``` - -This is useful if the root of your project is not managed directly by the VCS -but the parent and you are using a custom path for your git hooks. - -#### Mix path +### Custom mix path This library expects `elixir` to be installed in your system and the `mix` binary to be available. If you want to provide a specific path to run the `mix` executable, it can be done using the `mix_path` configuration. diff --git a/lib/git/path.ex b/lib/git/path.ex index 35636d4..7926ab2 100644 --- a/lib/git/path.ex +++ b/lib/git/path.ex @@ -6,28 +6,16 @@ defmodule GitHooks.Git.Path do @doc false @spec resolve_git_hooks_path() :: any def resolve_git_hooks_path do - git_path_config = Application.get_env(:git_hooks, :git_path, nil) - git_hooks_path_config = Application.get_env(:git_hooks, :git_hooks_path, nil) - - case {git_hooks_path_config, git_path_config} do - {nil, nil} -> - resolve_git_path_based_on_git_version("hooks") - - {nil, git_path_config} -> - "#{git_path_config}/hooks" - - {git_hooks_path_config, _} -> - git_hooks_path_config - end + resolve_git_path_based_on_git_version("hooks") end @doc false def resolve_app_path do - git_path = resolve_git_path_based_on_git_version() - git_dir = Application.get_env(:git_hooks, :git_path, git_path) - repo_dir = Path.dirname(git_dir) + repo_path = + resolve_git_path_based_on_git_version() + |> Path.dirname() - Path.relative_to(File.cwd!(), repo_dir) + Path.relative_to(File.cwd!(), repo_path) end @spec git_hooks_path_for(path :: String.t()) :: String.t() diff --git a/test/git/path_test.exs b/test/git/path_test.exs index db2e40f..8016085 100644 --- a/test/git/path_test.exs +++ b/test/git/path_test.exs @@ -10,33 +10,7 @@ defmodule GitHooks.Git.PathTest do end describe "resolve_git_hooks_path/0" do - test "returns the configuration for git_hooks_path" do - Application.put_env(:git_hooks, :git_hooks_path, "./custom-hooks-path") - - assert Path.resolve_git_hooks_path() == "./custom-hooks-path" - - Application.delete_env(:git_hooks, :git_hooks_path) - end - - test "returns the hooks path based on the git_path configuration" do - Application.put_env(:git_hooks, :git_path, "./custom-git-path") - - assert Path.resolve_git_hooks_path() == "./custom-git-path/hooks" - - Application.delete_env(:git_hooks, :git_path) - end - - test "prioritizes the git_hooks_path config over the git_path" do - Application.put_env(:git_hooks, :git_hooks_path, "./prioritized-custom-hooks-path") - Application.put_env(:git_hooks, :git_path, "./custom-git-path") - - assert Path.resolve_git_hooks_path() == "./prioritized-custom-hooks-path" - - Application.delete_env(:git_hooks, :git_hooks_path) - Application.delete_env(:git_hooks, :git_path) - end - - test "returns the git path when there is no other configuration" do + test "returns the git path of the project" do assert Path.resolve_git_hooks_path() == ".git/hooks" end end From 56eb663b69494590144d8b643cf30ade987e953d Mon Sep 17 00:00:00 2001 From: qgadrian Date: Mon, 7 Feb 2022 20:32:04 +0100 Subject: [PATCH 14/17] Publish pre-release `v0.7.0-pre` --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index ac18a5b..871c558 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule GitHooks.MixProject do use Mix.Project @source_url "https://github.com/qgadrian/elixir_git_hooks" - @version "0.6.6-pre" + @version "0.7.0-pre" def project do [ From 0115026b9616b8f540f6fa54a2297e3a117356e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Feb 2022 11:19:07 +0000 Subject: [PATCH 15/17] Bump credo from 1.6.2 to 1.6.3 Bumps [credo](https://github.com/rrrene/credo) from 1.6.2 to 1.6.3. - [Release notes](https://github.com/rrrene/credo/releases) - [Changelog](https://github.com/rrrene/credo/blob/master/CHANGELOG.md) - [Commits](https://github.com/rrrene/credo/compare/v1.6.2...v1.6.3) --- updated-dependencies: - dependency-name: credo dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index a07cc1c..0415f75 100644 --- a/mix.lock +++ b/mix.lock @@ -2,7 +2,7 @@ "blankable": {:hex, :blankable, "1.0.0", "89ab564a63c55af117e115144e3b3b57eb53ad43ba0f15553357eb283e0ed425", [:mix], [], "hexpm", "7cf11aac0e44f4eedbee0c15c1d37d94c090cb72a8d9fddf9f7aec30f9278899"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"}, - "credo": {:hex, :credo, "1.6.2", "2f82b29a47c0bb7b72f023bf3a34d151624f1cbe1e6c4e52303b05a11166a701", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ae9dc112bc368e7b145c547bec2ed257ef88955851c15057c7835251a17211c6"}, + "credo": {:hex, :credo, "1.6.3", "0a9f8925dbc8f940031b789f4623fc9a0eea99d3eed600fe831e403eb96c6a83", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1167cde00e6661d740fc54da2ee268e35d3982f027399b64d3e2e83af57a1180"}, "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, From 9803e1f3f54ed17940a71d8946192967b0bd18da Mon Sep 17 00:00:00 2001 From: qgadrian Date: Tue, 15 Feb 2022 19:44:55 +0100 Subject: [PATCH 16/17] Add initial support to mix tasks result --- lib/tasks/mix.ex | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/tasks/mix.ex b/lib/tasks/mix.ex index e8d9ab2..340a89a 100644 --- a/lib/tasks/mix.ex +++ b/lib/tasks/mix.ex @@ -75,24 +75,22 @@ defimpl GitHooks.Task, for: GitHooks.Tasks.Mix do Map.put(mix_task, :result, result) end - # Mix tasks always raise an error if they are not success, at the moment does - # not seems that handling the result is needed. Also, handling the result to - # check the success of a task is almost impossible, as it will depend on each - # implementation. - # - # XXX Since tests runs on the command, if they fail then this task is - # considered failed. - def success?(%MixTask{result: 1}), do: false - def success?(%MixTask{result: _}), do: true - - def print_result(%MixTask{task: task, result: 1} = mix_task) do - Printer.error("`#{task}` failed") + # Mix tasks raise an error if they are valid, but determining if they are + # success or not depends on the return of the task. + @success_results [0, :ok, nil] + + def success?(%MixTask{result: result}) when result in @success_results, do: true + def success?(%MixTask{result: _}), do: false + + def print_result(%MixTask{task: task, result: result} = mix_task) + when result in @success_results do + Printer.success("`#{task}` was successful") mix_task end - def print_result(%MixTask{task: task, result: _} = mix_task) do - Printer.success("`#{task}` was successful") + def print_result(%MixTask{task: task, result: result} = mix_task) do + Printer.error("mix task `#{task}` failed, return result: #{inspect(result)}") mix_task end From 77b81f6caa77ec0a3d051051c46b0d1752a40447 Mon Sep 17 00:00:00 2001 From: qgadrian Date: Tue, 15 Feb 2022 19:46:11 +0100 Subject: [PATCH 17/17] Release `v0.7.0` --- README.md | 2 +- mix.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 26cca12..8680cc3 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Add to dependencies: ```elixir def deps do [ - {:git_hooks, "~> 0.6.5", only: [:dev], runtime: false} + {:git_hooks, "~> 0.7.0", only: [:dev], runtime: false} ] end ``` diff --git a/mix.exs b/mix.exs index 871c558..4e65899 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule GitHooks.MixProject do use Mix.Project @source_url "https://github.com/qgadrian/elixir_git_hooks" - @version "0.7.0-pre" + @version "0.7.0" def project do [