gitlint

repository·main·Indexed 21 days ago

https://github.com/jorisroovers/gitlint

A Python-based Git commit message linter that checks commit messages for style and consistency against defined rules. It can be installed as a git commit-msg hook, integrated with the pre-commit framework, or used in CI environments via gitlint-ci. Configuration is supported through a .gitlint INI file, CLI flags, and environment variables.

Tokens
16.8K
Snippets
57
Records
66
Agent score
76%

What's inside gitlint

  1. Understand the different types of gitlint rules

    main

    gitlint categorizes rules into four distinct types that you can use to enforce commit message standards:

    1. Built-in rules: These are included with the core gitlint installation and are enabled by default.
    2. Community contributed rules (contrib): These are additional rules provided by the community. They are disabled by default but can be activated via your configuration file.
    3. User-defined rules: Custom rules that you can write yourself using a few lines of Python code.
    4. Named rules: This allows you to create multiple instances of a single rule, each identified by a unique name of your choosing.
  2. How configuration rules work in gitlint

    main

    Configuration rules are specialized user-defined rules that are applied once per commit BEFORE any other rules are run. They are designed to dynamically modify gitlint's configuration or the commit object itself based on specific circumstances (e.g., modifying behavior for all 'Release' commits).

    Warning: Configuration rules can drastically change how gitlint behaves. Before implementing one, check if:

    1. gitlint supports your use-case out-of-the-box (e.g., ignore rules).
    2. A Contrib Rule already exists.
    3. You can achieve your goal using a standard Commit or Line user-defined rule.

    To implement a configuration rule, inherit from gitlint.rules.ConfigurationRule and implement the apply(self, config, commit) method.

    from gitlint.rules import ConfigurationRule
    
    class MyConfigRule(ConfigurationRule):
        name = "my-rule-name"
        id = "UCR1"
    
        def apply(self, config, commit):
            # Modify config or commit here
            pass
  3. How CommitRule and LineRule differ

    main

    Gitlint provides two types of user-defined rules for linting commit messages:

    1. CommitRule: Applied once per commit. Use this for complex checks that span multiple lines or logic that should only execute once per commit (e.g., checking for the existence of a specific signature anywhere in the body).
    2. LineRule: Applied on a line-by-line basis. Use this for simpler, reusable checks targeting either the commit message title or every line in the body.

    While every LineRule can be implemented as a CommitRule, the reverse is not true. The primary technical differences are the validate(...) method signature and the requirement of a target attribute for LineRules.

  4. Understand gitlint configuration precedence

    main

    When multiple configuration sources are present, gitlint applies them in the following order (from highest priority to lowest):

    1. Commit specific config (e.g., gitlint-ignore: all in the commit message)
    2. Configuration Rules (e.g., ignore-by-title)
    3. Commandline convenience flags (e.g., -vv, --silent, --ignore)
    4. Environment variables (e.g., GITLINT_VERBOSITY=3)
    5. Commandline configuration flags (e.g., -c title-max-length=123)
    6. Configuration file (local .gitlint file, or file specified using -C/--config)
    7. Default gitlint config
  5. Use staged mode for manual commit message linting

    main

    When linting a commit message via stdin or --msg-filename (where no actual git commit exists yet), enable --staged to allow gitlint to use heuristics (like checking git config or staged changes) to guess metadata like author, email, and branch. Without this, rules requiring metadata (like M1:author-valid-email) cannot be enforced.

    gitlint --staged
    # or in .gitlint
    [general]
    staged=true
  6. Enable user-defined rules in gitlint

    main

    gitlint allows you to write custom rules in Python. To use them, you must tell gitlint where to find your rule files using the extra-path option. This option can point to either a directory containing Python files or a specific Python file. Paths can be relative to your current working directory or absolute paths.

    ### Using .gitlint config
    ```ini
    [general]
    extra-path=tools/gitlint/myrules

    Using CLI

    gitlint --extra-path "tools/gitlint/myrules"
    # Or using -c style config flags
    gitlint -c general.extra-path=tools/gitlint/myrules

    Using Environment Variables

    GITLINT_EXTRA_PATH=tools/gitlint/myrules gitlint
  7. Implement a LineRule

    main

    To create a LineRule, extend the LineRule class.

    Requirements:

    • name: A human-friendly string.
    • id: A unique identifier (it is recommended to start with UL for User-defined Line-rules).
    • target: Specifies where the rule is applied. Use CommitMessageTitle to target only the title, or CommitMessageBody to target every line in the body.
    • validate(self, line, _commit): A method that receives the current line as the first argument and the commit object as the second.

    Configurability: You can define options_spec using classes like ListOption to make your rule configurable. Options are accessed via self.options["option-name"].value.

    Example: Enforcing that the commit title does not contain specific special characters.

    from gitlint.rules import LineRule, RuleViolation, CommitMessageTitle
    from gitlint.options import ListOption
    
    class SpecialChars(LineRule):
        """Enforce that the commit message title does not contain specific characters."""
    
        name = "title-no-special-chars"
        id = "UL1"
        target = CommitMessageTitle
    
        options_spec = [
            ListOption(
                "special-chars",
                ["$", "^", "%", "@", "!", "*", "(", ")"],
                "Comma separated list of chars that cannot occur in the title",
            )
        ]
    
        def validate(self, line, _commit):
            violations = []
            for char in self.options["special-chars"].value:
                if char in line:
                    msg = f"Title contains the special character '{char}'"
                    violation = RuleViolation(self.id, msg, line)
                    violations.append(violation)
    
            return violations
  8. Implement a CommitRule

    main

    To create a CommitRule, extend the CommitRule class.

    Requirements:

    • name: A human-friendly string.
    • id: A unique identifier (it is recommended to start with UC for User-defined Commit-rules).
    • validate(self, commit): A method that receives a single commit argument. It should return a RuleViolation (or a list of them) if the commit fails validation, or return nothing if it passes.

    Example: Checking for a "Signed-off-by" line in the commit body.

    from gitlint.rules import CommitRule, RuleViolation
    
    class SignedOffBy(CommitRule):
        """Enforce that each commit contains a "Signed-off-by" line."""
    
        name = "body-requires-signed-off-by"
        id = "UC2"
    
        def validate(self, commit):
            for line in commit.message.body:
                if line.startswith("Signed-off-by"):
                    return
    
            msg = "Body does not contain a 'Signed-off-by' line"
            return [RuleViolation(self.id, msg, line_nr=1)]
  9. Use gitlint-ci in CI environments

    main

    For Continuous Integration (CI) environments, use the gitlint-ci hook. This hook is designed to run in CI and can be configured to lint multiple commits.

    1. Add both gitlint and gitlint-ci to your .pre-commit-config.yaml.
    2. Invoke the hook in your CI environment using the manual stage.

    By default, gitlint-ci only lints the latest commit. To lint a range of commits (e.g., all commits in a specific branch), pass the --commits argument with the branch name.

    -   repo: https://github.com/jorisroovers/gitlint
        rev:  # Fill in a tag / sha here (e.g. v0.19.1)
        hooks:
        -   id: gitlint
        -   id: gitlint-ci
            args: [--debug, --commits, mybranch]
    # To run the CI hook
    pre-commit run --hook-stage manual gitlint-ci
  10. Configure gitlint using a .gitlint file

    main

    The recommended way to configure gitlint is by creating a .gitlint file in your repository. This file uses the INI format and allows you to manage general settings, ignore specific rules, enable community-contributed rules, and customize individual rule parameters.

    Key sections include:

    • [general]: Used for global settings like ignore, contrib, and extra-path.
    • [rule-name] or [rule-id]: Used to configure specific rule parameters (e.g., [T1] or [title-max-length]).
    [general]
    # Ignore rules by name or ID
    ignore=title-trailing-punctuation, T3
    
    # Enable community rules
    contrib=contrib-title-conventional-commits,CC1
    
    # Path to user-defined rules
    extra-path=./gitlint_rules/my_rules.py
    
    [title-max-length]
    line-length=80 
    
    [title-min-length]
    min-length=5
  11. Report violations in user-defined rules

    main

    To signal a violation within a custom rule, the validate(...) method must return a list of RuleViolation objects.

    Key requirements for validate(...):

    • If there are no violations, you can return an empty list [] or simply skip the return statement.
    • If there is a single violation, you must return a list containing that one item (e.g., [RuleViolation(...)]).
    • For LineRules, if you do not explicitly set the line_nr, gitlint will attempt to automatically set it to the correct line number.
    def validate(self, commit):
        for line_nr, line in commit.message.body:
            if "Jon Snow" in line:
                # Add 1 to the line_nr to offset the title which is on the first line
                violation_line_nr = line_nr + 1
                msg = "Commit message has the words 'Jon Snow' in it"
                return [RuleViolation(self.id, msg, line, violation_line_nr)]
        return []