whiskers.git / tools / check-private.sh
1#!/usr/bin/env bash
2# Fail if any tracked file contains a term from .private-terms. See tools/README.md.
3#
4#     tools/check-private.sh [terms-file]      (default: .private-terms at the repository root)
5#     tools/check-private.sh --message FILE    a commit message (the commit-msg hook)
6#     tools/check-private.sh --push            every commit not yet on any remote: its files, names and message
7#                                              (the pre-push hook: history is public, not just the tip)
8#
9# One term per line, matched case-insensitively as a fixed string; blank lines and lines starting
10# with # are ignored. The terms file is gitignored and never committed. Prints file:line for each
11# hit (not the matching text, so the output does not itself spread the term), exit 1 on any.
12set -euo pipefail
13cd "$(dirname "$0")/.."
14mode=tree; msgfile=
15case "${1:-}" in
16  --message) mode=message; msgfile="${2:?--message needs a file}"; shift 2 ;;
17  --push) mode=push; shift ;;
18esac
19terms="${1:-.private-terms}"
20if [ ! -f "$terms" ]; then
21  if [ "${WHISKERS_REQUIRE_PRIVATE_TERMS:-0}" = 1 ]; then
22    echo "check-private: $terms is missing and WHISKERS_REQUIRE_PRIVATE_TERMS=1" >&2
23    exit 2
24  fi
25  echo "check-private: no $terms; nothing to check against (see tools/README.md)" >&2
26  exit 0
27fi
28patterns="$(mktemp)"
29trap 'rm -f "$patterns"' EXIT
30grep -v -e '^[[:space:]]*$' -e '^[[:space:]]*#' "$terms" | sed 's/\r$//' > "$patterns" || true
31if [ ! -s "$patterns" ]; then
32  echo "check-private: $terms has no terms" >&2
33  exit 0
34fi
35if [ "$mode" = message ]; then
36  if grep -q -i -F -f "$patterns" "$msgfile"; then
37    echo "check-private: the commit message contains a private term; reword it" >&2
38    exit 1
39  fi
40  echo "check-private: message clean"
41  exit 0
42fi
43if [ "$mode" = push ]; then
44  # Commits that are on no remote yet: what a push would publish. Files, names and messages of each.
45  bad=0
46  for c in $(git rev-list HEAD --not --remotes); do
47    h="$(git grep -I -l -i -F -f "$patterns" "$c" -- . ':!.private-terms' ':!third-party/pixel-icons' ':!crates/whiskers-icons/allowlist.txt' || true)"
48    n="$(git ls-tree -r --name-only "$c" | grep -v -e '^third-party/pixel-icons/' -e '^crates/whiskers-icons/allowlist.txt$' | grep -i -F -f "$patterns" || true)"
49    if [ -n "$h$n" ] || git log -1 --format=%B "$c" | grep -q -i -F -f "$patterns"; then
50      echo "check-private: commit $(git log -1 --format=%h "$c") would publish a private term (files, names or message); rewrite it before pushing" >&2
51      bad=1
52    fi
53  done
54  [ "$bad" = 0 ] || exit 1
55  echo "check-private: the commits to push are clean"
56  exit 0
57fi
58# -I skips binary files, -n gives line numbers; -o is not used so the term is not echoed.
59# Not checked: the vendored icon pack (its file names and README are upstream's, and a short term matches
60# inside ordinary words) and the allowlist that has to name each of its icons.
61skip=(':!.private-terms' ':!third-party/pixel-icons' ':!crates/whiskers-icons/allowlist.txt')
62hits="$(git grep -I -n -i -F -f "$patterns" -- . "${skip[@]}" | cut -d: -f1,2 || true)"
63# Binary files (screenshots) are checked by name only.
64names="$(git ls-files -- . "${skip[@]}" | grep -i -F -f "$patterns" | sed 's/$/:0 (file name)/' || true)"
65all="$(printf '%s\n%s\n' "$hits" "$names" | sed '/^$/d')"
66if [ -n "$all" ]; then
67  echo "check-private: a private term appears in tracked files:" >&2
68  echo "$all" >&2
69  exit 1
70fi
71echo "check-private: clean ($(wc -l < "$patterns") term(s) checked)"