[gjs: 2/5] tools: Add clang-format pre-commit hook



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]