Skip to main content
git-cliff can include commits from Git submodules in your changelog, allowing you to track changes across your main repository and its dependencies.

Enabling Submodule Support

Enable submodule commit processing in your configuration file:
[git]
recurse_submodules = true
With this setting, git-cliff will process commits from all submodules and include them in the changelog.

Basic Example

Here’s a basic template that includes submodule commits:
[git]
recurse_submodules = true

[changelog]
body = """
{% for submodule_path, commits in submodule_commits %}
    ### {{ submodule_path | upper_first }}
    {% for group, commits in commits | group_by(attribute="group") %}
        #### {{ group | upper_first }}
        {% for commit in commits %}
            - {{ commit.message | upper_first }}\
        {% endfor %}
    {% endfor %}
{% endfor %}
"""
Example output:
### lib/external-api
#### Features
- Add authentication support
- Implement rate limiting

#### Bug Fixes
- Fix timeout handling

### themes/default
#### Features
- Add dark mode support

Template Variables

submodule_commits

A map of submodule paths to their commits:
{% for submodule_path, commits in submodule_commits %}
    Submodule: {{ submodule_path }}
    Commits: {{ commits | length }}
{% endfor %}
Each commit has the same properties as regular commits:
  • id - commit SHA
  • message - commit message
  • group - commit group (Features, Bug Fixes, etc.)
  • scope - commit scope
  • links - related links/issues

Complete Template Example

A comprehensive template that includes both main repository and submodule changes:
[git]
recurse_submodules = true

[changelog]
header = """
# Changelog

All notable changes to this project will be documented in this file.
"""

body = """
{% if version %}
    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}
    ## [Unreleased]
{% endif %}

### Main Repository
{% for group, commits in commits | group_by(attribute="group") %}
    #### {{ group | upper_first }}
    {% for commit in commits %}
        - {{ commit.message | upper_first }}
    {% endfor %}
{% endfor %}

{% if submodule_commits %}
### Submodules
{% for submodule_path, submodule_commits_list in submodule_commits %}
    #### {{ submodule_path }}
    {% for group, commits in submodule_commits_list | group_by(attribute="group") %}
        **{{ group | upper_first }}**
        {% for commit in commits %}
            - {{ commit.message | upper_first }}
        {% endfor %}
    {% endfor %}
{% endfor %}
{% endif %}
"""

Checking for Submodule Updates

You can conditionally render the submodules section:
{% if submodule_commits %}
### Submodule Updates

{% for submodule_path, commits in submodule_commits %}
    #### {{ submodule_path }}
    - Updated with {{ commits | length }} commit(s)
    {% for commit in commits %}
        - {{ commit.id | truncate(length=7, end="") }}: {{ commit.message }}
    {% endfor %}
{% endfor %}
{% else %}
_No submodule updates in this release._
{% endif %}
If a release does not contain any submodule updates, submodule_commits is an empty map.

Submodule Commit Details

Access detailed submodule commit information:
{% for submodule_path, commits in submodule_commits %}
### Submodule: {{ submodule_path }}

{% for commit in commits %}
- **{{ commit.group }}**: {{ commit.message }}
  - SHA: `{{ commit.id }}`
  - Scope: {{ commit.scope | default(value="N/A") }}
  {% if commit.breaking %}
  - ⚠️ BREAKING CHANGE
  {% endif %}
{% endfor %}
{% endfor %}

Filtering Submodule Commits

You can filter submodule commits just like regular commits:
[git]
recurse_submodules = true

commit_parsers = [
  { message = "^feat", group = "Features" },
  { message = "^fix", group = "Bug Fixes" },
  { message = "^docs", skip = true },  # Skip documentation commits
]
These parsers apply to both main repository and submodule commits.

Common Patterns

Group by Type Across Submodules

### Features
{% for submodule_path, commits in submodule_commits %}
    {% for commit in commits | filter(attribute="group", value="Features") %}
        - [{{ submodule_path }}] {{ commit.message }}
    {% endfor %}
{% endfor %}

### Bug Fixes
{% for submodule_path, commits in submodule_commits %}
    {% for commit in commits | filter(attribute="group", value="Bug Fixes") %}
        - [{{ submodule_path }}] {{ commit.message }}
    {% endfor %}
{% endfor %}

Submodule Summary

{% if submodule_commits %}
### Updated Submodules

{% for submodule_path, commits in submodule_commits %}
- **{{ submodule_path }}**: {{ commits | length }} commit(s)
{% endfor %}
{% endif %}

Detailed Submodule Changes

{% for submodule_path, commits in submodule_commits %}
<details>
<summary>{{ submodule_path }} ({{ commits | length }} changes)</summary>

{% for group, group_commits in commits | group_by(attribute="group") %}
#### {{ group }}
{% for commit in group_commits %}
- {{ commit.message }} ([`{{ commit.id | truncate(length=7, end="") }}`]({{ repository_url }}/commit/{{ commit.id }}))
{% endfor %}
{% endfor %}

</details>
{% endfor %}

Practical Example

Complete configuration for a project with submodules:
[changelog]
header = "# Changelog\n\n"
body = """
{% if version %}
## [{{ version }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}
## [Unreleased]
{% endif %}

{% if commits %}
### Main Repository Changes
{% for group, commits in commits | group_by(attribute="group") %}
#### {{ group | upper_first }}
{% for commit in commits %}
- {{ commit.message | split(pat="\n") | first | upper_first }}{% if commit.links %} ([#{{ commit.links[0] }}]({{ commit.links[0] }})){% endif %}
{% endfor %}
{% endfor %}
{% endif %}

{% if submodule_commits %}
### Submodule Updates
{% for submodule_path, submodule_commits_list in submodule_commits %}
#### πŸ“¦ {{ submodule_path }}
{% for group, commits in submodule_commits_list | group_by(attribute="group") %}
**{{ group }}**
{% for commit in commits %}
- {{ commit.message | split(pat="\n") | first }}
{% endfor %}
{% endfor %}
{% endfor %}
{% endif %}
"""

[git]
recurse_submodules = true
conventional_commits = true
filter_unconventional = true

commit_parsers = [
  { message = "^feat", group = "Features" },
  { message = "^fix", group = "Bug Fixes" },
  { message = "^perf", group = "Performance" },
  { message = "^docs", skip = true },
  { message = "^test", skip = true },
  { message = "^chore", skip = true },
]

Usage Examples

Generate Changelog with Submodules

# Ensure submodules are initialized
git submodule update --init --recursive

# Generate changelog
git cliff -o CHANGELOG.md

Preview Submodule Changes

# Show what's changed in submodules
git cliff --unreleased --strip header --strip footer

Update Submodules and Changelog

#!/bin/bash

# Update all submodules to latest
git submodule update --remote

# Generate changelog including submodule updates
git cliff --unreleased --tag v1.0.0 -o CHANGELOG.md

# Commit changes
git add .
git commit -m "chore: update submodules and changelog"
git tag v1.0.0

Limitations

Nested submodules (submodules within submodules) are not yet supported.
Only direct submodules of the main repository are processed. If you have nested submodules:
project/
β”œβ”€β”€ .git/
β”œβ”€β”€ submodule-a/  ← Included
β”‚   └── .git/
└── submodule-b/  ← Included
    β”œβ”€β”€ .git/
    └── nested-submodule/  ← NOT included
        └── .git/

Troubleshooting

Submodules Not Appearing

If submodule commits don’t appear in the changelog:
# 1. Verify submodules are initialized
git submodule status

# 2. Update submodules
git submodule update --init --recursive

# 3. Check for submodule changes
git submodule foreach git log --oneline -5

# 4. Enable verbose output
git cliff -vv

Empty Submodule Sections

If submodule sections are empty:
# Check if submodules have updates in the range
git diff HEAD~1 HEAD --submodule=log

# Verify configuration
grep recurse_submodules cliff.toml

Template Not Rendering Submodules

Verify your template includes the submodule_commits variable:
[changelog]
body = """
{{ submodule_commits | json_encode() }}
"""
Then run:
git cliff --unreleased
This will show the raw submodule data for debugging.

See Also

Build docs developers (and LLMs) love