GitTrailers

Parse, serialize, and manipulate Git commit-message trailers in pure Elixir, with behavior anchored to git interpret-trailers and zero runtime dependencies.

CIHex versionHex downloadsHexDocsElixir 1.14+License: MIT

Git trailers are structured Key: value records in the final block of a commit message, such as Signed-off-by, Reviewed-by, Fixes, and Co-authored-by. Their detection rules are more precise than a footer regex: Git supports folded values, configurable separators, a 25% tolerance rule, recognized keys, and patch-divider handling.

git_trailers implements those rules for in-memory binaries. It never shells out, reads Git configuration, touches a repository, or performs network access.

Installation

Add git_trailers to your dependencies:

def deps do
[
{:git_trailers, "~> 1.0"}
]
end

Parse

GitTrailers.parse/2 returns a tagged result containing the subject, body, block position, divider status, and parsed trailer structs:

message = """
Add audit export
Keep the report ordering stable.
Reviewed-by: Alice <alice@example.com>
Signed-off-by: Bob <bob@example.com>
"""
{:ok, result} = GitTrailers.parse(message)
result.subject
#=> "Add audit export"
result.body
#=> "\nKeep the report ordering stable.\n"
Enum.map(result.trailers, &{&1.key, &1.value})
#=> [
#=> {"Reviewed-by", "Alice <alice@example.com>"},
#=> {"Signed-off-by", "Bob <bob@example.com>"}
#=> ]
result.block_start
#=> 4

Message content is never considered malformed. If Git's block rules do not accept a trailer block, parsing succeeds with trailers: [] and block_start: -1. Invalid argument types and invalid options raise ArgumentError.

Folded RFC 822-style values are unfolded by default while exact source bytes remain in raw:

{:ok, %{trailers: [trailer]}} =
GitTrailers.parse("subject\n\nCc: Alice\n Bob\n")
trailer.value
#=> "Alice Bob"
trailer.raw
#=> "Cc: Alice\n Bob\n"

Set unfold: false to keep physical folding in value as well.

Parse options

GitTrailers.parse(message,
separators: ":%=",
known_keys: ["Audit-key"],
divider: false,
unfold: false
)

Add and update trailers

GitTrailers.add/3 accepts {key, value} tuples, maps with :key and :value, or parsed GitTrailers.Trailer structs:

GitTrailers.add(
"Fix export\n",
[
{"Fixes", "#42"},
%{key: "Reviewed-by", value: "Alice <alice@example.com>"}
]
)
#=> "Fix export\n\nFixes: #42\nReviewed-by: Alice <alice@example.com>\n"

When a trailer block exists, additions are applied sequentially and key matching is case-insensitive. The default behavior adds at the end unless the same key and value are already adjacent to that insertion point.

Placement and duplicate policies

where controls placement:

if_exists controls a trailer whose key already exists:

if_missing is :add by default and may be set to :do_nothing. Use trim_empty: true to remove existing whitespace-only trailers and ignore incoming empty values.

GitTrailers.add(message, [{"Reviewed-by", "Bob"}],
where: :after,
if_exists: :add_if_different,
if_missing: :add,
trim_empty: true
)

No-op mutations return the original binary byte-for-byte. When a block changes, that block is emitted with LF and canonical spacing; bytes outside it, including CRLF content and divider material, are preserved.

Format and serialize

Format one trailer with a one-character separator:

GitTrailers.format({" Fixes ", " #42 "})
#=> "Fixes: #42"
GitTrailers.format(%{key: "Bug", value: "42"}, "#")
#=> "Bug# 42"

Serialize a collection in canonical form. The result uses LF between records and has no final newline:

GitTrailers.serialize([{"Fixes", "#42"}, {"Reviewed-by", "Alice"}])
#=> "Fixes: #42\nReviewed-by: Alice"

Keys may contain ASCII letters, digits, and hyphens. Values may contain ordinary Unicode text but not CR or LF; use parsed folded values rather than constructing multiline serializer input.

Git compatibility

The implementation targets the behavior documented by git-interpret-trailers(1) and Git 2.54.0's trailer.c:

CI runs the language-neutral conformance corpus shared with the TypeScript git-trailers package and performs differential comparisons against a pinned Git 2.54.0 binary. The suite also includes real-world Linux and AI-attribution fixtures, property tests, and 100% production-module coverage.

Compatibility

Scope

The package parses, formats, serializes, and manipulates commit-message text. It deliberately does not:

Use git log --format=%B, a repository library, or another transport layer to obtain the message binary, then pass that binary to GitTrailers.

Provenance

Behavior is tested against independently expressed cases adapted from Git's t7513-interpret-trailers.sh, a shared language-neutral corpus, and pinned real-world fixtures. Attribution details are recorded in THIRD_PARTY_NOTICES.md.

License

MIT. See LICENSE.