Git trailers are a powerful source of metadata as parsed by the Git Interpret Trailers command. Even better, trailers can be applied to commits and tags as documented here:
-
Git Commit Trailers (added in Git 2.32.0).
-
Git Tag Trailers (added in Git 2.46.0).
Unfortunately, trailers are severely underutilized so I’d like to show you why they are important and how you can make the most of them to improve your workflow. 🚀
Quick Start
By default, you can leverage the --trailer
option as first introduced in Git 2.32.0 when creating commit messages. Example:
git commit --message "Fixed log format" --trailer "Milestone: patch"
git show
When you run the above, you’ll end up with the following output (depending on how you like to format git show
):
717dcffd2eed G Brooke Kuhlmann Fixed log format (HEAD -> demo) 1 second ago. Milestone: patch lib/demo/container.rb | 8 ++++++++ 1 file changed, 8 insertions(+)
Notice the Milestone trailer is added at the bottom of the commit message. This where all trailers (metadata) exists which provides a rich source of information for post-processing tools like Milestoner (more on this shortly). Even better, you can add multiple trailers at once. Example:
git commit --message "Fixed log format" \
--trailer "Issue: abc" \
--trailer "Milestone: patch" \
--trailer "Tracker: tana"
git show
# 181c9172ae4e G Brooke Kuhlmann Fixed log format (HEAD -> demo) 1 second ago.
#
# Issue: abc
# Milestone: patch
# Tracker: tana
#
# lib/demo/container.rb | 8 ++++++++
# 1 file changed, 8 insertions(+)
Once again, notice we have the following trailer information at the bottom of the commit message:
Issue: abc Milestone: patch Tracker: tana
In this case, we know this commit is a patch for the abc issue as found within the Tana issue tracker. That’s a lot of useful information we can use later without degrading the readability of our commit messages. 🚀
We’re not limited to applying trailers to our commits, we can use them in our tags too as introduced in Git 2.46.0. Example:
git tag 0.0.0 --message "Version 0.0.0" \
--no-sign \
--trailer Commits:1 \
--trailer Files:1 \
--trailer Deletions:0 \
--trailer Insertions:10
Then we can the verify our tag as follows:
git tag --verify
# object 23dec31973e5654c00f700e3755b32c55574b93a
# type commit
# tag 0.0.0
# tagger Brooke Kuhlmann <[email protected]> 1725826447 -0600
#
# Version 0.0.0
#
# Commits: 1
# Files: 1
# Deletions: 0
# Insertions: 10
# error: no signature found
Notice, as with commits, we see our trailer information at the bottom of our tag. Different trailers are used for tags because capturing metrics is more interesting for tags than commits but we’ll talk more about this difference shortly.
Commits
Now that you have a taste of what trailers are, let’s talk about why trailers are important. We’ll start with Git Commit Trailers followed by Git Tag Trailers.
Context
The most obvious is avoiding these kinds of commit messages which are unnecessarily unfriendly to read due to not being proper human friendly sentences:
[FIX]: Corrected log output. fix: Corrected log output [Billy Bob]: Fixed log output [ACME abc]: Fixed log output #abc: Fixed log output Fixed log output [ACME: abc] Fixed log output [Project: ACME, Issue: abc]
Notice the examples above all deal with the same kind of commit message which could me more easily be distilled and reformatted to the following as previously discussed in my Git Commit Anatomy article:
Fixed log output by sorting all keys Necessary to ensure each log message is consistent for post-processing. Issue: abc
Now we have a nicely formatted Git commit message with a project subject (i.e. what), body (i.e. why), and trailers (i.e. metadata) which yields the following benefits:
-
👀 Readability: Each commit reads like a proper English sentence without the need to filter additional noise.
-
📖 Code Reviews: As more of these nicely formatted commit messages are made, we have commit messages that tell a story of how the implementation was architected. Each commit becomes a page in a book. This vastly improves the speed of code review feedback so you can get more code rebased onto the
main
branch. -
📰 Milestones: Due to each commit message being human friendly, you can disseminate information to multiple parties at once: Engineering, Marketing, Project Managers, Customers, etc. All of this is possible by automatically generating release notes based on your Git Trailer metadata. Tools, like Milestoner, automate this process for you.
-
🚀 Deploys: Again, using a tool like Milestoner, you can automate the versioning of your next deploy based on Git Trailer metadata. Even better, the next version can be automatically calculated for you based on the Milestone trailer.
Kinds
There is no specification on the kinds of trailers you can or can’t use because, by default, they only need to be key/value pairs at the bottom of your commit message. That said, I’ve been working to formalize this more to where I generally use all or some of the following:
Co-authored-by: River Tam <[email protected]> Format: asciidoc Issue: 123 Milestone: patch Reviewer: github Signed-of-by: Malcolm Reynolds <[email protected]> Tracker: tana
The above can be broken down as follows:
-
Co-authored-by: Optional. Any/all collaborators that you worked with. This is a great way to give credit and celebrate your fellow team members and/or open source contributors.
-
Format: Optional. The format (or MIME Type) you wrote your commit message in. This allows version release notes to be automatically rendered in multiple formats (i.e. HTML, RSS, JSON, ASCII Doc, Markdown, plain text, etc.) for beautiful and highly interactive release notes.
-
Issue: Optional. The ID of the issue you are working on. When automating version release notes and/or deployments, this information is highly valuable to fellow team members and/or stakeholders. In some cases, you might not have an issue because you took the initiative and improved/fixed the implementation anyway and that’s perfectly fine too. Kudos!
-
Milestone: Optional. The milestone you are targeting with your change. When supplied, this enables automatic version bumping by calculating the highest change (i.e. major, minor, or patch) and updating the version accordingly.
-
Reviewer: Optional. The code review system used. When combined with the
tracker
andissue
keys, the associated code review link can be automatically generated for reporting purposes. -
Signed-off-by: Optional. Denotes who signed off on the work. Works well with the
git commit --signoff
orgit commit --no-signoff
commands, for example. -
Tracker: Optional. The issue tracking system used. When combined with the
issue
key, the associated issue tracker link can be automatically generated for version reporting purposes.
Formats
The Git Interpret Trailers specification doesn’t have a hard limit what you can use for a trailer but the general rule of thumb is to format your trailers as follows:
-
Must always be placed at the bottom of your commit message.
-
Must have a space between the end of your commit message and start of trailers.
-
Each trailer needs to be a key/value pair delimited by a colon. Example:
Key: value
. -
Each key should have the first letter capitalized.
-
Each key can be delimited by dashes if necessary. Example:
Co-authored-by
.
Given the above, you could create the following commit with a subject, body, and trailer information:
touch demo.txt
git add demo.txt
git commit --trailer "Issue: abc" \
--trailer "Milestone: patch" \
--trailer "Tracker: tana"
Now you can use Git Interpret Trailers to inspect the trailers of any commit (be it saved or unsaved):
# Saved Commit (i.e. the last commit)
git log --format=%B -1 | git interpret-trailers --parse
# Issue: abc
# Milestone: patch
# Tracker: tana
# Unsaved Commit (i.e. the last commit made via your editor)
git interpret-trailers --only-trailers .git/COMMIT_EDITMSG
# Issue: abc
# Milestone: patch
# Tracker: tana
Taking this a step further, you can include trailers in your logs by using pretty formats. Here’s a simplified version of my Dotfiles gtail
function which displays all commits made since the last tag in reverse order:
git log --reverse \
--color \
--pretty=format:"%C(yellow)%h%C(reset) %C(bold blue)%an%C(reset) [%(trailers:key=Milestone,valueonly=true,separator=)] %s%C(bold cyan)%d%C(reset) %C(green)%cr.%C(reset)" \
"$(git describe --abbrev=0 --tags --always)..HEAD"
The format to pay attention to is: %(trailers:key=Milestone,valueonly=true,separator=)
. This formats my trailer as follows:
-
Looks for the
Milestone
trailer key. -
Only grabs the value of this key.
-
Ignores the trailer separator because we don’t want carriage returns in our output in order for each commit to be listed on a single line.
The result of the above looks like this:

Keep in mind that trailers always go at the end of a commit message, after the body per Git specification, and should never be used in the subject or body. Doing so ensures a commit message remains readable by fellow engineers and keeps the metadata in a format that is useful for post-processing by a program. Git Lint can aid in this endeavor by ensuring each commit message is consistently formatted and prevent garbage in, garbage out situations.
Hooks
We’ve discussed what Git Commit Trailers are and why they are important so now I want to teach you how to make use of them in an automated fashion. This is where Git Branch Descriptions and Git Hooks come into play.
Git Branch Descriptions, if not familiar, are a nice way to provide additional information about the current branch you are working with and you can add descriptions as easily as running the following from the command line:
git switch --create demo
git branch --edit-description
Assuming you saved the following message for your branch:
A demonstration branch Issue: abc Tracker: tana
You’d then be able to view this information via the following command:
git config --get branch.demo.description
# A demonstration branch
#
# Issue: abc
# Tracker: tana
💡 Ensure you always provide a subject for your branch description or git interpret-trailers
won’t be able to parse your trailers. In other words, you can’t have only trailers in your branch description you must have a subject, space, and trailers.
Next, you can automatically apply the above to each commit message by adding a prepare-commit-msg Git Hook which triggers after preparing the default commit message but before your editor is launched. As a quick start, you can copy and edit the sample Git Hook provided for you when creating a new repository (assuming you are currently in the root of your repository). Example:
cp .git/hooks/prepare-commit-msg.sample .git/hooks/prepare-commit-msg
$EDITOR .git/hooks/prepare-commit-msg
💡 I use global Git Hooks so I don’t have to maintain duplicate copies of my Git Hooks across multiple repositories. That said, using your current repository for experimentation is a quick way to get started.
Now that you are editing your .git/hooks/prepare-commit-msg
hook, you can copy and past the following implementation into it. Each line is documented so you can quickly grok the implementation:
#! /usr/bin/env bash
# Defines Git prepare commit message functionality.
# Enable safe defaults.
set -o nounset
set -o errexit
set -o pipefail
IFS=$'\n\t'
# The first argument, provided by Git, is the path to your commit message.
local commit_message_path="$1"
# The second argument, provided by Git, is the kind of commit being made.
local kind="$2"
# Set safe defaults.
local branch=""
local trailers=""
# We start by obtaining the name of the current branch.
branch="$(git branch --show-current | tr -d '\n')"
# Next, we let Git parse any trailers from the branch description as a new line delimited string.
# Use of (|| :) is a no operation to prevent premature Git hook abortion should the command fail.
trailers="$(git config --get "branch.$branch.description" | git interpret-trailers --parse || :)"
# Finally, we add trailers to the commit message based on kind of commit being created.
case "$kind" in
message)
printf "\n\n%s" "$trailers" >> "$commit_message_path";;
template)
sd --max-replacements 1 "\n#" "\n$trailers\n\n#" "$commit_message_path";;
esac
You might be curious about the case
statement. To elaborate, we can potentially have the following kinds of commits:
-
message
-
template
-
merge
-
squash
-
commit
We only care about the first two. The template
(kind) uses sd since sd
is easier to use than sed
or awk
. I also want to be conscious of any custom Git commit templates (i.e. ~/.config/git/template
, see my Git Configuration article for more info) so need the trailers to be added before the commit message comments begin. In other words, the end of the commit message.
When you combine the above together, you get a nice workflow. Example:
git switch --create demo
# Add description as shown above.
git branch --edit-description
touch one.txt
git add one.txt
# Add subject and body (notice your trailers are auto-injected!)
git commit
touch two.txt
git add two.txt
git commit
Now when you run git log
on the above (depending on how you like to format your logs), you should something similar to the following output:

Tags
Now that we’ve talked about Git Commit Trailers, let’s dive into Git Tag Trailers.
Kinds
Trailers serve a slightly different purpose than what we used for commits. For instance, you wouldn’t want to use the same trailers for tags as used with commits because the context is entirely different. Instead, using trailers that provide statistical information is better for reporting purposes. What I’ve found to be most useful is the following:
-
Commits: The total number of commits the tag consists of.
-
Files: The total number of files changed for tag.
-
Deletions: The total number of lines deleted for the tag.
-
Duration: The total amount of time (in seconds) from first to last commit before the tag was created.
-
Insertions: The number of lines changed for the tag.
-
URI: The URI to where your release notes are stored.
Formats
Using Git Interpret Trailers, you can format your trailers by piping tag output to interpret trailers for parsing:
git tag --verify 0.0.0 | git interpret-trailers --parse
Commits: 2
Deletions: 0
Duration: 92663
Files: 25
Insertions: 706
URI: https://alchemists.io/projects/test/versions
You can also list your tags with formatting as follows:
git tag --list \
--color \
--format="%(color:yellow)%(refname:short)%(color:reset)|%(taggerdate:short)|%(color:brightblue)%(taggername)%(color:reset)|%(color:green)%(trailers:key=Commits,key=Files,key=Deletions,key=Insertions,key=Duration,separator=|)%(color:reset)" \
| column -s "|" -t
The above will produce output that looks like this:

Notice I use separator=|
where the pipe character serves as a delimiter for all keys so the column
command can format everything as a table of data. To spruce this up further, you can apply color to individual trailers but requires more syntax. Example:
git tag --list \
--color \
--format="%(color:yellow)%(refname:short)%(color:reset)|%(taggerdate:short)|%(color:brightblue)%(taggername)%(color:reset)|%(color:brightmagenta)%(trailers:key=Commits,separator=)%(color:reset)|%(color:brightmagenta)%(trailers:key=Files,separator=)%(color:reset)|%(color:green)%(trailers:key=Deletions,separator=)%(color:reset)|%(color:red)%(trailers:key=Insertions,separator=)%(color:reset)|%(color:brightmagenta)%(trailers:key=Duration,separator=)%(color:reset)" \
| column -s "|" -t
The above will produce output as follows:

Notice I use the pipe character to delimit each trailer between the reseting and starting new colors. This is important for the column
command to delimit and format properly. You also have to supply separator=
with no value to prevent new line characters (default behavior) from showing up in your output. While tedious, there isn’t a way to avoid this behavior due to Git ensuring backwards compatibility with earlier versions.
The URI
trailer is not shown in any of the above examples because the URI can sometimes be long and consume too much screen real-estate so only statistical information is displayed instead. Feel free to customize for your own purposes, though.
For more on all of this, check out the following:
Tools
Assuming you’ve enjoyed learning about trailers and would like to put them to use in your own projects, I highly recommend the following tooling to aid in this endeavor (you can pilfer from my Dotfiles project as well):
-
Git Lint: Lints your Git commits so they are of high and consistent quality.
-
Milestoner: Automates the generation of release notes, versioning, and deployment of your project.
While the above tools are architected by me, and apologies for the self promotion, I’ve been using these tools on all of my projects for years to great effect. If you’ll indulge me a bit longer — and using Milestoner — you can build nice release notes for the above. Example (i.e. milestoner build --format web
):

All of this is made possible due to having tooling that can read and parse your Git Trailers so your commit messages remain enjoyable to read for faster collaboration with your colleagues. Not bad for an automated workflow! 🚀
Conclusion
I hope you’ve enjoyed learning about Git Trailers, what they are, why they are important, and how to leverage them so you can immediately apply them to your own workflow. Writing high quality software doesn’t have to be tedious nor does collaboration have to be grueling. With a little ingenuity and tooling, you can make your engineering workflows a breeze. Enjoy!