Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/git-hooks/pre-commit
12525 views
#!/usr/bin/env bash

# A pre-commit hook for SageMath
#
# Consider activating it so that your code is automatically checked before each
# commit:
#
#   $ git config core.hooksPath git-hooks

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

# Text formatting
RED='\e[1;31m'
GRN="\e[1;32m"
YLLW='\e[1;33m'
BLUE='\e[1;34m'
NRM='\e[0m'  # normal

display() {
  case "$1" in
    ruff)         printf "${YLLW}ruff${NRM}" ;;
    cython-lint)  printf "${GRN}cython-lint${NRM}" ;;
    clang-format) printf "${BLUE}clang-format${NRM}" ;;
  esac
}

# ---------------------------------------------------------------------------
# Init
# ---------------------------------------------------------------------------

init_file_lists() {
  python_files=$(git diff --staged --name-only | grep "\.py$\|\.pyi$")
  cython_files=$(git diff --staged --name-only | grep "\.pyx$\|\.pxd$\|\.pxi$")
  c_files=$(git diff --staged --name-only | grep "\.c$\|\.cpp$\|\.h$")
  [ -z "$python_files" ] && [ -z "$cython_files" ] && [ -z "$c_files" ] && exit 0
}

init_tools() {
  _check_tool() {
    command -v "$1" > /dev/null && return 0
    if [ "$mode" == "check-only" ]; then
      local tool=$(display "$1")
      printf "${YLLW}Warning${NRM}: $tool not found; skipping related checks.\n"
    fi
    return 1
  }

  ruff_flag=false
  cython_lint_flag=false
  clang_format_flag=false

  [ -n "$python_files" ] && _check_tool ruff         && ruff_flag=true
  [ -n "$cython_files" ] && _check_tool cython-lint  && cython_lint_flag=true
  [ -n "$c_files" ]      && _check_tool clang-format && clang_format_flag=true
}

# ---------------------------------------------------------------------------
# Tool config and common dispatcher
# ---------------------------------------------------------------------------

check_flags() {
  # E501: line too long
  # E741: ambiguous variable name
  case "$1" in
    ruff)         echo "check --config .github/workflows/ruff.toml --preview" ;;
    cython-lint)  echo "--ignore=E501,E741" ;;
    clang-format) echo "--dry-run --Werror" ;;
  esac
}

format_flags() {
  case "$1" in
    ruff)         echo "check --config .github/workflows/ruff.toml --preview -q --fix" ;;
    clang-format) echo "-i --Werror" ;;
  esac
}

files() {
  case "$1" in
    ruff)         echo "$python_files" ;;
    cython-lint)  echo "$cython_files" ;;
    clang-format) echo "$c_files" ;;
  esac
}

# run <check|format> <tool>
run() {
  local action="$1" tool="$2"
  local display=$(display $tool)

  if [ "$action" = "check" ]; then
    printf "%s: Checking code formatting..." "$display"
    if [ "$verbose" = true ]; then
      printf "\n"
      $tool $(check_flags $tool) $(files $tool)
    elif ! $tool $(check_flags $tool) $(files $tool) > /dev/null 2>&1; then
      printf " failure.\n" && return 1
    else
      printf " success.\n"
    fi

  elif [ "$action" = "format" ]; then
    if [ $tool == "cython-lint" ]; then
      printf "$display: auto-formatting not supported.\n"
      return 0
    fi
    printf "%s: Auto-formatting..." "$display"
    if ! $tool $(check_flags $tool) $(files $tool) > /dev/null 2>&1; then
      $tool $(format_flags $tool) $(files $tool)
      printf " check the unstaged changes.\n" && return 1
    fi
    printf " no changes required.\n"
  fi
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

mode="check-only"
verbose=false

for arg in "$@"; do
  case "$arg" in
    in-place) mode="in-place" ;;
    verbose)  verbose=true ;;
    *)
      printf "${RED}Error${NRM}: Unknown argument: $arg\n"
      exit 1
      ;;
  esac
done

init_file_lists
init_tools

action="check"
[ "$mode" = "in-place" ] && action="format"

changes=false
$ruff_flag         && { run "$action" ruff         || changes=true; }
$cython_lint_flag  && { run "$action" cython-lint  || changes=true; }
$clang_format_flag && { run "$action" clang-format || changes=true; }

if [ "$mode" = "check-only" ] && $changes; then
  printf "${RED}Error${NRM}: Ill-formatted files staged for commit.\n\
To view the issues, use 'git hook run pre-commit -- verbose'.\n\
To attempt to auto-format the files, use 'git hook run pre-commit -- in-place'.\n\
To avoid these pre-commit checks, use 'git commit --no-verify'.\n"
  exit 1
fi