diff --git a/scripts/format-commit.sh b/scripts/format-commit.sh new file mode 100755 index 00000000..9690878c --- /dev/null +++ b/scripts/format-commit.sh @@ -0,0 +1,173 @@ +#!/bin/bash -e + +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2020 Patryk Obara + +readonly SCRIPT=$(basename "$0") + +print_usage () { + echo "usage: $SCRIPT [-V|--verify|-d|--diff|-a|--amend] []" + echo + echo "Fixes formatting of C and C++ files, that were touched by " + echo "commits since . Changes are limited only to files and " + echo "lines that were modified. If a file was moved/renamed and only " + echo "slightly modified (less than 10%), then it's not reformatted." + echo + echo "If no is passed, then default is HEAD~1 - which means " + echo "that only lines touched by the latest commit will be fixed, " + echo "therefore usage of this script is simply:" + echo + echo " $ ./$SCRIPT" + echo + echo "Files are formatted only if .clang-format file is located in one " + echo "of the parent directories of the source file." + echo + echo "Optional parameter --diff (or --verify) displays diff of what was " + echo "formatted and exits with a failure status if diff is not empty" + echo "(this option is intended for CI usage):" + echo + echo " $ ./$SCRIPT --diff" + echo + echo "Optional parameter --amend will amend the latest commit for you." + echo + echo " $ ./$SCRIPT --amend" + echo + echo "If you want to format a whole file instead then use clang-format " + echo "directly, e.g.:" + echo + echo " $ clang-format path/to/file.cpp" +} + +main () { + case $1 in + -h|-help|--help) print_usage ;; + -d|--diff) handle_dependencies ; shift ; format "$@" ; assert_empty_diff ;; + -V|--verify) handle_dependencies ; shift ; format "$@" ; assert_empty_diff ;; + -a|--amend) handle_dependencies ; shift ; format "$@" ; amend ;; + *) handle_dependencies ; format "$@" ; show_tip ;; + esac +} + +handle_dependencies () { + assert_min_version git 1007010 "Use git in version 1.7.10 or newer." + assert_min_version clang-format 8000000 "Use clang-format in version 8.0.0 or newer." +} + +assert_min_version () { + if ! check_min_version "$1" "$2" ; then + echo "$3" + exit 1 + fi +} + +check_min_version () { + $1 --version + $1 --version \ + | sed -e "s|.* \([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*|\1 \2 \3|" \ + | test_version "$2" + return "${PIPESTATUS[2]}" +} + +test_version () { + read -r major minor patch + local -r version=$((major * 1000000 + minor * 1000 + patch)) + test "$1" -le "$version" +} + +format () { + local -r since_ref=${1:-HEAD~1} + pushd "$(git rev-parse --show-toplevel)" > /dev/null + echo "Using paths relative to: $(pwd)" + find_cpp_files "$since_ref" | run_clang_format + popd > /dev/null +} + +assert_empty_diff () { + if [ -n "$(git_diff HEAD)" ] ; then + git_diff HEAD + echo + echo "clang-format formatted some code for you." + echo + echo "Run 'git commit -a --amend' to save the result." + exit 1 + fi +} + +show_tip () { + if [ -n "$(git_diff HEAD)" ] ; then + echo + echo "clang-format formatted some code for you." + echo + echo "Run 'git diff' to see what changed." + echo "Run 'git commit -a --amend' to save the result." + exit 0 + fi +} + +amend () { + git commit -a --amend +} + +git_diff () { + git diff --no-ext-diff "$@" +} + +find_cpp_files () { + set +e + list_changed_files "$1" | grep -E "\.(h|hpp|c|cpp|cc)$" + set -e +} + +list_changed_files () { + git_diff \ + --no-renames \ + --diff-filter=AMrcd \ + --find-renames=90% \ + --find-copies=90% \ + --name-only \ + "$1" +} + +run_clang_format () { + while read -r src_file ; do + prepare_clang_params "$src_file" \ + | xargs --no-run-if-empty --verbose clang-format -i + done +} + +git_diff_to_clang_line_range () { + local -r file=$1 + git_diff --ignore-space-at-eol -U0 HEAD~1 "$file" \ + | grep -E "^@@" \ + | filter_line_range \ + | to_clang_line_range +} + +prepare_clang_params () { + local -r file=$1 + local -r range=$(git_diff_to_clang_line_range "$file") + # print file name only when there are any lines detected, otherwise + # clang-format would process the whole file + if [ -n "$range" ]; then + echo "$range \"$file\"" + fi +} + +# expects line in diff format: "@@ - + @@ " +# where is either or , +# +filter_line_range () { + sed -e 's|@@ .* +\([0-9]\+\),\?\([0-9]\+\)\? @@.*|\1 \2|' +} + +to_clang_line_range () { + while read -r from_line offset ; do + local to_line=$(( from_line + offset )) + if [[ $from_line -gt 0 && $from_line -le $to_line ]] ; then + echo "-lines=$from_line:$to_line" + fi + done +} + +main "$@"