diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..17aa6df --- /dev/null +++ b/.credo.exs @@ -0,0 +1,138 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + included: ["lib/", "src/", "web/", "apps/"], + excluded: [~r"/_build/", ~r"/deps/"] + }, + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + requires: [], + # + # Credo automatically checks for updates, like e.g. Hex does. + # You can disable this behaviour below: + check_for_updates: true, + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + strict: true, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: [ + {Credo.Check.Readability.ModuleDoc}, + {Credo.Check.Consistency.ExceptionNames}, + {Credo.Check.Consistency.LineEndings}, + {Credo.Check.Consistency.ParameterPatternMatching}, + {Credo.Check.Consistency.SpaceAroundOperators}, + {Credo.Check.Consistency.SpaceInParentheses}, + {Credo.Check.Consistency.TabsOrSpaces}, + + # For some checks, like AliasUsage, you can only customize the priority + # Priority values are: `low, normal, high, higher` + {Credo.Check.Design.AliasUsage, priority: :low}, + + # For others you can set parameters + + # If you don't want the `setup` and `test` macro calls in ExUnit tests + # or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just + # set the `excluded_macros` parameter to `[:schema, :setup, :test]`. + {Credo.Check.Design.DuplicatedCode, excluded_macros: []}, + + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + {Credo.Check.Design.TagTODO, exit_status: 2}, + {Credo.Check.Design.TagFIXME}, + + {Credo.Check.Readability.FunctionNames}, + {Credo.Check.Readability.LargeNumbers}, + {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 150}, + {Credo.Check.Readability.ModuleAttributeNames}, + {Credo.Check.Readability.ModuleNames}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs}, + {Credo.Check.Readability.ParenthesesInCondition}, + {Credo.Check.Readability.PredicateFunctionNames}, + {Credo.Check.Readability.PreferImplicitTry}, + {Credo.Check.Readability.RedundantBlankLines}, + {Credo.Check.Readability.StringSigils}, + {Credo.Check.Readability.TrailingBlankLine}, + {Credo.Check.Readability.TrailingWhiteSpace}, + {Credo.Check.Readability.VariableNames}, + {Credo.Check.Readability.Semicolons}, + {Credo.Check.Readability.SpaceAfterCommas}, + + {Credo.Check.Refactor.DoubleBooleanNegation}, + {Credo.Check.Refactor.CondStatements}, + {Credo.Check.Refactor.CyclomaticComplexity}, + {Credo.Check.Refactor.FunctionArity}, + {Credo.Check.Refactor.MatchInCondition}, + {Credo.Check.Refactor.NegatedConditionsInUnless}, + {Credo.Check.Refactor.NegatedConditionsWithElse}, + {Credo.Check.Refactor.Nesting, max_nesting: 3}, + {Credo.Check.Refactor.PipeChainStart}, + {Credo.Check.Refactor.UnlessWithElse}, + + {Credo.Check.Warning.BoolOperationOnSameValues}, + {Credo.Check.Warning.IExPry}, + {Credo.Check.Warning.IoInspect}, + {Credo.Check.Warning.LazyLogging}, + {Credo.Check.Warning.OperationOnSameValues}, + {Credo.Check.Warning.OperationWithConstantResult}, + {Credo.Check.Warning.UnusedEnumOperation}, + {Credo.Check.Warning.UnusedFileOperation}, + {Credo.Check.Warning.UnusedKeywordOperation}, + {Credo.Check.Warning.UnusedListOperation}, + {Credo.Check.Warning.UnusedPathOperation}, + {Credo.Check.Warning.UnusedRegexOperation}, + {Credo.Check.Warning.UnusedStringOperation}, + {Credo.Check.Warning.UnusedTupleOperation}, + + # Controversial and experimental checks (opt-in, just remove `, false`) + # + {Credo.Check.Refactor.ABCSize, false}, + {Credo.Check.Refactor.AppendSingleItem, false}, + {Credo.Check.Refactor.VariableRebinding, false}, + {Credo.Check.Warning.MapGetUnsafePass, false}, + {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, + + # Deprecated checks (these will be deleted after a grace period) + {Credo.Check.Readability.Specs, false}, + {Credo.Check.Warning.NameRedeclarationByAssignment, false}, + {Credo.Check.Warning.NameRedeclarationByCase, false}, + {Credo.Check.Warning.NameRedeclarationByDef, false}, + {Credo.Check.Warning.NameRedeclarationByFn, false}, + + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + ] +} diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..525446d --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30bc701 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +git_hooks-*.tar + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..03bfaba --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: elixir +elixir: +- 1.6 +env: +- MIX_ENV=test +script: mix coveralls.travis +notifications: + email: + on_success: change + on_failure: always diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8bd4007 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Adrián Quintás + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b02059 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +[![Coverage Status](https://coveralls.io/repos/github/qgadrian/elixir_git_hooks/badge.svg?branch=master)](https://coveralls.io/github/qgadrian/elixir_git_hooks?branch=master) +[![Hex version](https://img.shields.io/hexpm/v/sippet.svg "Hex version")](https://hex.pm/packages/git_hooks) +[![Hex Docs](https://img.shields.io/badge/hex-docs-9768d1.svg)](https://hexdocs.pm/elixir_git_hooks) +[![Build Status](https://travis-ci.org/qgadrian/metadata_plugs.svg?branch=master)](https://travis-ci.org/qgadrian/elixir_git_hooks.svg?branch=master) +[![Deps Status](https://beta.hexfaktor.org/badge/all/github/qgadrian/elixir_git_hooks.svg)](https://beta.hexfaktor.org/github/qgadrian/elixir_git_hooks) + +# GitHooks + +Installs [git hooks](https://git-scm.com/docs/githooks) that will run in Elixir project. + +## Table of Contents + +- [Installation](#installation) + - [Backup](#backup-current-hooks) + - [Automatic](#automatic-installation) + - [Manual](#manual-installation) +- [Configuration](#configuration) +- [Execution](#execution) + - [Automatic](#automatic-execution) + - [Manual](#manual-execution) + +## Installation + +Add to dependencies: + +```elixir +def deps do + [{:git_hooks, "~> 0.1.0"}] +end +``` + +The install the dependencies: + +```bash +mix deps.get +``` + +### Backup current hooks + +This project will backup your current git hooks files copying the files and adding the extension `.pre_git_hooks_backup`. + +### Automatic installation + +This library will install automatically the configured git hooks in your file. + +### Manual installation + +You can install manually the configured git hooks by running: + +```bash +mix git_hooks.install +``` + +## Configuration + +One or more git hooks can be configured, those git hooks will be the ones [installed](#installation) for your project. + +Currently there are supported two configuration options: + * **mix_tasks**: A list of the mix tasks that will run for the git hook + * **verbose**: The output of the mix tasks will be visible. This can be configured globally or per git hook. + +```elixir +config :git_hooks, + verbose: true, + git_hooks: [ + pre_commit: [ + mix_tasks: [ + "format" + ] + ], + pre_push: [ + verbose: false, + mix_tasks: [ + "dialyzer", + "test" + ] + ] + ] +``` + +## Execution + +### Automatic execution + +The git hooks will run automatically for each [git step](https://git-scm.com/docs/githooks#_hooks). + +### Manual execution + +You can run manually any configured git hook as well. + +For example, to run the pre_commit configuration: + +```bash +mix git_hooks.run pre_commit +``` diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..4fe2c29 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,50 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +use Mix.Config + +# This configuration is loaded before any dependency and is restricted +# to this project. If another project depends on this project, this +# file won't be loaded nor affect the parent project. For this reason, +# if you want to provide default values for your application for +# 3rd-party users, it should be done in your "mix.exs" file. + +# You can configure your application as: +# +# config :git_hooks, key: :value +# +# and access this configuration in your application as: +# +# Application.get_env(:git_hooks, :key) +# +# You can also configure a 3rd-party app: +# +# config :logger, level: :info +# + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# +# import_config "#{Mix.env}.exs" + +### Example configurations + +config :git_hooks, + git_hooks: [ + pre_commit: [ + verbose: true, + mix_tasks: [ + "format --check-formatted --dry-run", + "credo" + ] + ], + pre_push: [ + verbose: true, + mix_tasks: [ + "dialyzer", + "coveralls" + ] + ] + ] diff --git a/lib/config/config.ex b/lib/config/config.ex new file mode 100644 index 0000000..9df6457 --- /dev/null +++ b/lib/config/config.ex @@ -0,0 +1,50 @@ +defmodule GitHooks.Config do + @moduledoc false + + @supported_hooks [ + :pre_commit, + :pre_push, + :pre_rebase, + :pre_receive, + :pre_applypatch, + :post_update + ] + + @spec supported_hooks() :: list(atom()) + def supported_hooks, do: @supported_hooks + + @spec git_hooks() :: list(atom()) + def git_hooks do + :git_hooks + |> Application.get_env(:git_hooks, []) + |> Keyword.take(@supported_hooks) + |> Keyword.keys() + end + + @spec mix_tasks(atom()) :: list(String.t()) + def mix_tasks(git_hook_type) do + :git_hooks + |> Application.get_env(:git_hooks, []) + |> Keyword.get(git_hook_type, []) + |> Keyword.get(:mix_tasks, []) + end + + @spec verbose?(atom()) :: boolean() + def verbose?(git_hook_type) do + :git_hooks + |> Application.get_env(:git_hooks, []) + |> Keyword.get(git_hook_type, []) + |> Keyword.get(:verbose, Application.get_env(:git_hooks, :verbose, false)) + end + + @spec io_stream(atom()) :: any() + def io_stream(git_hook_type) do + case verbose?(git_hook_type) do + true -> + IO.stream(:stdio, :line) + + _ -> + "" + end + end +end diff --git a/lib/git_hooks.ex b/lib/git_hooks.ex new file mode 100644 index 0000000..bcbdd36 --- /dev/null +++ b/lib/git_hooks.ex @@ -0,0 +1,8 @@ +defmodule GitHooks do + @moduledoc """ + Module that provides the git hooks supported and installs automatically the configured hooks. + """ + + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + Mix.Tasks.GitHooks.Install.install(quiet: true) +end diff --git a/lib/mix/tasks/git_hooks/install.ex b/lib/mix/tasks/git_hooks/install.ex new file mode 100644 index 0000000..9efcb2a --- /dev/null +++ b/lib/mix/tasks/git_hooks/install.ex @@ -0,0 +1,75 @@ +defmodule Mix.Tasks.GitHooks.Install do + @moduledoc """ + This module installs the configured git hooks. + """ + + @shortdoc "This module install the configured git hooks." + + use Mix.Task + + alias GitHooks.Config + alias Mix.Project + alias GitHooks.Printer + + @impl true + def run(_args) do + install() + + :ok + end + + @spec install(Keyword.t()) :: any() + def install(opts \\ []) do + # Project.deps_path() + # |> Path.join("/git_hooks/priv/hook_template") + template_file = + :git_hooks + |> :code.priv_dir() + |> Path.join("/hook_template") + + 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 = + Project.deps_path() + |> Path.join("/../.git/hooks/#{git_hook_atom_as_kebab_string}") + + target_file_body = + String.replace(body, "$git_hook", git_hook_atom_as_string, global: true) + + unless opts[:quiet] do + Printer.warn( + "Writing git hook for `#{git_hook_atom_as_string}` to `#{target_file_path}`" + ) + end + + backup_current_hook(git_hook_atom_as_kebab_string) + + File.write(target_file_path, target_file_body) + File.chmod(target_file_path, 0o755) + + {:error, reason} -> + reason |> inspect() |> Printer.error() + end + end) + + :ok + end + + @spec backup_current_hook(String.t()) :: {:error, atom()} | {:ok, non_neg_integer()} + def backup_current_hook(git_hook_to_backup) do + source_file_path = + Project.deps_path() + |> Path.join("/../.git/hooks/#{git_hook_to_backup}") + + target_file_path = + Project.deps_path() + |> Path.join("/../.git/hooks/#{git_hook_to_backup}.pre_git_hooks_backup") + + File.copy(source_file_path, target_file_path) + end +end diff --git a/lib/mix/tasks/git_hooks/run.ex b/lib/mix/tasks/git_hooks/run.ex new file mode 100644 index 0000000..c549baf --- /dev/null +++ b/lib/mix/tasks/git_hooks/run.ex @@ -0,0 +1,89 @@ +defmodule Mix.Tasks.GitHooks.Run do + @moduledoc """ + Module that defines the a git hook that will the configured mix tasks . + + The task will exepect an argument that will determine which git hook to run. + + The supported git hooks are: + * pre_commit + * pre_push + * pre_rebase + * pre_receive + * pre_applypatch + * post_update + """ + + @shortdoc "Module that defines a git hook that will run the configured mix tasks." + + use Mix.Task + + alias GitHooks.Config + alias GitHooks.Printer + + @impl true + def run(args) do + git_hook_type = + args + |> List.first() + |> get_atom_from_arg() + |> check_is_valid_git_hook!() + + Printer.info("Running hooks for #{git_hook_type}") + + git_hook_type + |> Config.mix_tasks() + |> Enum.each(&run_commands(&1, git_hook_type)) + |> success_exit() + end + + @spec run_commands(String.t(), atom()) :: :ok | no_return + defp run_commands(mix_task, git_hook_type) do + "mix" + |> System.cmd( + String.split(mix_task, " "), + stderr_to_stdout: true, + into: Config.io_stream(git_hook_type) + ) + |> case do + {_result, 0} -> + Printer.success("`mix #{mix_task}` was successful") + + {result, _} -> + if !Config.verbose?(git_hook_type), do: IO.puts(result) + + Printer.error("#{Atom.to_string(git_hook_type)} failed on `mix #{mix_task}`") + error_exit() + end + end + + @spec get_atom_from_arg(String.t()) :: atom() | no_return + defp get_atom_from_arg(git_hook_type_arg) do + case git_hook_type_arg do + nil -> + Printer.error("You should provide a git hook type to run") + error_exit() + + git_hook_type -> + git_hook_type + |> Recase.to_snake() + |> String.to_atom() + end + end + + @spec check_is_valid_git_hook!(atom()) :: no_return + defp check_is_valid_git_hook!(git_hook_type) do + unless Enum.any?(Config.supported_hooks(), &(&1 == git_hook_type)) do + Printer.error("Invalid or unsupported hook `#{git_hook_type}`") + Printer.warn("Supported hooks are: #{inspect(Config.supported_hooks())}") + error_exit() + end + + git_hook_type + end + + @spec success_exit(any()) :: :ok + defp success_exit(_), do: :ok + + @spec error_exit(non_neg_integer) :: no_return + defp error_exit(error_code \\ 1), do: exit(error_code) +end diff --git a/lib/printer/printer.ex b/lib/printer/printer.ex new file mode 100644 index 0000000..fc8e60a --- /dev/null +++ b/lib/printer/printer.ex @@ -0,0 +1,29 @@ +defmodule GitHooks.Printer do + @moduledoc """ + Provides functions to print text in the a given color or format. + """ + + @spec info(String.t()) :: :ok + def info(message) do + IO.puts([IO.ANSI.blue(), 0x2197, " ", message]) + IO.write(IO.ANSI.default_color()) + end + + @spec warn(String.t()) :: :ok + def warn(message) do + IO.puts([IO.ANSI.yellow(), 0x26A0, " ", message]) + IO.write(IO.ANSI.default_color()) + end + + @spec success(String.t()) :: :ok + def success(message) do + IO.puts([IO.ANSI.green(), 0x2714, " ", message]) + IO.write(IO.ANSI.default_color()) + end + + @spec error(String.t()) :: :ok + def error(message) do + IO.puts([IO.ANSI.red(), 0xD7, " ", message]) + IO.write(IO.ANSI.default_color()) + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..5f8125b --- /dev/null +++ b/mix.exs @@ -0,0 +1,72 @@ +defmodule GitHooks.MixProject do + @moduledoc false + + use Mix.Project + + def project do + [ + app: :git_hooks, + version: "0.1.0", + elixir: "~> 1.6", + elixirc_paths: elixirc_paths(Mix.env()), + start_permanent: Mix.env() == :prod, + package: package(), + description: description(), + aliases: aliases(), + deps: deps(), + test_coverage: [tool: ExCoveralls], + preferred_cli_env: [ + coveralls: :test, + "coveralls.detail": :test, + "coveralls.post": :test, + "coveralls.html": :test + ], + dialyzer: [plt_add_deps: :transitive, plt_add_apps: [:mix]] + ] + end + + # # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + defp package do + [ + name: "git_hooks", + files: ["lib", "priv", "mix.exs", "README*"], + maintainers: ["Adrián Quintás"], + licenses: ["MIT"], + links: %{"Github" => "https://github.com/qgadrian/elixir_git_hooks"} + ] + end + + defp description do + "Add git hooks to Elixir projects" + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:ex_doc, ">= 0.0.0", only: :dev}, + {:excoveralls, "~> 0.8", only: :test}, + {:dialyxir, "~> 0.5", only: [:dev], runtime: false}, + {:credo, "~> 0.9", only: [:dev, :test], runtime: false}, + {:blankable, "~> 0.0.1"}, + {:recase, "~> 0.2"} + ] + end + + defp aliases do + [ + compile: ["compile --warnings-as-errors"], + coveralls: ["coveralls.html"], + "coveralls.html": ["coveralls.html"] + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..6f17e8a --- /dev/null +++ b/mix.lock @@ -0,0 +1,21 @@ +%{ + "blankable": {:hex, :blankable, "0.0.1", "2e0b4667fee684f0614620d31a34bb2731341cccb27ed903e330195819ba3ba0", [:mix], [], "hexpm"}, + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, + "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, + "credo": {:hex, :credo, "0.9.2", "841d316612f568beb22ba310d816353dddf31c2d94aa488ae5a27bb53760d0bf", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, + "excoveralls": {:hex, :excoveralls, "0.8.1", "0bbf67f22c7dbf7503981d21a5eef5db8bbc3cb86e70d3798e8c802c74fa5e27", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, + "hackney": {:hex, :hackney, "1.12.1", "8bf2d0e11e722e533903fe126e14d6e7e94d9b7983ced595b75f532e04b7fdc7", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "5.1.1", "cbc3b2fa1645113267cc59c760bafa64b2ea0334635ef06dbac8801e42f7279c", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, + "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, + "recase": {:hex, :recase, "0.3.0", "a3a6b2bfc9a1c3047b6f37d49ea52027ea59fd256505254b8e9d63c68d09ab89", [:mix], [], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, +} diff --git a/priv/hook_template b/priv/hook_template new file mode 100644 index 0000000..03b308b --- /dev/null +++ b/priv/hook_template @@ -0,0 +1,6 @@ +#!/bin/sh + +mix git_hooks.run $git_hook +[ $? -ne 0 ] && exit 1 +exit 0 +fi diff --git a/test/config/config_test.exs b/test/config/config_test.exs new file mode 100644 index 0000000..1403fd3 --- /dev/null +++ b/test/config/config_test.exs @@ -0,0 +1,71 @@ +defmodule GitHooks.ConfigTest do + @moduledoc false + + use ExUnit.Case, async: false + use GitHooks.TestSupport.ConfigCase + + alias GitHooks.Config + + describe "Given a git hook type" do + test "when there are not configured mix tasks then an empty list is returned" do + put_git_hook_config(:pre_commit, mix_tasks: ["help", "help deps"]) + + assert Config.mix_tasks(:unknown_hook) == [] + end + + test "when there are configured mix tasks then a list of the mix tasks is returned" do + mix_tasks = ["help", "help deps"] + + put_git_hook_config(:pre_commit, mix_tasks: mix_tasks) + + assert Config.mix_tasks(:pre_commit) == mix_tasks + end + + test "when the verbose is enabled for the git hook then the verbose config function returns true" do + put_git_hook_config(:pre_commit, verbose: true) + + assert Config.verbose?(:pre_commit) == true + end + + test "when the verbose is enabled globally then the verbose config function returns true" do + Application.put_env(:git_hooks, :verbose, true) + + assert Config.verbose?(:pre_commit) == true + end + + test "when the verbose is true globally but false for a githook then the verbose config function returns false" do + put_git_hook_config(:pre_commit, verbose: false) + Application.put_env(:git_hooks, :verbose, true) + + assert Config.verbose?(:pre_commit) == false + end + + test "when the git hook is unknown then the verbose config function returns false" do + put_git_hook_config(:pre_commit, verbose: true) + + assert Config.verbose?(:unknown_hook) == false + end + + test "when there are no supported git hooks configured then an empty list is returned" do + assert Config.git_hooks() == [] + end + + test "when request the git hooks types then a list of supported git hooks types is returned" do + put_git_hook_configs([:pre_commit, :pre_push]) + + assert Config.git_hooks() == [:pre_commit, :pre_push] + end + + test "when the verbose is enabled then a IO stream is returned" do + put_git_hook_configs([:pre_commit], verbose: true) + + assert Config.io_stream(:pre_commit) == IO.stream(:stdio, :line) + end + + test "when the verbose is disabled then an empty string is returned" do + put_git_hook_configs([:pre_commit, :pre_push], verbose: false) + + assert Config.io_stream(:pre_commit) == "" + end + end +end diff --git a/test/mix/tasks/git_hooks/run_test.exs b/test/mix/tasks/git_hooks/run_test.exs new file mode 100644 index 0000000..8a08a8c --- /dev/null +++ b/test/mix/tasks/git_hooks/run_test.exs @@ -0,0 +1,42 @@ +defmodule Mix.Tasks.RunTest do + @moduledoc false + + use ExUnit.Case, async: false + use GitHooks.TestSupport.ConfigCase + + import ExUnit.CaptureIO + + doctest Mix.Tasks.GitHooks.Run + + alias Mix.Tasks.GitHooks.Run + + describe "Given args for the mix git hook task" do + test "when no git hook type is provided then the process exits with 1" do + capture_io(fn -> + assert catch_exit(Run.run([])) == 1 + end) + end + + test "when the git hook type is not supported then the process exits with 1" do + capture_io(fn -> + assert catch_exit(Run.run(["invalid_hook"])) == 1 + end) + end + + test "when the git hook it's supported then it's executed and the task returns :ok" do + put_git_hook_config(:pre_commit, mix_tasks: ["help test"], verbose: true) + + capture_io(fn -> + assert Run.run(["pre-commit"]) == :ok + end) + end + + test "when a mix task of the git hook fails then it's executed and the task exits with 0" do + put_git_hook_config(:pre_commit, mix_tasks: ["this_task_is_going_to_fail"]) + + capture_io(fn -> + assert catch_exit(Run.run(["pre-commit"])) == 1 + end) + end + end +end diff --git a/test/support/config_case.ex b/test/support/config_case.ex new file mode 100644 index 0000000..13c6f41 --- /dev/null +++ b/test/support/config_case.ex @@ -0,0 +1,53 @@ +defmodule GitHooks.TestSupport.ConfigCase do + @moduledoc """ + This module provides a function to setup a git hook configuracion for the application. + """ + + use ExUnit.CaseTemplate + + using do + quote do + @default_verbose false + @default_mix_tasks ["help", "help deps"] + + setup do + on_exit(fn -> + cleanup_config() + end) + end + + @spec cleanup_config() :: :ok + def cleanup_config do + Application.delete_env(:git_hooks, :git_hooks) + Application.delete_env(:git_hooks, :verbose) + end + + @spec put_git_hook_config(atom(), Keyword.t()) :: :ok + def put_git_hook_config(git_hook_type, opts \\ []) do + git_hook_config = [ + verbose: opts[:verbose] || @default_verbose, + mix_tasks: opts[:mix_tasks] || @default_mix_tasks + ] + + git_hook_configuration = Keyword.new([{git_hook_type, git_hook_config}]) + + Application.put_env(:git_hooks, :git_hooks, git_hook_configuration) + end + + @spec put_git_hook_configs(list(atom()), Keyword.t()) :: :ok + def put_git_hook_configs(git_hook_types, opts \\ []) do + git_hook_config = [ + verbose: opts[:verbose] || @default_verbose, + mix_tasks: opts[:mix_tasks] || @default_mix_tasks + ] + + git_hook_configuration = + git_hook_types + |> Enum.map(&{&1, git_hook_config}) + |> Keyword.new() + + Application.put_env(:git_hooks, :git_hooks, git_hook_configuration) + end + end + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()