[gjs: 2/5] tools: Add clang-format pre-commit hook
- From: Philip Chimento <pchimento src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs: 2/5] tools: Add clang-format pre-commit hook
- Date: Wed, 11 Jul 2018 09:56:47 +0000 (UTC)
commit dbd555e865e73ee37bff077328328f920dc6f98c
Author: Philip Chimento <philip chimento gmail com>
Date: Mon Jun 18 18:18:59 2018 -0700
tools: Add clang-format pre-commit hook
Taken from https://github.com/barisione/clang-format-hooks/
This pre-commit hook will automatically format any newly committed C++
code.
Closes: #172
.clang-format | 24 ++++
doc/Hacking.md | 10 ++
tools/apply-format | 321 +++++++++++++++++++++++++++++++++++++++++
tools/git-pre-commit-format | 344 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 699 insertions(+)
---
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 00000000..46bb09ec
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,24 @@
+---
+# Global Options Go Here
+IndentWidth: 4
+ColumnLimit: 80
+---
+Language: Cpp
+BasedOnStyle: Google
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+ForEachMacros: []
+IndentWidth: 4
+---
+# We rely mostly on eslint for JavaScript linting, but this is a lax collection
+# of rules that will auto-fix some JS things. We really should use eslint --fix
+# instead, but we need to find a way to get that to operate on diffs like
+# clang-format does.
+Language: JavaScript
+AlignAfterOpenBracket: DontAlign
+AllowShortFunctionsOnASingleLine: false
+JavaScriptQuotes: Single
+PenaltyBreakBeforeFirstCallParameter: 1000000
+PenaltyExcessCharacter: 1
+SpacesInContainerLiterals: false
+...
diff --git a/doc/Hacking.md b/doc/Hacking.md
index 40710ad3..29a5b7ab 100644
--- a/doc/Hacking.md
+++ b/doc/Hacking.md
@@ -2,6 +2,16 @@
## Setting up ##
+We use the
+[Google style guide](https://google.github.io/styleguide/cppguide.html)
+for C++ code, with a few exceptions, 4-space indents being the main one.
+There is a handy git commit hook that will autoformat your code when you
+commit it.
+In your GJS checkout directory, run
+`tools/git-pre-commit-format install`.
+For more information, see
+<https://github.com/barisione/clang-format-hooks/>.
+
For the time being, we recommend using JHBuild to develop GJS.
Follow the [instructions from GNOME](https://wiki.gnome.org/HowDoI/Jhbuild) for
[JHBuild](https://git.gnome.org/browse/jhbuild/).
diff --git a/tools/apply-format b/tools/apply-format
new file mode 100755
index 00000000..9ea3ead5
--- /dev/null
+++ b/tools/apply-format
@@ -0,0 +1,321 @@
+#! /bin/bash
+#
+# Copyright 2018 Undo Ltd.
+#
+# https://github.com/barisione/clang-format-hooks
+
+# Force variable declaration before access.
+set -u
+# Make any failure in piped commands be reflected in the exit code.
+set -o pipefail
+
+readonly bash_source="${BASH_SOURCE[0]:-$0}"
+
+##################
+# Misc functions #
+##################
+
+function error_exit() {
+ for str in "$@"; do
+ echo -n "$str" >&2
+ done
+ echo >&2
+
+ exit 1
+}
+
+
+########################
+# Command line parsing #
+########################
+
+function show_help() {
+ if [ -t 1 ] && hash tput 2> /dev/null; then
+ local -r b=$(tput bold)
+ local -r i=$(tput sitm)
+ local -r n=$(tput sgr0)
+ else
+ local -r b=
+ local -r i=
+ local -r n=
+ fi
+
+ cat << EOF
+${b}SYNOPSIS${n}
+
+ To reformat git diffs:
+
+ ${i}$bash_source [OPTIONS] [FILES-OR-GIT-DIFF-OPTIONS]${n}
+
+ To reformat whole files, including unchanged parts:
+
+ ${i}$bash_source [-f | --whole-file] FILES${n}
+
+${b}DESCRIPTION${n}
+
+ Reformat C or C++ code to match a specified formatting style.
+
+ This command can either work on diffs, to reformat only changed parts of
+ the code, or on whole files (if -f or --whole-file is used).
+
+ ${b}FILES-OR-GIT-DIFF-OPTIONS${n}
+ List of files to consider when applying clang-format to a diff. This is
+ passed to "git diff" as is, so it can also include extra git options or
+ revisions.
+ For example, to apply clang-format on the changes made in the last few
+ revisions you could use:
+ ${i}\$ $bash_source HEAD~3${n}
+
+ ${b}FILES${n}
+ List of files to completely reformat.
+
+ ${b}-f, --whole-file${n}
+ Reformat the specified files completely (including parts you didn't
+ change).
+ The patch is printed on stdout by default. Use -i if you want to modify
+ the files on disk.
+
+ ${b}--staged, --cached${n}
+ Reformat only code which is staged for commit.
+ The patch is printed on stdout by default. Use -i if you want to modify
+ the files on disk.
+
+ ${b}-i${n}
+ Reformat the code and apply the changes to the files on disk (instead
+ of just printing the patch on stdout).
+
+ ${b}--apply-to-staged${n}
+ This is like specifying both --staged and -i, but the formatting
+ changes are also staged for commit (so you can just use "git commit"
+ to commit what you planned to, but formatted correctly).
+
+ ${b}--style STYLE${n}
+ The style to use for reformatting code.
+ If no style is specified, then it's assumed there's a .clang-format
+ file in the current directory or one of its parents.
+
+ ${b}--help, -h, -?${n}
+ Show this help.
+EOF
+}
+
+# getopts doesn't support long options.
+# getopt mangles stuff.
+# So we parse manually...
+declare positionals=()
+declare has_positionals=false
+declare whole_file=false
+declare apply_to_staged=false
+declare staged=false
+declare in_place=false
+declare style=file
+while [ $# -gt 0 ]; do
+ declare arg="$1"
+ shift # Past option.
+ case "$arg" in
+ -h | -\? | --help )
+ show_help
+ exit 0
+ ;;
+ -f | --whole-file )
+ whole_file=true
+ ;;
+ --apply-to-staged )
+ apply_to_staged=true
+ ;;
+ --cached | --staged )
+ staged=true
+ ;;
+ -i )
+ in_place=true
+ ;;
+ --style=* )
+ style="${arg//--style=/}"
+ ;;
+ --style )
+ [ $# -gt 0 ] || \
+ error_exit "No argument for --style option."
+ style="$1"
+ shift
+ ;;
+ -- )
+ # Stop processing further arguments.
+ if [ $# -gt 0 ]; then
+ positionals+=("$@")
+ has_positionals=true
+ fi
+ break
+ ;;
+ -* )
+ error_exit "Unknown argument: $arg"
+ ;;
+ *)
+ positionals+=("$arg")
+ ;;
+ esac
+done
+
+# Restore positional arguments, access them from "$@".
+if [ ${#positionals[@]} -gt 0 ]; then
+ set -- "${positionals[@]}"
+ has_positionals=true
+fi
+
+[ -n "$style" ] || \
+ error_exit "If you use --style you need to speficy a valid style."
+
+#######################################
+# Detection of clang-format & friends #
+#######################################
+
+# clang-format.
+declare format="${CLANG_FORMAT:-}"
+if [ -z "$format" ]; then
+ format=$(type -p clang-format)
+fi
+
+if [ -z "$format" ]; then
+ error_exit \
+ $'You need to install clang-format.\n' \
+ $'\n' \
+ $'On Ubuntu/Debian this is available in the clang-format package or, in\n' \
+ $'older distro versions, clang-format-VERSION.\n' \
+ $'On Fedora it\'s available in the clang package.\n' \
+ $'You can also speficy your own path for clang-format by setting the\n' \
+ $'$CLANG_FORMAT environment variable.'
+fi
+
+# clang-format-diff.
+if [ "$whole_file" = false ]; then
+ invalid="/dev/null/invalid/path"
+ if [ "${OSTYPE:-}" = "linux-gnu" ]; then
+ readonly sort_version=-V
+ else
+ # On macOS, sort doesn't have -V.
+ readonly sort_version=-n
+ fi
+ declare paths_to_try=()
+ # .deb packages directly from upstream.
+ # We try these first as they are probably newer than the system ones.
+ while read -r f; do
+ paths_to_try+=("$f")
+ done < <(compgen -G "/usr/share/clang/clang-format-*/clang-format-diff.py" | sort "$sort_version" -r)
+ # LLVM official releases (just untarred in /usr/local).
+ while read -r f; do
+ paths_to_try+=("$f")
+ done < <(compgen -G "/usr/local/clang+llvm*/share/clang/clang-format-diff.py" | sort "$sort_version" -r)
+ # Maybe it's in the $PATH already? This is true for Ubuntu and Debian.
+ paths_to_try+=( \
+ "$(type -p clang-format-diff 2> /dev/null || echo "$invalid")" \
+ "$(type -p clang-format-diff.py 2> /dev/null || echo "$invalid")" \
+ )
+ # Fedora.
+ paths_to_try+=( \
+ /usr/share/clang/clang-format-diff.py \
+ )
+ # Gentoo.
+ while read -r f; do
+ paths_to_try+=("$f")
+ done < <(compgen -G "/usr/lib/llvm/*/share/clang/clang-format-diff.py" | sort -n -r)
+ # Homebrew.
+ while read -r f; do
+ paths_to_try+=("$f")
+ done < <(compgen -G "/usr/local/Cellar/clang-format/*/share/clang/clang-format-diff.py" | sort -n -r)
+
+ declare format_diff=
+
+ # Did the user specify a path?
+ if [ -n "${CLANG_FORMAT_DIFF:-}" ]; then
+ format_diff="$CLANG_FORMAT_DIFF"
+ else
+ for path in "${paths_to_try[@]}"; do
+ if [ -e "$path" ]; then
+ # Found!
+ format_diff="$path"
+ if [ ! -x "$format_diff" ]; then
+ format_diff="python $format_diff"
+ fi
+ break
+ fi
+ done
+ fi
+
+ if [ -z "$format_diff" ]; then
+ error_exit \
+ $'Cannot find clang-format-diff which should be shipped as part of the same\n' \
+ $'package where clang-format is.\n' \
+ $'\n' \
+ $'Please find out where clang-format-diff is in your distro and report an issue\n' \
+ $'at https://github.com/barisione/clang-format-hooks/issues with details about\n' \
+ $'your operating system and setup.\n' \
+ $'\n' \
+ $'You can also speficy your own path for clang-format-diff by setting the\n' \
+ $'$CLANG_FORMAT_DIFF environment variable, for instance:\n' \
+ $'\n' \
+ $' CLANG_FORMAT_DIFF="python /.../clang-format-diff.py" \\\n' \
+ $' ' "$bash_source"
+ fi
+
+ readonly format_diff
+fi
+
+
+############################
+# Actually run the command #
+############################
+
+if [ "$whole_file" = true ]; then
+
+ [ "$has_positionals" = true ] || \
+ error_exit "No files to reformat specified."
+ [ "$staged" = false ] || \
+ error_exit "--staged/--cached only make sense when applying to a diff."
+
+ read -r -a format_args <<< "$format"
+ format_args+=("-style=file")
+ [ "$in_place" = true ] && format_args+=("-i")
+
+ "${format_args[@]}" "$@"
+
+else # Diff-only.
+
+ if [ "$apply_to_staged" = true ]; then
+ [ "$staged" = false ] || \
+ error_exit "You don't need --staged/--cached with --apply-to-staged."
+ [ "$in_place" = false ] || \
+ error_exit "You don't need -i with --apply-to-staged."
+ staged=true
+ readonly patch_dest=$(mktemp)
+ trap '{ rm -f "$patch_dest"; }' EXIT
+ else
+ readonly patch_dest=/dev/stdout
+ fi
+
+ declare git_args=(git diff -U0 --no-color)
+ [ "$staged" = true ] && git_args+=("--staged")
+
+ # $format_diff may contain a command ("python") and the script to excute, so we
+ # need to split it.
+ read -r -a format_diff_args <<< "$format_diff"
+ [ "$in_place" = true ] && format_diff_args+=("-i")
+
+ "${git_args[@]}" "$@" \
+ | "${format_diff_args[@]}" \
+ -p1 \
+ -style="$style" \
+ -iregex='^.*\.(c|cpp|cxx|cc|h|m|mm|js|java)$' \
+ > "$patch_dest" \
+ || exit 1
+
+ if [ "$apply_to_staged" = true ]; then
+ if [ ! -s "$patch_dest" ]; then
+ echo "No formatting changes to apply."
+ exit 0
+ fi
+ patch -p0 < "$patch_dest" || \
+ error_exit "Cannot apply patch to local files."
+ git apply -p0 --cached < "$patch_dest" || \
+ error_exit "Cannot apply patch to git staged changes."
+ fi
+
+fi
diff --git a/tools/git-pre-commit-format b/tools/git-pre-commit-format
new file mode 100755
index 00000000..36495093
--- /dev/null
+++ b/tools/git-pre-commit-format
@@ -0,0 +1,344 @@
+#! /bin/bash
+#
+# Copyright 2018 Undo Ltd.
+#
+# https://github.com/barisione/clang-format-hooks
+
+# Force variable declaration before access.
+set -u
+# Make any failure in piped commands be reflected in the exit code.
+set -o pipefail
+
+readonly bash_source="${BASH_SOURCE[0]:-$0}"
+
+if [ -t 1 ] && hash tput 2> /dev/null; then
+ readonly b=$(tput bold)
+ readonly i=$(tput sitm)
+ readonly n=$(tput sgr0)
+else
+ readonly b=
+ readonly i=
+ readonly n=
+fi
+
+function error_exit() {
+ for str in "$@"; do
+ echo -n "$b$str$n" >&2
+ done
+ echo >&2
+
+ exit 1
+}
+
+# realpath is not available everywhere.
+function realpath() {
+ if [ "${OSTYPE:-}" = "linux-gnu" ]; then
+ readlink -m "$@"
+ else
+ # Python should always be available on macOS.
+ # We use sys.stdout.write instead of print so it's compatible with both Python 2 and 3.
+ python -c "import sys; import os.path; sys.stdout.write(os.path.realpath('''$1''') + '\\n')"
+ fi
+}
+
+# realpath --relative-to is only available on recent Linux distros.
+# This function behaves identical to Python's os.path.relpath() and doesn't need files to exist.
+function rel_realpath() {
+ local -r path=$(realpath "$1")
+ local -r rel_to=$(realpath "${2:-$PWD}")
+
+ # Split the paths into components.
+ IFS='/' read -r -a path_parts <<< "$path"
+ IFS='/' read -r -a rel_to_parts <<< "$rel_to"
+
+ # Search for the first different component.
+ for ((idx=1; idx<${#path_parts[@]}; idx++)); do
+ if [ "${path_parts[idx]}" != "${rel_to_parts[idx]:-}" ]; then
+ break
+ fi
+ done
+
+ result=()
+ # Add the required ".." to the $result array.
+ local -r first_different_idx="$idx"
+ for ((idx=first_different_idx; idx<${#rel_to_parts[@]}; idx++)); do
+ result+=("..")
+ done
+ # Add the required components from $path.
+ for ((idx=first_different_idx; idx<${#path_parts[@]}; idx++)); do
+ result+=("${path_parts[idx]}")
+ done
+
+ if [ "${#result[@]}" -gt 0 ]; then
+ # Join the array with a "/" as separator.
+ echo "$(export IFS='/'; echo "${result[*]}")"
+ else
+ echo .
+ fi
+}
+
+# Find the top-level git directory (taking into account we could be in a submodule).
+declare git_test_dir=.
+declare top_dir
+while true; do
+ top_dir=$(cd "$git_test_dir" && git rev-parse --show-toplevel) || \
+ error_exit "You need to be in the git repository to run this script."
+
+ [ -e "$top_dir/.git" ] || \
+ error_exit "No .git directory in $top_dir."
+
+ if [ -d "$top_dir/.git" ]; then
+ # We are done! top_dir is the root git directory.
+ break
+ else
+ # We are in a submodule if .git is a file!
+ git_test_dir="$git_test_dir/.."
+ fi
+done
+
+readonly top_dir
+
+hook_path="$top_dir/.git/hooks/pre-commit"
+readonly hook_path
+
+me=$(realpath "$bash_source") || exit 1
+readonly me
+
+me_relative_to_hook=$(rel_realpath "$me" "$(dirname "$hook_path")") || exit 1
+readonly me_relative_to_hook
+
+my_dir=$(dirname "$me") || exit 1
+readonly my_dir
+
+apply_format="$my_dir/apply-format"
+readonly apply_format
+
+apply_format_relative_to_top_dir=$(rel_realpath "$apply_format" "$top_dir") || exit 1
+readonly apply_format_relative_to_top_dir
+
+function is_installed() {
+ if [ ! -e "$hook_path" ]; then
+ echo nothing
+ else
+ existing_hook_target=$(realpath "$hook_path") || exit 1
+ readonly existing_hook_target
+
+ if [ "$existing_hook_target" = "$me" ]; then
+ # Already installed.
+ echo installed
+ else
+ # There's a hook, but it's not us.
+ echo different
+ fi
+ fi
+}
+
+function install() {
+ if ln -s "$me_relative_to_hook" "$hook_path" 2> /dev/null; then
+ echo "Pre-commit hook installed."
+ else
+ local -r res=$(is_installed)
+ if [ "$res" = installed ]; then
+ error_exit "The hook is already installed."
+ elif [ "$res" = different ]; then
+ error_exit "There's already an existing pre-commit hook, but for something else."
+ elif [ "$res" = nothing ]; then
+ error_exit "There's no pre-commit hook, but we couldn't create a symlink."
+ else
+ error_exit "Unexpected failure."
+ fi
+ fi
+}
+
+function uninstall() {
+ local -r res=$(is_installed)
+ if [ "$res" = installed ]; then
+ rm "$hook_path" || \
+ error_exit "Couldn't remove the pre-commit hook."
+ elif [ "$res" = different ]; then
+ error_exit "There's a pre-commit hook installed, but for something else. Not removing."
+ elif [ "$res" = nothing ]; then
+ error_exit "There's no pre-commit hook, nothing to uninstall."
+ else
+ error_exit "Unexpected failure detecting the pre-commit hook status."
+ fi
+}
+
+function show_help() {
+ cat << EOF
+${b}SYNOPSIS${n}
+
+ $bash_source [install|uninstall]
+
+${b}DESCRIPTION${n}
+
+ Git hook to verify and fix formatting before committing.
+
+ The script is invoked automatically when you commit, so you need to call it
+ directly only to set up the hook or remove it.
+
+ To setup the hook run this script passing "install" on the command line.
+ To remove the hook run passing "uninstall".
+
+${b}CONFIGURATION${n}
+
+ You can configure the hook using the "git config" command.
+
+ ${b}hooks.clangFormatDiffInteractive${n} (default: true)
+ By default, the hook requires user input. If you don't run git from a
+ terminal, you can disable the interactive prompt with:
+ ${i}\$ git config hooks.clangFormatDiffInteractive false${n}
+
+ ${b}hooks.clangFormatDiffStyle${n} (default: file)
+ Unless a different style is specified, the hook expects a file named
+ .clang-format to exist in the repository. This file should contain the
+ configuration for clang-format.
+ You can specify a different style (in this example, the WebKit one)
+ with:
+ ${i}\$ git config hooks.clangFormatDiffStyle WebKit${n}
+EOF
+}
+
+if [ $# = 1 ]; then
+ case "$1" in
+ -h | -\? | --help )
+ show_help
+ exit 0
+ ;;
+ install )
+ install
+ exit 0
+ ;;
+ uninstall )
+ uninstall
+ exit 0
+ ;;
+ esac
+fi
+
+[ $# = 0 ] || error_exit "Invalid arguments: $*"
+
+
+# This is a real run of the hook, not a install/uninstall run.
+
+if [ -z "${GIT_DIR:-}" ] && [ -z "${GIT_INDEX_FILE:-}" ]; then
+ error_exit \
+ $'It looks like you invoked this script directly, but it\'s supposed to be used\n' \
+ $'as a pre-commit git hook.\n' \
+ $'\n' \
+ $'To install the hook try:\n' \
+ $' ' "$bash_source" $' install\n' \
+ $'\n' \
+ $'For more details on this script try:\n' \
+ $' ' "$bash_source" $' --help\n'
+fi
+
+[ -x "$apply_format" ] || \
+ error_exit \
+ $'Cannot find the apply-format script.\n' \
+ $'I expected it here:\n' \
+ $' ' "$apply_format"
+
+readonly style=$(cd "$top_dir" && git config hooks.clangFormatDiffStyle || echo file)
+
+readonly patch=$(mktemp)
+trap '{ rm -f "$patch"; }' EXIT
+"$apply_format" --style="$style" --cached > "$patch" || \
+ error_exit $'\nThe apply-format script failed.'
+
+if [ "$(wc -l < "$patch")" -eq 0 ]; then
+ echo "The staged content is formatted correctly."
+ exit 0
+fi
+
+
+# The code is not formatted correctly.
+
+interactive=$(cd "$top_dir" && git config --bool hooks.clangFormatDiffInteractive)
+if [ "$interactive" != false ]; then
+ # Interactive is the default, so anything that is not false is converted to
+ # true, including possibly invalid values.
+ interactive=true
+fi
+readonly interactive
+
+if [ "$interactive" = false ]; then
+ echo "${b}The staged content is not formatted correctly.${n}"
+ echo "You can fix the formatting with:"
+ echo " ${i}\$ ./$apply_format_relative_to_top_dir --apply-to-staged${n}"
+ echo
+ echo "You can also make this script interactive (if you use git from a terminal) with:"
+ echo " ${i}\$ git config hooks.clangFormatDiffInteractive true${n}"
+ exit 1
+fi
+
+if hash colordiff 2> /dev/null; then
+ colordiff < "$patch"
+else
+ echo "${b}(Install colordiff to see this diff in color!)${n}"
+ echo
+ cat "$patch"
+fi
+
+echo
+echo "${b}The staged content is not formatted correctly.${n}"
+echo "The patch shown above can be applied automatically to fix the formatting."
+echo
+
+echo "You can:"
+echo " [a]: Apply the patch"
+echo " [f]: Force and commit anyway (not recommended!)"
+echo " [c]: Cancel the commit"
+echo " [?]: Show help"
+echo
+
+readonly tty=${PRE_COMMIT_HOOK_TTY:-/dev/tty}
+
+while true; do
+ echo -n "What would you like to do? [a/f/c/?] "
+ read -r answer < "$tty"
+ case "$answer" in
+
+ [aA] )
+ patch -p0 < "$patch" || \
+ error_exit \
+ $'\n' \
+ $'Cannot apply patch to local files.\n' \
+ $'Have you modified the file locally after starting the commit?'
+ git apply -p0 --cached < "$patch" || \
+ error_exit \
+ $'\n' \
+ $'Cannot apply patch to git staged changes.\n' \
+ $'This may happen if you have some overlapping unstaged changes. To solve\n' \
+ $'you need to stage or reset changes manually.'
+ ;;
+
+ [fF] )
+ echo
+ echo "Will commit anyway!"
+ echo "You can always abort by quitting your editor with no commit message."
+ echo
+ echo -n "Press return to continue."
+ read -r < "$tty"
+ exit 0
+ ;;
+
+ [cC] )
+ error_exit "Commit aborted as requested."
+ ;;
+
+ \? )
+ echo
+ show_help
+ echo
+ continue
+ ;;
+
+ * )
+ echo 'Invalid answer. Type "a", "f" or "c".'
+ echo
+ continue
+
+ esac
+ break
+done
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]