Last active
January 12, 2024 20:43
-
-
Save zebreus/5480ed0ce728e49cce2db2523c6fc52b to your computer and use it in GitHub Desktop.
Bash pre-commit hook for the incremental adoption of code-formatting in large software projects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# # Overview | |
# The goal of the enforce-style script is to make sure that submitters run clang-format before submitting a patch. It does not enforce styling for existing manually formatted files, but will enforce that NEW/MANAGED files that are staged when committing are formatted with clang-format. This way we can incrementally adopt clang-format. | |
# | |
# # Managed files | |
# Managed files are those whose styling is managed by this script. This means that the styling is enforced for these files. A precommit-hook will automatically format these files and stage the result. Gerrit will also automatically format these files and stage the result. | |
# | |
# # Incremental adoption | |
# This script is designed for incremental adoption of styling. It achieves this by differentiating between managed (styling enforced) and ignored (styling not enforced) files. Initially all existing files were added to the list of ignored files. When a staged file matches the clang-format style, it is automatically removed from the list of ignored files and added as a managed file and styling will be enforced in the future. New files are managed by default. | |
# | |
# If you want to permanently want to disable styling for a whole directory, you can add it to the `.enforce-style-ignore` file. You can find more details on ignored files in the section below. Ignored files are not automatically formatted, nor is the styling enforced for them. For managed files the styling is enforced in a pre-commit hook and in gerrit. | |
# | |
# # Style guidelines | |
# The style guidelines are defined in `.clang-format`. The file tries to define a style that follows | |
# the established coreboot style. There are going to be | |
# some trade-offs in any code formatter. If you find a case where the formatter | |
# does not do what you want, you can exclude that section of a file as shown below. You should try to avoid this as much as possible and always add a comment explaining why you are excluding that section from formatting. | |
# | |
# Example: | |
# ``` | |
# // Explain why this region is excluded from formatting here. | |
# // clang-format off | |
# ... code that should not be formatted ... | |
# // clang-format on | |
# ``` | |
# | |
# # Ignored files | |
# Files can be ignored by adding them to `.enforce-style-ignore`. The format of | |
# this file is the same as the [`.gitignore` file](https://git-scm.com/docs/gitignore#_pattern_format). The script will | |
# automatically ignore all files that are matched by `.enforce-style-ignore`. | |
# | |
# Example: | |
# ``` | |
# # Ignore all files in the src/drivers/amd directory | |
# /src/drivers/amd | |
# ``` | |
# | |
# You can specify a custom ignore file location by setting the `IGNORE_FILE` variable. Files that are ignored by the regular `.gitignore` files are also ignored by enforce-style. | |
# | |
# If clang-format is not installed and ignored files have changed, enforce-style will only print a warning that you should install clang-format. Nothing else will happen. | |
# | |
# If clang-format is installed, enforce-style will print warnings for ignored files that are not formatted according to the style guidelines and suggest a patch that would format them correctly. | |
# Also if a ignored staged file matches the style guidelines, it is automatically removed from the list of ignored files and the updated list will be staged. | |
# | |
# # Managed files | |
# Styling is enforced for all managed files. | |
# | |
# Files that are not ignored are managed by enforce-style. When a staged managed file is to be committed, the script will automatically format it and stage the result. It will also format the file in the worktree. If the file in the worktree has unstaged changes, it will be formatted independently of the staged file so not to lose the unstaged changes. If you dont want enforce-style to format the worktree, you can set the `NEVER_MODIFY_WORKTREE` variable to `true`. This way only the staging area will be formatted. | |
# | |
# # Deleting/renaming/adding/modified files | |
# A staged file is either deleted, modified, added, or renamed. Only one. | |
# If a file is deleted it will be removed from the ignore list. | |
# If a file is created it will be added to the ignore list, if $IGNORE_NEW_FILES is set to true. | |
# If a file is renamed and on the ignore list, the old one will be removed and the new one will be added. | |
# If a file is renamed and not on the ignore list, the new one will not be added. | |
# If a file is modified it will be processed as usual. | |
# | |
# # Dependencies | |
# Besides some common unix tools (bash, diff, awk, sed, tr) this script only depends on git. However, if you want to commit changes to managed files you also need to have clang-format installed. You can either build it from source by runningn `make CPUS=$(nproc) clang` or by installing it with your systems package manager. | |
# | |
# If you want to develop this script you also need to install shunit2 to run the tests with `RUN_TESTS=true ./enforce-style`. | |
# | |
# # Features | |
# - [x] Has a mechanism to allow manual formatting of legacy files. | |
# - [x] Has a mechanism to make sure formatted files stay formatted. | |
# - [x] Has a mechanism to ignore files. | |
# - [x] Has an entrypoint for a pre-commit hook. | |
# - [ ] Has an entrypoint for gerrit or pre-receive hook. | |
# - [ ] Has an entrypoint for a pre-push hook. | |
# - [x] Support partially staged files. | |
# - [ ] Helpful error, info, and warning messages. | |
# - [x] Suggest patches for legacy files that are not formatted correctly. | |
# - [x] Automatically add files to the managed list if they are formatted correctly. | |
# - [x] Error out if managed files are modified but clang-format is not installed. | |
# - [ ] ~Clean~ Bash code | |
# - [x] Automated tests | |
# - [ ] Finished documentation | |
# - [ ] Has an initial ignore list for all existing files and unformatted directories. | |
# - [x] Dry run / verify mode | |
# - [x] The script focuses on formatting the staging area and not the worktree. | |
# - [x] Defined behaviour for deleting/renaming/adding files | |
# - [x] Makes sure that the script is in a valid environment (root of the repo) | |
# | |
# # Adding more formatters | |
# The script is designed to support more formatters than just clang-format. If you want to add support for a new formatter, you only need to modify the 'formatting primitives' and the 'formatting primitives helpers' sections. You can find more details in those sections. | |
# | |
######## State and configuration variables ######## | |
# State and configuration of this script are tracked in global variables. Configuration variables change the behaviour of the script while the state variables are used internally to track the state of the script. | |
# This function sets unset config variables to their default values. | |
function default_config() { | |
# Path to the clang-format executable. | |
# If this is not set, the script will try to find it automatically. | |
CLANG_FORMAT=${CLANG_FORMAT:-} | |
# If this is set to true, the script will delete old patches. | |
DELETE_OLD_PATCHES=${DELETE_OLD_PATCHES:-"false"} | |
# This file contains a list of files that should not be managed by enforce-style. | |
# | |
# The format of this file is the same as the format of the .gitignore file. | |
# | |
# IGNORE_FILE needs to point to a path relative to the root of the repository. | |
# The script uses the version of the file in the staging area to determine the list of ignored files. | |
IGNORE_FILE=${IGNORE_FILE:-"util/lint/.enforce-style-ignore"} | |
# These are the extensions that will be formatted with clang-format. | |
DEFAULT_CLANG_FORMAT_EXTENSIONS=(.cpp .cc .c\+\+ .cxx .cppm .ccm .cxxm .c\+\+m .c .cl .h .hh .hpp) | |
CLANG_FORMAT_EXTENSIONS=("${CLANG_FORMAT_EXTENSIONS[@]:-${DEFAULT_CLANG_FORMAT_EXTENSIONS[@]}}") | |
# If this is set to true, the script will never modify files in the worktree but only files in the staging area. | |
# | |
# So when commiting your staged changes will be always be formatted but the file in your worktree will not be. | |
NEVER_MODIFY_WORKTREE=${NEVER_MODIFY_WORKTREE:-"false"} | |
# Print errors instead of fixing problems. | |
# | |
# Instead of fixing things, the script will print a errors and exit with a non-zero exit code. | |
# It will also print more info about what the script is doing. | |
# | |
# If this is set to true the script will not modify anything in the worktree or staging area. | |
# | |
# `enforce-style check` just sets this variable to true. | |
VERIFY=${VERIFY:-"false"} | |
# Add newly created files to the list of ignored files. | |
IGNORE_NEW_FILES=${IGNORE_NEW_FILES:-"false"} | |
} | |
# This function resets the config variables to their default values. | |
function reset_config() { | |
unset CLANG_FORMAT | |
unset DELETE_OLD_PATCHES | |
unset IGNORE_FILE | |
unset CLANG_FORMAT_EXTENSIONS | |
unset NEVER_MODIFY_WORKTREE | |
default_config | |
} | |
# This function resets the state and configuration of the script. | |
function reset_state() { | |
# Will be filled with the list of files that have staged changes. | |
unset MODIFIED_FILES | |
unset DELETED_FILES | |
unset CREATED_FILES | |
unset RENAMED_FILES | |
# Will be filled with the list of files that are staged but not managed by enforce-style. | |
unset IGNORED_FILES | |
# Will be filled with the list of files that are staged and managed by enforce-style. | |
# MANAGED_FILES will contain the list of files that are staged and managed by enforce-style. It is the difference between MODIFIED_FILES and IGNORED_FILES. | |
unset MANAGED_FILES | |
# Will be filled with the list of files that are staged and managed by enforce-style but also have unstaged changes. | |
unset PARTIALLY_STAGED_MANAGED_FILES | |
# Will be filled with the list of files that are staged and managed by enforce-style and have no unstaged changes. | |
unset FULLY_STAGED_MANAGED_FILES | |
# Will be set to true if an error occured in VERIFY mode. | |
unset ERROR_OCCURRED | |
# Adjusts what called functions will print | |
# Can be "warnings", "errors", or "none". Everything else will be treated as "none". | |
unset LOGGING_MODE | |
# Will be set to the files that were removed from the ignore file. | |
# Used to print a summary at the end of the script. | |
unset FILES_THAT_WERE_REMOVED_FROM_THE_IGNORE_FILE | |
# Will be set if populate_new_and_deleted_files was called once to prevent it from being called again. | |
unset NEW_AND_DELETED_FILES_POPULATED | |
} | |
######## Formatting primitives ######## | |
# This section function defines the formatting primitives that are used in the rest of the script. | |
# | |
# If you want to add support for a new formatter, you need to modify the functions here. If you need some helper functions, for example to find the formatter, you can add them in the next section after this. | |
# assert that the required tools for the files are installed. | |
# | |
# It is guaranteed that this function is called before any other function in this section for a file. If you need to find the formatter, you can do it here. | |
# | |
# Returns 1 and prints errors if the required tools are not installed. | |
function has_required_tools() { | |
local filenames=("$@") | |
local clang_format_files | |
mapfile -t clang_format_files < <(filter_for_clang_format "${filenames[@]}") | |
if [[ ${#clang_format_files[@]} -ne 0 ]]; then | |
if ! populate_clang_format; then | |
case "$LOGGING_MODE" in | |
fatal | error) | |
print_message "$LOGGING_MODE" "files that require formatting with clang-format were changed, but $CLANG_FORMAT_ERROR." | |
;; | |
*) | |
print_message "warning" "files that could be formatted with clang-format were changed, but $CLANG_FORMAT_ERROR." | |
;; | |
esac | |
print_message "note" "To install clang-format either run 'make CPUS=$(nproc) clang' or install it with your system package manager." | |
ERROR_OCCURRED="true" | |
return 1 | |
fi | |
fi | |
return 0 | |
} | |
# Check if a file is relevant for enforce-style. | |
# | |
# Returns 0 if enforce-style knows how to format the file and 1 otherwise. | |
function is_relevant_file() { | |
check_clang_format "$1" | |
} | |
# Helper function to format a list of files. | |
# | |
# This and format_file_stream are the only functions that actually call clang-format. | |
# | |
# Prints an error/warning to stderr if the required formatter is not available. | |
function format_files() { | |
local files | |
files=("$@") | |
local clang_format_files | |
mapfile -t clang_format_files < <(filter_for_clang_format "${files[@]}") | |
if [[ ${#clang_format_files[@]} -ne 0 ]]; then | |
if verify_clang_format_command; then | |
"$CLANG_FORMAT" --style=file -i "${clang_format_files[@]}" | |
else | |
for file in "${clang_format_files[@]}"; do | |
print_file_message "$file:1" "auto" "file should be clang-formatted, but $CLANG_FORMAT_ERROR." | |
done | |
fi | |
fi | |
} | |
# Helper function to format a single file from stdin and write the result to stdout. | |
# | |
# The first argument is the filename. It is only used to determine the formatter. | |
# | |
# Prints an error/warning to stderr and returns 1 if the required formatter is not available. | |
function format_file_pipe() { | |
local filename | |
filename="$1" | |
if check_clang_format "$filename"; then | |
if verify_clang_format_command; then | |
"$CLANG_FORMAT" --style=file --assume-filename="$filename" - | |
return 0 | |
else | |
print_file_message "$file:1" "auto" "file should be clang-formatted, but $CLANG_FORMAT_ERROR." | |
cat | |
return 1 | |
fi | |
fi | |
# If no formatter matches: Just print the file | |
cat | |
} | |
# Generate warnings for a file from stdin if it is not formatted correctly. | |
# | |
# If LOGGING_MODE is set to error, all formatting warnings will be reported as errors. | |
function analyze_files() { | |
local files | |
files=("$@") | |
local clang_format_files | |
mapfile -t clang_format_files < <(filter_for_clang_format "${files[@]}") | |
if [[ ${#clang_format_files[@]} -ne 0 ]]; then | |
if verify_clang_format_command; then | |
local flags | |
flags=(--dry-run) | |
if [[ "$LOGGING_MODE" = "error" ]]; then | |
flags+=(--Werror) | |
fi | |
"$CLANG_FORMAT" --style=file "${flags[@]}" "${clang_format_files[@]}" | |
else | |
for file in "${clang_format_files[@]}"; do | |
print_file_message "$file:1" "auto" "file should get checked with clang-format, but $CLANG_FORMAT_ERROR." | |
done | |
fi | |
fi | |
return 0 | |
} | |
# Print warnings or errors for a file from stdin if it is not formatted correctly. | |
# | |
# The first argument is the filename. It is only used to determine the file type. | |
# | |
# If LOGGING_MODE is set to error, all formatting warnings will be reported as errors. In this case the function will return 1 if any errors were printed. | |
# | |
# Will return 1 if the formatter is not available. | |
function analyze_file_pipe() { | |
local filename="$1" | |
if check_clang_format "$filename"; then | |
if verify_clang_format_command; then | |
local flags | |
flags=(--dry-run) | |
if [[ "$LOGGING_MODE" = "error" ]]; then | |
flags+=(--Werror) | |
fi | |
"$CLANG_FORMAT" --style=file "${flags[@]}" --assume-filename="$filename" - | |
return $? | |
else | |
print_file_message "$file:1" "auto" "file should get checked with clang-format, but $CLANG_FORMAT_ERROR." | |
return 1 | |
fi | |
fi | |
} | |
######## Formatting primitives helpers ######## | |
# These functions are helper functions for the formatting primitives. You can add more helper functions here if you are adding a new formatter. | |
# Check if a file should be formatted with clang-format. | |
function check_clang_format() { | |
matches_extension "$1" "${CLANG_FORMAT_EXTENSIONS[@]}" | |
} | |
# Verify that the clang-format command is valid. | |
# | |
# In case of an error it returns 1 and the CLANG_FORMAT_ERROR variable is set to a human readable error message. | |
function verify_clang_format_command() { | |
local minimum_version="16.0.0" | |
# Shortcut if the clang-format command is already verified | |
if [[ -n "$VERIFIED_CLANG_FORMAT" ]] && [[ "$CLANG_FORMAT" == "$VERIFIED_CLANG_FORMAT" ]]; then | |
return 0 | |
fi | |
if [[ -z "$CLANG_FORMAT" ]]; then | |
CLANG_FORMAT_ERROR="no clang-format command was given" | |
return 1 | |
fi | |
local version | |
version=$("$CLANG_FORMAT" --version 2>/dev/null | grep -Eo '[0-9]+\.[0-9]+\.[0-9]') | |
if [[ -z "$version" ]]; then | |
CLANG_FORMAT_ERROR="failed to get the version of '$CLANG_FORMAT'" | |
return 1 | |
fi | |
if ! version_greater_or_equal "$minimum_version" "$version"; then | |
CLANG_FORMAT_ERROR="your clang-format version is too old ($version < $minimum_version)" | |
return 1 | |
fi | |
VERIFIED_CLANG_FORMAT="$CLANG_FORMAT" | |
} | |
# Populate CLANG_FORMAT with the path of the clang-format executable. | |
# | |
# If the crossgcc clang-format is found, use that because we know that it is the correct version. Otherwise, use the system clang-format. | |
# | |
# In case of an error it returns 1 and the CLANG_FORMAT_ERROR variable is set to a human readable error message. | |
function populate_clang_format() { | |
if [[ -n "$CLANG_FORMAT" ]]; then | |
verify_clang_format_command | |
return $? | |
fi | |
CLANG_FORMAT="$(pwd)/util/crossgcc/xgcc/bin/clang-format" | |
if verify_clang_format_command; then | |
return 0 | |
fi | |
CLANG_FORMAT=$(command -v clang-format) | |
if check_clang_format; then | |
return 0 | |
fi | |
CLANG_FORMAT_ERROR="you do not have clang-format (at least version 16.0.0) installed. Run 'make CPUS=$(nproc) clang' to build clang-format or install it with your package manager." | |
return 1 | |
} | |
# Filter a list of files for files that should be formatted with clang-format. | |
function filter_for_clang_format() { | |
local files | |
files=("$@") | |
local clang_format_files | |
for file in "${files[@]}"; do | |
if check_clang_format "$file"; then | |
echo "$file" | |
fi | |
done | |
} | |
######## Helper functions ######## | |
# Generic helper functions that are not really specific to this script. | |
# Check if a version is greater or equal than a reference version. | |
function version_greater_or_equal() { | |
local reference="$1" | |
local version="$2" | |
# Equal | |
if [[ "$reference" = "$version" ]]; then | |
return 0 | |
fi | |
# Reference is not the greater version | |
[[ "$reference" != "$(printf "%s\n%s" "$reference" "$version" | sort -Vr | head -n1)" ]] | |
} | |
# Change into the toplevel of the current repo. Fail if we are not in a git repo. | |
function cd_to_repo_root() { | |
local toplevel | |
toplevel=$(git rev-parse --show-toplevel) | |
if [[ $? -ne 0 ]]; then | |
print_message "fatal" "not in a git repository" | |
return 1 | |
fi | |
cd "$toplevel" || return 1 | |
} | |
# Reference: http://stackoverflow.com/questions/1055671/how-can-i-get-the-behavior-of-gnus-readlink-f-on-a-mac | |
function canonicalize_filename() { | |
local target_file=$1 | |
local physical_directory="" | |
local result="" | |
# Need to restore the working directory after work. | |
pushd "$(pwd)" >/dev/null || exit 1 | |
cd "$(dirname "$target_file")" || return 1 | |
target_file=$(basename "$target_file") | |
# Iterate down a (possible) chain of symlinks | |
while [ -L "$target_file" ]; do | |
target_file=$(readlink "$target_file") | |
cd "$(dirname "$target_file")" || return 1 | |
target_file=$(basename "$target_file") | |
done | |
# Compute the canonicalized name by finding the physical path | |
# for the directory we're in and appending the target file. | |
physical_directory=$(pwd -P) | |
result="$physical_directory"/"$target_file" | |
# restore the working directory after work. | |
popd >/dev/null || exit 1 | |
echo "$result" | |
} | |
# check whether the given file matches any of the set extensions | |
function matches_extension() { | |
local filename | |
filename=$(basename "$1") | |
shift | |
local extensions=("$@") | |
local extension=".${filename##*.}" | |
local ext | |
for ext in "${extensions[@]}"; do [[ "$ext" == "$extension" ]] && return 0; done | |
return 1 | |
} | |
# Check if a variable is declared. We need to do it this way, because empty arrays are weird in bash. | |
function is_declared() { | |
declare -p "$1" >/dev/null 2>&1 | |
} | |
######## Git helper functions ######## | |
# Helper functions for interacting with git. | |
# Get the git blob of a staged file. | |
function git_staged_blob() { | |
local file=$1 | |
git ls-files -s "$1" | cut -d" " -f2 | |
} | |
# Get the content of a staged file. | |
# | |
# Returns 1 if the file is not checked in. | |
function git_staged_file_content() { | |
local file=$1 | |
local blob | |
blob=$(git_staged_blob "$file") | |
[[ -z "$blob" ]] && return 1 | |
git show "$blob" | |
} | |
# Get the hash of current HEAD commit. | |
function get_head_commit() { | |
# git rev-parse failst if there is no previous commit. | |
# In that case, return the empty tree hash. | |
git rev-parse --verify HEAD 2>/dev/null || printf "4b825dc642cb6eb9a060e54bf8d69288fbee4904" | |
} | |
# Print a list of files that will be added/modified | |
function get_staged_files() { | |
git diff-index --cached --diff-filter=CM -M9 --name-only "$(get_head_commit)" -- | |
} | |
# Print a list of files that have been deleted | |
function get_deleted_files() { | |
git diff-index --cached --diff-filter=D -M9 --name-only "$(get_head_commit)" -- | |
} | |
# Print a list of files that have been added | |
function get_created_files() { | |
git diff-index --cached --diff-filter=A -M9 --name-only "$(get_head_commit)" -- | |
} | |
# Print a list of files that have been renamed | |
# Every line is like new_name<tab>old_name | |
function get_renamed_files() { | |
git diff-index --cached --diff-filter=R -M9 --raw "$(get_head_commit)" -- | cut -f2,3 | |
} | |
# Test if a staged file also has unstaged changes. | |
# | |
# Returns 0 if the file is staged and has unstaged changes and 1 otherwise. | |
# | |
# Undefined behaviour if the file is not staged at all. | |
function is_only_partially_staged() { | |
local file=$1 | |
test -n "$(git ls-files -m "$file" 2>/dev/null)" | |
} | |
######## Formatting helper functions ######## | |
# Wrappers around the formatting primitives defined in the first section. | |
# Print a diff that transforms the file from stdin into a formatted file. | |
# | |
# Prints nothing if the file is already formatted or if the formatting is not applicable. | |
function diff_against_formatted() { | |
# Path to a file containing the content that is beeing formatted. | |
local content | |
content=$(cat) | |
# Path to a where to file is located in the repository | |
local filename | |
filename="$1" | |
echo "$content" | format_file_pipe "$filename" | | |
diff -u <(echo "$content") - | | |
sed -e "1s|--- .*|--- a/$filename|" -e "2s|+++ .*|+++ b/$filename|" | |
} | |
# Create a patch that formats the supplied staged files in the staging area. | |
# | |
# This patch can be applied with `git apply --cached` to format the staging area. | |
function patch_for_files_in_staging_area() { | |
local files | |
files=("$@") | |
for file in "${files[@]}"; do | |
local blob | |
blob=$(git_staged_blob "$file") | |
# shellcheck disable=SC2002 | |
git show "$blob" | diff_against_formatted "$file" | |
done | |
} | |
# Create a patch that formats the supplied files. | |
# | |
# This patch can be applied with `git apply` to format the worktree. | |
function patch_for_files_in_worktree() { | |
local files | |
files=("$@") | |
for file in "${files[@]}"; do | |
# shellcheck disable=SC2002 | |
cat "$file" | diff_against_formatted "$file" | |
done | |
} | |
# Run clang-format on only in the staging area. | |
# | |
# Does not touch the worktree. | |
function format_files_in_staging_area() { | |
local files | |
files=("$@") | |
populate_clang_format | |
for file in "${files[@]}"; do | |
local unformatted_blob | |
local formatted_blob | |
local permissions | |
# Get the blob of the staged file | |
unformatted_blob=$(git_staged_blob "$file") | |
permissions=$(git ls-files -s "$file" | cut -d" " -f1) | |
# Format the blobs contents and load the result into the git object database | |
formatted_blob=$(git show "$unformatted_blob" | format_file_pipe "$file" | git hash-object -t blob -w --stdin --path "$file") | |
# Update the staging area with the new blob | |
git update-index --cacheinfo "$permissions,$formatted_blob,$file" | |
done | |
} | |
# Run clang-format on only in the staging area. | |
# | |
# Does not modify the worktree or staging area. | |
# | |
# If the first argument is 'error': errors will be printed instead of warnings and 1 will be returned if any errors occured. | |
function analyze_files_in_staging_area() { | |
local files=("$@") | |
populate_clang_format | |
local no_error="true" | |
for file in "${files[@]}"; do | |
local blob | |
# Get the blob of the staged file | |
blob=$(git_staged_blob "$file") | |
git show "$blob" | analyze_file_pipe "$file" | |
if [[ $? -ne 0 && "$LOGGING_MODE" == "error" ]]; then | |
no_error="false" | |
fi | |
done | |
"$no_error" | |
} | |
######## Message printing ######## | |
# This section contains functions that are used to print messages to the user. | |
# Check if stderr supports color | |
function stderr_supports_color() { | |
[[ -t 2 ]] && [[ "$(tput colors 2>/dev/null)" -ge 8 ]] | |
} | |
# Set global variables that are used to print colored messages to stderr. | |
# | |
# The variables are set to the ANSI escape codes for the colors if the terminal supports colors and to empty strings otherwise. | |
function set_color_variables() { | |
REMARK='\e[34m' | |
NOTE='\e[36m' | |
WARNING='\e[35m' | |
ERROR='\e[31m' | |
FATAL='\e[31m' | |
BOLD='\e[1m' | |
RESET='\e[0m' | |
if ! stderr_supports_color; then | |
REMARK='' | |
NOTE='' | |
WARNING='' | |
ERROR='' | |
FATAL='' | |
BOLD='' | |
RESET='' | |
fi | |
} | |
set_color_variables | |
# Print a message that relates to a file to stderr. | |
function print_file_message() { | |
local file="$1" | |
local type="$2" | |
local message="$3" | |
if [[ "$type" = "auto" ]]; then | |
type=${LOGGING_MODE:-"none"} | |
fi | |
if [[ "$LOGGING_MODE" = "none" ]]; then | |
return 0 | |
fi | |
local type_color | |
case "$type" in | |
remark) type_color="$REMARK" ;; | |
note) type_color="$NOTE" ;; | |
warning) type_color="$WARNING" ;; | |
error) type_color="$ERROR" ;; | |
fatal) type_color="$FATAL" ;; | |
*) type_color="$NOTE" ;; | |
esac | |
local message_color | |
case "$type" in | |
warning | error | fatal) message_color="$BOLD" ;; | |
*) message_color="" ;; | |
esac | |
local file_part | |
if [[ -n "$file" ]]; then | |
file_part="$BOLD$file:$RESET " | |
fi | |
echo -e "$file_part$type_color$BOLD$type:$RESET $message_color$message$RESET" >&2 | |
} | |
# Print a message to stderr. | |
function print_message() { | |
print_file_message "" "$@" | |
} | |
######## Analyze repo state ######## | |
# These functions are used to analyze the state of the repository. They are used to populate the state variables. | |
# Set the MODIFIED_FILES variable to a list of files that are staged for commit and relevant for enforce-style. | |
function populate_modified_files() { | |
declare -ga MODIFIED_FILES=() | |
while IFS='' read -r staged_file; do | |
if is_relevant_file "$staged_file"; then | |
MODIFIED_FILES+=("$staged_file") | |
fi | |
done < <(get_staged_files) | |
} | |
# Set the DELETED_FILES variable to a list of files that are staged for commit and relevant for enforce-style. | |
function populate_deleted_files() { | |
DELETED_FILES=() | |
while IFS='' read -r staged_file; do | |
if is_relevant_file "$staged_file"; then | |
DELETED_FILES+=("$staged_file") | |
fi | |
done < <(get_deleted_files) | |
} | |
# Set the CREATED_FILES variable to a list of files that are staged for commit and relevant for enforce-style. | |
function populate_created_files() { | |
CREATED_FILES=() | |
while IFS='' read -r staged_file; do | |
if is_relevant_file "$staged_file"; then | |
CREATED_FILES+=("$staged_file") | |
fi | |
done < <(get_created_files) | |
} | |
# Set the RENAMED_FILES variable to a list of files that are staged for renaming and are relevant for enforce-style. | |
# | |
# Also sets the RENAMED_FILES_MAP variable to a map of old file names to the new file names. | |
function populate_renamed_files() { | |
RENAMED_FILES=() | |
declare -gA RENAMED_FILES_MAP=() | |
while IFS='' read -r staged_file; do | |
local old_name="${staged_file%%$'\t'*}" | |
local new_name="${staged_file#*$'\t'}" | |
if is_relevant_file "$new_name"; then | |
RENAMED_FILES+=("$new_name") | |
RENAMED_FILES_MAP["$old_name"]="$new_name" | |
fi | |
done < <(get_renamed_files) | |
} | |
# Print filter the list of given files for those that are ignored by the ignore file. | |
function filter_for_ignored_files() { | |
local ignore_file="$1" | |
shift | |
local files=("$@") | |
local flags | |
if [[ -n "$ignore_file" ]]; then | |
flags=("-c" "core.excludesFile=$ignore_file") | |
fi | |
git "${flags[@]}" check-ignore --no-index "${files[@]}" | |
} | |
# Copy the staged ignore file to a temporary file and return its path. | |
function get_staged_ignores_as_file() { | |
# Patch can be a perl expression that can be used to modify the ignore file | |
local patch | |
patch="${1:-""}" | |
if [[ -z "$IGNORE_FILE" ]]; then | |
# No ignore file | |
return 1 | |
fi | |
local staged_ignore_file_blob | |
staged_ignore_file_blob=$(git_staged_blob "$IGNORE_FILE") | |
if [[ -z "$staged_ignore_file_blob" ]]; then | |
# Not staged | |
return 1 | |
fi | |
local hash | |
hash=$(echo "$blob$patch" | shasum | cut -f1 -d" ") | |
temporary_ignore_file="${TMPDIR:-/tmp}/.enforce-style-ignore-$hash" | |
mkdir -p "${TMPDIR:-/tmp}" | |
git show "$staged_ignore_file_blob" | ([[ -n "$patch" ]] && perl -pe "$patch" || cat) >"$temporary_ignore_file" | |
echo "$temporary_ignore_file" | |
} | |
# Populate IGNORED_FILES based on IGNORE_FILE and MODIFIED_FILES | |
# | |
# IGNORED_FILES will contain the list of files that are staged but not managed by enforce-style. | |
function populate_ignored_files() { | |
# Patch can be a perl expression that can be used to update the ignore file in the staging area. | |
local patch | |
patch="$1" | |
if [[ -z "$IGNORE_FILE" ]]; then | |
IGNORED_FILES=() | |
return 0 | |
fi | |
if ! is_declared MODIFIED_FILES; then | |
populate_modified_files | |
fi | |
if [[ "${#MODIFIED_FILES[@]}" -eq 0 ]]; then | |
IGNORED_FILES=() | |
return 0 | |
fi | |
local temporary_ignore_file | |
temporary_ignore_file="$(get_staged_ignores_as_file "$patch")" | |
if [[ $? -ne 0 ]]; then | |
if [[ -f "$IGNORE_FILE" ]]; then | |
print_file_message "$IGNORE_FILE:1" "warning" "the ignore file will not be used because it is not checked into the repository" | |
else | |
print_file_message "$IGNORE_FILE:1" "warning" "the ignore file will not be used because it does not exist" | |
fi | |
# No staged ignore file; Nothing to ignore | |
IGNORED_FILES=() | |
return 0 | |
fi | |
mapfile -t IGNORED_FILES < <(filter_for_ignored_files "$temporary_ignore_file" "${MODIFIED_FILES[@]}") | |
rm "$temporary_ignore_file" | |
} | |
# Remove a file from the list of ignored files | |
# | |
# Only removes a file if it is explicitly ignored (absolute git path like /foo/bar.c) and not if it is ignored by a pattern (like bar.c) or directory (/foo). | |
# | |
# Second argument is the reason why the file is removed from the ignore file. Will be printed to the user. | |
# | |
# Third argument is the new filename. If it is set, the file will be renamed in the ignore file. | |
# | |
# If the first argument is empty and the second argument is not, the file will always be added to the ignore file. | |
# | |
# When renaming a file, the new file will not be added to the ignore file if the old file was not explicitly ignored. | |
# | |
# Does nothing if IGNORE_FILE is not set or does not exist. | |
# | |
# Will set the UPDATE_IGNORE_FILE_EXPRESSION variable to a perl expression that can be used to update the ignore file in the staging area. | |
function update_ignore_file() { | |
local filename | |
filename="$1" | |
local reason | |
reason="${2:-"of reasons"}" | |
local renamed_filename="$3" | |
UPDATE_IGNORE_FILE_EXPRESSION="" | |
local filename_in_gitignore="${filename/#[^\/]//&}" | |
local new_filename_in_gitignore | |
local perl_replace_expression='s|^\Q'"$filename_in_gitignore"'\E[\n]?$||g' | |
local perl_print_line_expression='say $. if m|^\Q'"$filename_in_gitignore"'\E[\n]?$|' | |
if [[ -n $renamed_filename ]]; then | |
new_filename_in_gitignore="${renamed_filename/#[^\/]//&}" | |
if [[ -n $filename ]]; then | |
# Rename | |
perl_replace_expression='s|^\Q'"$filename_in_gitignore"'\E$|'"$new_filename_in_gitignore"'|g' | |
else | |
# Add | |
perl_replace_expression='BEGIN{$/=undef};s|([\n]?)$|\n'"$new_filename_in_gitignore"'|' | |
perl_print_line_expression='say $. if m|^\Q'"$new_filename_in_gitignore"'\E[\n]?$|' | |
fi | |
fi | |
local blob | |
blob=$(git_staged_blob "$IGNORE_FILE") | |
if [[ -z "$blob" ]]; then | |
# No staged ignore file; Nothing to do | |
return 0 | |
fi | |
line=$(git show "$blob" | perl -nE "$perl_print_line_expression" | head -n1) | |
# Early return if it is already removed or added | |
if [[ -n "$renamed_filename" && -z "$filename" ]]; then | |
if [[ -n "$line" ]]; then | |
# New line already exists; Nothing to do | |
return 0 | |
fi | |
# shellcheck disable=SC2016 | |
perl_print_line_expression='END{say $x}$x=$.+1;$x=$.if m|^$|' | |
line=$(git show "$blob" | perl -nE "$perl_print_line_expression" | head -n1) | |
else | |
if [[ -z "$line" ]]; then | |
# Old line is already removed; Nothing to do | |
return 0 | |
fi | |
fi | |
# After this point we only abort if this is a dry run | |
UPDATE_IGNORE_FILE_EXPRESSION="$perl_replace_expression" | |
local change_description="unignored" | |
if [[ -n "$renamed_filename" ]]; then | |
change_description="changed to '$renamed_filename'" | |
if [[ -z "$filename" ]]; then | |
change_description="ignored" | |
fi | |
fi | |
local change_main_file=$filename | |
if [[ -n "$renamed_filename" && -z "$filename" ]]; then | |
change_main_file=$renamed_filename | |
fi | |
if [[ "$VERIFY" == "true" ]]; then | |
print_file_message "$IGNORE_FILE:$line" "warning" "'$change_main_file' should be $change_description because $reason" | |
return 0 | |
fi | |
# Filter the ignore file in the staging area | |
local permissions | |
local modified_blob | |
permissions=$(git ls-files -s "$IGNORE_FILE" | cut -d" " -f1) | |
modified_blob=$(git show "$blob" | perl -pe "$perl_replace_expression" | git hash-object -t blob -w --stdin --path "$IGNORE_FILE") | |
# Remove the line from the ignore file in the staging area | |
git update-index --cacheinfo "$permissions,$modified_blob,$IGNORE_FILE" | |
if [[ -f "$IGNORE_FILE" && $NEVER_MODIFY_WORKTREE != "true" ]]; then | |
# Remove the line from the ignore file in the worktree | |
perl -i -pe "$perl_replace_expression" "$IGNORE_FILE" | |
fi | |
FILES_THAT_WERE_REMOVED_FROM_THE_IGNORE_FILE+=("$filename") | |
print_file_message "$IGNORE_FILE:$line" "remark" "'$change_main_file' was $change_description because $reason" | |
} | |
######## enforce-style core ######## | |
# This section contains the core of the enforce-style script. It is responsible for formatting files and managing the list of managed and ignored files. | |
# Manage new and deleted files before doing anything else. | |
# | |
# Also adds the files to IGNORED_FILES and to MODIFIED_FILES if they are relevant for enforce-style. | |
# | |
# If VERIFY is set to true, a heuristic is used to determine if a renamed file was ignored before. This is not 100% accurate, but it is good enough for now. | |
# | |
# Adds all managed files that are partially staged to the PARTIALLY_STAGED_MANAGED_FILES array and all managed files that are fully staged to the FULLY_STAGED_MANAGED_FILES array. | |
# | |
# A file is partially staged if it is staged but also has unstaged changes. A file is fully staged if it is staged and has no unstaged changes. | |
function prepare_new_and_deleted_files() { | |
if [[ "$NEW_AND_DELETED_FILES_POPULATED" = "true" ]]; then | |
# Already ran once. | |
# Running it again is not an error, but it is also not necessary. | |
return 0 | |
fi | |
# Reset the partially staged files | |
unset FULLY_STAGED_MANAGED_FILES | |
unset PARTIALLY_STAGED_MANAGED_FILES | |
# Force reload of modified files | |
populate_modified_files | |
populate_deleted_files | |
populate_created_files | |
populate_renamed_files | |
local update_ignore_file_expression="" | |
for file in "${DELETED_FILES[@]}"; do | |
update_ignore_file "$file" "it was deleted" | |
update_ignore_file_expression+=";$UPDATE_IGNORE_FILE_EXPRESSION" | |
done | |
for file in "${CREATED_FILES[@]}"; do | |
MODIFIED_FILES+=("$file") | |
if [[ "$IGNORE_NEW_FILES" = "true" ]]; then | |
update_ignore_file "" "enforce-style is currently configured to ignore all new files" "$file" | |
update_ignore_file_expression+=";$UPDATE_IGNORE_FILE_EXPRESSION" | |
fi | |
done | |
for previous_file in "${!RENAMED_FILES_MAP[@]}"; do | |
local new_file="${RENAMED_FILES_MAP["$previous_file"]}" | |
MODIFIED_FILES+=("$new_file") | |
update_ignore_file "$previous_file" "the file was renamed" "$new_file" | |
update_ignore_file_expression+=";$UPDATE_IGNORE_FILE_EXPRESSION" | |
done | |
if [[ $VERIFY = "true" ]]; then | |
# If we are in verify mode, we did not modify the ignore file previously | |
populate_ignored_files "$update_ignore_file_expression" | |
else | |
populate_ignored_files | |
fi | |
# Populate managed files | |
mapfile -t MANAGED_FILES < <(comm -23 <(printf '%s\n' "${MODIFIED_FILES[@]}" | sort) <(printf '%s\n' "${IGNORED_FILES[@]}" | sort)) | |
mapfile -t PARTIALLY_STAGED_MANAGED_FILES < <(git ls-files -m "${MANAGED_FILES[@]}" 2>/dev/null) | |
mapfile -t FULLY_STAGED_MANAGED_FILES < <(comm -23 <(printf '%s\n' "${MANAGED_FILES[@]}" | sort) <(printf '%s\n' "${PARTIALLY_STAGED_MANAGED_FILES[@]}" | sort)) | |
NEW_AND_DELETED_FILES_POPULATED="true" | |
} | |
# Format managed files with clang-format and stage the result | |
function format_managed_files() { | |
prepare_new_and_deleted_files | |
if [[ "${#MANAGED_FILES[@]}" -eq 0 ]]; then | |
return 0 | |
fi | |
LOGGING_MODE="fatal" | |
if ! has_required_tools "${MANAGED_FILES[@]}"; then | |
return 1 | |
fi | |
LOGGING_MODE="error" | |
if [[ "true" = "$VERIFY" ]]; then | |
analyze_files_in_staging_area "${MANAGED_FILES[@]}" | |
if [[ $? -ne 0 ]]; then | |
ERROR_OCCURRED="true" | |
return 1 | |
fi | |
return 0 | |
fi | |
if [[ "true" = "$NEVER_MODIFY_WORKTREE" ]]; then | |
# Only format the staging area, not the worktree. | |
format_files_in_staging_area "${MANAGED_FILES[@]}" | |
return 0 | |
fi | |
# When the files are fully staged, we can just format them and stage the result. | |
# This way we only need to call clang-format once. | |
if [[ ${#FULLY_STAGED_MANAGED_FILES[@]} -ne 0 ]]; then | |
format_files "${FULLY_STAGED_MANAGED_FILES[@]}" | |
git add "${FULLY_STAGED_MANAGED_FILES[@]}" >/dev/null 2>&1 | |
fi | |
# When the files are only partially staged, we need to format the staging area and the worktree separately. | |
if [[ ${#PARTIALLY_STAGED_MANAGED_FILES[@]} -ne 0 ]]; then | |
# Format and update staging area without touching the worktree | |
format_files_in_staging_area "${PARTIALLY_STAGED_MANAGED_FILES[@]}" | |
# Separatly format the files in the worktree | |
format_files "${PARTIALLY_STAGED_MANAGED_FILES[@]}" | |
fi | |
} | |
# Process unmanaged files and stage the result | |
function process_ignored_files() { | |
prepare_new_and_deleted_files | |
if [[ "${#IGNORED_FILES[@]}" -eq 0 ]]; then | |
return 0 | |
fi | |
LOGGING_MODE="warning" | |
if ! has_required_tools; then | |
print_message "note" "The currently staged files are excluded from formatting, but keep in mind that you need clang-format if you want to commit files where styling is enforced." | |
fi | |
# Print warnings for ignored files that are not formatted correctly | |
LOGGING_MODE="warning" | |
analyze_files_in_staging_area "${IGNORED_FILES[@]}" | |
# Delete old patches | |
if [[ "true" = "$DELETE_OLD_PATCHES" ]]; then | |
rm -f "${TMPDIR:-/tmp}"/enforce-style-*.patch | |
fi | |
# Offer patches for ignored files that are not formatted correctly | |
local worktree_patch | |
local staged_patch | |
local worktree_patch_file | |
local staged_patch_file | |
worktree_patch=$(patch_for_files_in_worktree "${IGNORED_FILES[@]}") | |
staged_patch=$(patch_for_files_in_staging_area "${IGNORED_FILES[@]}") | |
if [[ -n "$worktree_patch" ]]; then | |
worktree_patch_file="${TMPDIR:-/tmp}/enforce-style-$(echo "$worktree_patch" | sha1sum | head -c10).patch" | |
echo "$worktree_patch" >"$worktree_patch_file" | |
fi | |
if [[ -n "$staged_patch" ]]; then | |
staged_patch_file="${TMPDIR:-/tmp}/enforce-style-$(echo "$staged_patch" | sha1sum | head -c10).patch" | |
echo "$staged_patch" >"$staged_patch_file" | |
fi | |
if [[ -n "$worktree_patch_file" && "$worktree_patch_file" = "$staged_patch_file" ]]; then | |
print_message "remark" "apply this patch to fix these warnings: 'git apply --index \"$worktree_patch_file\"'" | |
else | |
if [[ -n "$staging_patch_file" ]]; then | |
print_message "remark" "apply this patch to fix the styling for your staged changes: 'git apply --cached \"$worktree_patch_file\"'" | |
fi | |
if [[ -n "$worktree_patch_file" ]]; then | |
print_message "remark" "your worktree contains partially staged files so you need a separate patch for that: 'git apply \"$worktree_patch_file\"'" | |
fi | |
fi | |
# Remove files from the ignore list if they match the style guidelines | |
LOGGING_MODE="error" | |
for file in "${IGNORED_FILES[@]}"; do | |
local blob | |
blob=$(git_staged_blob "$file") | |
if git show "$blob" | analyze_file_pipe "$file" 2>/dev/null; then | |
update_ignore_file "$file" "it matches the coreboot style" | |
fi | |
done | |
} | |
function main() { | |
# Load the default values for all unset config variables | |
default_config | |
reset_state | |
# Make sure we are in the root of the repository | |
cd_to_repo_root || return 1 | |
# case "$1" in | |
case "$1" in | |
enforce | "") | |
format_managed_files | |
if [[ "true" = "$ERROR_OCCURRED" ]]; then | |
return 1 | |
fi | |
process_ignored_files | |
if [[ "true" = "$ERROR_OCCURRED" ]]; then | |
return 1 | |
fi | |
;; | |
check) | |
VERIFY="true" | |
format_managed_files | |
if [[ "true" = "$ERROR_OCCURRED" ]]; then | |
return 1 | |
fi | |
process_ignored_files | |
if [[ "true" = "$ERROR_OCCURRED" ]]; then | |
return 1 | |
fi | |
;; | |
*) | |
echo "Usage: $0 {enforce|check}" >&2 | |
return 1 | |
;; | |
esac | |
if [[ ${#MANAGED_FILES[@]} -ne 0 ]]; then | |
echo "Enforced formatting for ${#MANAGED_FILES[@]} files." >&2 | |
fi | |
if [[ ${#IGNORED_FILES[@]} -ne 0 ]]; then | |
echo "Checked formatting for ${#IGNORED_FILES[@]} files." >&2 | |
fi | |
if [[ ${#FILES_THAT_WERE_REMOVED_FROM_THE_IGNORE_FILE[@]} -ne 0 ]]; then | |
echo "Ensuring correct formatting for ${#FILES_THAT_WERE_REMOVED_FROM_THE_IGNORE_FILE[@]} new files." >&2 | |
fi | |
} | |
######## Entrypoint ######## | |
if test "${RUN_TESTS}" != "true"; then | |
main "$@" | |
exit $? | |
else | |
######## Tests ######## | |
# enforce-style uses shunit2 for testing. You can run the tests with `RUN_TESTS=true ./enforce-style`. | |
function test_process_ignored_files_removes_deleted_file_from_ignores_when_enforcing() { | |
setup_test_repo | |
IGNORE_FILE=".enforce-style-ignore" | |
cat <<EOF >"$IGNORE_FILE" | |
/unformatted.cpp | |
EOF | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
git add unformatted.cpp >/dev/null 2>&1 | |
git commit -m "Add ignored file" >/dev/null 2>&1 | |
git rm unformatted.cpp >/dev/null 2>&1 | |
VERIFY=false | |
process_ignored_files 2>/dev/null | |
# # Assert that the output contains a information that the file was removed from the ignore list | |
# assertContains "$output" "removed" | |
# Assert that the file was removed from the ignore list | |
local ignore_file_content | |
mapfile -t ignore_file_content <"$IGNORE_FILE" | |
assertArrayNotContains "/unformatted.cpp" "${ignore_file_content[@]}" | |
# Assert that the file was removed from the staged ignore list | |
mapfile -t ignore_file_content < <(git_staged_file_content "$IGNORE_FILE") | |
assertArrayNotContains "/unformatted.cpp" "${ignore_file_content[@]}" | |
} | |
function test_update_ignore_file_works() { | |
setup_test_repo | |
IGNORE_FILE=".enforce-style-ignore" | |
cat <<EOF >"$IGNORE_FILE" | |
/foo.c | |
/bar.c | |
/baz.c | |
EOF | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
local ignore_file_content | |
update_ignore_file "bar.c" 2>/dev/null | |
mapfile -t ignore_file_content < <(cat "$IGNORE_FILE") | |
assertArrayContains "/foo.c" "${ignore_file_content[@]}" | |
assertArrayNotContains "/bar.c" "${ignore_file_content[@]}" | |
assertArrayContains "/baz.c" "${ignore_file_content[@]}" | |
update_ignore_file "/foo.c" 2>/dev/null | |
mapfile -t ignore_file_content < <(cat "$IGNORE_FILE") | |
assertArrayNotContains "/foo.c" "${ignore_file_content[@]}" | |
assertArrayNotContains "/bar.c" "${ignore_file_content[@]}" | |
assertArrayContains "/baz.c" "${ignore_file_content[@]}" | |
update_ignore_file "baz.c" 2>/dev/null | |
mapfile -t ignore_file_content < <(cat "$IGNORE_FILE") | |
assertArrayNotContains "/foo.c" "${ignore_file_content[@]}" | |
assertArrayNotContains "/bar.c" "${ignore_file_content[@]}" | |
assertArrayNotContains "/baz.c" "${ignore_file_content[@]}" | |
assertEquals 0 "$(wc -l <"$IGNORE_FILE")" | |
} | |
function test_update_ignore_file_can_rename() { | |
setup_test_repo | |
IGNORE_FILE=".enforce-style-ignore" | |
cat <<EOF >"$IGNORE_FILE" | |
/foo.c | |
/bar.c | |
/baz.c | |
EOF | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
local ignore_file_content | |
update_ignore_file "bar.c" "" "rab.c" 2>/dev/null | |
mapfile -t ignore_file_content < <(cat "$IGNORE_FILE") | |
assertArrayContains "/foo.c" "${ignore_file_content[@]}" | |
assertArrayNotContains "/bar.c" "${ignore_file_content[@]}" | |
assertArrayContains "/baz.c" "${ignore_file_content[@]}" | |
assertArrayNotContains "/oof.c" "${ignore_file_content[@]}" | |
assertArrayContains "/rab.c" "${ignore_file_content[@]}" | |
assertArrayNotContains "/zab.c" "${ignore_file_content[@]}" | |
update_ignore_file "/foo.c" "" "/oof.c" 2>/dev/null | |
mapfile -t ignore_file_content < <(cat "$IGNORE_FILE") | |
assertArrayNotContains "/foo.c" "${ignore_file_content[@]}" | |
assertArrayNotContains "/bar.c" "${ignore_file_content[@]}" | |
assertArrayContains "/baz.c" "${ignore_file_content[@]}" | |
assertArrayContains "/oof.c" "${ignore_file_content[@]}" | |
assertArrayContains "/rab.c" "${ignore_file_content[@]}" | |
assertArrayNotContains "/zab.c" "${ignore_file_content[@]}" | |
update_ignore_file "baz.c" "" "/zab.c" 2>/dev/null | |
mapfile -t ignore_file_content < <(cat "$IGNORE_FILE") | |
assertArrayNotContains "/foo.c" "${ignore_file_content[@]}" | |
assertArrayNotContains "/bar.c" "${ignore_file_content[@]}" | |
assertArrayNotContains "/baz.c" "${ignore_file_content[@]}" | |
assertArrayContains "/oof.c" "${ignore_file_content[@]}" | |
assertArrayContains "/rab.c" "${ignore_file_content[@]}" | |
assertArrayContains "/zab.c" "${ignore_file_content[@]}" | |
assertEquals "$(wc -l <"$IGNORE_FILE")" 3 | |
} | |
function test_update_ignore_file_can_add_a_new_file() { | |
setup_test_repo | |
IGNORE_FILE=".enforce-style-ignore" | |
cat <<EOF >"$IGNORE_FILE" | |
/foo.c | |
EOF | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
local ignore_file_content | |
update_ignore_file "" "" "bar.c" 2>/dev/null | |
mapfile -t ignore_file_content < <(cat "$IGNORE_FILE") | |
assertArrayContains "/foo.c" "${ignore_file_content[@]}" | |
assertArrayContains "/bar.c" "${ignore_file_content[@]}" | |
assertArrayNotContains "/baz.c" "${ignore_file_content[@]}" | |
assertEquals "${#ignore_file_content[@]}" 2 | |
update_ignore_file "" "" "bar.c" 2>/dev/null | |
mapfile -t ignore_file_content < <(cat "$IGNORE_FILE") | |
assertEquals "${#ignore_file_content[@]}" 2 | |
update_ignore_file "" "" "bar.c" 2>/dev/null | |
mapfile -t ignore_file_content < <(cat "$IGNORE_FILE") | |
assertEquals "${#ignore_file_content[@]}" 2 | |
update_ignore_file "" "" "baz.c" 2>/dev/null | |
mapfile -t ignore_file_content < <(cat "$IGNORE_FILE") | |
assertArrayContains "/foo.c" "${ignore_file_content[@]}" | |
assertArrayContains "/bar.c" "${ignore_file_content[@]}" | |
assertArrayContains "/baz.c" "${ignore_file_content[@]}" | |
assertEquals "${#ignore_file_content[@]}" 3 | |
} | |
function test_update_ignore_file_prints_messages() { | |
setup_test_repo | |
IGNORE_FILE=".enforce-style-ignore" | |
cat <<EOF >"$IGNORE_FILE" | |
/foo.c | |
/bar.c | |
/baz.c | |
EOF | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
local ignore_file_content | |
# Test remove messages | |
messages=$(update_ignore_file "foo.c" "toast 12345" 2>&1) | |
assertContains "$messages" "unignored" | |
assertContains "$messages" "toast 12345" | |
# Test rename messages | |
messages=$(update_ignore_file "bar.c" "testtesttest messssssage" "rab.c" 2>&1) | |
assertContains "$messages" "changed" | |
assertContains "$messages" "testtesttest messssssage" | |
# Test already removed messages | |
messages=$(update_ignore_file "foo.c" "abcdefg" 2>&1) | |
assertNotContains "$messages" "unignored" | |
assertNotContains "$messages" "changed" | |
assertNotContains "$messages" "abcdefg" | |
} | |
function test_analyze_files_in_staging_area_fails_when_there_are_unformatted_managed_files() { | |
setup_test_repo | |
git add unformatted.cpp >/dev/null 2>&1 | |
LOGGING_MODE="error" | |
analyze_files_in_staging_area unformatted.cpp 2>/dev/null | |
assertEquals 1 $? | |
} | |
function test_analyze_files_in_staging_area_succeeds_when_there_are_only_formatted_managed_files() { | |
setup_test_repo | |
format_files unformatted.cpp >/dev/null 2>&1 | |
git add unformatted.cpp >/dev/null 2>&1 | |
LOGGING_MODE="error" | |
analyze_files_in_staging_area unformatted.cpp | |
assertEquals 0 $? | |
} | |
function test_analyze_files_in_staging_area_succeeds_when_all_staged_files_are_formatted_even_if_the_unstaged_versions_are_not() { | |
setup_test_repo | |
git add unformatted.cpp >/dev/null 2>&1 | |
format_files_in_staging_area unformatted.cpp >/dev/null 2>&1 | |
LOGGING_MODE="error" | |
analyze_files_in_staging_area unformatted.cpp 2>/dev/null | |
assertEquals 0 $? | |
} | |
# Setup a test repo with an initial commit and a few unstaged files | |
function setup_test_repo() { | |
cd "$(mktemp -dp "$SHUNIT_TMPDIR")" || fail | |
git init >/dev/null 2>&1 | |
git config --local user.email "[email protected]" | |
git config --local user.name "test" | |
IGNORE_FILE=".enforce-style-ignore" | |
touch "$IGNORE_FILE" | |
cat <<EOF >"formatted.cpp" | |
int main(int argc, char *argv[]) { | |
return 0; | |
} | |
EOF | |
cat <<EOF >"unformatted.cpp" | |
int main(int argc, | |
char *argv[]) { | |
return 0; | |
} | |
int formattedA(int argc, char *argv[]) { return 0; } | |
int formattedB(int argc, char *argv[]) { return 0; } | |
int another_unformatted_fn | |
(int argc, char *argv[]) { | |
return 0; | |
} | |
int formattedC(int argc, char *argv[]) { return 0; } | |
EOF | |
echo "Test repo" >README.md | |
git add "$IGNORE_FILE" README.md >/dev/null 2>&1 | |
git commit -m "Initial commit" >/dev/null 2>&1 | |
} | |
function test_matches_extension() { | |
assertTrue "matches_extension foo.c .c .hpp .cpp" | |
assertTrue "matches_extension foo.cpp .c .hpp .cpp" | |
assertFalse "matches_extension foo.sh .c .hpp .cpp" | |
assertTrue "matches_extension foo.sh .sh" | |
assertFalse "matches_extension foo.cpp .sh" | |
} | |
function test_populate_renamed_files() { | |
setup_test_repo | |
git add unformatted.cpp >/dev/null 2>&1 | |
git commit -m "Add file" >/dev/null 2>&1 | |
git mv unformatted.cpp unformatted2.cpp >/dev/null 2>&1 | |
populate_renamed_files | |
assertEquals "unformatted2.cpp" "${RENAMED_FILES[0]}" | |
assertEquals "unformatted2.cpp" "${RENAMED_FILES_MAP[unformatted.cpp]}" | |
assertEquals 1 "${#RENAMED_FILES[@]}" | |
} | |
function test_get_head_commit_seems_to_work() { | |
cd "$(mktemp -dp "$SHUNIT_TMPDIR")" || fail | |
git init >/dev/null 2>&1 | |
assertEquals "4b825dc642cb6eb9a060e54bf8d69288fbee4904" "$(get_head_commit)" | |
touch file.cpp | |
git add file.cpp >/dev/null 2>&1 | |
git commit -m "Initial commit" >/dev/null 2>&1 | |
assertNotEquals "4b825dc642cb6eb9a060e54bf8d69288fbee4904" "$(get_head_commit)" | |
} | |
function test_get_staged_files_seems_to_work() { | |
setup_test_repo | |
# No staged files; nothing to do | |
reset_state | |
populate_modified_files | |
assertEquals 0 ${#MODIFIED_FILES[@]} | |
# One added staged file; should not be detected as by populate_modified_files | |
touch file.cpp | |
git add file.cpp >/dev/null 2>&1 | |
reset_state | |
populate_modified_files | |
assertEquals ${#MODIFIED_FILES[@]} 0 | |
# No staged files again | |
git commit -m "Initial commit" >/dev/null 2>&1 | |
reset_state | |
populate_modified_files | |
assertEquals ${#MODIFIED_FILES[@]} 0 | |
# One modified staged file; should be detected as by populate_modified_files | |
echo "foo" >file.cpp | |
git add file.cpp >/dev/null 2>&1 | |
reset_state | |
populate_modified_files | |
assertEquals ${#MODIFIED_FILES[@]} 1 | |
assertEquals "file.cpp" "${MODIFIED_FILES[0]}" | |
} | |
function test_populate_ignored_files() { | |
setup_test_repo | |
cat <<EOF >"$IGNORE_FILE" | |
/foo.c | |
/bar.c | |
/toast/bar.c | |
/test/*.c | |
!/test/baz.c | |
EOF | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
MODIFIED_FILES=(foo.c bar.c baz.c toast/foo.c toast/bar.c test/foo.c test/baz.c test/bar.c) | |
populate_ignored_files | |
assertArrayContains "foo.c" "${IGNORED_FILES[@]}" | |
assertArrayContains "bar.c" "${IGNORED_FILES[@]}" | |
assertArrayNotContains "baz.c" "${IGNORED_FILES[@]}" | |
assertArrayNotContains "toast/foo.c" "${IGNORED_FILES[@]}" | |
assertArrayContains "toast/bar.c" "${IGNORED_FILES[@]}" | |
assertArrayContains "test/foo.c" "${IGNORED_FILES[@]}" | |
assertArrayNotContains "test/baz.c" "${IGNORED_FILES[@]}" | |
assertArrayContains "test/bar.c" "${IGNORED_FILES[@]}" | |
} | |
function test_prepare_new_and_deleted_files_detects_new_files() { | |
setup_test_repo | |
cat <<EOF >"$IGNORE_FILE" | |
/foo.c | |
/bar.c | |
/toast/bar.c | |
/test/*.c | |
!/test/baz.c | |
EOF | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
mkdir -p toast | |
mkdir -p test | |
# shellcheck disable=SC2002 | |
cat unformatted.cpp | tee foo.c bar.c baz.c toast/foo.c toast/bar.c test/foo.c test/baz.c test/bar.c >/dev/null | |
git add foo.c bar.c baz.c toast/foo.c toast/bar.c test/foo.c test/baz.c test/bar.c >/dev/null 2>&1 | |
prepare_new_and_deleted_files | |
assertArrayNotContains "foo.c" "${MANAGED_FILES[@]}" | |
assertArrayNotContains "bar.c" "${MANAGED_FILES[@]}" | |
assertArrayContains "baz.c" "${MANAGED_FILES[@]}" | |
assertArrayContains "toast/foo.c" "${MANAGED_FILES[@]}" | |
assertArrayNotContains "toast/bar.c" "${MANAGED_FILES[@]}" | |
assertArrayNotContains "test/foo.c" "${MANAGED_FILES[@]}" | |
assertArrayContains "test/baz.c" "${MANAGED_FILES[@]}" | |
assertArrayNotContains "test/bar.c" "${MANAGED_FILES[@]}" | |
} | |
function test_prepare_new_and_deleted_files_renames_ignores_on_file_rename() { | |
setup_test_repo | |
cat <<EOF >"$IGNORE_FILE" | |
/unformatted.cpp | |
EOF | |
git add "$IGNORE_FILE" >&/dev/null | |
git add unformatted.cpp >&/dev/null | |
reset_state | |
prepare_new_and_deleted_files >&/dev/null | |
assertArrayContains "unformatted.cpp" "${IGNORED_FILES[@]}" | |
assertArrayNotContains "unformatted2.cpp" "${IGNORED_FILES[@]}" | |
git commit -m test >&/dev/null | |
git mv unformatted.cpp unformatted2.cpp >&/dev/null | |
reset_state | |
prepare_new_and_deleted_files >&/dev/null | |
assertArrayNotContains "unformatted.cpp" "${IGNORED_FILES[@]}" | |
assertArrayContains "unformatted2.cpp" "${IGNORED_FILES[@]}" | |
} | |
function test_prepare_new_and_deleted_files_works_without_staged_files() { | |
setup_test_repo | |
git commit -m "Make sure there are no staged files" >/dev/null 2>&1 | |
prepare_new_and_deleted_files | |
assertEquals 0 ${#MANAGED_FILES[@]} | |
} | |
function test_patch_produces_same_result_as_directly_formatting() { | |
setup_test_repo | |
cp unformatted.cpp directly_formatted.cpp | |
cp unformatted.cpp patch_formatted.cpp | |
# Directly format the file as a reference. | |
format_files directly_formatted.cpp | |
# Format the file using a patch | |
local patch | |
# shellcheck disable=SC2002 | |
patch=$(cat patch_formatted.cpp | diff_against_formatted patch_formatted.cpp) | |
git apply <(echo "$patch") | |
# Ensure that the result is the same as the directly formatted file. | |
assertEquals "$(cat patch_formatted.cpp)" "$(cat directly_formatted.cpp)" | |
} | |
function test_patch_works_when_there_is_nothing_wrong() { | |
setup_test_repo | |
# Directly format the file | |
format_files unformatted.cpp | |
# Generate a patch | |
local patch | |
# shellcheck disable=SC2002 | |
patch=$(cat unformatted.cpp | diff_against_formatted unformatted.cpp) | |
# Make sure the patch is empty | |
assertEquals "$patch" "" | |
} | |
function test_is_only_partially_staged() { | |
cd "$(mktemp -dp "$SHUNIT_TMPDIR")" || fail | |
git init >/dev/null 2>&1 | |
echo "foo" >file.cpp | |
git add file.cpp &>/dev/null | |
reset_state | |
prepare_new_and_deleted_files 2>/dev/null | |
assertArrayNotContains "file.cpp" "${PARTIALLY_STAGED_MANAGED_FILES[@]}" | |
assertArrayContains "file.cpp" "${FULLY_STAGED_MANAGED_FILES[@]}" | |
echo "bar" >>file.cpp | |
reset_state | |
prepare_new_and_deleted_files 2>/dev/null | |
assertArrayContains "file.cpp" "${PARTIALLY_STAGED_MANAGED_FILES[@]}" | |
assertArrayNotContains "file.cpp" "${FULLY_STAGED_MANAGED_FILES[@]}" | |
} | |
function test_format_managed_files_works_for_managed_fully_staged_files() { | |
setup_test_repo | |
local unformatted_hash | |
unformatted_hash=$(shasum unformatted.cpp) | |
# Unstaged files should not be formatted. | |
format_managed_files | |
assertEquals "$unformatted_hash" "$(shasum unformatted.cpp)" | |
# shellcheck disable=SC2046 # Should be fine in tests | |
assertArrayContains "unformatted.cpp" $(git ls-files -z -om --exclude-standard | tr '\0' ' ') | |
reset_state | |
# Fully staged files should be formatted. | |
git add unformatted.cpp >/dev/null 2>&1 | |
format_managed_files | |
# bash | |
# The file should have been formatted as it is staged. | |
assertNotEquals "$unformatted_hash" "$(shasum unformatted.cpp)" | |
# Make sure the changes are staged. | |
# shellcheck disable=SC2046 # Should be fine in tests | |
assertArrayNotContains "unformatted.cpp" $(git ls-files -z -om --exclude-standard | tr '\0' ' ') | |
# Partially staged files should get formatted only in the staging area. | |
} | |
function test_format_managed_files_works_for_managed_partially_staged_files() { | |
setup_test_repo | |
cp unformatted.cpp partially.cpp | |
git add partially.cpp >/dev/null 2>&1 | |
printf "\nint other() { return 3; }" >>partially.cpp | |
local before_unstaged_hash | |
before_unstaged_hash=$(shasum partially.cpp) | |
local before_staged_blob | |
before_staged_blob=$(git_staged_blob partially.cpp) | |
# Workdir should not change for partially staged files. | |
format_managed_files | |
local after_unstaged_hash | |
after_unstaged_hash=$(shasum partially.cpp) | |
local after_staged_blob | |
after_staged_blob=$(git_staged_blob partially.cpp) | |
assertNotEquals "$before_unstaged_hash" "$after_unstaged_hash" | |
assertNotEquals "$before_staged_blob" "$after_staged_blob" | |
assertNotEquals "$(cat partially.cpp)" "$(git show "$after_staged_blob")" | |
reset_state | |
git add partially.cpp >/dev/null 2>&1 | |
format_managed_files | |
local final_unstaged_hash | |
final_unstaged_hash=$(shasum partially.cpp) | |
local final_staged_blob | |
final_staged_blob=$(git_staged_blob partially.cpp) | |
assertEquals "$after_unstaged_hash" "$final_unstaged_hash" | |
assertEquals "$(cat partially.cpp)" "$(git show "$final_staged_blob")" | |
assertNotEquals "$after_staged_blob" "$final_staged_blob" | |
} | |
function test_format_managed_files_respects_never_modify_worktree_config() { | |
setup_test_repo | |
NEVER_MODIFY_WORKTREE="true" | |
cp unformatted.cpp partially.cpp | |
git add partially.cpp >/dev/null 2>&1 | |
printf "\nint other() { return 3; }" >>partially.cpp | |
local before_unstaged_hash | |
before_unstaged_hash=$(shasum partially.cpp) | |
local before_staged_blob | |
before_staged_blob=$(git_staged_blob partially.cpp) | |
# Workdir should not change for partially staged files. | |
format_managed_files | |
local after_unstaged_hash | |
after_unstaged_hash=$(shasum partially.cpp) | |
local after_staged_blob | |
after_staged_blob=$(git_staged_blob partially.cpp) | |
assertEquals "$before_unstaged_hash" "$after_unstaged_hash" | |
assertNotEquals "$before_staged_blob" "$after_staged_blob" | |
assertNotEquals "$(cat partially.cpp)" "$(git show "$after_staged_blob")" | |
reset_state | |
git add partially.cpp >/dev/null 2>&1 | |
format_managed_files | |
local final_unstaged_hash | |
final_unstaged_hash=$(shasum partially.cpp) | |
local final_staged_blob | |
final_staged_blob=$(git_staged_blob partially.cpp) | |
assertEquals "$before_unstaged_hash" "$final_unstaged_hash" | |
assertNotEquals "$(cat partially.cpp)" "$(git show "$final_staged_blob")" | |
assertNotEquals "$after_staged_blob" "$final_staged_blob" | |
} | |
function test_process_ignored_files_does_not_change_files() { | |
setup_test_repo | |
IGNORE_FILE=".enforce-style-ignore" | |
cat <<EOF >"$IGNORE_FILE" | |
/unformatted.cpp | |
EOF | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
cp unformatted.cpp unformatted_old.cpp | |
git add unformatted.cpp >/dev/null 2>&1 | |
process_ignored_files 2>/dev/null | |
assertEquals "$(cat unformatted_old.cpp)" "$(cat unformatted.cpp)" | |
} | |
function test_process_ignored_files_outputs_patch() { | |
setup_test_repo | |
cat <<EOF >"$IGNORE_FILE" | |
/unformatted.cpp | |
EOF | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
cp unformatted.cpp unformatted_old.cpp | |
git add unformatted.cpp >/dev/null 2>&1 | |
local output | |
output=$(process_ignored_files 2>&1) | |
# Assert that the output contains something that looks like a patch recommendation | |
assertContains "$output" "git apply" | |
} | |
function test_process_ignored_files_outputs_warnings() { | |
setup_test_repo | |
cat <<EOF >"$IGNORE_FILE" | |
/unformatted.cpp | |
EOF | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
cp unformatted.cpp unformatted_old.cpp | |
git add unformatted.cpp >/dev/null 2>&1 | |
local output | |
output=$(process_ignored_files 2>&1) | |
# Assert that the output contains something that looks like a warning | |
assertContains "$output" "warning" | |
} | |
function test_process_ignored_files_removes_already_formatted_file_from_ignores() { | |
setup_test_repo | |
IGNORE_FILE=".enforce-style-ignore" | |
cat <<EOF >"$IGNORE_FILE" | |
/unformatted.cpp | |
EOF | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
cp unformatted.cpp unformatted_old.cpp | |
format_files unformatted.cpp | |
git add unformatted.cpp >/dev/null 2>&1 | |
output=$(process_ignored_files 2>&1) | |
# Assert that the output contains a information that the file was removed from the ignore list | |
assertContains "$output" "coreboot style" | |
# Assert that the file was removed from the ignore list | |
local ignore_file_content | |
mapfile -t ignore_file_content <"$IGNORE_FILE" | |
assertArrayNotContains "/unformatted.cpp" "${ignore_file_content[@]}" | |
# Assert that the file was removed from the staged ignore list | |
mapfile -t ignore_file_content < <(git_staged_file_content "$IGNORE_FILE") | |
assertArrayNotContains "/unformatted.cpp" "${ignore_file_content[@]}" | |
} | |
function test_format_files_does_not_modify_files_with_the_wrong_extension() { | |
setup_test_repo | |
cp unformatted.cpp unformatted | |
format_files unformatted | |
assertEquals "$(cat unformatted)" "$(cat unformatted.cpp)" | |
} | |
function test_format_managed_files_does_not_modify_files_with_the_wrong_extension() { | |
setup_test_repo | |
cat <<EOF >"$IGNORE_FILE" | |
/unformatted.cpp | |
EOF | |
cp unformatted.cpp unformatted | |
git add unformatted >/dev/null 2>&1 | |
format_managed_files | |
assertEquals "$(cat unformatted)" "$(cat unformatted.cpp)" | |
} | |
function test_version_comparison() { | |
assertTrue 'version_greater_or_equal "16.0.0" "16.0.0"' | |
assertTrue 'version_greater_or_equal "16.0.0" "16.0.1"' | |
assertTrue 'version_greater_or_equal "16.0.0" "17.0.1"' | |
assertTrue 'version_greater_or_equal "16.0.0" "111.0.1"' | |
assertTrue 'version_greater_or_equal "16.0.0" "17"' | |
assertFalse 'version_greater_or_equal "16.0.1" "16"' | |
assertFalse 'version_greater_or_equal "16.0.0" "15.0.0"' | |
assertFalse 'version_greater_or_equal "16.0.0" "15.999.999"' | |
assertFalse 'version_greater_or_equal "16.0.0" "9.999.999"' | |
assertFalse 'version_greater_or_equal "16.0.0" ""' | |
assertFalse 'version_greater_or_equal "16.0.0" "0"' | |
assertFalse 'version_greater_or_equal "16.0.0" "7"' | |
assertFalse 'version_greater_or_equal "16.0.0" "15"' | |
} | |
function test_is_relevant_file_works_for_cpp_files() { | |
assertTrue 'is_relevant_file "foo.cpp"' | |
assertTrue 'is_relevant_file "foo.hpp"' | |
assertTrue 'is_relevant_file "foo.c"' | |
assertTrue 'is_relevant_file "foo.h"' | |
assertTrue 'is_relevant_file "foo.hh"' | |
assertFalse 'is_relevant_file "foo.inl"' | |
assertFalse 'is_relevant_file "foo.inc"' | |
assertTrue 'is_relevant_file "foo.extra.cpp"' | |
assertTrue 'is_relevant_file ".cpp"' | |
assertTrue 'is_relevant_file "bar/foo.cpp"' | |
assertTrue 'is_relevant_file "bar/.cpp"' | |
assertFalse 'is_relevant_file ".cppX"' | |
assertFalse 'is_relevant_file "bar/.cppX"' | |
assertFalse 'is_relevant_file ".cpp/bar"' | |
assertFalse 'is_relevant_file "foo/bar.cpp/baz"' | |
} | |
function test_enforce_style_check_fails_when_there_are_unformatted_managed_files() { | |
setup_test_repo | |
git add unformatted.cpp >/dev/null 2>&1 | |
(RUN_TESTS=false main check 2>/dev/null) | |
assertEquals 1 $? | |
} | |
function test_enforce_style_check_succeeds_when_there_are_only_formatted_managed_files() { | |
setup_test_repo | |
format_files unformatted.cpp >/dev/null 2>&1 | |
git add unformatted.cpp >/dev/null 2>&1 | |
(RUN_TESTS=false main check >/dev/null 2>&1) | |
assertEquals 0 $? | |
} | |
function test_enforce_style_check_succeeds_when_all_staged_files_are_formatted_even_if_the_unstaged_versions_are_not() { | |
setup_test_repo | |
git add unformatted.cpp >/dev/null 2>&1 | |
format_files_in_staging_area unformatted.cpp >/dev/null 2>&1 | |
(RUN_TESTS=false main check 2>/dev/null) | |
assertEquals 0 $? | |
} | |
function test_enforce_style_warning_if_ignore_file_missing() { | |
setup_test_repo | |
git add unformatted.cpp >/dev/null 2>&1 | |
IGNORE_FILE=".no-enforce-style-ignore" | |
messages=$(RUN_TESTS=false main check 2>&1) | |
# Assert that the output contains a warning about the missing ignore file | |
assertContains "$messages" "warning" | |
assertContains "$messages" "exist" | |
assertContains "$messages" "$IGNORE_FILE" | |
} | |
function test_enforce_style_check_succeeds_when_there_is_a_unformatted_deleted_file() { | |
setup_test_repo | |
git add unformatted.cpp >/dev/null 2>&1 | |
echo "" >"$IGNORE_FILE" | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
git commit -m "Add managed file" >/dev/null 2>&1 | |
git rm unformatted.cpp unformatted2.cpp >/dev/null 2>&1 | |
(RUN_TESTS=false main check 2>&1) | |
assertEquals "$?" 0 | |
} | |
function test_enforce_style_enforce_succeeds_when_there_are_unformatted_managed_files() { | |
setup_test_repo | |
git add unformatted.cpp >/dev/null 2>&1 | |
(RUN_TESTS=false main enforce 2>/dev/null) | |
assertEquals 0 $? | |
} | |
function test_enforce_style_ignored_renamed_file_stays_ignored() { | |
setup_test_repo | |
git add unformatted.cpp >/dev/null 2>&1 | |
echo "/unformatted.cpp" >"$IGNORE_FILE" | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
(RUN_TESTS=false main check &>/dev/null) | |
assertEquals "$?" 0 | |
git commit -m "Add ignored file" >/dev/null 2>&1 | |
git mv unformatted.cpp unformatted2.cpp >/dev/null 2>&1 | |
(RUN_TESTS=false main check &>/dev/null) | |
assertEquals "$?" 0 | |
} | |
function test_enforce_ignores_new_files_if_the_config_is_set() { | |
setup_test_repo | |
git add unformatted.cpp >/dev/null 2>&1 | |
echo "" >"$IGNORE_FILE" | |
git add "$IGNORE_FILE" >/dev/null 2>&1 | |
IGNORE_NEW_FILES=true | |
(RUN_TESTS=false main check &>/dev/null) | |
assertEquals "$?" 0 | |
IGNORE_NEW_FILES=false | |
(RUN_TESTS=false main check &>/dev/null) | |
assertNotEquals "$?" 0 | |
# No error, because the file is ignored | |
IGNORE_NEW_FILES=true | |
(RUN_TESTS=false main enforce &>/dev/null) | |
assertEquals "$?" 0 | |
# No error, because the previous enforce run added the file to the ignore file | |
IGNORE_NEW_FILES=false | |
(RUN_TESTS=false main check &>/dev/null) | |
assertEquals "$?" 0 | |
echo "" >"$IGNORE_FILE" | |
git add "$IGNORE_FILE" &>/dev/null | |
# No error, because the file gets fixed | |
IGNORE_NEW_FILES=false | |
(RUN_TESTS=false main enforce &>/dev/null) | |
assertEquals "$?" 0 | |
# No error, because the file got fixed | |
IGNORE_NEW_FILES=false | |
(RUN_TESTS=false main check &>/dev/null) | |
assertEquals "$?" 0 | |
} | |
function setUp { | |
PREVIOUS_WORKING_DIR=$(pwd) | |
PREVIOUS_IFS="$IFS" | |
reset_state | |
reset_config | |
populate_clang_format | |
} | |
function tearDown { | |
# Restore working directory | |
cd "$PREVIOUS_WORKING_DIR" || exit 1 | |
IFS="$PREVIOUS_IFS" | |
} | |
# Get the file and line of the callers caller. | |
function caller_line() { | |
local line | |
local file | |
line=$(caller 1 | awk '{print $1}') | |
file="${BASH_SOURCE[0]}" | |
echo "$file:$line" | |
} | |
# Usage: assertArrayContains <needle> <haystack> | |
function assertArrayContains { | |
local expected="$1" | |
shift | |
local actual=("$@") | |
local element | |
local found=false | |
for element in "${actual[@]}"; do | |
if [[ "$element" == "$expected" ]]; then | |
found=true | |
break | |
fi | |
done | |
assertTrue " $(caller_line): Expected '$expected' to be in the array but it is not." "$found" | |
} | |
# Usage: assertArrayNotContains <needle> <haystack> | |
function assertArrayNotContains { | |
local expected="$1" | |
shift | |
local actual=("$@") | |
local element | |
local found=true | |
for element in "${actual[@]}"; do | |
if [[ "$element" == "$expected" ]]; then | |
found=false | |
break | |
fi | |
done | |
assertTrue " $(caller_line): Expected '$expected' not to be in the array but it is." "$found" | |
} | |
function test_populate_clang_format() { | |
populate_clang_format | |
assertEquals "$(basename "$CLANG_FORMAT")" clang-format | |
} | |
# shellcheck source=/dev/null | |
. shunit2 | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment