Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .ci/check-commentflow.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash

# Verify, or with --write impose, commentflow reflow of the comment blocks.

set -uo pipefail

source "$(dirname "${BASH_SOURCE[0]}")/common.sh" || exit 2

write=0
case "${1:-}" in
--check) shift ;;
--write)
write=1
shift
;;
esac

COMMENTFLOW=${COMMENTFLOW:-commentflow}

files=()
if [ "$#" -gt 0 ]; then
files=("$@")
else
require_repo
collect_files '*.c' '*.h' '*.sh'
files=(${FILES[@]+"${FILES[@]}"})
fi

[ "${#files[@]}" -gt 0 ] || exit 0
if ! command -v "$COMMENTFLOW" > /dev/null 2>&1; then
echo "Error: $COMMENTFLOW not found" >&2
echo "Install it from https://github.com/sysprog21/commentflow" >&2
exit 2
fi
if [ "$write" -eq 1 ]; then
exec "$COMMENTFLOW" -- "${files[@]}"
fi

status=0
"$COMMENTFLOW" --check -- "${files[@]}" || status=$?
if [ "$status" -eq 1 ]; then
echo "Run 'make indent' to reflow comments." >&2
fi
exit "$status"
63 changes: 56 additions & 7 deletions .ci/check-format.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,61 @@
#!/usr/bin/env bash

SOURCES=$(find $(git rev-parse --show-toplevel) | egrep "\.(c|cxx|cpp|h|hpp)\$")
# Verify, or with --write impose, clang-format conformance for the C sources.

set -x
set -uo pipefail

for file in ${SOURCES};
do
clang-format-18 ${file} > expected-format
diff -u -p --label="${file}" --label="expected coding style" ${file} expected-format
source "$(dirname "${BASH_SOURCE[0]}")/common.sh" || exit 2

write=0
case "${1:-}" in
--check) shift ;;
--write)
write=1
shift
;;
esac

CLANG_FORMAT=$(find_clang_format) || {
echo "Error: clang-format version 20 is required" >&2
exit 2
}

files=()
if [ "$#" -gt 0 ]; then
files=("$@")
else
require_repo
collect_files '*.c' '*.h'
files=(${FILES[@]+"${FILES[@]}"})
fi

[ "${#files[@]}" -gt 0 ] || exit 0
if [ "$write" -eq 1 ]; then
exec "$CLANG_FORMAT" -i "${files[@]}"
fi

# One batched pass answers "is anything unformatted" in a third of the time the
# per-file loop below takes. The loop only has to run when the answer is yes,
# and then only to produce the diff that says what to change.
list=$(mktemp) || exit 2
trap 'rm -f "$list"' EXIT
printf '%s\n' "${files[@]}" > "$list"
"$CLANG_FORMAT" --dry-run -Werror --files="$list" > /dev/null 2>&1 && exit 0

failed=0
expected=$(mktemp) || exit 2
trap 'rm -f "$list" "$expected"' EXIT
for file in "${files[@]}"; do

# An index entry with no file behind it -- a sparse checkout, or a deletion
# staged but not yet committed -- is nothing to format.
[ -f "$file" ] || continue
if ! "$CLANG_FORMAT" "$file" > "$expected"; then
echo "Error: $CLANG_FORMAT failed on $file" >&2
exit 1
fi
diff -u -p --label="$file" --label="expected coding style" \
"$file" "$expected" || failed=1
done
exit $(clang-format-18 --output-replacements-xml ${SOURCES} | egrep -c "</replacement>")

exit "$failed"
97 changes: 84 additions & 13 deletions .ci/check-newline.sh
Original file line number Diff line number Diff line change
@@ -1,17 +1,88 @@
#!/usr/bin/env bash

ret=0
show=0
# Reference: https://medium.com/@alexey.inkin/how-to-force-newline-at-end-of-files-and-why-you-should-do-it-fdf76d1d090e
while IFS= read -rd '' f; do
if file --mime-encoding "$f" | grep -qv binary; then
tail -c1 < "$f" | read -r _ || show=1
if [ $show -eq 1 ]; then
echo "Warning: No newline at end of file $f"
ret=1
show=0
fi
# Ensure every text file uses LF line endings and ends with a newline, which is
# what the [*] section of .editorconfig asks editors to do.

set -uo pipefail

source "$(dirname "${BASH_SOURCE[0]}")/common.sh" || exit 2

write=0
case "${1:-}" in
--check) shift ;;
--write)
write=1
shift
;;
esac

files=()
if [ "$#" -gt 0 ]; then
files=("$@")
else
require_repo
collect_files
files=(${FILES[@]+"${FILES[@]}"})
fi

[ "${#files[@]}" -gt 0 ] || exit 0
if ! command -v file > /dev/null 2>&1; then
echo "Error: file not found" >&2
exit 2
fi

# One file(1) run for the whole set: it spends most of its time loading
# libmagic, so paying that once rather than per file is the difference between
# the newline check dominating "make check-style" and disappearing into it.
encodings=()
while IFS= read -r encoding; do
encodings+=("$encoding")
done < <(file -b --mime-encoding -- "${files[@]}")

# Answers are matched to inputs by position, so a reply that ran short would
# quietly reclassify the rest of the tree as text and report nonsense about it.
if [ "${#encodings[@]}" -ne "${#files[@]}" ]; then
echo "Error: file described ${#encodings[@]} of ${#files[@]} files" >&2
exit 2
fi

text=()
for i in "${!files[@]}"; do
[ "${encodings[i]}" = binary ] && continue
text+=("${files[i]}")
done
[ "${#text[@]}" -gt 0 ] || exit 0

failed=0
for path in "${text[@]}"; do
last=$(tail -c1 < "$path") || exit 2
[ -n "$last" ] || continue
if [ "$write" -eq 1 ]; then
printf '\n' >> "$path"
continue
fi
done < <(git ls-files -z src tools tests)
echo "No newline at end of file: $path" >&2
failed=1
done

# grep exits 0 having found a carriage return, 1 having found none, and 2 on a
# real error, which says nothing about the tree and must not read as clean. Its
# answer goes through a file because the names it prints are NUL separated and a
# command substitution would drop the separators along with them.
matches=$(mktemp) || exit 2
trap 'rm -f "$matches"' EXIT
grep -lZ $'\r' -- "${text[@]}" > "$matches"
status=$?
if [ "$status" -gt 1 ]; then
echo "Error: grep failed to scan for carriage returns" >&2
exit 2
fi
while IFS= read -r -d '' path; do

# Reported even under --write: a carriage return sits inside the text, and
# stripping one is an edit to content rather than to layout.
echo "CRLF line ending: $path" >&2
failed=1
done < "$matches"

exit $ret
exit "$failed"
66 changes: 66 additions & 0 deletions .ci/check-shell.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash

# Verify, or with --write impose, shfmt formatting for the shell scripts, and
# lint them with ShellCheck.

set -uo pipefail

source "$(dirname "${BASH_SOURCE[0]}")/common.sh" || exit 2

write=0
case "${1:-}" in
--check) shift ;;
--write)
write=1
shift
;;
esac

SHFMT=${SHFMT:-shfmt}
SHELLCHECK=${SHELLCHECK:-shellcheck}

files=()
if [ "$#" -gt 0 ]; then
files=("$@")
else
require_repo
collect_files '*.sh'
files=(${FILES[@]+"${FILES[@]}"})
fi

[ "${#files[@]}" -gt 0 ] || exit 0
if ! command -v "$SHFMT" > /dev/null 2>&1; then
echo "Error: $SHFMT not found" >&2
exit 2
fi
if [ "$write" -eq 1 ]; then
exec "$SHFMT" -w -- "${files[@]}"
fi

failed=0
"$SHFMT" -d -- "${files[@]}" || failed=1

# The test scripts embed shecc expressions that read as shell and trip
# ShellCheck. Naming what is exempt rather than what is covered keeps a new
# directory linted by default instead of silently skipped.
lint_files=()
for file in "${files[@]}"; do
case "$file" in
tests/*) ;;
*) lint_files+=("$file") ;;
esac
done

[ "${#lint_files[@]}" -gt 0 ] || exit "$failed"
if ! command -v "$SHELLCHECK" > /dev/null 2>&1; then
echo "Error: $SHELLCHECK not found" >&2

# A formatting violation shfmt already found is a verdict on the tree, and
# outranks the linter that could not run: reporting 2 here would file it
# under "unavailable" and let the pre-commit hook wave it through.
[ "$failed" -eq 0 ] || exit "$failed"
exit 2
fi
Comment thread
jserv marked this conversation as resolved.
"$SHELLCHECK" --severity=warning -- "${lint_files[@]}" || failed=1

exit "$failed"
74 changes: 74 additions & 0 deletions .ci/common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash

# Shared helpers for the style checks. Sourced, not executed.
#
# Every .ci/check-*.sh honors the same exit-code contract, which
# scripts/git-pre-commit.sh depends on to stay advisory: 0 when clean, 1 when
# the tree violates the rule, and 2 when the check could not run at all, which
# in practice means a missing tool. Only 1 blocks a commit.

# Abort unless the current directory sits inside a git repository. Without this
# the checks below find no files and report success, which reads exactly like a
# clean tree.
require_repo()
{
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "Error: not a git repository" >&2
exit 2
fi
}

# Fill the FILES array with every file matching the given pathspecs that git
# would either track or add, so that a new file is checked by the same rule that
# formats it.
#
# The listing goes through a temporary file rather than a process substitution
# because git's exit status has to be read in this shell: inside "< <(...)" a
# failure exits only the subshell, the loop sees end of input, and an
# enumeration that never happened arrives as an empty list, which every checker
# reads as a clean tree.
#
# An index entry can also outlive its file, during a staged deletion or in a
# sparse checkout, and there is nothing for any checker to read at that path.
collect_files()
{
local listing status file
listing=$(mktemp) || exit 2
Comment thread
jserv marked this conversation as resolved.
trap 'rm -f "$listing"' EXIT
git ls-files -z --cached --others --exclude-standard -- "$@" > "$listing"
status=$?
FILES=()
if [ "$status" -ne 0 ]; then
echo "Error: could not enumerate the worktree" >&2
exit 2
fi
while IFS= read -r -d '' file; do

# -f rather than -e: a submodule gitlink and a symlink to a directory
# are both entries in the index that no file-oriented checker can read.
[ -f "$file" ] || continue
FILES+=("$file")
done < "$listing"
rm -f "$listing"
trap - EXIT
}

# Print the name of a clang-format at the version the project pins, or nothing.
# CLANG_FORMAT names a specific binary to use instead of searching.
find_clang_format()
{
local candidate candidates
if [ -n "${CLANG_FORMAT:-}" ]; then
candidates=("$CLANG_FORMAT")
else
candidates=(clang-format-20 clang-format)
fi
for candidate in "${candidates[@]}"; do
if command -v "$candidate" > /dev/null 2>&1 \
&& "$candidate" --version 2> /dev/null | grep -qE 'version 20\.'; then
echo "$candidate"
return 0
fi
done
return 1
}
24 changes: 24 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Top-level EditorConfig file
root = true

# Enforced repository-wide by .ci/check-newline.sh
[*]
end_of_line = lf
insert_final_newline = true

# Matching .clang-format's "UseTab: Never" and "IndentWidth: 4"
[*.{c,h}]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

# Shell adds the shfmt-specific keys, which no editor reads but which keep
# "shfmt -d" and "shfmt -w" agreeing without duplicating flags in the Makefile.
[*.sh]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
function_next_line = true
switch_case_indent = true
space_redirects = true
binary_next_line = true
Loading