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
denydeliberately still applies: GitHub's "Squash and merge" collectsCo-authored-bytrailers from the commits it squashes, which is the most likely way an unwanted trailer reaches a protected branch.revert --
git revertwrites its own message.fixup -- a
fixup!orsquash!commit is transient.none -- skip nothing. Needed because this option, unlike
denyandrequire, has a default thatundefcannot 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
-
How git decides what a trailer is. This plugin asks it rather than guessing.
-
The general commit-message checks -- title, width, spelling,
Signed-off-bypresence. 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.