diff --git a/.gitignore b/.gitignore index b418cc8eff..989fc31f59 100644 --- a/.gitignore +++ b/.gitignore @@ -150,6 +150,9 @@ src/config # codespell dictionary scripts/dictionary/ +# marker files for pre-commit and pre-push hooks to check if staged files have been formatted +scripts/.format_markers/ + # clangd for nvim lsp src/.clangd diff --git a/environment_setup/setup_software.sh b/environment_setup/setup_software.sh index c2c65d90ec..f3701b7087 100755 --- a/environment_setup/setup_software.sh +++ b/environment_setup/setup_software.sh @@ -18,6 +18,8 @@ CURR_DIR=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")") cd "$CURR_DIR" || exit +git config core.hooksPath "$CURR_DIR/../scripts/githooks" + source util.sh g_arch=$(uname -m) # Global variable. No function should use this name. diff --git a/scripts/githooks/pre-commit b/scripts/githooks/pre-commit new file mode 100755 index 0000000000..7d1c1bac7c --- /dev/null +++ b/scripts/githooks/pre-commit @@ -0,0 +1,35 @@ +#!/bin/sh + +# From the list of files on stdin (separated by newline), return formattable files to stdout +filter_formattable_files() { + grep -E '\.(h|cpp|c|cc|hpp|tpp|proto|py|md|yml)$|/BUILD$' +} + +# Output the modification time of a file (or 0 if nonexistent) +get_mtime() { + case "$(uname -s)" in + Darwin*) + stat -f %m "$1" 2> /dev/null || echo 0 + ;; + *) + # No one will run this on BSD or other Unixes, so this should be fine + stat -c %Y "$1" 2> /dev/null || echo 0 + ;; + esac +} + +# Find repo root, marker for the current branch, and marker mtime (0 if marker does not exist) +toplevel="$(git rev-parse --show-toplevel)" +marker="$toplevel/scripts/.format_markers/$(git rev-parse --abbrev-ref HEAD)" +mtime=$(get_mtime "$marker") + +git diff --staged --name-only | filter_formattable_files | while read -r file; do + # Compare marker mtime with file mtime + if [ $mtime -lt $(get_mtime "$toplevel/$file") ]; then + # The file was modified AFTER formatting. Remove the marker to tell the pre-push hook + # that this commit introduces unformatted code. + rm -f "$marker" 2> /dev/null + echo "Warning: Code is unformatted." + break + fi +done diff --git a/scripts/githooks/pre-push b/scripts/githooks/pre-push new file mode 100755 index 0000000000..d2cb527959 --- /dev/null +++ b/scripts/githooks/pre-push @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# Prompt user for confirmation at a [y/N] prompt and return 0 when user responds "yes" or "y" +confirm() { + read -r -p "$1 [y/N] " response < /dev/tty + case "$response" in + [yY][eE][sS]|[yY]) + return 0 + ;; + *) + return 1 + ;; + esac +} + +scriptsdir="$(git rev-parse --show-toplevel)/scripts" + +# If marker for current branch does not exist +if ! [ -f "$scriptsdir/.format_markers/$(git rev-parse --abbrev-ref HEAD)" ]; then + echo "Warning: Code is unformatted. Please run $scriptsdir/lint_and_format.sh." + if confirm 'Continue without formatting?'; then + echo "Continuing push anyway." + exit 0 + else + echo "Aborting push." + exit 1 + fi +fi diff --git a/scripts/lint_and_format.sh b/scripts/lint_and_format.sh index deadf465eb..54c138a72e 100755 --- a/scripts/lint_and_format.sh +++ b/scripts/lint_and_format.sh @@ -149,4 +149,10 @@ run_eof_new_line run_git_diff_check run_ansible_lint +# Update markers, telling Git hooks that formatting has been done +# (Per-branch, so switching branches doesn't confuse the hooks) +branch="$(git rev-parse --abbrev-ref HEAD)" +mkdir -p "$CURR_DIR/.format_markers/$(dirname "$branch")" +touch "$CURR_DIR/.format_markers/${branch}" + exit 0