NAME

Git::MoreHooks::CheckTrailers - GitHooks plugin to enforce a commit message trailer policy

VERSION

version 0.020

SYNOPSIS

Use package via Git::Hooks interface (git config file).

DESCRIPTION

STATUS

Package Git::MoreHooks is currently being developed so changes in the existing hooks are possible.

[githooks]
    plugin = Git::MoreHooks::CheckTrailers
[githooks "checktrailers"]
    deny = Co-Authored-By
    require = AI-assisted-by

Check the trailers of a commit message -- the Token: value lines in its last paragraph -- against a policy the repository sets. Three independent things can be asked for: that named trailers are absent, that named trailers are present, and that they appear in a given order.

The plugin ships with no trailer names at all and does nothing until a repository names its own. That is deliberate: there is no settled spelling for a disclosure trailer -- the Linux kernel, Fedora and LLVM say Assisted-by:, other projects say AI-assisted-by: -- and a hook has no business holding an opinion about it.

Why a plugin, when CheckLog has match

Git::Hooks::CheckLog can already reject a message matching a pattern, and for a simple ban that is enough. What it cannot do is distinguish a line git reads as a trailer from one that merely looks like it, which this plugin needs for both its clearest error message and its order check, nor can it tell Signed-off-by: a person from Signed-off-by: a program.

What this is for

The case it was written for is a tool writing Co-Authored-By: into a commit it helped with. That trailer is an authorship trailer carrying Name <email>, and GitHub and GitLab turn it into contributor attribution -- so naming a program there asserts that a person co-authored the commit, and mints a contributor who does not exist. A disclosure trailer of the repository's own choosing says the true thing instead.

It generalises: any trailer a project wants kept out of, or insisted upon in, its history.

How the trailers are read

By git interpret-trailers --parse, not by parsing the message here. The harm this plugin prevents is whatever git and the forges read as a trailer, and the only parser guaranteed not to drift from git is git. In particular a message whose last paragraph holds a body line above its trailers, or a space before a colon, or a value folded over two lines, is parsed by git perfectly well while a stricter reading sees no trailers at all.

Lines that look like trailers but which git does not read as such are reported separately and more mildly -- see "githooks.checktrailers.deny-in-body BOOL".

USAGE

As a Git::Hooks plugin, enabled by name:

[githooks]
    plugin = Git::MoreHooks::CheckTrailers

It hooks itself to:

  • commit-msg, applypatch-msg

    Locally, as the commit is made.

  • update, pre-receive, ref-update, commit-received, submit

    In the remote repository, once per affected reference.

  • patchset-created, draft-published

    For Gerrit.

The server side is the real gate. git rebase, git filter-branch and non-interactive git cherry-pick do not run commit-msg at all, so a client-side check is incomplete by nature, and git commit --no-verify is always available. Enable the plugin in the bare repository too.

This plugin never rewrites a message. A prepare-commit-msg hook that stripped an unwanted trailer would run before the tool appended it, and would silently edit what the author wrote. Checking and failing is the only honest behaviour, and every message it emits names the amendment to make.

CONFIGURATION

The plugin is configured by the following options.

githooks.checktrailers.deny SPEC

May be specified more than once. Reject the commit if a matching trailer is present. No default: with none configured, nothing is denied.

githooks.checktrailers.require SPEC

May be specified more than once. Reject the commit if no matching trailer is present. No default.

Leave it unset in a repository where only some commits are AI-assisted: a commit written by a person should not be made to claim otherwise. To require a trailer on one branch only, scope the plugin with githooks.ref rather than relaxing the rule. The literal value undef clears any inherited setting:

[githooks "checktrailers"]
    require = undef

SPEC syntax

SPEC ::= TOKENMATCH [ whitespace VALUEMATCH ]

TOKENMATCH is either a trailer token (letters, digits and -, as git itself allows) or a qr// pattern. VALUEMATCH, if given, is a qr// pattern or a literal substring. Both are matched case-insensitively, and the spacing around the colon never matters, because git normalises it before this plugin sees it.

deny    = Co-Authored-By
deny    = Signed-off-by qr/(?i:claude|copilot|gemini|codex|cursor|aider)/
require = AI-assisted-by

The second line is the reason VALUEMATCH exists: a sign-off by a person is exactly what a DCO wants, and a sign-off by a program is what the Linux kernel forbids. Only the latter is rejected.

A qr// must end at its own delimiter, so trailing flags (qr/.../msx) are a configuration error rather than something silently dropped. Write qr/(?x: ... )/ instead.

Not matched, deliberately: Co_Authored_By, CoAuthoredBy and other spellings git does not parse as trailers. They cannot create a false attribution, so they are prose. A repository that wants them caught anyway writes a pattern: deny = qr/co[-_]?authored[-_]?by/.

githooks.checktrailers.order TOKEN

May be specified more than once; the order of the options is the required relative order of the trailers. No default.

Only trailers named here are constrained, and a trailer that is absent is never an error -- that is what require is for. The special value * stands for "any other trailer", which is how "the sign-off comes last" is said:

[githooks "checktrailers"]
    order = *
    order = AI-assisted-by
    order = Signed-off-by

githooks.checktrailers.deny-in-body BOOL

Default false. A deny check also looks at lines which merely look like the forbidden trailer -- at column zero, with a Token: value shape -- and reports them separately, because git will start reading them as trailers the moment the message is reflowed, squash-merged or gains a Signed-off-by.

By default only the last paragraph is examined that way. Setting this option true examines every paragraph after the title, which catches more and also starts matching prose: the commit which adds a deny rule to a repository usually has to name the forbidden trailer in order to explain itself.

githooks.checktrailers.require-skip KIND

May be specified more than once. Default: merge revert fixup. Kinds of commit which the require check does not apply to. deny is never skipped.

  • merge -- a merge commit has no footer of its own. Note that deny deliberately still applies: GitHub's "Squash and merge" collects Co-authored-by trailers from the commits it squashes, which is the most likely way an unwanted trailer reaches a protected branch.

  • revert -- git revert writes its own message.

  • fixup -- a fixup! or squash! commit is transient.

  • none -- skip nothing. Needed because this option, unlike deny and require, has a default that undef cannot clear.

githooks.checktrailers.help-on-error MESSAGE

Not an option of this plugin but of Git::Hooks itself, and the right place for a repository's own explanation:

[githooks "checktrailers"]
    help-on-error = "Co-Authored-By names a human co-author. Disclose AI \
        assistance with 'AI-assisted-by:' instead. See AI_DISCLOSURE.md."

EXPORTS

This module exports the routines below, which can be used directly without the rest of the Git::Hooks infrastructure. Each returns a count of the problems it found and reports them through $git->fault.

check_message_file GIT, MSG

Implements the commit-msg and applypatch-msg hooks.

check_ref GIT, REF

Implements the update and pre-receive hooks, checking every commit affecting REF.

check_patchset GIT, BRANCH, COMMIT

Implements the Gerrit hooks.

message_errors GIT, COMMIT, MSG

The whole check for one message. COMMIT may be undefined, as it is on the client side.

REFERENCES

  • git-interpret-trailers

    How git decides what a trailer is. This plugin asks it rather than guessing.

  • Git::Hooks::CheckLog

    The general commit-message checks -- title, width, spelling, Signed-off-by presence. This plugin is about trailer policy specifically and composes with it.

AUTHOR

'Mikko Koivunalho <mikkoi@cpan.org>'

COPYRIGHT AND LICENSE

This software is copyright (c) 2022 by Mikko Koivunalho.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.