GitTrailers

View Source

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

CI Hex version Hex downloads HexDocs Elixir 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

  • :separators — accepted separator characters; defaults to ":". The first matching character on a line is retained in trailer.separator.
  • :divider — stop before a --- patch divider; defaults to true.
  • :unfold — join continuation lines with spaces; defaults to true.
  • :known_keys — additional case-insensitive keys that count as recognized for Git's 25% trailer-block rule.
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:

  • :end — at the end of the block; the default.
  • :start — at the beginning of the whole block.
  • :after — after the last matching key, or at the end when absent.
  • :before — before the first matching key, or at the end when absent.

if_exists controls a trailer whose key already exists:

  • :add_if_different_neighbor — add unless the insertion neighbor has the same key and value; the default.
  • :add_if_different — add unless any matching trailer has the same value.
  • :add — always add.
  • :replace — replace the matching trailer nearest the insertion point.
  • :do_nothing — leave the message unchanged.

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:

  • A block must be preceded by a blank or whitespace-only line.
  • An all-trailer final paragraph is accepted.
  • A mixed paragraph requires a recognized Git prefix or a key supplied through :known_keys, with trailer records satisfying Git's 25% rule.
  • Continuation lines begin with a space or tab.
  • --- dividers and the standard scissors line bound the message region examined by default.
  • Configured separators are explicit options; ambient Git configuration is never read.

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

  • Elixir 1.14 through 1.20 and OTP 25 through 29 are exercised in CI.
  • Linux, macOS, and Windows are covered.
  • The package has zero runtime dependencies.
  • Inputs and outputs are binaries; callers choose how commit messages are obtained.

Scope

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

  • read Git configuration or trailer.<alias> definitions;
  • execute cmd or command trailer hooks;
  • run Git or read a repository;
  • interpret Conventional Commits types, scopes, or breaking-change semantics;
  • parse patch or mbox data beyond locating divider boundaries.

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.