├── .exguard.exs ├── .formatter.exs ├── .github └── workflows │ └── elixir.yml ├── .gitignore ├── .sourcelevel.yml ├── .tool-versions ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── lib ├── git_pair.ex ├── git_pair │ ├── actions.ex │ ├── behaviours │ │ ├── hook_behaviour.ex │ │ ├── storage_behaviour.ex │ │ └── system_behaviour.ex │ ├── cli.ex │ └── hook.ex └── storage.ex ├── mix.exs ├── mix.lock └── test ├── actions_test.exs ├── storage_test.exs └── test_helper.exs /.exguard.exs: -------------------------------------------------------------------------------- 1 | use ExGuard.Config 2 | 3 | guard("unit-test", run_on_start: false) 4 | |> command("mix test --color") 5 | |> watch(~r{\.(ex|exs)\z}i) 6 | |> ignore(~r{deps}) 7 | |> notification(:auto) 8 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Setup elixir 13 | uses: erlef/setup-beam@v1 14 | with: 15 | elixir-version: 1.18.1-otp-27 # Define the elixir version [required] 16 | otp-version: 27.2 # Define the OTP version [required] 17 | - uses: actions/cache@v1 18 | with: 19 | path: deps 20 | key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} 21 | restore-keys: | 22 | ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} 23 | - name: Install Dependencies 24 | run: | 25 | mix local.rebar --force 26 | mix local.hex --force 27 | mix deps.get 28 | - name: Run Tests 29 | run: mix test 30 | - name: Build escript 31 | run: mix escript.build 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | git_pair-*.tar 24 | -------------------------------------------------------------------------------- /.sourcelevel.yml: -------------------------------------------------------------------------------- 1 | # This configuration was used SourceLevel to review the elixirsc/git-pair repository 2 | # on 18066519bb13d4ee674a9ccbe4f35bf76ba7e767. 3 | # You can make this the default configuration for future reviews by moving this 4 | # file to your repository as `.sourcelevel.yml` and pushing it to GitHub, and tweak 5 | # it as you wish - To know more on how to change this file to better review your 6 | # repository you can go to https://docs.sourcelevel.io/configuration and see the configuration 7 | # details. 8 | --- 9 | styleguide: sourcelevel/linters 10 | engines: 11 | credo: 12 | enabled: true 13 | channel: latest 14 | fixme: 15 | enabled: true 16 | remark-lint: 17 | enabled: true 18 | exclude_paths: 19 | - test -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.18.1-otp-27 2 | erlang 27.2 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | To start contributing, first you need to run in your own machine: 2 | 3 | ## Setup 4 | 5 | ``` 6 | git clone git@github.com:elixirsc/git-pair.git 7 | cd git-pair 8 | mix deps.get 9 | mix test 10 | ``` 11 | 12 | ## Build 13 | 14 | To build `escript` we use [`Mix`](https://hexdocs.pm/mix/master/Mix.Tasks.Escript.Build.html): 15 | 16 | ``` 17 | mix escript.build 18 | ``` 19 | 20 | This will generate a binary file under `_build/git-pair`. 21 | 22 | ## Backlog 23 | 24 | Then check [Project Backlog](https://github.com/elixirsc/git-pair/projects/1) and you're good to go! :rocket: 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Elixir |> SC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitPair 2 | 3 | ![Elixir CI](https://github.com/elixirsc/git-pair/workflows/Elixir%20CI/badge.svg) 4 | 5 | Automatically adds [`Co-authored-by`](https://git.wiki.kernel.org/index.php/CommitMessageConventions) mark to commits when you're pairing. 6 | 7 | ### Learn more 8 | 9 | - [GitHub Help](https://help.github.com/en/github/committing-changes-to-your-project/creating-a-commit-with-multiple-authors) 10 | 11 | ## About 12 | 13 | This is an experiment created by [ElixirSC](https://www.meetup.com/elixirsc/) meetup group in our Hacking Sessions. 14 | 15 | We're porting Golang project [thechutrain/git-pair](https://github.com/thechutrain/git-pair) to Elixir using [Erlang escript](http://erlang.org/doc/man/escript.html). 16 | 17 | ## Install 18 | 19 | You can install it with `escript.install` from [hex](https://hex.pm/packages/git_pair): 20 | 21 | ``` 22 | mix escript.install hex git_pair 23 | ``` 24 | 25 | **NOTE:** If you use `asdf`, you need to "export" `git-pair` binary to `asdf` recognized binaries `PATH`: 26 | 27 | ``` 28 | asdf reshim elixir 29 | ``` 30 | 31 | If no version was specified it will get the current version. 32 | 33 | ### Development 34 | 35 | If you want to fetch the development version. You can install directly from this repo: 36 | 37 | ``` 38 | mix escript.install github elixirsc/git-pair branch main 39 | ``` 40 | 41 | ## Usage 42 | 43 | ### Initialize 44 | 45 | ``` 46 | git pair init 47 | ``` 48 | 49 | ### List Pairs 50 | 51 | ``` 52 | git pair status 53 | ``` 54 | 55 | ### Adding Pair 56 | 57 | ``` 58 | git pair add github-username 59 | ``` 60 | 61 | ### Removing Pair 62 | 63 | ``` 64 | git pair rm github-username 65 | ``` 66 | 67 | ### Stop pairing with everyone 68 | 69 | ``` 70 | git pair stop 71 | ``` 72 | 73 | ## How it works 74 | 75 | When you run `git pair init`, it will register [`pre-commit` hook](https://github.com/git/git/blob/master/templates/hooks--pre-commit.sample) to wrap calls to our binary that will add `Co-authored-by` stored in `.git/config`. 76 | 77 | ### Backlog 78 | 79 | To check our backlog check out our [Project Board](https://github.com/elixirsc/git-pair/projects/1). 80 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | import Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :git_pair, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:git_pair, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | config :git_pair, 25 | storage: GitPair.Storage 26 | 27 | config :git_pair, 28 | hook: GitPair.Hook 29 | 30 | config :git_pair, 31 | command_runner: System 32 | 33 | # Import environment specific config. This must remain at the bottom 34 | # of this file so it overrides the configuration defined above. 35 | import_config "#{Mix.env()}.exs" 36 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :git_pair, 4 | command_runner: GitPair.SystemMock 5 | 6 | config :git_pair, 7 | storage: GitPair.StorageMock 8 | 9 | config :git_pair, 10 | hook: GitPair.HookMock 11 | -------------------------------------------------------------------------------- /lib/git_pair.ex: -------------------------------------------------------------------------------- 1 | defmodule GitPair do 2 | @moduledoc """ 3 | Documentation for `GitPair`. 4 | """ 5 | 6 | @version Mix.Project.config()[:version] 7 | 8 | def version(), do: @version 9 | end 10 | -------------------------------------------------------------------------------- /lib/git_pair/actions.ex: -------------------------------------------------------------------------------- 1 | defmodule GitPair.Actions do 2 | @commit_msg_hook_content """ 3 | #!/bin/sh 4 | set -e 5 | 6 | # Hook from git-pair 👥 7 | git pair _modify_commit_msg $@ #adds all of the arguments in bash 8 | """ 9 | 10 | @commit_msg_hook_path "./.git/hooks/commit-msg" 11 | 12 | alias GitPair.Hook 13 | alias GitPair.Storage 14 | 15 | def init() do 16 | File.mkdir_p!(Path.dirname(@commit_msg_hook_path)) 17 | 18 | case File.write(@commit_msg_hook_path, @commit_msg_hook_content) do 19 | :ok -> 20 | File.chmod(@commit_msg_hook_path, 0o755) 21 | {:ok, "Initialize with success"} 22 | 23 | {:error, :enotdir} -> 24 | {:error, "You must initialize in a git repository"} 25 | 26 | {:error, :enoent} -> 27 | {:error, "File does not exist"} 28 | 29 | _ -> 30 | {:error, "Failed to initialize git-pair for this repository"} 31 | end 32 | end 33 | 34 | def add([username, email]) do 35 | {result, user_data} = storage().add([username, email]) 36 | 37 | output(result, "User #{user_data[:identifier]} (#{user_data[:email]}) added") 38 | end 39 | 40 | def add(username) do 41 | {result, user_data} = storage().add(username) 42 | 43 | output(result, "User #{user_data[:identifier]} (#{user_data[:email]}) added") 44 | end 45 | 46 | def rm(identifier) do 47 | {result, user_data} = storage().remove(identifier) 48 | 49 | output(result, "User #{user_data[:identifier]} removed") 50 | end 51 | 52 | def status() do 53 | case storage().fetch_all() do 54 | {:ok, collaborators} when collaborators != [] -> 55 | collaborators = 56 | collaborators 57 | |> Enum.map(fn collaborator -> 58 | "#{collaborator[:identifier]} <#{collaborator[:email]}>" 59 | end) 60 | |> Enum.join("\n") 61 | 62 | output("Pairing with:\n\n" <> collaborators) 63 | 64 | _ -> 65 | output("You aren't pairing with anyone") 66 | end 67 | end 68 | 69 | def stop() do 70 | case storage().remove_all() do 71 | {:ok, []} -> 72 | output("You aren't pairing with anyone") 73 | 74 | {:ok, coauthors} -> 75 | coauthors_message = "You were pairing previously with:\n" <> 76 | Enum.join(coauthors, "\n") 77 | 78 | output("Pairing session stopped!\n\n" <> coauthors_message) 79 | 80 | _ -> 81 | output("Failed to stop pairing session") 82 | end 83 | end 84 | 85 | def version() do 86 | output("You're using v#{GitPair.version()}") 87 | end 88 | 89 | def _modify_commit_msg(path) do 90 | {:ok, coauthors} = storage().fetch_all() 91 | 92 | case hook().modify_commit_msg(path, coauthors) do 93 | {:ok} -> 94 | {:ok, "Success! Co-authors registered."} 95 | _ -> 96 | {:nothing} 97 | end 98 | end 99 | 100 | defp output(:ok, message) do 101 | {:ok, message} 102 | end 103 | 104 | defp output(:error, _message) do 105 | {:error, "Failed to execute command"} 106 | end 107 | 108 | defp output(message) do 109 | {:ok, message} 110 | end 111 | 112 | def storage() do 113 | Application.get_env(:git_pair, :storage, Storage) 114 | end 115 | 116 | def hook() do 117 | Application.get_env(:git_pair, :hook, Hook) 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/git_pair/behaviours/hook_behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule GitPair.HookBehaviour do 2 | @moduledoc false 3 | 4 | @callback modify_commit_msg(String.t(), list()) :: {atom()} 5 | end 6 | -------------------------------------------------------------------------------- /lib/git_pair/behaviours/storage_behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule GitPair.StorageBehaviour do 2 | @moduledoc false 3 | 4 | @callback add(String.t()) :: {atom(), list()} 5 | @callback add(list(String.t())) :: {atom(), list()} 6 | 7 | @callback remove(String.t()) :: {atom(), list()} 8 | @callback remove_all() :: {atom(), list()} 9 | 10 | @callback fetch(String.t()) :: {atom(), list()} 11 | @callback fetch_all() :: {atom(), list()} 12 | end 13 | -------------------------------------------------------------------------------- /lib/git_pair/behaviours/system_behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule GitPair.SystemBehaviour do 2 | @moduledoc false 3 | 4 | @callback cmd(String.t(), list()) :: {String.t(), integer} 5 | end 6 | -------------------------------------------------------------------------------- /lib/git_pair/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule GitPair.CLI do 2 | @moduledoc """ 3 | GitPair.CLI is the entrypoint for the git-pair utility. 4 | """ 5 | 6 | alias GitPair.Actions 7 | 8 | @switches [ 9 | add: :string, 10 | help: :boolean, 11 | h: :boolean 12 | ] 13 | 14 | @aliases [ 15 | a: :add, 16 | h: :help 17 | ] 18 | 19 | @help [ 20 | add: "Add [username] as co-author for next commits", 21 | help: "Display this message 🤡", 22 | init: "Initialize pairing session", 23 | rm: "Remove [username] as co-author for next commits", 24 | stop: "Stop pairing with everyone", 25 | status: "Display pairs list", 26 | version: "Display package version" 27 | ] 28 | 29 | def main(argv) do 30 | argv 31 | |> parse_args 32 | |> parse_command 33 | |> execute_command 34 | |> print_result 35 | end 36 | 37 | defp parse_args(args), do: OptionParser.parse(args, strict: @switches, aliases: @aliases) 38 | 39 | defp parse_command({_, [action | args], _}), do: {action, args} 40 | 41 | defp execute_command({"help", []}) do 42 | @help 43 | |> Enum.map(fn detail -> 44 | {command, explanation} = detail 45 | "#{command}: #{explanation}" 46 | end) 47 | |> (&{:ok, Enum.join(&1, "\n")}).() 48 | end 49 | 50 | defp execute_command({action, []}) do 51 | apply(Actions, String.to_atom(action), []) 52 | end 53 | 54 | defp execute_command({action, args}) do 55 | apply(Actions, String.to_atom(action), [args]) 56 | end 57 | 58 | defp print_result({:ok, message}) do 59 | IO.puts(message) 60 | end 61 | 62 | defp print_result({:error, message}) do 63 | IO.puts("Fail: #{message}") 64 | end 65 | 66 | defp print_result({:nothing}), do: true 67 | end 68 | -------------------------------------------------------------------------------- /lib/git_pair/hook.ex: -------------------------------------------------------------------------------- 1 | defmodule GitPair.Hook do 2 | def modify_commit_msg(_path, []), do: {:nothing} 3 | 4 | def modify_commit_msg(path, coauthors) do 5 | co_authors_message = make_co_authored_by(coauthors) 6 | 7 | File.open(path, [:append]) 8 | |> elem(1) 9 | |> IO.binwrite(co_authors_message) 10 | 11 | {:ok} 12 | end 13 | 14 | defp make_co_authored_by(coauthors) do 15 | "\n" <> 16 | (Enum.map(coauthors, fn coauthor -> 17 | "Co-authored-by: #{coauthor[:identifier]} <#{coauthor[:email]}>" 18 | end) 19 | |> Enum.join("\n")) 20 | |> IO.iodata_to_binary 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/storage.ex: -------------------------------------------------------------------------------- 1 | defmodule GitPair.Storage do 2 | @git_config "config" 3 | @key "pair" 4 | @github_noreply_email_domain "users.noreply.github.com" 5 | @success_exit_status 0 6 | 7 | def add([identifier, email]) do 8 | run(["--add", "#{@key}.#{identifier}.identifier", identifier]) 9 | run(["--add", "#{@key}.#{identifier}.email", email]) 10 | 11 | {:ok, 12 | [ 13 | identifier: identifier, 14 | email: email 15 | ]} 16 | end 17 | 18 | def add([identifier]) do 19 | add([identifier, "#{identifier}@#{@github_noreply_email_domain}"]) 20 | end 21 | 22 | def remove(identifier) do 23 | {_message, result} = run(["--remove-section", "#{@key}.#{identifier}"]) 24 | 25 | build_result(result, 26 | identifier: identifier 27 | ) 28 | end 29 | 30 | def remove_all() do 31 | case fetch_all() do 32 | {:ok, coauthors} when coauthors != [] -> 33 | coauthor_identifiers = Enum.map(coauthors, fn coauthor -> 34 | {:ok, coauthor} = remove(coauthor[:identifier]) 35 | 36 | coauthor[:identifier] 37 | end) 38 | 39 | {:ok, coauthor_identifiers} 40 | _ -> 41 | {:ok, []} 42 | end 43 | end 44 | 45 | def fetch(identifier) do 46 | {result, @success_exit_status} = run(["--get", "#{@key}.#{identifier}.email"]) 47 | 48 | [email, _tail] = String.split(result, "\n") 49 | 50 | {:ok, 51 | [ 52 | identifier: identifier, 53 | email: email 54 | ]} 55 | end 56 | 57 | def fetch_all do 58 | case run(["--get-regexp", "#{@key}.*.identifier"]) do 59 | {collaborators, @success_exit_status} -> 60 | collaborators = 61 | String.split(collaborators, "\n") 62 | |> (fn collaborators -> 63 | List.delete_at(collaborators, length(collaborators) - 1) 64 | end).() 65 | |> Enum.map(fn collaborator -> 66 | [_key, collaborator] = String.split(collaborator, " ") 67 | 68 | {:ok, collaborator_data} = fetch(collaborator) 69 | 70 | collaborator_data 71 | end) 72 | 73 | {:ok, collaborators} 74 | 75 | _ -> 76 | {:ok, []} 77 | end 78 | end 79 | 80 | defp build_result(@success_exit_status, data) do 81 | {:ok, data} 82 | end 83 | 84 | defp build_result(_exit_status, data) do 85 | {:error, data} 86 | end 87 | 88 | defp run(command) do 89 | runner().cmd("git", [@git_config | command]) 90 | end 91 | 92 | defp runner() do 93 | Application.get_env(:git_pair, :command_runner, System) 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule GitPair.MixFile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :git_pair, 7 | description: "Automatically adds Co-authored-by mark to commits when you're pairing", 8 | version: "0.4.1", 9 | elixir: "~> 1.10", 10 | escript: escript(), 11 | deps: deps(), 12 | package: package(), 13 | 14 | # Docs 15 | name: "GitPair", 16 | source_url: "https://github.com/elixirsc/git-pair", 17 | homepage_url: "https://hex.pm/packages/git-pair/", 18 | docs: docs() 19 | ] 20 | end 21 | 22 | # Run "mix help compile.app" to learn about applications. 23 | def application do 24 | [extra_applications: [:logger]] 25 | end 26 | 27 | # Run "mix help deps" to learn about dependencies. 28 | defp deps do 29 | [ 30 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 31 | {:mox, "~> 0.5.2", only: :test}, 32 | {:ex_doc, ">= 0.0.0", only: :dev} 33 | ] 34 | end 35 | 36 | def escript do 37 | [ 38 | main_module: GitPair.CLI, 39 | name: "git-pair", 40 | path: "git-pair" 41 | ] 42 | end 43 | 44 | defp package do 45 | [ 46 | name: "git_pair", 47 | licenses: ["MIT"], 48 | links: %{"GitHub" => "https://github.com/elixirsc/git-pair"} 49 | ] 50 | end 51 | 52 | defp docs do 53 | [ 54 | main: "GitPair", 55 | extras: ~w(README.md) 56 | ] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, 3 | "ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "cf60e1b3e2efe317095b6bb79651f83a2c1b3edcb4d319c421d7fcda8b3aff26"}, 4 | "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, 6 | "mox": {:hex, :mox, "0.5.2", "55a0a5ba9ccc671518d068c8dddd20eeb436909ea79d1799e2209df7eaa98b6c", [:mix], [], "hexpm", "df4310628cd628ee181df93f50ddfd07be3e5ecc30232d3b6aadf30bdfe6092b"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, 8 | } 9 | -------------------------------------------------------------------------------- /test/actions_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GitPair.ActionsTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Mox 5 | 6 | alias GitPair.Actions 7 | alias GitPair.HookMock 8 | alias GitPair.StorageMock 9 | 10 | setup :verify_on_exit! 11 | 12 | test ".add calls git config add command passing username" do 13 | expect(StorageMock, :add, fn identifier -> 14 | {:ok, 15 | [ 16 | identifier: identifier, 17 | email: "fake_user@users.noreply.github.com" 18 | ]} 19 | end) 20 | 21 | {result, message} = Actions.add(["fake_user"]) 22 | 23 | assert result == :ok 24 | assert message == "User fake_user (fake_user@users.noreply.github.com) added" 25 | end 26 | 27 | test ".add calls git config add command passing identifier and email" do 28 | expect(StorageMock, :add, fn [identifier, email] -> 29 | {:ok, 30 | [ 31 | identifier: identifier, 32 | email: email 33 | ]} 34 | end) 35 | 36 | {result, message} = Actions.add(["fake_user", "fake_user@example.com"]) 37 | 38 | assert result == :ok 39 | assert message == "User fake_user (fake_user@example.com) added" 40 | end 41 | 42 | test ".rm removes identity from storage" do 43 | expect(StorageMock, :remove, fn identifier -> 44 | {:ok, 45 | [ 46 | identifier: identifier 47 | ]} 48 | end) 49 | 50 | {result, message} = Actions.rm(["fake-user"]) 51 | 52 | assert result == :ok 53 | assert message == "User fake-user removed" 54 | end 55 | 56 | test ".status prints a list of collaborators when pairing" do 57 | expect(StorageMock, :fetch_all, fn -> 58 | {:ok, 59 | [ 60 | [ 61 | identifier: "fake_user", 62 | email: "fake_user@example.com" 63 | ], 64 | [ 65 | identifier: "fake_user_2", 66 | email: "fake_user_2@example.com" 67 | ] 68 | ]} 69 | end) 70 | 71 | {result, message} = Actions.status() 72 | 73 | assert result == :ok 74 | 75 | assert message == 76 | "Pairing with:\n\nfake_user \nfake_user_2 " 77 | end 78 | 79 | test ".status prints a message when not pairing" do 80 | expect(StorageMock, :fetch_all, fn -> 81 | {:ok, []} 82 | end) 83 | 84 | {result, message} = Actions.status() 85 | 86 | assert result == :ok 87 | 88 | assert message == "You aren't pairing with anyone" 89 | end 90 | 91 | test ".stop stops pairing session by removing coauthors from storage" do 92 | expect(StorageMock, :remove_all, fn -> 93 | {:ok, ["fake_user", "fake_user_2"]} 94 | end) 95 | 96 | {result, message} = Actions.stop() 97 | 98 | assert result == :ok 99 | assert message == "Pairing session stopped!\n\nYou were pairing previously with:\nfake_user\nfake_user_2" 100 | end 101 | 102 | test ".version displays current app version" do 103 | {result, message} = Actions.version() 104 | 105 | assert result == :ok 106 | assert message == "You're using v#{GitPair.version()}" 107 | end 108 | 109 | test "._modify_commit_msg/1 add coauthors to commit message file when pairing" do 110 | expect(StorageMock, :fetch_all, fn -> 111 | {:ok, 112 | [ 113 | [ 114 | identifier: "fake_user", 115 | email: "fake_user@example.com" 116 | ], 117 | [ 118 | identifier: "fake_user_2", 119 | email: "fake_user_2@example.com" 120 | ] 121 | ]} 122 | end) 123 | 124 | expect(HookMock, :modify_commit_msg, fn _path, _coauthors -> 125 | {:ok} 126 | end) 127 | 128 | {result, message} = Actions._modify_commit_msg("/tmp/COMMIT_MSG") 129 | 130 | assert result == :ok 131 | assert message == "Success! Co-authors registered." 132 | end 133 | 134 | test "._modify_commit_msg/1 do nothing to commit message file when not pairing" do 135 | expect(StorageMock, :fetch_all, fn -> 136 | {:ok, []} 137 | end) 138 | 139 | expect(HookMock, :modify_commit_msg, fn _path, _coauthors -> 140 | {:nothing} 141 | end) 142 | 143 | {result} = Actions._modify_commit_msg("/tmp/COMMIT_MSG") 144 | 145 | assert result == :nothing 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /test/storage_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GitPair.StorageTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Mox 5 | 6 | alias GitPair.Storage 7 | alias GitPair.SystemMock 8 | 9 | setup :verify_on_exit! 10 | 11 | describe "add/1" do 12 | test "store user with identifier with GitHub no-reply email" do 13 | command_prefix = ["config", "--add"] 14 | 15 | expect(SystemMock, :cmd, fn _cmd, options -> 16 | assert options == command_prefix ++ ["pair.fake_user.identifier", "fake_user"] 17 | {"", 0} 18 | end) 19 | 20 | expect(SystemMock, :cmd, fn _cmd, options -> 21 | assert options == 22 | command_prefix ++ ["pair.fake_user.email", "fake_user@users.noreply.github.com"] 23 | 24 | {"", 0} 25 | end) 26 | 27 | {result, user_data} = Storage.add(["fake_user"]) 28 | 29 | assert result == :ok 30 | 31 | assert user_data == [ 32 | identifier: "fake_user", 33 | email: "fake_user@users.noreply.github.com" 34 | ] 35 | end 36 | end 37 | 38 | describe "add/2" do 39 | test "stores user with identifier and email" do 40 | command_prefix = ["config", "--add"] 41 | 42 | expect(SystemMock, :cmd, fn _cmd, options -> 43 | assert options == command_prefix ++ ["pair.fake_user.identifier", "fake_user"] 44 | {"", 0} 45 | end) 46 | 47 | expect(SystemMock, :cmd, fn _cmd, options -> 48 | assert options == command_prefix ++ ["pair.fake_user.email", "fake@example.com"] 49 | {"", 0} 50 | end) 51 | 52 | {result, user_data} = Storage.add(["fake_user", "fake@example.com"]) 53 | 54 | assert result == :ok 55 | 56 | assert user_data == [ 57 | identifier: "fake_user", 58 | email: "fake@example.com" 59 | ] 60 | end 61 | end 62 | 63 | test "remove/1 removes user with identifier" do 64 | command_prefix = ["config", "--remove-section"] 65 | 66 | expect(SystemMock, :cmd, fn _cmd, options -> 67 | assert options == command_prefix ++ ["pair.fake_user"] 68 | {"", 0} 69 | end) 70 | 71 | {result, user_data} = Storage.remove("fake_user") 72 | 73 | assert result == :ok 74 | 75 | assert user_data == [ 76 | identifier: "fake_user" 77 | ] 78 | end 79 | 80 | test "remove/1 fails to remove nonexisting user with identifier" do 81 | command_prefix = ["config", "--remove-section"] 82 | 83 | expect(SystemMock, :cmd, fn _cmd, options -> 84 | assert options == command_prefix ++ ["pair.fake_user"] 85 | {"", 128} 86 | end) 87 | 88 | {result, user_data} = Storage.remove("fake_user") 89 | 90 | assert result == :error 91 | 92 | assert user_data == [ 93 | identifier: "fake_user" 94 | ] 95 | end 96 | 97 | test "remove_all/0 removes all coauthors" do 98 | fetch_all_command_prefix = ["config", "--get-regexp", "pair.*.identifier"] 99 | 100 | expect(SystemMock, :cmd, fn _cmd, options -> 101 | assert options == fetch_all_command_prefix 102 | 103 | {"pair.fake_user.identifier fake_user\npair.fake_user_2.identifier fake_user_2\n", 0} 104 | end) 105 | 106 | fetch_command_prefix = ["config", "--get"] 107 | 108 | expect(SystemMock, :cmd, fn _cmd, options -> 109 | assert options == fetch_command_prefix ++ ["pair.fake_user.email"] 110 | 111 | {"fake_user@example.com\n", 0} 112 | end) 113 | 114 | expect(SystemMock, :cmd, fn _cmd, options -> 115 | assert options == fetch_command_prefix ++ ["pair.fake_user_2.email"] 116 | 117 | {"fake_user_2@example.com\n", 0} 118 | end) 119 | 120 | command_prefix = ["config", "--remove-section"] 121 | 122 | expect(SystemMock, :cmd, fn _cmd, options -> 123 | assert options == command_prefix ++ ["pair.fake_user"] 124 | {"", 0} 125 | end) 126 | 127 | expect(SystemMock, :cmd, fn _cmd, options -> 128 | assert options == command_prefix ++ ["pair.fake_user_2"] 129 | {"", 0} 130 | end) 131 | 132 | {result, coauthor_identifiers} = Storage.remove_all() 133 | 134 | assert result == :ok 135 | 136 | assert coauthor_identifiers == ["fake_user", "fake_user_2"] 137 | end 138 | 139 | test "fetch/1 returns pair information with identifier and email" do 140 | command_prefix = ["config", "--get"] 141 | 142 | expect(SystemMock, :cmd, fn _cmd, options -> 143 | assert options == command_prefix ++ ["pair.fake_user.email"] 144 | {"fake_user@example.com\n", 0} 145 | end) 146 | 147 | {result, user_data} = Storage.fetch("fake_user") 148 | 149 | assert result == :ok 150 | 151 | assert user_data == [ 152 | identifier: "fake_user", 153 | email: "fake_user@example.com" 154 | ] 155 | end 156 | 157 | test "fetch_all/0 returns a list of collaborators" do 158 | fetch_all_command_prefix = ["config", "--get-regexp", "pair.*.identifier"] 159 | 160 | expect(SystemMock, :cmd, fn _cmd, options -> 161 | assert options == fetch_all_command_prefix 162 | 163 | {"pair.fake_user.identifier fake_user\npair.fake_user_2.identifier fake_user_2\n", 0} 164 | end) 165 | 166 | fetch_command_prefix = ["config", "--get"] 167 | 168 | expect(SystemMock, :cmd, fn _cmd, options -> 169 | assert options == fetch_command_prefix ++ ["pair.fake_user.email"] 170 | 171 | {"fake_user@example.com\n", 0} 172 | end) 173 | 174 | expect(SystemMock, :cmd, fn _cmd, options -> 175 | assert options == fetch_command_prefix ++ ["pair.fake_user_2.email"] 176 | 177 | {"fake_user_2@example.com\n", 0} 178 | end) 179 | 180 | {result, collaborators} = Storage.fetch_all() 181 | 182 | assert result == :ok 183 | 184 | assert collaborators == [ 185 | [ 186 | identifier: "fake_user", 187 | email: "fake_user@example.com" 188 | ], 189 | [ 190 | identifier: "fake_user_2", 191 | email: "fake_user_2@example.com" 192 | ] 193 | ] 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Mox.defmock(GitPair.SystemMock, for: GitPair.SystemBehaviour) 2 | Mox.defmock(GitPair.StorageMock, for: GitPair.StorageBehaviour) 3 | Mox.defmock(GitPair.HookMock, for: GitPair.HookBehaviour) 4 | 5 | ExUnit.start() 6 | --------------------------------------------------------------------------------