Skip to content

Latest commit

Β 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

commitlint-lite

Lightweight commit message linter. Conventional Commits validation with zero dependencies.

npm version License: MIT Node.js

Why commitlint-lite?

  • Zero dependencies β€” No bloat, just pure Node.js
  • Fast β€” Instant validation with minimal overhead
  • Simple β€” One job, done well
  • Standard β€” Follows Conventional Commits specification

Perfect for projects that want commit message validation without the heavyweight tooling.

Installation

npm install -D commitlint-lite

Or use directly with npx:

npx commitlint-lite "feat: add login"

Usage

Command Line

# Validate a commit message
commitlint-lite "feat: add login feature"

# Read from stdin (useful for git hooks)
echo "fix(auth): resolve token issue" | commitlint-lite --stdin

# Install git hook automatically
commitlint-lite --init-hook

# JSON output
commitlint-lite "feat: new feature" --json

Git Hook Setup

The easiest way:

commitlint-lite --init-hook

This creates .git/hooks/commit-msg automatically.

Manual setup (.git/hooks/commit-msg):

#!/usr/bin/env sh
MESSAGE_FILE="$1"
MESSAGE=$(cat "$MESSAGE_FILE")

npx commitlint-lite --stdin <<EOF
$MESSAGE
EOF

Make it executable:

chmod +x .git/hooks/commit-msg

Configuration

Create .commitlintrc.json in your project root:

{
  "types": ["feat", "fix", "docs", "style", "refactor", "test", "chore"],
  "maxLength": 72,
  "requireScope": false
}

Options

Option Type Default Description
types string[] ["feat", "fix", "docs", "style", "refactor", "test", "chore", "ci", "perf", "build", "revert"] Allowed commit types
maxLength number 100 Maximum commit message length
requireScope boolean false Whether scope is required

Conventional Commits Format

type(scope): description

Examples:
βœ“ feat: add user authentication
βœ“ fix(api): resolve timeout issue
βœ“ docs: update installation guide
βœ— added new stuff (invalid)

Default Types

  • feat β€” New feature
  • fix β€” Bug fix
  • docs β€” Documentation changes
  • style β€” Code style changes (formatting, etc.)
  • refactor β€” Code refactoring
  • test β€” Adding or updating tests
  • chore β€” Maintenance tasks
  • ci β€” CI/CD changes
  • perf β€” Performance improvements
  • build β€” Build system changes
  • revert β€” Revert previous commit

Basic Examples

Example 1: Valid commits (various formats)

$ commitlint-lite "feat: add login feature"
βœ“ Commit message is valid

$ commitlint-lite "fix(auth): resolve token expiration"
βœ“ Commit message is valid

$ commitlint-lite "docs: update README"
βœ“ Commit message is valid

$ commitlint-lite "feat!: breaking API change"
βœ“ Commit message is valid (breaking change marker)

All exit with code 0.

Example 2: Invalid format

$ commitlint-lite "added new stuff"
βœ— Commit message validation failed:
  β€’ Invalid commit message format. Expected: type(scope): description
  
$ commitlint-lite "Update README"
βœ— Commit message validation failed:
  β€’ Invalid commit message format. Expected: type(scope): description
  β€’ Must start with a valid type (feat, fix, docs, etc.)

Exit code: 1 (blocks commit in git hook)

Example 3: Invalid type

$ commitlint-lite "invalid: test feature"
βœ— Commit message validation failed:
  β€’ Invalid commit type "invalid". Allowed types: feat, fix, docs, style, refactor, test, chore, ci, perf, build, revert

$ commitlint-lite "feature: add login"
βœ— Commit message validation failed:
  β€’ Invalid commit type "feature". Did you mean "feat"?

Catches typos and enforces standard types.

Example 4: Length violation

$ commitlint-lite "feat: add comprehensive user authentication system with OAuth2, JWT tokens, refresh mechanism, and role-based access control for enterprise users"
βœ— Commit message validation failed:
  β€’ Commit message exceeds maximum length (148 > 100 characters)
  β€’ Consider shortening: "feat: add user authentication with OAuth2 and RBAC"

Enforces concise commit messages.

Example 5: Git hook in action

After running commitlint-lite --init-hook:

$ git commit -m "added stuff"
βœ— Commit message validation failed:
  β€’ Invalid commit message format. Expected: type(scope): description

Commit aborted.

$ git commit -m "feat: add user dashboard"
βœ“ Commit message is valid
[main abc123d] feat: add user dashboard
 1 file changed, 50 insertions(+)

Catches bad commits before they enter history.

Example 6: Multi-line commit with body

$ commitlint-lite "feat(api): add user endpoints

Implemented CRUD operations for user management:
- GET /api/users
- POST /api/users
- PUT /api/users/:id
- DELETE /api/users/:id"

βœ“ Commit message is valid

Only the first line is validated; body is preserved.

Example 7: Required scope configuration

With .commitlintrc.json:

{
  "requireScope": true,
  "types": ["feat", "fix", "docs"]
}
$ commitlint-lite "feat: add login"
βœ— Commit message validation failed:
  β€’ Scope is required. Use format: type(scope): description

$ commitlint-lite "feat(auth): add login"
βœ“ Commit message is valid

Enforces scope for better categorization.

Example 8: Breaking changes

$ commitlint-lite "feat!: remove legacy API"
βœ“ Commit message is valid

$ commitlint-lite "feat(api)!: change authentication flow

BREAKING CHANGE: OAuth2 is now required for all endpoints"
βœ“ Commit message is valid

# Both formats are valid for breaking changes:
# - type!: description
# - type(scope)!: description
# - Body contains "BREAKING CHANGE:"

Example 9: JSON output for automation

$ commitlint-lite "feat: add login" --json
{
  "valid": true,
  "type": "feat",
  "scope": null,
  "description": "add login",
  "errors": []
}

$ commitlint-lite "invalid message" --json
{
  "valid": false,
  "type": null,
  "scope": null,
  "description": null,
  "errors": [
    "Invalid commit message format. Expected: type(scope): description"
  ]
}

Use in scripts:

#!/bin/bash
RESULT=$(commitlint-lite "feat: test" --json)
IS_VALID=$(echo $RESULT | jq '.valid')

if [ "$IS_VALID" == "true" ]; then
  echo "Valid commit message"
else
  echo "Invalid: $(echo $RESULT | jq -r '.errors[]')"
  exit 1
fi

Example 10: Team-specific types

Configure custom types for your team's workflow:

{
  "types": [
    "feat",
    "fix",
    "docs",
    "style",
    "refactor",
    "test",
    "chore",
    "hotfix",
    "release",
    "wip"
  ],
  "maxLength": 72,
  "requireScope": true
}
$ commitlint-lite "hotfix(billing): fix payment processing"
βœ“ Commit message is valid

$ commitlint-lite "wip(ui): work in progress on new design"
βœ“ Commit message is valid

$ commitlint-lite "release: v2.0.0"
βœ— Commit message validation failed:
  β€’ Scope is required. Use format: type(scope): description

$ commitlint-lite "release(v2.0.0): major version bump"
βœ“ Commit message is valid

Integration Guides

CI/CD Integration

GitHub Actions

Basic validation:

# .github/workflows/commitlint.yml
name: Lint Commit Messages

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  commitlint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # Get full history for PR commits
      
      - name: Validate PR title
        run: npx commitlint-lite "${{ github.event.pull_request.title }}"
      
      - name: Validate all commits in PR
        run: |
          git log --format=%s origin/${{ github.base_ref }}..HEAD | while read msg; do
            echo "Checking: $msg"
            npx commitlint-lite "$msg"
          done

With detailed feedback:

name: Commit Linting

on: [pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      
      - name: Check commits
        id: commitlint
        run: |
          ERRORS=""
          git log --format=%s origin/main..HEAD | while read msg; do
            RESULT=$(npx commitlint-lite "$msg" --json)
            VALID=$(echo $RESULT | jq -r '.valid')
            
            if [ "$VALID" == "false" ]; then
              ERRORS="$ERRORS\n❌ $msg"
              echo "$RESULT" | jq -r '.errors[]' | sed 's/^/  /'
            fi
          done
          
          if [ ! -z "$ERRORS" ]; then
            echo "::error::Invalid commit messages found"
            exit 1
          fi
      
      - name: Comment on PR
        if: failure()
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## ❌ Commit Message Validation Failed
              
              Please follow [Conventional Commits](https://www.conventionalcommits.org/) format:
              
              \`\`\`
              type(scope): description
              \`\`\`
              
              **Valid types:** feat, fix, docs, style, refactor, test, chore, ci, perf, build, revert
              
              **Examples:**
              - \`feat: add user authentication\`
              - \`fix(api): resolve timeout issue\`
              - \`docs: update installation guide\`
              `
            });

Enforce on merge:

name: Enforce Conventional Commits

on:
  pull_request:
    types: [opened, edited, synchronize]

jobs:
  validate-pr-title:
    runs-on: ubuntu-latest
    steps:
      - name: Validate PR title
        run: |
          npx commitlint-lite "${{ github.event.pull_request.title }}" || {
            echo "❌ PR title must follow Conventional Commits format"
            echo "Example: feat: add new feature"
            exit 1
          }

GitLab CI

# .gitlab-ci.yml
commitlint:
  stage: validate
  script:
    - |
      git log --format=%s origin/main..$CI_COMMIT_SHA | while read msg; do
        echo "Validating: $msg"
        npx commitlint-lite "$msg" || exit 1
      done
  only:
    - merge_requests

CircleCI

# .circleci/config.yml
version: 2.1

jobs:
  commitlint:
    docker:
      - image: node:18
    steps:
      - checkout
      - run:
          name: Validate commits
          command: |
            git log --format=%s origin/main..HEAD | while read msg; do
              npx commitlint-lite "$msg"
            done

workflows:
  validate:
    jobs:
      - commitlint

Jenkins

// Jenkinsfile
pipeline {
  agent any
  
  stages {
    stage('Lint Commits') {
      steps {
        script {
          def commits = sh(
            script: "git log --format=%s origin/main..HEAD",
            returnStdout: true
          ).trim().split('\n')
          
          commits.each { msg ->
            def result = sh(
              script: "npx commitlint-lite '${msg}' --json",
              returnStdout: true
            )
            
            def json = readJSON text: result
            if (!json.valid) {
              error("Invalid commit: ${msg}")
            }
          }
        }
      }
    }
  }
}

Pre-commit Hooks

Using Husky

# Install Husky
npm install --save-dev husky

# Initialize
npx husky install

# Add commit-msg hook
npx husky add .husky/commit-msg 'npx commitlint-lite --stdin < "$1"'

Or create .husky/commit-msg manually:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# Read commit message
MESSAGE=$(cat "$1")

# Validate
npx commitlint-lite "$MESSAGE" || {
  echo ""
  echo "❌ Commit message does not follow Conventional Commits format"
  echo ""
  echo "Format: type(scope): description"
  echo ""
  echo "Examples:"
  echo "  feat: add new feature"
  echo "  fix(api): resolve bug"
  echo "  docs: update README"
  echo ""
  echo "Valid types: feat, fix, docs, style, refactor, test, chore"
  exit 1
}

Make it executable:

chmod +x .husky/commit-msg

Manual Git Hook

#!/usr/bin/env sh
# .git/hooks/commit-msg

MESSAGE_FILE="$1"
MESSAGE=$(cat "$MESSAGE_FILE")

# Validate commit message
npx commitlint-lite "$MESSAGE" 2>&1 | tee /tmp/commitlint-error.log

if [ ${PIPESTATUS[0]} -ne 0 ]; then
  echo ""
  echo "πŸ’‘ Tips:"
  echo "  - Use present tense: 'add feature' not 'added feature'"
  echo "  - Keep it short: max 72 characters for first line"
  echo "  - Use scope to specify affected area: 'fix(auth): ...'"
  echo ""
  echo "To skip this check (not recommended):"
  echo "  git commit --no-verify"
  exit 1
fi

Using lint-staged

Combine with lint-staged for comprehensive pre-commit checks:

{
  "scripts": {
    "prepare": "husky install"
  },
  "lint-staged": {
    "*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
    "*": "commitlint-lite"
  }
}

Package.json Scripts

{
  "scripts": {
    "commit": "git-cz",
    "commitlint": "commitlint-lite",
    "commitlint:last": "git log -1 --pretty=%B | commitlint-lite --stdin",
    "commitlint:all": "git log --format=%s | commitlint-lite --stdin",
    "commitlint:pr": "git log origin/main..HEAD --format=%s | commitlint-lite --stdin",
    "prepare": "husky install",
    "release": "npm run commitlint:all && standard-version"
  }
}

Usage:

# Validate last commit
npm run commitlint:last

# Validate all commits in branch
npm run commitlint:pr

# Before release, ensure all commits are valid
npm run release

Interactive Commit Tool

Create a helper script for guided commits:

#!/bin/bash
# scripts/commit.sh

echo "🎯 Guided Commit"
echo ""

# Select type
echo "Select commit type:"
select TYPE in feat fix docs style refactor test chore ci perf build; do
  break
done

# Get scope (optional)
read -p "Scope (optional, press Enter to skip): " SCOPE

# Get description
read -p "Description: " DESC

# Build commit message
if [ -z "$SCOPE" ]; then
  MSG="$TYPE: $DESC"
else
  MSG="$TYPE($SCOPE): $DESC"
fi

# Validate
echo ""
echo "Commit message: $MSG"
npx commitlint-lite "$MSG"

if [ $? -eq 0 ]; then
  echo ""
  read -p "Proceed with commit? (y/N): " CONFIRM
  if [ "$CONFIRM" == "y" ]; then
    git commit -m "$MSG"
  fi
else
  echo "❌ Invalid commit message. Please try again."
  exit 1
fi

Add to package.json:

{
  "scripts": {
    "commit": "bash scripts/commit.sh"
  }
}

Use: npm run commit

Monorepo Integration

Lerna:

// lerna.json
{
  "command": {
    "version": {
      "conventionalCommits": true,
      "message": "chore(release): publish %s",
      "allowBranch": "main"
    }
  }
}

Validate all commits:

lerna exec -- commitlint-lite --all

Nx:

// workspace.json
{
  "tasksRunnerOptions": {
    "default": {
      "options": {
        "commitPrefix": "[{project}]"
      }
    }
  }
}

Validate with project prefix:

# Commit format: feat(api): description
commitlint-lite "feat(api): add endpoint"

Framework-Specific Examples

React / Next.js Projects

Common commit types for React projects:

{
  "types": [
    "feat",      // New feature
    "fix",       // Bug fix
    "style",     // CSS/styling changes
    "refactor",  // Code refactoring
    "perf",      // Performance improvements
    "test",      // Tests
    "docs",      // Documentation
    "chore"      // Build/config changes
  ],
  "scopes": [
    "ui",        // UI components
    "api",       // API integration
    "auth",      // Authentication
    "routing",   // Routing/navigation
    "state",     // State management
    "build",     // Build configuration
    "deps"       // Dependencies
  ]
}

Example commits:

commitlint-lite "feat(ui): add responsive navbar"
commitlint-lite "fix(auth): resolve token refresh issue"
commitlint-lite "style(ui): improve button hover effects"
commitlint-lite "refactor(state): migrate to zustand"
commitlint-lite "perf(api): implement request caching"

Vue / Nuxt Projects

{
  "types": ["feat", "fix", "style", "refactor", "perf", "test", "docs", "chore"],
  "scopes": [
    "components",
    "composables",
    "pages",
    "layouts",
    "plugins",
    "middleware",
    "store"
  ]
}

Backend (Node.js/Express)

{
  "types": ["feat", "fix", "refactor", "perf", "test", "docs", "chore", "security"],
  "scopes": [
    "api",
    "auth",
    "db",
    "middleware",
    "routes",
    "validation",
    "logging"
  ]
}

Example commits:

commitlint-lite "feat(api): add user profile endpoints"
commitlint-lite "fix(db): resolve connection pool leak"
commitlint-lite "security(auth): add rate limiting"
commitlint-lite "perf(db): optimize query performance"

Mobile (React Native)

{
  "types": ["feat", "fix", "style", "refactor", "perf", "test", "docs", "chore"],
  "scopes": [
    "ios",
    "android",
    "navigation",
    "ui",
    "notifications",
    "storage"
  ]
}

DevOps / Infrastructure

{
  "types": ["feat", "fix", "chore", "ci", "build", "deploy"],
  "scopes": [
    "docker",
    "k8s",
    "terraform",
    "ansible",
    "ci",
    "monitoring"
  ]
}

Example commits:

commitlint-lite "ci(github): add automated testing workflow"
commitlint-lite "build(docker): optimize image size"
commitlint-lite "deploy(k8s): update ingress configuration"

Troubleshooting

Issue 1: Git hook not triggering

Problem: Commits are not being validated even after installing the hook.

Cause: Hook file not executable or Git hooks disabled.

Solution:

# Check if hook exists
ls -la .git/hooks/commit-msg

# Make it executable
chmod +x .git/hooks/commit-msg

# Test the hook
.git/hooks/commit-msg .git/COMMIT_EDITMSG

# Check if hooks are disabled
git config --get core.hooksPath
# Should be empty or .git/hooks

# If using Husky, ensure it's initialized
npx husky install

Issue 2: Hook works locally but not in CI

Problem: Commits pass validation locally but fail in CI.

Cause: Different configurations or CI checking all commits in PR.

Solution:

# Local validation (last commit only)
git log -1 --pretty=%B | commitlint-lite --stdin

# CI validation (all commits in PR)
git log origin/main..HEAD --format=%s | commitlint-lite --stdin

# Align local and CI
{
  "scripts": {
    "commitlint:ci": "git log origin/main..HEAD --format=%s | while read msg; do commitlint-lite \"$msg\"; done"
  }
}

Test locally before pushing:

npm run commitlint:ci

Issue 3: Breaking change format not recognized

Problem: Commits with breaking changes are not validated correctly.

Cause: Missing ! or BREAKING CHANGE: in body.

Solution:

Valid breaking change formats:

# Option 1: Add ! after type
commitlint-lite "feat!: remove deprecated API"
βœ“ Valid

# Option 2: Add ! after scope
commitlint-lite "feat(api)!: change authentication"
βœ“ Valid

# Option 3: Include BREAKING CHANGE in body
commitlint-lite "feat: new auth system

BREAKING CHANGE: OAuth2 is now required"
βœ“ Valid

Issue 4: Merge commit failing validation

Problem: Merge commits like "Merge branch 'feature' into main" fail validation.

Cause: Merge commits don't follow conventional format.

Solution:

# Skip validation for merge commits in hook
#!/usr/bin/env sh
# .git/hooks/commit-msg

MESSAGE=$(cat "$1")

# Skip merge commits
if echo "$MESSAGE" | grep -q "^Merge "; then
  exit 0
fi

# Validate other commits
npx commitlint-lite "$MESSAGE"

Or configure Git to use conventional merge messages:

git config --global merge.commitMessage "merge(branch): %s"

Issue 5: Revert commits failing validation

Problem: git revert generates messages that fail validation.

Cause: Default revert message doesn't follow conventional format.

Solution:

Configure Git to use conventional revert format:

# .git/hooks/prepare-commit-msg
#!/usr/bin/env sh

COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2

# If this is a revert commit
if [ "$COMMIT_SOURCE" = "revert" ]; then
  # Extract reverted commit message
  REVERTED_MSG=$(cat "$COMMIT_MSG_FILE" | grep "^Revert " | sed 's/^Revert "//' | sed 's/"$//')
  
  # Format as: revert: <original message>
  echo "revert: $REVERTED_MSG" > "$COMMIT_MSG_FILE"
fi

Or manually format:

# Instead of:
git revert abc123

# Use:
git revert abc123 --no-commit
git commit -m "revert(api): undo breaking change from abc123"

Issue 6: Scope with special characters

Problem: Scopes with special characters (e.g., feat(API/v2): ...) fail validation.

Cause: Regex expects alphanumeric scopes.

Solution:

Use kebab-case for scopes:

# ❌ Bad
commitlint-lite "feat(API/v2): add endpoint"

# βœ… Good
commitlint-lite "feat(api-v2): add endpoint"
commitlint-lite "feat(api): add v2 endpoint"

Configure custom scope pattern (advanced):

// commitlint.config.js
module.exports = {
  rules: {
    'scope-case': [2, 'always', ['lower-case', 'kebab-case']]
  }
};

Issue 7: Long commit messages truncated

Problem: Detailed commit messages are rejected for length.

Cause: maxLength includes description but you need to add details.

Solution:

Use commit body for details:

# ❌ Bad - Everything in one line
git commit -m "feat(api): add user authentication with OAuth2, JWT tokens, refresh mechanism, and RBAC"

# βœ… Good - Short description + detailed body
git commit -m "feat(api): add user authentication" -m "
Implemented:
- OAuth2 integration
- JWT token generation
- Refresh token mechanism
- Role-based access control (RBAC)

Closes #123"

Configure longer maxLength if needed:

{
  "maxLength": 120
}

Issue 8: Team not following conventional commits

Problem: Team members keep committing with invalid messages.

Cause: Not familiar with format or forgetting.

Solution:

  1. Add commit template:
# .gitmessage
# type(scope): subject
#
# type: feat, fix, docs, style, refactor, test, chore
# scope: component/module affected
# subject: short description (50 chars max)
#
# Examples:
# feat(auth): add login functionality
# fix(api): resolve timeout issue
# docs: update README

git config --local commit.template .gitmessage
  1. Use interactive commit tool:
npm install -g commitizen cz-conventional-changelog

# Configure
echo '{"path": "cz-conventional-changelog"}' > .czrc

# Use
npm run commit  # or git cz
  1. Add CI feedback:
# Post helpful comment on invalid commits
- name: Comment on PR
  if: failure()
  run: |
    echo "See https://www.conventionalcommits.org/ for format guide"

Issue 9: False positives with co-authored commits

Problem: Co-authored-by trailer causes validation to fail.

Cause: Validator checking entire message including trailers.

Solution:

Validator should only check first line:

# .git/hooks/commit-msg
#!/usr/bin/env sh

MESSAGE=$(cat "$1")
FIRST_LINE=$(echo "$MESSAGE" | head -n 1)

# Validate only first line
npx commitlint-lite "$FIRST_LINE"

Co-authored commits are fine:

git commit -m "feat(api): add new endpoint

Co-authored-by: Jane Doe <jane@example.com>
Co-authored-by: John Smith <john@example.com>"

Issue 10: Emoji in commit messages

Problem: Team uses emoji but validation fails.

Cause: Emoji not part of conventional format.

Solution:

Option 1: Disable emoji (recommended for consistency)

# ❌ Don't use emoji
git commit -m "✨ feat: add feature"

# βœ… Use conventional format
git commit -m "feat: add feature"

Option 2: Allow emoji in description (configure):

{
  "allowEmoji": true
}
# Emoji after description is OK
commitlint-lite "feat: add feature ✨"
βœ“ Valid

Option 3: Use emoji as scope:

commitlint-lite "feat(✨): add feature"
# Depends on scope validation rules

Best Practices

1. Commit early, commit often

Make small, focused commits:

# ❌ Bad - One huge commit
git commit -m "feat: complete user management system"
# Changed 50 files, 2000+ lines

# βœ… Good - Multiple focused commits
git commit -m "feat(api): add user CRUD endpoints"
git commit -m "feat(ui): create user management UI"
git commit -m "test(api): add user endpoint tests"
git commit -m "docs(api): document user endpoints"

Benefits:

  • Easier to review
  • Easier to revert
  • Better git history

2. Use descriptive but concise descriptions

Get to the point quickly:

# ❌ Bad - Too vague
git commit -m "fix: fix bug"
git commit -m "feat: add stuff"

# ❌ Bad - Too detailed
git commit -m "feat: implement a comprehensive user authentication system with support for OAuth2, JWT tokens, refresh token rotation, and role-based access control for enterprise applications"

# βœ… Good - Clear and concise
git commit -m "feat(auth): add OAuth2 authentication"
git commit -m "fix(api): prevent memory leak in connection pool"
git commit -m "docs(README): add installation instructions"

Keep first line under 72 characters. Add details in the body.

3. Use scopes consistently

Define scopes early and document them:

// .commitlintrc.json
{
  "scopes": [
    "api",      // Backend API
    "ui",       // User interface
    "auth",     // Authentication
    "db",       // Database
    "ci",       // CI/CD
    "docs"      // Documentation
  ]
}

Create a CONTRIBUTING.md:

## Commit Scopes

- `api` - Backend API changes
- `ui` - UI components and pages
- `auth` - Authentication and authorization
- `db` - Database schema and queries
- `ci` - CI/CD configuration
- `docs` - Documentation

4. Include breaking changes properly

Always document breaking changes:

# βœ… Good - Clear breaking change
git commit -m "feat(api)!: change authentication endpoint

BREAKING CHANGE: /auth/login now requires OAuth2.
Update client code to use new OAuth2 flow.

Migration guide: docs/migration-v2.md"

Breaking changes should:

  • Use ! marker
  • Include BREAKING CHANGE: in body
  • Explain what changed
  • Provide migration guide

5. Link commits to issues

Reference issue numbers in commit body:

git commit -m "fix(api): resolve timeout issue" -m "
Increased timeout from 5s to 30s for large queries.
Added retry mechanism for transient failures.

Closes #123
Fixes #456
Relates to #789"

Most platforms recognize:

  • Closes #123 - Closes issue
  • Fixes #123 - Closes issue
  • Resolves #123 - Closes issue
  • Relates to #123 - Links without closing

6. Use revert type for reversions

Make reverts explicit:

# ❌ Bad
git revert abc123
# Creates: "Revert 'feat: add feature'"

# βœ… Good
git revert abc123 --no-edit
git commit --amend -m "revert(api): remove unstable endpoint

This reverts commit abc123.
Endpoint caused memory leaks in production."

7. Write commit messages in imperative mood

Use present tense, imperative mood:

# ❌ Bad - Past tense
git commit -m "fix: fixed login bug"
git commit -m "feat: added new feature"

# ❌ Bad - Present continuous
git commit -m "fix: fixing login bug"

# βœ… Good - Imperative mood
git commit -m "fix: resolve login issue"
git commit -m "feat: add user authentication"
git commit -m "refactor: simplify error handling"

Think: "This commit will [your message]"

8. Separate refactoring from features

Don't mix refactoring with feature work:

# ❌ Bad - Mixed changes
git commit -m "feat: add login and refactor auth code"

# βœ… Good - Separate commits
git commit -m "refactor(auth): simplify token validation"
git commit -m "feat(auth): add login functionality"

Makes code review easier and rollbacks safer.

9. Use conventional commits for better changelogs

Conventional commits enable automated changelog generation:

# Your commits
git log --oneline
feat: add user authentication
fix: resolve memory leak
docs: update API documentation
chore: update dependencies

# Generated changelog (using standard-version)
## [1.1.0] - 2024-01-15

### Features
- add user authentication

### Bug Fixes
- resolve memory leak

### Documentation
- update API documentation

Tools that work with conventional commits:

  • standard-version
  • semantic-release
  • conventional-changelog

10. Configure commitlint for your team

Create .commitlintrc.json and commit it:

{
  "types": ["feat", "fix", "docs", "style", "refactor", "test", "chore"],
  "maxLength": 72,
  "requireScope": true,
  "scopes": [
    "api",
    "ui",
    "auth",
    "db",
    "ci"
  ]
}

Document in CONTRIBUTING.md:

## Commit Guidelines

We use [Conventional Commits](https://www.conventionalcommits.org/).

Format: `type(scope): description`

**Types:** feat, fix, docs, style, refactor, test, chore
**Scopes:** api, ui, auth, db, ci

**Examples:**
```bash
feat(api): add user endpoints
fix(auth): resolve token issue
docs(README): update installation guide

Automated validation runs on every commit.


## Programmatic Usage

```typescript
import { parseCommitMessage, validateCommit, loadConfig } from 'commitlint-lite';

const message = 'feat(auth): add login';
const parsed = parseCommitMessage(message);
const config = loadConfig();
const result = validateCommit(parsed, config);

if (result.valid) {
  console.log('Valid commit!');
} else {
  console.error('Errors:', result.errors);
}

Why Zero Dependencies?

Most commit linting tools pull in dozens of dependencies, adding bloat and potential security risks. commitlint-lite uses only Node.js built-in modules, making it:

  • Faster to install
  • Faster to run
  • Easier to audit
  • More reliable long-term

Comparison

Tool Dependencies Install Size Speed
commitlint-lite 0 ~20KB ⚑️ Instant
@commitlint/cli 170+ ~15MB 🐒 Slow

πŸŽ“ Advanced Usage

Programmatic API

Use commitlint-lite as a library in your Node.js code:

Basic Validation

import { validateCommit, CommitValidationResult } from 'commitlint-lite';

const result: CommitValidationResult = validateCommit('feat: add login');

if (result.valid) {
  console.log('βœ… Valid commit message');
} else {
  console.error('❌ Invalid commit message');
  result.errors.forEach(error => console.error(`  - ${error}`));
}

Custom Configuration

import { validateCommit, CommitConfig } from 'commitlint-lite';

const config: CommitConfig = {
  types: ['feat', 'fix', 'docs', 'refactor'],
  maxLength: 50,
  requireScope: true,
  scopes: ['api', 'ui', 'db'],  // Optional: restrict allowed scopes
  disallowedScopes: ['wip', 'temp']  // Optional: forbid certain scopes
};

const result = validateCommit('feat(api): add endpoint', config);

Batch Validation

Validate multiple commits (e.g., all commits in a PR):

import { execSync } from 'child_process';
import { validateCommit } from 'commitlint-lite';

// Get all commits in current branch vs main
const commits = execSync('git log main..HEAD --format=%s')
  .toString()
  .trim()
  .split('\n');

const results = commits.map(msg => ({
  message: msg,
  result: validateCommit(msg)
}));

const invalid = results.filter(r => !r.result.valid);

if (invalid.length > 0) {
  console.error(`❌ ${invalid.length} invalid commit(s):`);
  invalid.forEach(({ message, result }) => {
    console.error(`\n"${message}"`);
    result.errors.forEach(err => console.error(`  - ${err}`));
  });
  process.exit(1);
}

console.log(`βœ… All ${commits.length} commits are valid!`);

Add to package.json:

{
  "scripts": {
    "validate:commits": "node scripts/validate-commits.js"
  }
}

Custom Rules Engine

Build your own validation rules:

import { validateCommit, CommitValidationResult } from 'commitlint-lite';

interface CustomRule {
  name: string;
  validate: (message: string) => string | null;  // null = pass, string = error
}

const customRules: CustomRule[] = [
  {
    name: 'no-ticket-number',
    validate: (msg) => {
      if (/TICKET-\d+/.test(msg)) {
        return 'Do not include ticket numbers in commit messages';
      }
      return null;
    }
  },
  {
    name: 'no-wip',
    validate: (msg) => {
      if (/\bwip\b/i.test(msg)) {
        return 'WIP commits are not allowed in main branch';
      }
      return null;
    }
  },
  {
    name: 'require-imperative',
    validate: (msg) => {
      const description = msg.split(':')[1]?.trim();
      if (description && /ed$|ing$/.test(description.split(' ')[0])) {
        return 'Use imperative mood (e.g., "add" not "added" or "adding")';
      }
      return null;
    }
  }
];

function validateWithCustomRules(message: string): CommitValidationResult {
  // First, validate with standard rules
  const result = validateCommit(message);
  
  if (!result.valid) return result;
  
  // Then apply custom rules
  const customErrors: string[] = [];
  
  for (const rule of customRules) {
    const error = rule.validate(message);
    if (error) {
      customErrors.push(`[${rule.name}] ${error}`);
    }
  }
  
  if (customErrors.length > 0) {
    return {
      valid: false,
      errors: [...result.errors, ...customErrors],
      warnings: result.warnings
    };
  }
  
  return result;
}

// Usage
const result = validateWithCustomRules('feat: adding new feature');
// ❌ Invalid: "Use imperative mood"

Organization-Wide Configuration

Share configuration across all projects:

1. Create shared config package:

mkdir my-commitlint-config
cd my-commitlint-config
npm init -y

index.js:

module.exports = {
  types: [
    'feat',     // New feature
    'fix',      // Bug fix
    'docs',     // Documentation
    'style',    // Formatting
    'refactor', // Code restructuring
    'test',     // Adding tests
    'chore',    // Maintenance
    'perf',     // Performance improvements
    'ci',       // CI/CD changes
    'build',    // Build system
    'revert'    // Revert previous commit
  ],
  scopes: [
    'api',
    'ui',
    'db',
    'auth',
    'billing',
    'admin'
  ],
  maxLength: 72,
  requireScope: true,
  disallowedScopes: ['wip', 'temp', 'test']
};

2. Publish to npm:

npm publish --access public

3. Use in projects:

npm install -D @mycompany/commitlint-config

.commitlintrc.json:

{
  "extends": "@mycompany/commitlint-config"
}

Pre-Commit Hooks with Husky

Modern approach using Husky 8+:

# Install Husky
npm install -D husky
npx husky init

# Create commit-msg hook
cat > .husky/commit-msg << 'EOF'
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx commitlint-lite --stdin < "$1"
EOF

chmod +x .husky/commit-msg

Alternative: Simple Husky (Husky 4 style):

{
  "husky": {
    "hooks": {
      "commit-msg": "commitlint-lite --stdin"
    }
  }
}

GitHub Actions Integration

Validate PR commits in CI:

.github/workflows/commitlint.yml:

name: Commit Lint

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  commitlint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Fetch all history for commit range
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      
      - name: Install commitlint-lite
        run: npm install -g commitlint-lite
      
      - name: Validate PR commits
        run: |
          commits=$(git log origin/${{ github.base_ref }}..HEAD --format=%s)
          echo "$commits" | while read msg; do
            echo "Checking: $msg"
            echo "$msg" | commitlint-lite --stdin
          done

Advanced: With annotations:

      - name: Validate commits with annotations
        run: |
          commits=$(git log origin/${{ github.base_ref }}..HEAD --format=%H:%s)
          failed=0
          
          while read commit; do
            hash=$(echo "$commit" | cut -d: -f1)
            msg=$(echo "$commit" | cut -d: -f2-)
            
            if ! echo "$msg" | commitlint-lite --stdin; then
              echo "::error file=.git/objects,title=Invalid Commit::$msg (commit: $hash)"
              failed=1
            fi
          done <<< "$commits"
          
          exit $failed

GitLab CI Integration

.gitlab-ci.yml:

commitlint:
  stage: test
  image: node:20
  script:
    - npm install -g commitlint-lite
    - |
      git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
      commits=$(git log origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME..HEAD --format=%s)
      echo "$commits" | while read msg; do
        echo "Validating: $msg"
        echo "$msg" | commitlint-lite --stdin || exit 1
      done
  only:
    - merge_requests

Pre-Push Validation

Validate all unpushed commits before pushing:

.git/hooks/pre-push:

#!/usr/bin/env sh

# Get commits that are about to be pushed
commits=$(git log @{u}..HEAD --format=%s 2>/dev/null)

if [ -z "$commits" ]; then
  # No remote branch yet, check all commits
  commits=$(git log --format=%s)
fi

echo "πŸ” Validating commits before push..."

failed=0
while read msg; do
  if ! echo "$msg" | npx commitlint-lite --stdin; then
    failed=1
  fi
done <<< "$commits"

if [ $failed -eq 1 ]; then
  echo ""
  echo "❌ Push rejected: Invalid commit messages found"
  echo "πŸ’‘ Fix commits with: git rebase -i"
  exit 1
fi

echo "βœ… All commits valid!"
chmod +x .git/hooks/pre-push

Interactive Commit Message Helper

Create an interactive CLI to guide users:

scripts/commit-helper.js:

#!/usr/bin/env node

const readline = require('readline');
const { validateCommit } = require('commitlint-lite');
const { execSync } = require('child_process');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

const ask = (question) => new Promise(resolve => rl.question(question, resolve));

const types = [
  'feat     - New feature',
  'fix      - Bug fix',
  'docs     - Documentation',
  'style    - Formatting',
  'refactor - Code restructuring',
  'test     - Adding tests',
  'chore    - Maintenance'
];

(async () => {
  console.log('πŸ“ Commit Message Helper\n');
  
  console.log('Available types:');
  types.forEach(t => console.log(`  ${t}`));
  console.log('');
  
  const type = await ask('Type: ');
  const scope = await ask('Scope (optional): ');
  const description = await ask('Description: ');
  const breaking = await ask('Breaking change? (y/n): ');
  
  let message = type.trim();
  if (scope.trim()) {
    message += `(${scope.trim()})`;
  }
  if (breaking.toLowerCase() === 'y') {
    message += '!';
  }
  message += `: ${description.trim()}`;
  
  console.log(`\nπŸ“‹ Generated message:`);
  console.log(`   ${message}\n`);
  
  const result = validateCommit(message);
  
  if (!result.valid) {
    console.error('❌ Invalid commit message:');
    result.errors.forEach(err => console.error(`  - ${err}`));
    rl.close();
    process.exit(1);
  }
  
  const confirm = await ask('Commit with this message? (y/n): ');
  
  if (confirm.toLowerCase() === 'y') {
    try {
      execSync(`git commit -m "${message}"`, { stdio: 'inherit' });
      console.log('βœ… Committed!');
    } catch (err) {
      console.error('❌ Commit failed');
    }
  }
  
  rl.close();
})();

Add to package.json:

{
  "scripts": {
    "commit": "node scripts/commit-helper.js"
  }
}

Usage:

npm run commit

Monorepo Support

Validate commits with package-specific scopes:

commitlint.config.js:

const fs = require('fs');
const path = require('path');

// Auto-discover packages
const packagesDir = path.join(__dirname, 'packages');
const packages = fs.readdirSync(packagesDir)
  .filter(name => fs.statSync(path.join(packagesDir, name)).isDirectory());

module.exports = {
  types: ['feat', 'fix', 'docs', 'refactor', 'test', 'chore'],
  scopes: [
    ...packages,           // All package names
    'root',                // Monorepo root changes
    'ci',                  // CI/CD
    'deps'                 // Dependency updates
  ],
  requireScope: true,
  maxLength: 100
};

Example commits:

git commit -m "feat(api): add authentication"
git commit -m "fix(ui): resolve button styling"
git commit -m "chore(deps): update dependencies"
git commit -m "ci(root): add GitHub Actions"

Smart Scope Validation

Validate scope matches changed files:

scripts/validate-scope.js:

const { execSync } = require('child_process');
const { validateCommit } = require('commitlint-lite');

// Get commit message
const message = process.argv[2] || execSync('git log -1 --format=%s').toString().trim();

// Get changed files
const files = execSync('git diff --name-only HEAD^')
  .toString()
  .trim()
  .split('\n');

// Extract scope from commit
const match = message.match(/^\w+\(([^)]+)\):/);
const scope = match ? match[1] : null;

if (scope) {
  // Check if scope matches changed files
  const scopeMatches = files.some(file => file.startsWith(`${scope}/`));
  
  if (!scopeMatches) {
    console.error(`❌ Scope "${scope}" doesn't match changed files:`);
    files.forEach(f => console.error(`  - ${f}`));
    process.exit(1);
  }
}

// Validate with commitlint-lite
const result = validateCommit(message);
if (!result.valid) {
  console.error('❌ Invalid commit message:');
  result.errors.forEach(err => console.error(`  - ${err}`));
  process.exit(1);
}

console.log('βœ… Commit message valid!');

Automated Changelog Generation

Generate CHANGELOG.md from conventional commits:

scripts/generate-changelog.js:

const { execSync } = require('child_process');
const fs = require('fs');

const commits = execSync('git log --format=%s')
  .toString()
  .trim()
  .split('\n')
  .map(msg => {
    const match = msg.match(/^(\w+)(?:\(([^)]+)\))?!?: (.+)$/);
    if (!match) return null;
    
    return {
      type: match[1],
      scope: match[2] || '',
      breaking: msg.includes('!'),
      description: match[3]
    };
  })
  .filter(Boolean);

const grouped = commits.reduce((acc, commit) => {
  if (!acc[commit.type]) acc[commit.type] = [];
  acc[commit.type].push(commit);
  return acc;
}, {});

let changelog = '# Changelog\n\n';
changelog += '## [Unreleased]\n\n';

const typeLabels = {
  feat: '### ✨ Features',
  fix: '### πŸ› Bug Fixes',
  docs: '### πŸ“š Documentation',
  refactor: '### ♻️ Refactoring',
  test: '### πŸ§ͺ Tests',
  chore: '### πŸ”§ Chores'
};

for (const [type, label] of Object.entries(typeLabels)) {
  if (grouped[type]) {
    changelog += `${label}\n\n`;
    grouped[type].forEach(commit => {
      const scope = commit.scope ? `**${commit.scope}**: ` : '';
      const breaking = commit.breaking ? ' ⚠️ BREAKING' : '';
      changelog += `- ${scope}${commit.description}${breaking}\n`;
    });
    changelog += '\n';
  }
}

fs.writeFileSync('CHANGELOG.md', changelog);
console.log('βœ… CHANGELOG.md generated!');

❓ FAQ

Q: Can I use this with Husky?

Yes! commitlint-lite works perfectly with Husky:

npm install -D husky
npx husky init
cat > .husky/commit-msg << 'EOF'
npx commitlint-lite --stdin < "$1"
EOF
chmod +x .husky/commit-msg

Q: Does this work with non-conventional commits?

No. commitlint-lite strictly enforces Conventional Commits.

If you need flexible validation, use the programmatic API with custom rules.

Q: Can I customize error messages?

Not directly, but you can wrap it:

const { validateCommit } = require('commitlint-lite');

function customValidate(message) {
  const result = validateCommit(message);
  
  if (!result.valid) {
    console.error('❌ Your commit message is invalid!\n');
    console.error('Expected format: type(scope): description\n');
    console.error('Errors:');
    result.errors.forEach(err => console.error(`  - ${err}`));
    console.error('\nπŸ’‘ Example: feat(api): add user login');
    process.exit(1);
  }
}

Q: Does this validate commit body/footer?

Not yet. Currently only validates the subject line (first line).

Coming soon:

  • Body validation
  • Footer validation (e.g., Closes #123)
  • Multiline commit support

Q: Can I use emojis in commits?

Yes, but they're not part of the type:

# ❌ Wrong
git commit -m "✨ feat: add feature"

# βœ… Correct
git commit -m "feat: ✨ add feature"

Or use gitmoji after the type:

git commit -m "feat: :sparkles: add feature"

Q: What's the difference from @commitlint/cli?

Feature commitlint-lite @commitlint/cli
Dependencies 0 170+
Install size ~20KB ~15MB
Speed ⚑️ Instant 🐒 Slow (~500ms)
Configuration Simple JSON Complex JS/TS
Plugin system No Yes
Custom rules DIY (API) Yes (plugins)

Use commitlint-lite if:

  • You want zero dependencies
  • You need speed
  • You follow standard Conventional Commits
  • You value simplicity

Use @commitlint/cli if:

  • You need complex custom rules
  • You want plugin ecosystem
  • You need extensive customization

Q: Can I enforce ticket numbers in commits?

Yes, with custom rules:

const { validateCommit } = require('commitlint-lite');

function validateWithTicket(message) {
  const result = validateCommit(message);
  
  if (result.valid && !/TICKET-\d+/.test(message)) {
    result.valid = false;
    result.errors.push('Commit must include ticket number (e.g., TICKET-123)');
  }
  
  return result;
}

Recommended format:

git commit -m "feat(api): add login endpoint [TICKET-123]"

Q: Does this work with merge commits?

Yes, but merge commits typically bypass hooks.

To enforce:

# .git/hooks/prepare-commit-msg
#!/usr/bin/env sh

if [ "$(git rev-parse --abbrev-ref HEAD)" = "main" ]; then
  # Prevent direct commits to main
  echo "❌ Direct commits to main are not allowed"
  exit 1
fi

Q: Can I use this with GitLab/Bitbucket?

Yes! Works with any Git provider.

GitLab CI example:

commitlint:
  script:
    - npm install -g commitlint-lite
    - git log -1 --format=%s | commitlint-lite --stdin

Bitbucket Pipelines:

pipelines:
  pull-requests:
    '**':
      - step:
          name: Validate commits
          script:
            - npm install -g commitlint-lite
            - git log origin/$BITBUCKET_PR_DESTINATION_BRANCH..HEAD --format=%s | while read msg; do echo "$msg" | commitlint-lite --stdin; done

Q: How do I rewrite invalid commit messages?

Interactive rebase:

# Rewrite last 3 commits
git rebase -i HEAD~3

# Change "pick" to "reword" for commits you want to fix
# Save and close

# Git will prompt you to edit each message
# Fix to valid format: type(scope): description

Amend last commit:

git commit --amend -m "feat(api): add endpoint"

Q: Can I disable validation temporarily?

Yes:

# Skip commit-msg hook
git commit --no-verify -m "wip: temporary commit"

# Or set environment variable
SKIP_COMMITLINT=1 git commit -m "wip: testing"

Hook with skip support:

#!/usr/bin/env sh
# .git/hooks/commit-msg

if [ "$SKIP_COMMITLINT" = "1" ]; then
  echo "⚠️  Skipping commit validation"
  exit 0
fi

npx commitlint-lite --stdin < "$1"

πŸ›£οΈ Roadmap

  • Body validation - Validate commit body and footer
  • Scope enumeration - Restrict scopes to predefined list
  • Emoji support - First-class emoji in types
  • Multiline commits - Better handling of body/footer
  • Custom formatters - JSON, TAP, JUnit output
  • GitHub Action - One-click CI integration
  • VS Code extension - Real-time validation

License

MIT Β© MUIN

Contributing

Issues and pull requests welcome! Visit GitHub.

About

Lightweight commit message linter. Conventional Commits validation with zero dependencies.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages