├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── asciicast.gif ├── hook.sh ├── test.sh └── test └── test-hook.bats /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Test 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | submodules: recursive 12 | 13 | - uses: addnab/docker-run-action@v3 14 | with: 15 | options: -v ${{ github.workspace }}:/work 16 | image: bash:latest 17 | run: apk add git && /work/test.sh -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw* 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/libs/bats"] 2 | path = test/libs/bats 3 | url = https://github.com/sstephenson/bats.git 4 | [submodule "test/libs/bats-support"] 5 | path = test/libs/bats-support 6 | url = https://github.com/ztombol/bats-support.git 7 | [submodule "test/libs/bats-assert"] 8 | path = test/libs/bats-assert 9 | url = https://github.com/ztombol/bats-assert.git 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tim Perry 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 | # git-confirm [![Build Status](https://github.com/pimterry/git-confirm/workflows/CI/badge.svg)](https://github.com/pimterry/git-confirm/actions) 2 | 3 | Git hook to catch placeholders and temporary changes (TODO / @ignore) before you commit them. 4 | 5 | [![Asciicast DEMO](asciicast.gif)](https://asciinema.org/a/dc7dr433caze9f8p65bitqs77?speed=2&autoplay=1) 6 | 7 | Git Confirm: 8 | 9 | * Stops you ever accidentally committing bad temporary changes. 10 | * Is interactive, checking each match with you so you can't miss it (and can still include it if you like). 11 | * Only considers lines newly `add`ed and about to be committed, so no false positives. 12 | * Includes (diff-colorized) context with each match 13 | * Installs in any project with a single command 14 | * Is configurable to match any number of strings, through standard git config 15 | * Is well tested. See [tests/test-hook.bats](https://github.com/pimterry/git-confirm/blob/master/test/test-hook.bats#L40-L9999). 16 | * Works on Linux, OSX and Windows ([in Powershell at least](https://twitter.com/afnpires/status/768403583263973376)), with no dependencies. 17 | 18 | ## To Install 19 | In the root of your Git repository, run: 20 | 21 | ```bash 22 | curl -sSfL https://cdn.rawgit.com/pimterry/git-confirm/v0.2.2/hook.sh > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit 23 | ``` 24 | (*Note the version number*) 25 | 26 | All done. If you want to check it's installed correctly you can run: 27 | 28 | ```bash 29 | echo "TODO" > ./test-git-confirm 30 | git add ./test-git-confirm 31 | 32 | # Should prompt you to confirm added 'TODO'. Press 'n' to cancel commit. 33 | git commit -m "Testing git confirm" 34 | ``` 35 | 36 | *If you're security conscious, you may be reasonably suspicious of 37 | [curling executable files](https://www.seancassidy.me/dont-pipe-to-your-shell.html). 38 | Here you're on HTTPS throughout though, and you're not piping directly to execution so you can 39 | check contents and the hash (against MD5 9ee7ff55f7688f9055a9056bd2617a02 for v0.2.2) before using this, if you like.* 40 | 41 | ## To Configure 42 | 43 | By default, git-confirm will catch and warn about lines including 'TODO' only. 44 | 45 | If you want to match a different pattern, you can override this default and set your own patterns: 46 | 47 | ```bash 48 | git config --add hooks.confirm.match "TODO" 49 | ``` 50 | 51 | Matches are passed verbatim to your local `grep`, and are treated as regular expressions. Note that all matches are case-sensitive. 52 | 53 | You can repeatedly add patterns, and each of them will be matched in turn. To get, remove or totally 54 | clear your config, use the standard [Git Config](https://git-scm.com/docs/git-config) commands: 55 | 56 | ```bash 57 | git config --get-all hooks.confirm.match 58 | git config --unset hooks.confirm.match 'TODO' 59 | git config --unset-all hooks.confirm.match 60 | ``` 61 | 62 | ## Contributing 63 | Want to file a bug? That's great! Please search issues first though to check it hasn't already been filed, and provide as much information as you can (your OS, terminal and Git-Confirm version as a minimum). 64 | 65 | Want to help improve Git-Confirm? 66 | 67 | * Check out the project: 68 | `git clone --recursive https://github.com/pimterry/git-confirm.git` 69 | 70 | (Note 'recursive' - this ensures submodules are included) 71 | * Check the tests pass locally: `./test.sh` 72 | * Add tests for your change in test/test-hook.bats 73 | 74 | Check out the [BATS](https://github.com/sstephenson/bats) documentation if you're not familiar with it, or just crib from the existing tests. 75 | * Add any documentation required to this README. 76 | * Commit and push your changes 77 | * Open a PR! 78 | 79 | Need any ideas? Take a look at the Git Confirm [issues](https://github.com/pimterry/git-confirm/issues/) to quickly see the next features to look at. 80 | 81 | ## Release process 82 | 83 | * Make changes 84 | * Update Curl version number and hash (`md5 ./hook.sh`) in README. 85 | * Commit everything 86 | * Tag with new version numbers (`git tag vX.Y.Z`) 87 | * Push including tags (`git push origin --tags`) 88 | -------------------------------------------------------------------------------- /asciicast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimterry/git-confirm/48494e2df13602c7dc33a91e44a4bb50f99cb6c8/asciicast.gif -------------------------------------------------------------------------------- /hook.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # If we have a STDIN, use it, otherwise get one 4 | if tty >/dev/null 2>&1; then 5 | TTY=$(tty) 6 | else 7 | TTY=/dev/tty 8 | fi 9 | 10 | IFS=$'\n' 11 | 12 | # http://djm.me/ask 13 | ask() { 14 | while true; do 15 | 16 | if [ "${2:-}" = "Y" ]; then 17 | prompt="Y/n" 18 | default=Y 19 | elif [ "${2:-}" = "N" ]; then 20 | prompt="y/N" 21 | default=N 22 | else 23 | prompt="y/n" 24 | default= 25 | fi 26 | 27 | # Ask the question (not using "read -p" as it uses stderr not stdout) 28 | echo -n "$1 [$prompt] " 29 | 30 | # Read the answer 31 | read REPLY < "$TTY" 32 | 33 | # Default? 34 | if [ -z "$REPLY" ]; then 35 | REPLY=$default 36 | fi 37 | 38 | # Check if the reply is valid 39 | case "$REPLY" in 40 | Y*|y*) return 0 ;; 41 | N*|n*) return 1 ;; 42 | esac 43 | 44 | done 45 | } 46 | 47 | check_file() { 48 | local file=$1 49 | local match_pattern=$2 50 | 51 | local file_changes_with_context=$(git diff -U999999999 -p --cached --color=always -- $file) 52 | 53 | # From the diff, get the green lines starting with '+' and including '$match_pattern' 54 | local matched_additions=$(echo "$file_changes_with_context" | grep -C4 $'^\e\\[32m\+.*'"$match_pattern") 55 | 56 | if [ -n "$matched_additions" ]; then 57 | echo -e "\n$file additions match '$match_pattern':\n" 58 | 59 | for matched_line in $matched_additions 60 | do 61 | echo "$matched_line" 62 | done 63 | 64 | if ask "Include this in your commit?"; then 65 | echo 'Including' 66 | else 67 | echo "Not committing, because $file matches $match_pattern" 68 | exit 1 69 | fi 70 | fi 71 | } 72 | 73 | # Actual hook logic: 74 | 75 | MATCH=$(git config --get-all hooks.confirm.match) 76 | if [ -z "$MATCH" ]; then 77 | echo "Git-Confirm: hooks.confirm.match not set, defaulting to 'TODO'" 78 | echo 'Add matches with `git config --add hooks.confirm.match "string-to-match"`' 79 | MATCH='TODO' 80 | fi 81 | 82 | for file in `git diff --cached -p --name-status | cut -c3-`; do 83 | for match_pattern in $MATCH 84 | do 85 | check_file $file $match_pattern 86 | done 87 | done 88 | exit 89 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run this file to run all the tests in test/*.bats 4 | 5 | parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 6 | cd "$parent_path" 7 | ./test/libs/bats/bin/bats test/*.bats -------------------------------------------------------------------------------- /test/test-hook.bats: -------------------------------------------------------------------------------- 1 | #!./libs/bats/bin/bats 2 | 3 | load 'libs/bats-support/load' 4 | load 'libs/bats-assert/load' 5 | 6 | # Set up stubs for faking TTY input 7 | export FAKE_TTY="$BATS_TMPDIR/fake_tty" 8 | function tty() { echo $FAKE_TTY; } 9 | export -f tty 10 | 11 | # Remember where the hook is 12 | BASE_DIR=$(dirname $BATS_TEST_DIRNAME) 13 | # Set up a directory for our git repo 14 | TMP_DIRECTORY=$(mktemp -d) 15 | 16 | setup() { 17 | # Clear initial TTY input 18 | echo "" > $FAKE_TTY 19 | 20 | # Set up a git repo 21 | cd $TMP_DIRECTORY 22 | git init 23 | git config user.email "test@git-confirm" 24 | git config user.name "Git Confirm Tests" 25 | cp "$BASE_DIR/hook.sh" ./.git/hooks/pre-commit 26 | } 27 | 28 | teardown() { 29 | if [ $BATS_TEST_COMPLETED ]; then 30 | echo "Deleting $TMP_DIRECTORY" 31 | rm -rf $TMP_DIRECTORY 32 | else 33 | echo "** Did not delete $TMP_DIRECTORY, as test failed **" 34 | fi 35 | 36 | cd $BATS_TEST_DIRNAME 37 | } 38 | 39 | @test "Should let you make normal all-good commits" { 40 | echo "Some content" > my_file 41 | git add my_file 42 | run git commit -m "Content" 43 | 44 | assert_success 45 | refute_line --partial "my_file additions match 'TODO'" 46 | } 47 | 48 | @test "Should reject commits containing a TODO if the user rejects the prompt" { 49 | echo "TODO - Add more content" > my_file 50 | git add my_file 51 | 52 | echo "n" > $FAKE_TTY 53 | run git commit -m "Commit with TODO" 54 | 55 | assert_failure 56 | assert_line --partial "my_file additions match 'TODO'" 57 | } 58 | 59 | @test "Should accept commits containing a TODO if the user accepts the prompt" { 60 | echo "TODO - Add more content" > my_file 61 | git add my_file 62 | 63 | echo "y" > $FAKE_TTY 64 | run git commit -m "Commit with TODO" 65 | 66 | assert_success 67 | assert_line --partial "my_file additions match 'TODO'" 68 | } 69 | 70 | @test "Should includes changed line numbers in message" { 71 | skip # TODO - Disabled, whilst broken by move to git diffing. 72 | 73 | cat << EOF > file_to_commit 74 | start 75 | TODO 76 | end 77 | EOF 78 | git add file_to_commit 79 | 80 | echo "y" > $FAKE_TTY 81 | run git commit -m "Commit with TODO" 82 | 83 | assert_success 84 | assert_line --partial "2:TODO" 85 | } 86 | 87 | @test "Should includes only the changed line + context in message" { 88 | cat << EOF > file_to_commit 89 | File start 90 | . 91 | . 92 | . 93 | . 94 | . 95 | . 96 | . 97 | line before 98 | TODO - add things 99 | line after 100 | EOF 101 | git add file_to_commit 102 | 103 | echo "y" > $FAKE_TTY 104 | run git commit -m "Commit with TODO" 105 | 106 | refute_line --partial "File start" 107 | assert_line --partial "line before" 108 | assert_line --partial "TODO - add things" 109 | assert_line --partial "line after" 110 | } 111 | 112 | @test "Doesn't warn for all-good changes to files that already have a TODO" { 113 | echo "TODO" > my_file 114 | echo "More Content" >> my_file 115 | git add my_file 116 | git commit -m "Commit with a TODO" --no-verify 117 | 118 | echo "More all-good content" >> my_file 119 | git add my_file 120 | 121 | run git commit -m "Commit with no new TODOs" 122 | assert_success 123 | } 124 | 125 | @test "Doesn't warn for changes that haven't been added to the commit" { 126 | echo "Content to commit" > my_file 127 | git add my_file 128 | 129 | echo "Content un-added, with TODO" >> my_file 130 | 131 | run git commit -m "Commit with TODO un-added" 132 | assert_success 133 | } 134 | 135 | @test "Complains if hooks.confirm.match is not set" { 136 | run git commit --allow-empty -m "Empty commit" 137 | 138 | assert_success 139 | assert_line --partial "hooks.confirm.match not set" 140 | } 141 | 142 | @test "Doesn't complain if hooks.confirm.match is set" { 143 | git config --add hooks.confirm.match "FIXME" 144 | run git commit --allow-empty -m "Empty commit" 145 | 146 | assert_success 147 | refute_line --partial "hooks.confirm.match not set" 148 | } 149 | 150 | @test "Uses different match from hooks.confirm.match if set" { 151 | git config --add hooks.confirm.match "FIXME" 152 | 153 | echo "FIXME" > my_file 154 | git add my_file 155 | echo "n" > $FAKE_TTY 156 | run git commit --allow-empty -m "Commit including fix me" 157 | 158 | assert_failure 159 | assert_line --partial "my_file additions match 'FIXME'" 160 | } 161 | 162 | @test "Matches against multiple configured matchers" { 163 | git config --add hooks.confirm.match "FIXME" 164 | git config --add hooks.confirm.match "@ignore" 165 | 166 | echo "FIXME" > fixme_file 167 | echo "@ignore" > ignored_test_file 168 | git add fixme_file ignored_test_file 169 | 170 | echo "y" > $FAKE_TTY 171 | run git commit -m "Add two bad matches" 172 | 173 | assert_line --partial "fixme_file additions match 'FIXME'" 174 | assert_line --partial "ignored_test_file additions match '@ignore'" 175 | } 176 | 177 | @test "Support regex matches" { 178 | git config --add hooks.confirm.match "hello.*world" 179 | 180 | echo "hello crazy world" > my_code 181 | git add my_code 182 | 183 | echo "n" > $FAKE_TTY 184 | run git commit -m "Commit code matching regex" 185 | 186 | assert_failure 187 | assert_line --partial "my_code additions match 'hello.*world'" 188 | } 189 | --------------------------------------------------------------------------------