Skip to content

Pre-commit Hooks for thai-lint

Purpose: User guide for installing and using pre-commit hooks with thai-lint

Scope: Local development workflow automation and code quality enforcement

Overview: Pre-commit hooks automatically run code quality checks before commits and pushes, ensuring all code meets quality standards before it enters version control. This guide covers installation, configuration, usage, and troubleshooting for integrating thai-lint with your git workflow.


What Are Pre-commit Hooks?

Pre-commit hooks are automated checks that run before git commits and pushes. They help maintain code quality by:

  • Preventing Bad Code: Stop commits with linting errors or failing tests
  • Automating Quality Checks: Run formatters, linters, and tests automatically
  • Saving Time: Catch issues locally before pushing to remote
  • Enforcing Standards: Ensure all team members follow the same quality gates

Quick Start

Install pre-commit hooks in 3 simple steps:

# 1. Install pre-commit framework
pip install pre-commit

# 2. Install git hooks
pre-commit install
pre-commit install --hook-type pre-push

# 3. Test it works
pre-commit run --all-files

That's it! Your hooks are installed and will run automatically on every commit and push.


Installation

Prerequisites

Before installing pre-commit hooks, ensure you have:

  • Git repository initialized (git init)
  • Python 3.9 or higher installed
  • thai-lint installed (pip install thailint)

Step 1: Install Pre-commit Framework

Install the pre-commit framework using pip:

# Using pip
pip install pre-commit

# Or using pip3
pip3 install pre-commit

# Verify installation
pre-commit --version
# Output: pre-commit 3.x.x

Alternative installation methods:

# Using Homebrew (macOS)
brew install pre-commit

# Using conda
conda install -c conda-forge pre-commit

Step 2: Install Git Hooks

Install the hooks into your git repository:

# Install pre-commit hooks (runs on 'git commit')
pre-commit install
# Output: pre-commit installed at .git/hooks/pre-commit

# Install pre-push hooks (runs on 'git push')
pre-commit install --hook-type pre-push
# Output: pre-commit installed at .git/hooks/pre-push

Verify installation:

# Check hooks were created
ls -la .git/hooks/pre-commit .git/hooks/pre-push

# View hook contents
cat .git/hooks/pre-commit

Step 3: Test Hooks

Test the hooks on your codebase:

# Run all hooks on all files
pre-commit run --all-files

# Expected: Hooks run, may show some violations to fix

Note: The first run is slower as tools are downloaded and cached. Subsequent runs are much faster.


Configuration

thai-lint uses a .pre-commit-config.yaml file to configure hooks. Here are two recommended configurations:

repos:
  - repo: local
    hooks:
      # ========================================
      # BRANCH PROTECTION
      # ========================================
      - id: no-commit-to-main
        name: Prevent commits to main branch
        entry: bash -c 'branch=$(git rev-parse --abbrev-ref HEAD); if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then echo "Direct commits to main/master branch are not allowed! Create a feature branch instead."; exit 1; fi'
        language: system
        pass_filenames: false
        stages: [pre-commit]
        always_run: true

      # ========================================
      # PRE-COMMIT - Format and lint changed files
      # ========================================
      - id: thailint-file-placement
        name: Check file placement (changed files)
        entry: poetry run thailint file-placement
        language: system
        files: \.(py|ts|tsx|js|jsx)$
        pass_filenames: true
        stages: [pre-commit]

      - id: thailint-nesting
        name: Check nesting depth (changed files)
        entry: poetry run thailint nesting --max-depth 3
        language: system
        files: \.(py|ts|tsx|js|jsx)$
        pass_filenames: true
        stages: [pre-commit]

      - id: thailint-srp
        name: Check SRP violations (changed files)
        entry: poetry run thailint srp
        language: system
        files: \.(py|ts|tsx|js|jsx)$
        pass_filenames: true
        stages: [pre-commit]

      # ========================================
      # PRE-PUSH - Full validation on entire codebase
      # ========================================
      - id: pre-push-lint-all
        name: Run all linters (entire codebase)
        entry: bash -c 'poetry run thailint file-placement . && poetry run thailint nesting . && poetry run thailint srp .'
        language: system
        pass_filenames: false
        stages: [pre-push]
        always_run: true
repos:
  - repo: local
    hooks:
      # ========================================
      # BRANCH PROTECTION
      # ========================================
      - id: no-commit-to-main
        name: Prevent commits to main branch
        entry: bash -c 'branch=$(git rev-parse --abbrev-ref HEAD); if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then echo "Direct commits to main/master branch are not allowed! Create a feature branch instead."; exit 1; fi'
        language: system
        pass_filenames: false
        stages: [pre-commit]
        always_run: true

      # ========================================
      # PRE-COMMIT - Format and lint changed files
      # ========================================
      - id: thailint-docker-file-placement
        name: Check file placement (Docker, changed files)
        entry: bash -c 'docker run --rm -v $(pwd):/workspace washad/thailint:latest file-placement "$@"' --
        language: system
        files: \.(py|ts|tsx|js|jsx)$
        pass_filenames: true
        stages: [pre-commit]

      - id: thailint-docker-nesting
        name: Check nesting depth (Docker, changed files)
        entry: bash -c 'docker run --rm -v $(pwd):/workspace washad/thailint:latest nesting --max-depth 3 "$@"' --
        language: system
        files: \.(py|ts|tsx|js|jsx)$
        pass_filenames: true
        stages: [pre-commit]

      - id: thailint-docker-srp
        name: Check SRP violations (Docker, changed files)
        entry: bash -c 'docker run --rm -v $(pwd):/workspace washad/thailint:latest srp "$@"' --
        language: system
        files: \.(py|ts|tsx|js|jsx)$
        pass_filenames: true
        stages: [pre-commit]

      # ========================================
      # PRE-PUSH - Full validation on entire codebase
      # ========================================
      - id: pre-push-lint-all-docker
        name: Run all linters (Docker, entire codebase)
        entry: bash -c 'docker run --rm -v $(pwd):/workspace washad/thailint:latest file-placement /workspace && docker run --rm -v $(pwd):/workspace washad/thailint:latest nesting /workspace && docker run --rm -v $(pwd):/workspace washad/thailint:latest srp /workspace'
        language: system
        pass_filenames: false
        stages: [pre-push]
        always_run: true

Customizing Configuration

Add more protected branches:

# Protect develop branch too
entry: bash -c 'branch=$(git rev-parse --abbrev-ref HEAD); if [ "$branch" = "main" ] || [ "$branch" = "develop" ]; then ...'

Skip specific hooks temporarily:

# Skip all hooks (emergency only)
git commit --no-verify -m "Emergency fix"

# Skip specific hook
SKIP=lint-full-changed git commit -m "Skip linting this once"


Usage

Normal Development Workflow

Pre-commit hooks run automatically during your normal git workflow:

# 1. Create feature branch
git checkout -b feature/my-feature

# 2. Make changes to your code
echo "def hello(): print('Hello')" > src/hello.py

# 3. Stage changes
git add src/hello.py

# 4. Commit (hooks run automatically)
git commit -m "Add hello function"
# Hooks run:
# ✓ Branch protection passed
# ✓ Format passed
# ✓ Lint checks passed
# Commit created

# 5. Push (pre-push hooks run)
git push origin feature/my-feature
# Hooks run:
# ✓ Full linting passed
# ✓ All tests passed
# Push completed

What Hooks Do

Pre-commit hooks (run on git commit): 1. Branch Protection: Prevents commits directly to main/master 2. Auto-format: Fixes formatting issues automatically 3. Lint Changed Files: Runs linters only on files you changed (fast)

Pre-push hooks (run on git push): 1. Full Linting: Runs comprehensive linting on entire codebase 2. All Tests: Runs complete test suite

Manual Hook Execution

You can run hooks manually without committing:

# Run all hooks on all files
pre-commit run --all-files

# Run all hooks on staged files only
pre-commit run

# Run specific hook
pre-commit run lint-full-changed

# Run specific hook on specific file
pre-commit run --files src/cli.py

Using thai-lint in Pre-commit Hooks

Basic thai-lint Hook

Add thai-lint to your .pre-commit-config.yaml:

repos:
  - repo: local
    hooks:
      # Pass changed files to thailint (fast, recommended)
      - id: thailint-file-placement
        name: Check file placement (changed files)
        entry: thailint file-placement
        language: system
        files: \.(py|ts|tsx|js|jsx)$
        pass_filenames: true

      - id: thailint-nesting
        name: Check nesting depth (changed files)
        entry: thailint nesting
        language: system
        files: \.(py|ts|tsx|js|jsx)$
        pass_filenames: true

      # Or lint entire project (slower but comprehensive)
      - id: thailint-full
        name: Check all files
        entry: thailint file-placement .
        language: system
        pass_filenames: false
        always_run: true

Key Configuration: - pass_filenames: true - Pre-commit will pass the list of changed files directly to thailint - files: \.(py|ts|tsx|js|jsx)$ - Only run on relevant file types - This approach is much faster than linting the entire project on every commit

thai-lint with Config File

repos:
  - repo: local
    hooks:
      - id: thailint-nesting
        name: Check nesting depth with config (changed files)
        entry: thailint nesting --config .thailint.yaml
        language: system
        files: \.(py|ts|tsx|js|jsx)$
        pass_filenames: true

thai-lint in Docker

If you're using thai-lint via Docker, you can pass changed files:

repos:
  - repo: local
    hooks:
      # Pass changed files to Docker container
      - id: thailint-docker-changed
        name: Run thailint in Docker (changed files)
        entry: bash -c 'docker run --rm -v $(pwd):/workspace washad/thailint:latest nesting "$@"' --
        language: system
        files: \.(py|ts|tsx|js|jsx)$
        pass_filenames: true

      # Or lint entire project
      - id: thailint-docker-full
        name: Run thailint in Docker (full project)
        entry: docker run --rm -v $(pwd):/workspace washad/thailint:latest nesting /workspace
        language: system
        pass_filenames: false
        always_run: true

Workflow Examples

Example 1: Successful Commit

$ git commit -m "Add new feature"

Prevent commits to main branch..................................Passed
Auto-fix formatting issues......................................Passed
Run all linting checks (changed files only)....................Passed

[feature/my-feature abc123] Add new feature
 1 file changed, 10 insertions(+)

Example 2: Failed Commit (Linting Error)

$ git commit -m "Add buggy code"

Prevent commits to main branch..................................Passed
Auto-fix formatting issues......................................Passed
Run all linting checks (changed files only)....................Failed
- hook id: lint-full-changed
- exit code: 1

src/buggy.py:5:1: E302 expected 2 blank lines, found 1
src/buggy.py:10:80: E501 line too long (85 > 79 characters)

# Fix the errors
$ just format
$ git add -u
$ git commit -m "Add feature (fixed linting)"
# Hooks pass, commit succeeds

Example 3: Protected Branch Violation

$ git checkout main
$ git commit -m "Direct commit to main"

Prevent commits to main branch..................................Failed
- hook id: no-commit-to-main
- exit code: 1

Direct commits to main/master branch are not allowed! Create a feature branch instead.

# Create feature branch instead
$ git checkout -b feature/my-feature
$ git commit -m "Add feature"
# Hooks pass, commit succeeds

Troubleshooting

Issue: Pre-commit Not Found

Symptoms: pre-commit: command not found

Solution: Install pre-commit framework

pip install pre-commit
# or
pip3 install pre-commit

Issue: Hooks Not Running

Symptoms: Commits succeed without running hooks

Solution: Install git hooks

pre-commit install
pre-commit install --hook-type pre-push

Verify:

ls -la .git/hooks/pre-commit .git/hooks/pre-push

Issue: Hooks Failing with Command Not Found

Symptoms: poetry: command not found or docker: command not found

Solution: Install the required tool for your configuration

For Poetry:

# Install Poetry
curl -sSL https://install.python-poetry.org | python3 -

# Or using pip
pip install poetry

# Verify installation
poetry --version

For Docker:

# Install Docker (varies by OS)
# See: https://docs.docker.com/get-docker/

# Verify installation
docker --version

Issue: Want to Skip Hooks Temporarily

Symptoms: Need to commit urgently without running hooks

Solution: Use --no-verify (emergency only)

git commit --no-verify -m "Emergency fix"

Warning: Only use for genuine emergencies. Fix issues immediately after.

Issue: Hooks Are Too Slow

Symptoms: Hooks take >30 seconds

Solutions: 1. First run is slower (tools downloaded and cached) 2. Subsequent runs should be faster (3-10s) 3. Pre-commit hooks only lint changed files (fast) 4. Pre-push hooks lint all files (slower, but comprehensive)

Issue: Hook Fails But I Don't Understand Why

Solution: Run the command manually with verbose output

For Poetry:

# Run specific hook manually
pre-commit run thailint-nesting --verbose

# Run thailint directly
poetry run thailint nesting . --format text

For Docker:

# Run specific hook manually
pre-commit run thailint-docker-nesting --verbose

# Run Docker command directly
docker run --rm -v $(pwd):/workspace washad/thailint:latest nesting /workspace


Best Practices

1. Always Work on Feature Branches

# Good: Create feature branch
git checkout -b feature/my-feature

# Bad: Commit directly to main (blocked by hooks)
git checkout main
git commit -m "..."  # Blocked by branch protection

2. Fix Issues Before Committing

Save time by running linters before commit:

# Run linters manually first
poetry run thailint file-placement .
poetry run thailint nesting .
poetry run thailint srp .

# Or with Docker
docker run --rm -v $(pwd):/workspace washad/thailint:latest file-placement /workspace

# Stage fixes
git add -u

# Commit (hooks pass faster)
git commit -m "Your message"

3. Commit Small Changes Frequently

  • Smaller commits = faster hooks
  • Easier to fix issues
  • Better git history

4. Review Hook Output

When hooks fail: 1. Read the error messages 2. Understand why it failed 3. Fix the actual issue 4. Don't skip hooks without good reason

5. Keep Hooks Updated

Update pre-commit framework regularly:

# Update pre-commit
pip install --upgrade pre-commit

# Update hook dependencies
pre-commit autoupdate

# Test after updates
pre-commit run --all-files

Advanced Configuration

Skip Specific Files

- id: thailint-nesting
  name: Check nesting depth
  entry: thailint nesting
  language: system
  files: \.py$
  exclude: ^tests/|^migrations/

Run Hooks in Parallel

- id: lint-parallel
  name: Run linting in parallel
  entry: bash -c 'just lint & just test & wait'
  language: system

Custom Error Messages

- id: custom-check
  name: Custom validation
  entry: bash -c 'echo "Running custom checks..." && ./scripts/validate.sh'
  language: system

Integration with CI/CD

Run the same hooks in CI/CD to ensure consistency:

# GitHub Actions example
name: Pre-commit Checks

on: [push, pull_request]

jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      - name: Install dependencies
        run: pip install pre-commit
      - name: Run pre-commit
        run: pre-commit run --all-files

Summary

Pre-commit hooks help maintain code quality by:

Preventing bad commits: Stop low-quality code from entering version control Automating checks: Run formatters, linters, and tests automatically Saving time: Catch issues locally before CI/CD Enforcing standards: Consistent quality across all contributors

Quick reference:

# Install
pip install pre-commit
pre-commit install
pre-commit install --hook-type pre-push

# Use
git commit -m "..."    # Runs pre-commit hooks automatically
git push              # Runs pre-push hooks automatically

# Manual run
pre-commit run --all-files

# Skip (emergency only)
git commit --no-verify -m "..."


Resources

Support

For issues with pre-commit hooks: - Check this guide's troubleshooting section - Run hooks manually: pre-commit run --all-files --verbose - Review .pre-commit-config.yaml configuration - Check thai-lint documentation: docs/