#!/usr/bin/env bash # clang-format gate. Whole repo has format debt (139/141 first-party files fail), # so default mode checks only lines CHANGED vs base. --all checks whole files. # Usage: tools/lint.sh [base-commit] | tools/lint.sh --all set -euo pipefail cd "$(dirname "$0")/.." CF="${CLANG_FORMAT:-$(command -v clang-format || true)}" if [ -z "$CF" ]; then echo "lint: clang-format not found (pip install clang-format)" >&2 exit 2 fi PATHSPEC=( '*.cpp' '*.hpp' '*.h' ':(exclude)ext/*' ':(exclude)java/*' ':(exclude)rustybits/*' ':(exclude)windows/*' ':(exclude)attic/*' ) fail=0 check() { # $1=file, rest=--lines args; empty args = whole file local f=$1 shift if ! "$CF" --dry-run --Werror "$@" "$f"; then fail=1 fi } if [ "${1:-}" = "--all" ]; then while IFS= read -r f; do check "$f"; done < <(git ls-files "${PATHSPEC[@]}") else base=${1:-} if [ -z "$base" ]; then base=$(git merge-base HEAD '@{upstream}' 2>/dev/null || echo HEAD~1) fi # tracked changed files: only changed line ranges (MR = modified/renamed content) while IFS= read -r f; do args=() while IFS= read -r hunk; do # new-side header is "+start,count" (git diff format), not "+start,end" [[ $hunk =~ \+([0-9]+)(,([0-9]+))? ]] || continue start=${BASH_REMATCH[1]} count=${BASH_REMATCH[3]:-1} end=$((start + count - 1)) if [ "$count" -eq 0 ]; then # deletion-only: nearest surviving line start=$((start > 1 ? start - 1 : 1)) end=$start fi args+=(--lines="$start:$end") done < <(git diff -U0 --no-color "$base" --diff-filter=ACMR -- "$f" | grep '^@@' || true) [ ${#args[@]} -eq 0 ] && continue check "$f" "${args[@]}" done < <(git diff --name-only --diff-filter=ACMR "$base" -- "${PATHSPEC[@]}") # untracked new files: whole file (no debt possible yet) while IFS= read -r f; do check "$f"; done < <(git ls-files --others --exclude-standard "${PATHSPEC[@]}") fi exit $fail