Skip to main content

Template Syntax

git-cliff uses Tera as its template engine. Tera has a syntax based on Jinja2 and Django templates.

Delimiters

Tera uses three types of delimiters that cannot be changed:
  • {{ and }} for expressions (variables, function calls)
  • {% or {%- and %} or -%} for statements (control structures)
  • {# and #} for comments

Variables

Use double curly braces to output variables:
{{ version }}
{{ commit.message }}
{{ timestamp | date(format="%Y-%m-%d") }}
Access object properties with dot notation:
{{ commit.author.name }}
{{ commit.author.email }}
{{ remote.github.owner }}

Control Flow

Conditionals

Use if statements to conditionally render content:
{% if version %}
    ## [{{ version }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}
    ## [Unreleased]
{% endif %}
Check for existence:
{% if commit.scope %}
    *({{ commit.scope }})* 
{% endif %}
Complex conditions:
{% if commit.breaking %}
    [**breaking**] {{ commit.message }}
{% elif commit.scope %}
    ({{ commit.scope }}) {{ commit.message }}
{% else %}
    {{ commit.message }}
{% endif %}

Loops

Iterate over arrays with for loops:
{% for commit in commits %}
    - {{ commit.message }}
{% endfor %}
Group and iterate:
{% for group, commits in commits | group_by(attribute="group") %}
    ### {{ group | upper_first }}
    {% for commit in commits %}
        - {{ commit.message }}
    {% endfor %}
{% endfor %}
Nested grouping (group by type, then scope):
{% for group, commits in commits | group_by(attribute="group") %}
    ### {{ group | upper_first }}
    {% for scope, scoped_commits in commits | group_by(attribute="scope") %}
        #### {{ scope | upper_first }}
        {% for commit in scoped_commits %}
            - {{ commit.message }}
        {% endfor %}
    {% endfor %}
{% endfor %}

Whitespace Control

Add - to remove whitespace:
{%- if version -%}
    Content without extra newlines
{%- endif -%}

Filters

Filters transform values using the pipe | operator:
{{ commit.message | upper_first }}
{{ commit.id | truncate(length=7, end="") }}
{{ version | trim_start_matches(pat="v") }}
Chain multiple filters:
{{ group | striptags | trim | upper_first }}

Built-in Tera Filters

Commonly used Tera filters:
FilterDescriptionExample
upperConvert to uppercase\{\{ "hello" | upper \}\}HELLO
lowerConvert to lowercase\{\{ "HELLO" | lower \}\}hello
trimRemove whitespace\{\{ " text " | trim \}\}text
truncateTruncate string\{\{ "hello" | truncate(length=3) \}\}hel...
splitSplit string\{\{ "a,b,c" | split(pat=",") \}\}[a, b, c]
firstGet first item\{\{ items | first \}\}
lastGet last item\{\{ items | last \}\}
lengthGet length\{\{ items | length \}\}
dateFormat timestamp\{\{ timestamp | date(format="%Y-%m-%d") \}\}
defaultDefault value\{\{ value | default(value="N/A") \}\}
See the Tera documentation for all built-in filters.

Custom git-cliff Filters

git-cliff provides custom filters for changelog generation:

upper_first

Converts the first character to uppercase:
{{ "hello world" | upper_first }}
Output: Hello world

find_regex

Finds all occurrences of a regex pattern:
{{ "hello world, hello universe" | find_regex(pat="hello") }}
Output: [hello, hello]

replace_regex

Replaces all occurrences matching a regex pattern:
{{ "hello world" | replace_regex(from="o", to="a") }}
Output: hella warld

split_regex

Splits a string by a regex pattern:
{{ "hello world, hello universe" | split_regex(pat=" ") }}
Output: [hello, world,, hello, universe]

Macros

Macros are reusable template functions. Define them once and call them multiple times:

Defining Macros

{%- macro remote_url() -%}
  https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}

Macros with Parameters

{% macro print_commit(commit) -%}
    - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
        {% if commit.breaking %}[**breaking**] {% endif %}\
        {{ commit.message | upper_first }} - \
        ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))
{% endmacro -%}

Calling Macros

{% for commit in commits %}
    {{ self::print_commit(commit=commit) }}
{% endfor %}

Real-World Example

Here’s a complete template from cliff.toml:
body = """
{%- macro remote_url() -%}
  https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}

{% macro print_commit(commit) -%}
    - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
        {% if commit.breaking %}[**breaking**] {% endif %}\
        {{ commit.message | upper_first }} - \
        ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))
{% endmacro -%}

{% if version %}\
    {% if previous.version %}\
        ## [{{ version | trim_start_matches(pat="v") }}]\
          ({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
    {% else %}\
        ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
    {% endif %}\
{% else %}\
    ## [unreleased]
{% endif %}\

{% for group, commits in commits | group_by(attribute="group") %}
    ### {{ group | striptags | trim | upper_first }}
    {% for commit in commits
    | filter(attribute="scope")
    | sort(attribute="scope") %}
        {{ self::print_commit(commit=commit) }}
    {%- endfor %}
    {% for commit in commits %}
        {%- if not commit.scope -%}
            {{ self::print_commit(commit=commit) }}
        {% endif -%}
    {% endfor -%}
{% endfor -%}
"""
This template:
  • Defines a remote_url() macro for GitHub URLs
  • Defines a print_commit() macro for formatting commit entries
  • Conditionally renders version header with comparison links
  • Groups commits by type (Features, Bug Fixes, etc.)
  • Sorts scoped commits within each group
  • Formats commits with scope, breaking change indicators, and links

Comments

Add comments that won’t appear in output:
{# This is a comment and won't be rendered #}
{{ commit.message }} {# inline comment #}

Next Steps

  • Context - Learn about available template variables
  • Examples - Explore complete template configurations

Build docs developers (and LLMs) love