pre-commit.sh (5891B)
1 #!/bin/sh 2 # 3 # Copyright (c) Microsoft Corporation. 4 # Licensed under the MIT License. 5 # 6 # A hook script to verify what is about to be committed. 7 # Called by "git commit" with no arguments. The hook should 8 # exit with non-zero status after issuing an appropriate message if 9 # it wants to stop the commit. 10 # To avoid having this script called, use "git commit -n" 11 12 # Get files that need to be checked against clang-format into an array 13 14 # To understand the regex format in bash, take a look at https://tldp.org/LDP/abs/html/x17129.html 15 # For example, this is how the regex (\.cpp|\.h)$ matches the example file client/include/SFSClient.h: 16 # (\.cpp|\.h): Match either .cpp or .h. The . character is escaped because it has a special meaning in regex. 17 # $: Match end of string 18 pattern_to_include_in_clang_format='(\.cpp|\.h)$' 19 pattern_to_include_in_cmake_format='CMakeLists.txt' 20 21 # Get the names of staged files (--cached) that are not deleted (--diff-filter=d) 22 all_staged_files=$(git diff --cached --name-only --diff-filter=d) 23 mapfile -t filtered_files <<<"$all_staged_files" 24 25 declare -A files_with_unstaged_changes 26 27 clang_format_files=() 28 cmake_format_files=() 29 30 # Only those that match a pattern will be analyzed 31 max_index=${#filtered_files[@]} 32 for ((i = 0; i < max_index; i++)); do 33 if [[ "${filtered_files[$i]}" =~ $pattern_to_include_in_clang_format || "${filtered_files[$i]}" =~ $pattern_to_include_in_cmake_format ]]; then 34 if [[ "${filtered_files[$i]}" =~ $pattern_to_include_in_clang_format ]]; then 35 clang_format_files+=("${filtered_files[$i]}") 36 elif [[ "${filtered_files[$i]}" =~ $pattern_to_include_in_cmake_format ]]; then 37 cmake_format_files+=("${filtered_files[$i]}") 38 fi 39 40 # Collect files that will be analyzed but have unstaged changes, as we don't want to overwrite these changes 41 # `git diff` is empty if a file has no unstaged changes 42 if [[ -n "$(git diff "${filtered_files[$i]}")" ]]; then 43 files_with_unstaged_changes["${filtered_files[$i]}"]=1 44 fi 45 fi 46 done 47 48 # Early stop if no files to check 49 if [[ ${clang_format_files[*]} == "" && ${cmake_format_files[*]} == "" ]]; then 50 exit 0 51 fi 52 53 RED='\033[0;31m' 54 YELLOW='\033[0;33m' 55 NC='\033[0m' # No color 56 57 # 58 # clang-format 59 # 60 61 # Check that clang-format is installed 62 python_dir="$(python -c 'import os,sysconfig;print(sysconfig.get_path("scripts",f"{os.name}_user"))')" 63 clang_format="$python_dir/clang-format.exe" 64 65 if [[ ! -x "$(command -v "$clang_format")" ]]; then 66 echo -e "${RED}There are staged changes that need to be checked by the formatter." 67 echo -e "${RED}clang-format toolset was not found. Re-run scripts\Setup.ps1 to install it.${NC}" 68 exit 1 69 fi 70 71 formatted_files=() 72 unformatted_files=() 73 for file in "${clang_format_files[@]}"; do 74 # Check if file will be modified so we can properly inform the user of modified files 75 # Use `clang-format -n` which does a dry run and outputs a non-empty response if there's something to format 76 # Pass contents through stdin to get only the staged changes. --assume-filename is used to find the closest .clang-format 77 # `git show :<file>` outputs the staged version of a file 78 if [[ -n $(git show ":$file" | "$clang_format" -n --assume-filename "$file" 2>&1) ]]; then 79 80 # If the file has unstaged changes, we can't format it 81 if [[ -v "files_with_unstaged_changes["$file"]" ]] ; then 82 unformatted_files+=("$file") 83 else 84 # Collect file that will be formatted and then actually format in-place 85 formatted_files+=("$file") 86 "$clang_format" -i "$file" 87 fi 88 fi 89 done 90 91 92 # Report modified files at end so we can run both formatters and report all changes at once 93 94 # 95 # cmake-format 96 # 97 cmake_format="$python_dir/cmake-format.exe" 98 99 if [[ ! -x "$(command -v "$cmake_format")" ]]; then 100 echo -e "${RED}There are staged changes that need to be checked by the formatter." 101 echo -e "${RED}cmake-format toolset was not found. Re-run scripts\Setup.ps1 to install it.${NC}" 102 exit 1 103 fi 104 105 tmp_formatted_name="tmp_formatted" 106 107 for file in "${cmake_format_files[@]}"; do 108 # Check if file will be modified so we can properly inform the user of modified files 109 # First create a temporary file with only staged changes since cmake-format does not support stdin 110 git show ":$file" > $tmp_formatted_name 111 # Use `cmake-format --check` which does a dry run and returns 1 if there's something to format 112 if ! "$cmake_format" --check "$tmp_formatted_name" > /dev/null 2>&1; then 113 114 # If the file has unstaged changes, we can't format it 115 if [[ -v "files_with_unstaged_changes["$file"]" ]] ; then 116 unformatted_files+=("$file") 117 else 118 # Collect file that will be formatted and then actually format in-place 119 formatted_files+=("$file") 120 "$cmake_format" -i "$file" 121 fi 122 fi 123 rm $tmp_formatted_name 124 done 125 126 IFS=$'\n' sorted_formatted_files=($(sort <<<"${formatted_files[*]}")) 127 IFS=$'\n' sorted_unformatted_files=($(sort <<<"${unformatted_files[*]}")) 128 unset IFS 129 130 exit_code=0 131 132 # If there were formatted files, we report the changes 133 if [[ ${sorted_formatted_files[*]} != "" ]]; then 134 echo -e "${YELLOW}There are files that have been automatically formatted. Review and stage the changes before committing:" 135 for file in "${sorted_formatted_files[@]}"; do 136 echo -e "M\t$file" 137 done 138 echo -e "${NC}" 139 exit_code=1 140 fi 141 142 # If there were files that could not be formatted, we report them 143 if [[ ${unformatted_files[*]} != "" ]]; then 144 echo -e "${RED}There are files that need to be formatted but have unstaged changes. Either stage the files or stash/discard the unstaged changes:" 145 for file in "${unformatted_files[@]}"; do 146 echo -e "M\t$file" 147 done 148 echo -e "${NC}" 149 exit_code=1 150 fi 151 152 exit $exit_code