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
31 changes: 31 additions & 0 deletions .git-hooks/pre_commit/check_executable_shebangs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module Overcommit::Hook::PreCommit
# Any tracked file with the executable bit set must start with a shebang
# (previously pre-commit's check-executables-have-shebangs and
# check-shebang-scripts-are-executable, combined).
class CheckExecutableShebangs < Base
def run
errors = []

`git ls-files -sz`.split("\0").each do |entry|
mode_part, path = entry.split("\t", 2)
mode = mode_part.split(' ').first.to_i(8) & 0o111
next if mode.zero?

next unless File.exist?(path) && !File.directory?(path)

first = File.open(path) { |f| f.readline } rescue ''
if !first.start_with?('#!')
errors << "#{path}: executable file is missing a shebang"
elsif (File.stat(path).mode & 0o111).zero?
errors << "#{path}: script starts with a shebang but is not executable"
end
end

return :pass if errors.empty?

[:fail, errors.join("\n")]
end
end
end
16 changes: 16 additions & 0 deletions .git-hooks/pre_commit/codespell.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module Overcommit::Hook::PreCommit
# Runs `codespell` on the files being committed.
class Codespell < Base
def run
result = execute(
%w[codespell --ignore-words-list=Claus,Technik --skip=_site,.git,node_modules],
args: applicable_files,
)
return :pass if result.success?

[:fail, (result.stdout + result.stderr).chomp]
end
end
end
30 changes: 30 additions & 0 deletions .git-hooks/pre_commit/final_newline.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module Overcommit::Hook::PreCommit
# Checks that files end with a single newline (previously pre-commit's
# end-of-file-fixer, without the auto-fix).
class FinalNewline < Base
def run
errors = []

applicable_files.each do |file|
next unless File.exist?(file) && !File.directory?(file)

content = File.binread(file)
# Skip binaries and files that are not UTF-8 text (ends-of-files
# only make sense for text; the previous pre-commit hook ignored
# binaries too).
next if content.empty?
next if content.include?("\0") || !content.valid_encoding?

unless content.end_with?("\n")
errors << "#{file}: missing final newline"
end
end

return :pass if errors.empty?

[:fail, errors.join("\n")]
end
end
end
29 changes: 29 additions & 0 deletions .git-hooks/pre_commit/refactoring_sample_width.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module Overcommit::Hook::PreCommit
# Enforces the 72-char pane width on refactoring sample sources
# (see the refactoring skill's LESSONS.md 'pane width').
class RefactoringSampleWidth < Base
MAX_WIDTH = 72

def run
errors = []

applicable_files.each do |file|
next unless File.exist?(file)

File.foreach(file).with_index(1) do |line, line_no|
content = line.chomp
next if content.length <= MAX_WIDTH

errors << "#{file}: line #{line_no} is #{content.length} chars " \
"(> #{MAX_WIDTH})"
end
end

return :pass if errors.empty?

[:fail, errors.join("\n")]
end
end
end
38 changes: 38 additions & 0 deletions .git-hooks/pre_commit/spellr.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module Overcommit::Hook::PreCommit
# Runs spellr (the Ruby spell checker) on the files being committed.
# spellr splits camelCase and snake_case identifiers, skips URLs and
# hex-like strings via heuristics, obeys .gitignore, and consults
# .spellr.yml plus the wordlists in .spellr_wordlists/. It runs from
# the repo root so the config is found wherever the hook triggers.
#
# NOTE: spellr applies .spellr.yml excludes only when given a directory
# or nothing; explicit file arguments bypass them. Overcommit passes
# files, so re-apply the same excludes here.
class Spellr < Base
EXCLUDES = [
# match on the repo-relative path and the bare basename, because
# overcommit passes absolute paths in --run mode
lambda do |path|
rel = path.sub(%r{#{Regexp.escape(Overcommit::Utils.repo_root)}/?}, '')
rel =~ %r{^\.claude/} ||
rel =~ %r{\.(scala|hs|js)$} ||
rel == 'run.sh' ||
rel =~ %r{^pages/refactorings/} ||
rel == '.gitignore' ||
File.basename(rel).start_with?('.')
end
].freeze

def run
files = applicable_files.reject { |f| EXCLUDES.any? { |re| re.call(f) } }
return :pass if files.empty?

result = execute(%w[bundle exec spellr], args: files)
return :pass if result.success?

[:fail, (result.stdout + result.stderr).chomp]
end
end
end
27 changes: 27 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Lint

on:
pull_request:
branches: [master]
push:
branches: [master]

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true

- name: Run the lint suite
run: scripts/lint.sh

- name: Build the site
run: bundle exec jekyll build
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,6 @@ typings/
dist-newstyle/
*.hi
*.o

# Local linting venv (see README "Lint")
/.venv/
12 changes: 12 additions & 0 deletions .mdlrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Markdown style for the site, shared by every entry point that runs mdl
# (the overcommit Mdl hook, `scripts/lint.sh`, CI, and ad-hoc manual runs).
# mdl picks this up automatically from the current directory.

# Jekyll pages and posts start with YAML front matter, which is not prose.
ignore_front_matter true

# When given a directory, only look at files git tracks, so generated
# output (_site/, .jekyll-cache/) and dependency dirs are never linted.
git_recurse true

style "#{File.dirname(__FILE__)}/mdl_style.rb"
94 changes: 94 additions & 0 deletions .overcommit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Linting for the site, Ruby/bundler based (replaces the previous
# pre-commit/python setup with the same checks):
#
# Local hooks: bundle exec overcommit --install
# Whole repo: scripts/lint.sh (= `bundle exec overcommit --run`)
# CI: .github/workflows/lint.yml runs scripts/lint.sh
#
# Local hooks and CI execute this same configuration, so they cannot drift.
#
# Signature verification is off so CI can run `overcommit --run` on a fresh
# checkout (signatures live in the local git config, which CI never has).
verify_signatures: false

# Make the installed git hooks resolve gems through the bundle, so hooks find
# `mdl` and friends without a system-wide gem install.
gemfile: Gemfile

# Only print hook output when a hook warns or fails.
quiet: true

PreCommit:
AuthorName:
enabled: false

AuthorEmail:
enabled: false

# Hygiene checks (previously pre-commit-hooks):
# trailing whitespace -> TrailingWhitespace, CRLF check dropped (repo is
# LF only), case conflicts -> CaseConflicts, merge markers ->
# MergeConflicts, YAML/JSON syntax -> YamlSyntax/JsonSyntax, final
# newline -> FinalNewline, shebang/exec-bit consistency ->
# CheckExecutableShebangs.

CaseConflicts:
enabled: true

MergeConflicts:
enabled: true

TrailingWhitespace:
enabled: true
requires_files: true

JsonSyntax:
enabled: true
requires_files: true
exclude:
# search.json is a Jekyll/Liquid template with front matter,
# not a plain JSON file.
- 'assets/data/search.json'

YamlSyntax:
enabled: true
requires_files: true

FinalNewline:
enabled: true
requires_files: true

CheckExecutableShebangs:
enabled: true
requires_files: true

# Markdown style (previously markdownlint-cli2). Rules live in .mdlrc +
# mdl_style.rb, shared with any editor or manual `mdl` run.
Mdl:
enabled: true
requires_files: true
command: ['bundle', 'exec', 'mdl']
exclude:
# The refactorings section carries raw-HTML citation blocks (single
# <li> lines with entities/URLs) and ref-link prose that the
# refactoring skill writes deliberately; those can't be wrapped to
# 80 cols without breaking the markup. Exempt only that section.
- 'pages/refactorings/**/*'
# .claude/skills are agent-internal docs, not site prose.
- '.claude/**/*'
# The catalogue index page has the same citation conventions.
- 'pages/refactorings.md'

# Spelling with spellr (Ruby, replaces codespell — see
# .git-hooks/pre_commit/spellr.rb). Wordlist: .spellr_wordlists/.
Spellr:
enabled: true
requires_files: true

# Refactoring sample sources must stay within the 72-char pane width
# (previously scripts/check-sample-width.sh via pre-commit).
RefactoringSampleWidth:
enabled: true
requires_files: true
include:
- 'pages/refactorings/**/*.{scala,hs}'
29 changes: 29 additions & 0 deletions .spellr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Spellr configuration (https://github.com/robotdana/spellr), the Ruby/bundler
# spell checker. Checked files obey .gitignore; these excludes are for
# Jekyll/generated trees that git ignores anyway, kept explicit so `spellr`
# behaves identically from any directory.
#
# Policy: spellr checks the site's prose (Markdown, YAML/JSON data, HTML,
# SCSS). Non-prose files are excluded: refactoring source samples (.scala .hs)
# and scripts are code with their own vocabulary, and .claude/skills are
# agent-internal docs full of tool-specific terms — none are site spelling.
excludes:
- .git/
- _site/
- .jekyll-cache/
- vendor/
- node_modules/
- .venv/
- Gemfile.lock
- .claude/
- '*.scala'
- '*.hs'
- '*.js'
- 'run.sh'
- pages/refactorings/

# Site-specific spelling lives in .spellr_wordlists/english.txt; regenerate
# or extend it with `bundle exec spellr --wordlist`.
languages:
english:
locale: US
Loading
Loading