#!/usr/bin/env bash # Code formatter - runs targeted formatters based on what changed from trunk. # Usage: format.sh [--all] [--pre-commit] [--pre-push] [--lint] # (default) Check all changes relative to trunk including uncommitted work # --all Format everything, skip change detection (previous behavior) # --pre-commit Only check staged changes # --pre-push Only check committed changes relative to trunk # --lint Also run linters before formatting set -eufo pipefail run_lint=false format_all=false mode="default" for arg in "$@"; do case "$arg" in --lint) run_lint=true ;; --all) format_all=true ;; --pre-commit|--pre-push) [[ "$mode" == "default" ]] || { echo "Cannot use both --pre-commit and --pre-push" >&2; exit 1; } mode="${arg#--}" ;; *) echo "Unknown option: $arg" >&2 echo "Usage: $0 [--all] [--pre-commit] [--pre-push] [--lint]" >&2 exit 1 ;; esac done section() { echo "- $*" >&2 } # Find what's changed compared to trunk (skip if --all) trunk_ref="$(git rev-parse --verify trunk 2>/dev/null || echo "")" if [[ "$format_all" == "false" && -n "$trunk_ref" ]]; then base="$(git merge-base HEAD "$trunk_ref" 2>/dev/null || echo "")" if [[ -n "$base" ]]; then case "$mode" in pre-commit) changed="$(git diff --name-only --cached)" ;; pre-push) changed="$(git diff --name-only "$base" HEAD)" ;; default) committed="$(git diff --name-only "$base" HEAD)" staged="$(git diff --name-only --cached)" unstaged="$(git diff --name-only)" untracked="$(git ls-files --others --exclude-standard)" changed="$(printf '%s\n%s\n%s\n%s' "$committed" "$staged" "$unstaged" "$untracked" | sort -u)" ;; esac else format_all=true fi elif [[ "$format_all" == "false" ]]; then # No trunk ref found, format everything format_all=true fi # Helper to check if a pattern matches changed files changed_matches() { [[ "$format_all" == "true" ]] || echo "$changed" | grep -qE "$1" } WORKSPACE_ROOT="$(bazel info workspace)" # Capture baseline to detect formatter-introduced changes (allows pre-existing uncommitted work) baseline="$(git status --porcelain)" # Always run buildifier and copyright section "Buildifier" echo " buildifier" >&2 bazel run //:buildifier section "Copyright" echo " update_copyright" >&2 bazel run //scripts:update_copyright # Run language formatters only if those files changed if changed_matches '^java/'; then section "Java" echo " google-java-format" >&2 GOOGLE_JAVA_FORMAT="$(bazel run --run_under=echo //scripts:google-java-format)" find "${WORKSPACE_ROOT}/java" -type f -name '*.java' -exec "$GOOGLE_JAVA_FORMAT" --replace {} + fi if changed_matches '^javascript/selenium-webdriver/'; then section "JavaScript" echo " prettier" >&2 NODE_WEBDRIVER="${WORKSPACE_ROOT}/javascript/selenium-webdriver" bazel run //javascript:prettier -- "${NODE_WEBDRIVER}" --write "${NODE_WEBDRIVER}/.prettierrc" --log-level=warn fi if changed_matches '^rb/|^rake_tasks/|^Rakefile'; then section "Ruby" echo " rubocop -a" >&2 if [[ "$run_lint" == "true" ]]; then bazel run //rb:rubocop -- -a else bazel run //rb:rubocop -- -a --fail-level F fi fi if changed_matches '^rust/'; then section "Rust" echo " rustfmt" >&2 bazel run @rules_rust//:rustfmt fi if changed_matches '^py/'; then section "Python" if [[ "$run_lint" == "true" ]]; then echo " ruff check" >&2 bazel run //py:ruff-check fi echo " ruff format" >&2 bazel run //py:ruff-format fi if changed_matches '^dotnet/'; then section ".NET" echo " dotnet format" >&2 bazel run //dotnet:format -- style --severity warn bazel run //dotnet:format -- whitespace fi # Run shellcheck and actionlint when --lint is passed if [[ "$run_lint" == "true" ]]; then section "Shell/Actions" echo " actionlint (with shellcheck)" >&2 SHELLCHECK="$(bazel run --run_under=echo @multitool//tools/shellcheck)" bazel run @multitool//tools/actionlint:cwd -- -shellcheck "$SHELLCHECK" fi # Check if formatting introduced new changes (comparing to baseline) if [[ "$(git status --porcelain)" != "$baseline" ]]; then echo "" >&2 echo "Formatters modified files:" >&2 git diff --name-only >&2 exit 1 fi echo "Format check passed." >&2