├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── sailr.json ├── sailr.sh ├── scripts ├── install.sh ├── new_version.sh ├── run_functional_tests.sh └── run_unit_tests.sh └── tests └── test-install.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Enda Phelan 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: install 3 | install: 4 | git config --global init.templatedir '~/.git-templates' 5 | mkdir -p ~/.git-templates/hooks 6 | ln -sf $$PWD/sailr.sh $$HOME/.git-templates/hooks/commit-msg 7 | mkdir -p $$HOME/.sailr 8 | 9 | .PHONY: uninstall 10 | uninstall: 11 | rm -rf $$HOME/.git-templates/hooks/commit-msg 12 | 13 | .PHONY: update 14 | update: 15 | git pull 16 | 17 | .PHONY: test 18 | test: 19 | ./scripts/run_unit_tests.sh 20 | ./scripts/run_functional_tests.sh $$HOME/.git-templates/hooks/commit-msg 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://www.codetriage.com/craicoverflow/sailr/badges/users.svg) 2 | 3 | # Sailr 4 | 5 | Sailr helps you follow the [Conventional Commits](https://www.conventionalcommits.org) conventional by installing a configurable `commit-msg` into your Git projects. 6 | 7 | ## Table of Contents 8 | + [About](#about) 9 | + [Getting Started](#getting_started) 10 | + [Usage](#usage) 11 | 12 | ### Prerequisites 13 | 14 | To use Sailr, you must have [jq](https://stedolan.github.io/jq/download/) installed. 15 | 16 | ### Installing 17 | 18 | ```sh 19 | curl -o- https://raw.githubusercontent.com/craicoverflow/sailr/master/scripts/install.sh | bash 20 | ``` 21 | 22 | ### Uninstalling 23 | 24 | Remove the `commit-msg` Git hook from your project: 25 | 26 | ```sh 27 | rm /.git/hooks/commit-msg 28 | ``` 29 | 30 | ## Usage 31 | 32 | Once installed, you must run `git init` in your Git projects to (re)initialize your repository. The hook will look for a configuration file in the following locations (in order): 33 | 34 | 1. The root of your Git project. 35 | 2. `SAILR_CONFIG`: You can set a custom location for your `sailr.json` config by setting the `SAILR_CONFIG` environment variable. Example: `SAILR_CONFIG=$HOME/.sailr/sailr.json`. 36 | 37 | ```json 38 | { 39 | "enabled": true, 40 | "revert": true, 41 | "length": { 42 | "min": 1, 43 | "max": 52 44 | }, 45 | "types": [ 46 | "build", 47 | "ci", 48 | "docs", 49 | "feat", 50 | "fix", 51 | "perf", 52 | "refactor", 53 | "style", 54 | "test", 55 | "chore" 56 | ] 57 | } 58 | ``` 59 | 60 | **Note**: you can disable Sailr in your project by setting `enabled` to `false` in `sailr.json`. 61 | 62 | ### Unit Testing 63 | 64 | Unit tests are written in Python. They are located in `./tests` 65 | 66 | To run all tests: 67 | 68 | ```sh 69 | make test 70 | ``` -------------------------------------------------------------------------------- /sailr.json: -------------------------------------------------------------------------------- 1 | { 2 | "enabled": true, 3 | "revert": true, 4 | "length": { 5 | "min": 10, 6 | "max": 52 7 | }, 8 | "types": [ 9 | "build", 10 | "chore", 11 | "ci", 12 | "docs", 13 | "feat", 14 | "fix", 15 | "perf", 16 | "refactor", 17 | "style", 18 | "test" 19 | ] 20 | } -------------------------------------------------------------------------------- /sailr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | LIGHT_GRAY="\033[0;37m" 4 | YELLOW="\033[33m" 5 | CYAN="\033[36m" 6 | RED="\033[31m" 7 | UNDO_COLOR="\033[0m" 8 | 9 | release_tag=master 10 | sailr_repo="https://github.com/craicoverflow/sailr/tree/$release_tag" 11 | 12 | # checks that jq is usable 13 | check_jq_exists_and_executable() { 14 | if ! [ -x "$(command -v jq)" ]; then 15 | echo "\`commit-msg\` hook failed. Please install jq." 16 | exit 1 17 | fi 18 | } 19 | 20 | # check if the config file exists 21 | # if it doesnt we dont need to run the hook 22 | check_sailr_config() { 23 | if [[ ! -f "$CONFIG" ]]; then 24 | echo "Sailr config file is missing. To set one see $sailr_repo#usage" 25 | exit 0 26 | fi 27 | } 28 | 29 | set_config() { 30 | local_config="$PWD/sailr.json" 31 | 32 | if [ -f "$local_config" ]; then 33 | CONFIG=$local_config 34 | elif [ -n "$SAILR_CONFIG" ]; then 35 | CONFIG=$SAILR_CONFIG 36 | fi 37 | } 38 | 39 | # set values from config file to variables 40 | set_config_values() { 41 | enabled=$(jq -r .enabled "$CONFIG") 42 | 43 | if [[ ! $enabled ]]; then 44 | exit 0 45 | fi 46 | 47 | revert=$(jq -r .revert "$CONFIG") 48 | types=($(jq -r '.types[]' "$CONFIG")) 49 | min_length=$(jq -r .length.min "$CONFIG") 50 | max_length=$(jq -r .length.max "$CONFIG") 51 | } 52 | 53 | # build the regex pattern based on the config file 54 | build_regex() { 55 | set_config_values 56 | 57 | regexp="^[.0-9]+$|" 58 | 59 | if $revert; then 60 | regexp="${regexp}^([Rr]evert|[Mm]erge):? )?.*$|^(" 61 | fi 62 | 63 | for type in "${types[@]}" 64 | do 65 | regexp="${regexp}$type|" 66 | done 67 | 68 | regexp="${regexp%|})(\(.+\))?: " 69 | 70 | regexp="${regexp}.{$min_length,$max_length}$" 71 | } 72 | 73 | print_error() { 74 | echo "${RED}[Invalid Commit Message]${UNDO_COLOR}" 75 | echo "------------------------" 76 | } 77 | 78 | set_config 79 | 80 | # check if the repo has a sailr config file 81 | check_sailr_config 82 | 83 | # make sure jq is installed 84 | check_jq_exists_and_executable 85 | 86 | # get the first line of the commit message 87 | INPUT_FILE=$1 88 | commit_message=`head -n1 $INPUT_FILE` 89 | 90 | build_regex 91 | 92 | commit_msg_len=${#commit_message} 93 | if [[ $commit_msg_len -lt $min_length || $commit_msg_len -gt $max_length ]]; then 94 | print_error 95 | echo "${LIGHT_GRAY}Expected length: Min=${CYAN}$min_length${UNDO_COLOR} Max=${CYAN}$max_length${UNDO_COLOR}" 96 | echo "Actual length: ${YELLOW}${commit_msg_len}${UNDO_COLOR}" 97 | exit 1 98 | fi 99 | 100 | if [[ ! $commit_message =~ $regexp ]]; then 101 | print_error 102 | echo "${LIGHT_GRAY}Expected prefixes: ${CYAN}${types[@]}${UNDO_COLOR}" 103 | # echo "${LIGHT_GRAY}Expected Regex: ${CYAN}$regexp${UNDO_COLOR}" 104 | echo "Actual commit message: ${YELLOW}\"$commit_message\"${UNDO_COLOR}" 105 | exit 1 106 | fi 107 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | repo_url="https://github.com/craicoverflow/sailr" 4 | release_tag=master 5 | script_file="https://raw.githubusercontent.com/craicoverflow/sailr/$release_tag/sailr.sh" 6 | 7 | function init { 8 | if [ ! -z $1 ]; then 9 | echo "Unsupported argument '$1'" 10 | fi 11 | 12 | destination="${PWD}/.git/hooks" 13 | 14 | download_status=$(curl $script_file -o "${destination}/commit-msg") 15 | chmod u+x "${destination}/commit-msg" 16 | 17 | echo -e "\nInstalled Sailr as \e[33mcommit-msg\033[0m hook in \e[32m$destination\033[0m." 18 | echo "For usage see https://github.com/craicoverflow/sailr#usage" 19 | } 20 | 21 | init $1 22 | -------------------------------------------------------------------------------- /scripts/new_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | find $PWD -type f \( ! -iname "new_version.sh" \) | xargs sed -i -e "s|master|$1|g" *.* -------------------------------------------------------------------------------- /scripts/run_functional_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COMMIT_MSG_SCRIPT=$1 4 | 5 | # build ci docs feat fix perf refactor style test chore 6 | allowed_commit_messages=("build: change build details" \ 7 | "ci: make continuous integration change" \ 8 | "docs: updates some documentation" \ 9 | "feat: implements a new feature" \ 10 | "fix: correct typo in function" \ 11 | "perf: improve perf" \ 12 | "refactor: implement a refactor" \ 13 | "style: make a style change" \ 14 | "test(ui): add a new test" \ 15 | "chore: complete a simple chore" \ 16 | "Revert \"test: add a new test\" " \ 17 | "Revert: \"refactor(labels): implement a refactor\" " \ 18 | "revert: \"feat: implements a new feature\" " \ 19 | "Merge branch 'feature-branch'" \ 20 | "merge branch 'Feature-Branch'" \ 21 | "0.1.0") 22 | 23 | disallowed_commit_messages=("implements a new feature" \ 24 | "" \ 25 | "docs: implements a new feature that we have been greatly anticipating") 26 | 27 | TMPFILE=$(mktemp) 28 | ERROR_FILE=$(mktemp) 29 | COUNT=0 30 | for allowed_commit_message in "${allowed_commit_messages[@]}" 31 | do 32 | echo "$allowed_commit_message" > $TMPFILE 33 | $COMMIT_MSG_SCRIPT $TMPFILE 34 | # Track the number of failed tests 35 | if [[ "$?" != "0" ]] 36 | then 37 | COUNT=$(echo "$COUNT + 1" | bc -l) 38 | echo "---" 39 | fi 40 | done 41 | 42 | for disallowed_commit_message in "${disallowed_commit_messages[@]}" 43 | do 44 | echo "$disallowed_commit_message" > $TMPFILE 45 | # Since it passed, there is no error to be seen. 46 | $COMMIT_MSG_SCRIPT $TMPFILE >> $TMPFILE 47 | # Track the number of failed tests 48 | if [[ "$?" == "0" ]] 49 | then 50 | echo -e "\e[1;32mFailed: $disallowed_commit_message\003\e[0m" >> $ERROR_FILE 51 | COUNT=$(echo "$COUNT + 1" | bc -l) 52 | fi 53 | done 54 | 55 | # Print summary of the number of failed tests 56 | cat $ERROR_FILE 57 | if [[ $COUNT -eq 0 ]] 58 | then 59 | echo -e "\e[1;32m All tests passed!\033\e[0m" 60 | elif [[ $COUNT -eq 1 ]] 61 | then 62 | echo -e "\e[1;32m $COUNT test failed\033\e[0m" 63 | elif [[ $COUNT -gt 1 ]] 64 | then 65 | echo -e "\e[1;32m $COUNT tests failed\033\e[0m" 66 | fi 67 | 68 | # Cleanup 69 | rm $TMPFILE 70 | rm $ERROR_FILE 71 | -------------------------------------------------------------------------------- /scripts/run_unit_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | tests=(./tests/*.py) 4 | 5 | for ((i=0; i<${#tests[@]}; i++)); do 6 | python "${tests[$i]}" 7 | done 8 | -------------------------------------------------------------------------------- /tests/test-install.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | 5 | class TestInstallation(unittest.TestCase): 6 | 7 | os.chdir(os.environ["HOME"]) 8 | cwd = os.getcwd() 9 | 10 | def test_git_template(self): 11 | self.assertTrue( 12 | os.path.isdir(os.path.join(self.cwd, ".git-templates")), 13 | ".git directory not found", 14 | ) 15 | self.assertTrue( 16 | os.path.isdir(os.path.join(self.cwd, ".git-templates/hooks")), 17 | ".git directory not found", 18 | ) 19 | 20 | self.assertTrue( 21 | os.path.isfile(os.path.join(self.cwd, ".git-templates/hooks/commit-msg")) 22 | ) 23 | 24 | 25 | if __name__ == "__main__": 26 | unittest.main() 27 | --------------------------------------------------------------------------------