PromptHub
Developer Tools Open Source

Stop Writing Changelogs Manually! Use git-cliff Instead

B

Bright Coding

Author

13 min read
17 views
Stop Writing Changelogs Manually! Use git-cliff Instead

Stop Writing Changelogs Manually! Use git-cliff Instead

What if I told you that your entire team is wasting hours every release cycle on a task that should take seconds?

Picture this: It's 2 AM. Your production deployment is blocked because the changelog isn't updated. Someone — maybe you — is manually sifting through 47 git commits, trying to decipher which ones are "features," which are "bug fixes," and which are just "developer tantrums in commit message form." You're copy-pasting hashes, rewriting messages, praying you don't miss a breaking change that will destroy your users' weekend.

Sound familiar?

Here's the dirty secret most development teams don't want to admit: manual changelog maintenance is technical debt masquerading as process. It's error-prone, inconsistent, and soul-crushingly tedious. Worse, it gets worse as your project scales. That 10-person startup with 50 commits per release? Try 500 commits across a monorepo with 12 packages. Good luck with that spreadsheet.

But what if your git history already contained everything needed to generate a perfect changelog? What if a single command could transform your conventional commits into a beautifully formatted, customizable release document?

Enter git-cliff — the Rust-powered changelog generator that's making manual changelog maintenance look like using a stone tablet in the age of cloud computing. Created by Orhun Parmaksız, this open-source tool doesn't just automate changelog creation; it weaponizes your commit history into a competitive advantage. With support for conventional commits, regex-powered custom parsers, and templating that would make Jinja2 jealous, git-cliff is quietly becoming the secret weapon of teams who ship fast and document flawlessly.

Ready to reclaim your release nights? Let's dive deep.


What is git-cliff?

git-cliff is a highly customizable changelog generator that extracts meaningful release notes directly from your Git commit history. Born from the Rust ecosystem and maintained with obsessive attention to performance and flexibility, it represents a fundamental shift in how modern teams approach release documentation.

The project was created by Orhun Parmaksız, a prolific open-source contributor whose work on Rust tooling has earned recognition across the developer community. Under his stewardship, git-cliff has evolved from a clever utility into a production-grade tool trusted by teams ranging from solo maintainers to enterprise monorepos. The project's mascot — a mountain cliff — symbolizes the steep but rewarding climb from chaotic commit history to pristine release documentation.

But why is git-cliff trending now? Three converging forces are driving its adoption:

First, the Conventional Commits specification has reached critical mass. What started as an Angular team convention is now the de facto standard for structured commit messages across JavaScript, Rust, Python, Go, and beyond. Teams realized that if they're already writing feat:, fix:, and BREAKING CHANGE: prefixes, they're sitting on structured data that should be machine-readable.

Second, the Rust ecosystem's emphasis on performance matters more than you'd think. When you're generating changelogs for repositories with 10,000+ commits across years of history, a Node.js tool that takes 30 seconds becomes a CI/CD bottleneck. git-cliff's Rust foundation delivers sub-second execution even on massive repositories.

Third, developer experience has become a retention issue. Engineers are quitting jobs over broken release processes. Tools that eliminate toil — especially soul-crushing, error-prone toil — are being adopted with the urgency of fire extinguishers in a burning building.

The project's credibility is further cemented by its KaiCode Open Source Festival 2024 second-place finish and its feature in Console #141 newsletter, alongside a dedicated RustLab 2023 talk. This isn't a weekend hack; it's infrastructure.


Key Features That Separate git-cliff from the Pack

Let's dissect what makes git-cliff not just another changelog tool, but a generational leap in release automation:

🎯 Conventional Commits Native

Unlike tools that bolt on conventional commit support as an afterthought, git-cliff was architected around this specification from day one. It parses type(scope): subject patterns with military precision, automatically categorizing commits into Features, Bug Fixes, Performance Improvements, and more. The parser handles footers, breaking change indicators, and multi-line commit messages without breaking a sweat.

🔧 Regex-Powered Custom Parsers

Here's where git-cliff exposes its true power. Not every team uses conventional commits (yet). Not every legacy repository can be retrofitted. git-cliff's custom commit parsers let you define arbitrary regex patterns to extract meaning from any commit format. Have a team that writes [BUGFIX] message or JIRA-123: message? Write a regex, map it to categories, and you're generating changelogs without changing a single habit.

🎨 Template Engine Mastery

Built on Rust's Tera templating engine (Jinja2-inspired), git-cliff's template system is absurdly flexible. You control every whitespace, every link format, every emoji placement. Want GitHub-style release notes? Markdown tables? AsciiDoc for enterprise docs? HTML for your website? The template doesn't just format — it transforms, with access to commit metadata, authors, timestamps, and parsed structural data.

⚡ Blazing Performance

Rust's zero-cost abstractions and memory safety guarantees translate to real speed. Benchmarks consistently show git-cliff outperforming comparable tools by 10-50x on large repositories. When your CI pipeline runs this on every PR, those milliseconds compound into meaningful cost savings.

🏗️ Monorepo Architecture

Modern development increasingly happens in monorepos, and git-cliff answers with scoped changelog generation. Generate separate changelogs per package, filter commits by path, and maintain independent versioning — all from a single configuration. This isn't an enterprise add-on; it's core functionality.

🔗 GitHub/GitLab Integration

Native support for remote context fetching means git-cliff can enrich your changelog with GitHub Pull Request titles, GitLab Merge Request descriptions, and contributor avatars. Your changelog becomes a living document connected to your actual development workflow, not a dead text file.


Real-World Use Cases Where git-cliff Dominates

Use Case 1: The Solo Maintainer's Sanity Saver

You're maintaining 5 open-source libraries. Every release means 5 changelogs, 5 GitHub releases, 5 npm/crates.io publishes. With git-cliff configured once per repo, your release script becomes: git cliff --bump -o CHANGELOG.md && git add . && git commit -m "chore(release): version bump". Reclaim your weekends.

Use Case 2: Enterprise Compliance Nightmare → Dream

Your Fortune 500 employer requires traceability: every production change must link to a ticket, every security fix must be auditable. git-cliff's custom parsers extract JIRA IDs, ServiceNow references, or internal ticket numbers from commits, generating compliance-ready documentation that auditors actually praise instead of punish.

Use Case 3: Monorepo Release Orchestration

Your company has 12 packages in one repository. Previously, you maintained 12 separate CHANGELOG.md files by hand, often missing changes that affected multiple packages. git-cliff's commit filtering by path ensures each package's changelog contains exactly the commits that touched its files. Independent versioning becomes possible without independent repositories.

Use Case 4: Open Source Community Growth

Your project just crossed 100 contributors. Manually crediting everyone in release notes is now impossible. git-cliff automatically surfaces new contributors, commit counts per author, and can even generate "Thanks to our contributors" sections. Community members feel seen, and you don't spend release day in git shortlog hell.


Step-by-Step Installation & Setup Guide

Installation Options

git-cliff provides multiple installation paths depending on your ecosystem:

Cargo (Rust's Package Manager)

# Install the latest stable release
cargo install git-cliff

# Or install from git for bleeding-edge features
cargo install --git https://github.com/orhun/git-cliff

Homebrew (macOS/Linux)

brew install git-cliff

Docker

# Pull the official image
docker pull orhunp/git-cliff:latest

# Run with mounted repository
docker run -v $(pwd):/app orhunp/git-cliff:latest --config /app/cliff.toml

GitHub Releases (Binary Download)

# Download pre-built binaries from:
# https://github.com/orhun/git-cliff/releases
# Available for: Linux (x64, ARM), macOS (x64, Apple Silicon), Windows (x64)

Arch Linux (AUR)

yay -S git-cliff  # or your preferred AUR helper

Initial Configuration

Create cliff.toml in your repository root:

# Basic configuration for conventional commits
[changelog]
# template for the changelog header
header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n"
# template for the changelog body
body = """
{% if version %}\
    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
    ## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
    ### {{ group | striptags | trim | upper_first }}
    {% for commit in commits %}
        - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
            {% if commit.breaking %}[**breaking**] {% endif %}\
            {{ commit.message | upper_first }}\
    {% endfor %}
{% endfor %}\n
"""
# template for the changelog footer
footer = "<!-- generated by git-cliff -->"
# remove leading and trailing whitespaces from templates
trim = true

[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for parsing and grouping commits
commit_parsers = [
    { message = "^feat", group = "Features" },
    { message = "^fix", group = "Bug Fixes" },
    { message = "^doc", group = "Documentation" },
    { message = "^perf", group = "Performance" },
    { message = "^refactor", group = "Refactoring" },
    { message = "^style", group = "Styling" },
    { message = "^test", group = "Testing" },
    { message = "^chore\\(release\\): prepare for", skip = true },
    { message = "^chore", group = "Miscellaneous Tasks" },
    { body = "^.+", group = "Details" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# regex for matching git tags
tag_pattern = "v[0-9].*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"

Environment Setup for CI/CD

# Example GitHub Actions workflow
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  changelog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # REQUIRED: full git history for proper changelog generation
      
      - name: Generate changelog
        uses: orhun/git-cliff-action@v2
        with:
          config: cliff.toml
          args: --verbose
        env:
          OUTPUT: CHANGELOG.md
      
      - name: Commit changelog
        run: |
          git config user.name 'github-actions[bot]'
          git config user.email 'github-actions[bot]@users.noreply.github.com'
          git add CHANGELOG.md
          git commit -m "docs: update changelog" || exit 0
          git push

Critical setup note: Always use fetch-depth: 0 in CI. Shallow clones break git-cliff's ability to analyze commit history properly.


REAL Code Examples from the Repository

Let's examine actual patterns from git-cliff's ecosystem, adapted for clarity:

Example 1: Basic Changelog Generation

# Generate changelog for entire repository history
git cliff

# Generate changelog and write to file
git cliff --output CHANGELOG.md

# Generate changelog for specific tag range
git cliff --latest  # since last tag
git cliff --unreleased  # only unreleased changes
git cliff v1.0.0..v2.0.0  # specific range

What's happening here? The git cliff command analyzes your repository's git history, parses commits according to your cliff.toml configuration, and renders the template. Without arguments, it processes all commits from the first tag to HEAD. The --output flag redirects from stdout to a file, while range specifiers let you generate incremental changelogs — crucial for CI pipelines that only want to document "what's new since last release."

Example 2: Configuration with Custom Commit Parsers

# cliff.toml - Advanced configuration for non-standard commit formats
[git]
# Enable conventional commits as baseline
conventional_commits = true

# Custom parsers for legacy commit formats your team hasn't migrated yet
commit_parsers = [
    # Standard conventional commits
    { message = "^feat", group = "<!-- 0 -->🚀 Features" },
    { message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
    
    # Legacy format: [BUGFIX-1234] message
    { message = "^\\[BUGFIX-(\\d+)\\] (.+)", group = "<!-- 1 -->🐛 Bug Fixes", default_scope = "legacy" },
    
    # Legacy format: HOTFIX: message (treat as urgent fix)
    { message = "^HOTFIX: (.+)", group = "<!-- 1 -->🐛 Bug Fixes", default_scope = "hotfix" },
    
    # Skip automated dependency updates
    { message = "^chore\\(deps\\)", skip = true },
    
    # Catch-all for unparsed commits (don't lose data!)
    { message = "^.+", group = "<!-- 9 -->📝 Other" },
]

# Protect breaking changes even if they'd match a skip parser
protect_breaking_commits = true

The power revealed: This configuration handles mixed commit formats during migration periods. The regex ^\[BUGFIX-(\d+)\] (.+) captures legacy ticket references while mapping to modern categories. The <!-- 0 -->, <!-- 1 --> HTML comments are a secret sorting trick — they control section order without rendering in output. protect_breaking_commits = true ensures a BREAKING CHANGE: footer never gets accidentally skipped, even if the commit otherwise matches a skip pattern.

Example 3: Templating with GitHub Integration

# cliff.toml - Template with GitHub PR enrichment
[changelog]
header = "# Changelog\n"
body = """
{% if version %}
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}
## [Unreleased]
{% endif %}

{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}

{% for commit in commits %}
- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message | upper_first }}\
  {% if commit.github.pr_title %} ([PR #{{ commit.github.pr_number }}]({{ commit.github.pr_url }})){% endif %}\
  {% if commit.github.labels %} {{ commit.github.labels | join(attr="name") | map(attribute="name") }}{% endif %}
  {% if commit.breaking %}
  > ⚠️ **BREAKING CHANGE**: {{ commit.breaking_description }}
  {% endif %}
{% endfor %}
{% endfor %}

{% if github.contributors | length > 0 %}
### 👥 Contributors
{% for contributor in github.contributors %}
- @{{ contributor.username }} ({{ contributor.pr_count }} PRs)
{% endfor %}
{% endif %}
"""

[git]
conventional_commits = true

[remote.github]
owner = "your-org"
repo = "your-repo"

This template is doing something extraordinary. Beyond basic formatting, it:

  • Enriches commits with PR metadata: The commit.github.pr_title and related fields are populated by fetching from GitHub's API, turning dry commit messages into rich, linked documentation
  • Surfaces breaking changes prominently: The {% if commit.breaking %} block ensures no consumer misses a breaking change
  • Celebrates contributors: Automatic contributor sections build community recognition without manual curation
  • Uses Tera's filter pipeline: trim_start_matches(pat="v") removes "v" prefixes from version tags; upper_first capitalizes sentences; striptags removes HTML comment sorting markers

Example 4: Monorepo Scoped Generation

# Generate changelog only for packages/core directory
git cliff --include-path "packages/core/**" --repository ../../

# Generate separate changelogs for each package in CI
for pkg in packages/*; do
  pkg_name=$(basename "$pkg")
  git cliff \
    --config "${pkg}/cliff.toml" \
    --include-path "${pkg}/**" \
    --output "${pkg}/CHANGELOG.md" \
    --tag-pattern "${pkg_name}-v*"
done

The monorepo magic: The --include-path filter ensures only commits touching specific directories are included. Combined with --tag-pattern matching scoped version tags (core-v1.2.3, ui-v2.0.0), each package maintains independent versioning history. The --repository flag supports running from subdirectory while analyzing parent git history.


Advanced Usage & Best Practices

🚀 Performance Optimization

For repositories with 10,000+ commits, use --topo-order carefully — it ensures correct merge commit handling but adds overhead. For pure speed on linear history, sort_commits = "newest" with topo_order = false maximizes throughput.

🔒 Reproducible Builds

Pin your git-cliff version in CI. Template syntax evolves, and "latest" can break your pipeline unexpectedly:

- uses: orhun/git-cliff-action@v2.4.0  # Pin exact version

🧪 Pre-commit Integration

Validate your cliff.toml and generate preview changelogs before committing:

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: git-cliff-check
        name: Validate changelog generation
        entry: git cliff --unreleased --strip header
        language: system
        pass_filenames: false
        always_run: true

📊 Metrics-Driven Releases

Track "time to changelog" as a team metric. If it takes more than 30 seconds, your automation is broken. git-cliff should make this instantaneous.


Comparison with Alternatives

Feature git-cliff git-journal clog-cli cocogitto release-plz
Language Rust (blazing fast) Rust Rust Rust Rust
Conventional Commits ✅ Native ✅ Yes ✅ Yes ✅ Yes ✅ Yes
Custom Parsers ✅ Regex-powered ⚠️ Limited ❌ No ❌ No ❌ No
Templating Engine ✅ Tera (Jinja2) ⚠️ Basic ⚠️ Basic ❌ No ⚠️ Limited
Monorepo Support ✅ Built-in ❌ No ❌ No ⚠️ Partial ✅ Yes
GitHub/GitLab Integration ✅ Native ❌ No ❌ No ❌ No ✅ GitHub only
Performance (10k commits) ⚡ <1s ~5s ~3s ~2s ~10s
Active Maintenance ✅ Very active ⚠️ Stale ❌ Archived ✅ Active ✅ Active
Editor Support ✅ Emacs, community ❌ No ❌ No ❌ No ❌ No

Why git-cliff wins: The combination of custom regex parsers and Tera templating creates unmatched flexibility. While release-plz excels at the full release automation pipeline, and cocogitto enforces commit conventions, only git-cliff lets you work with any commit format and output any document structure. It's the Unix philosophy applied to changelogs: do one thing, do it exceptionally, compose with other tools.


FAQ: Your Burning Questions Answered

Q1: Does git-cliff require my team to use Conventional Commits?

No. While conventional commits unlock the smoothest experience, git-cliff's custom regex parsers can handle virtually any commit format. Migration can be gradual — configure parsers for your legacy format today, add conventional commit rules tomorrow.

Q2: Can I use git-cliff with private GitHub/GitLab instances?

Absolutely. The remote integration uses standard API tokens. Configure GITHUB_TOKEN or GITLAB_TOKEN environment variables, and point to your instance's API endpoint in configuration.

Q3: How does git-cliff handle merge commits?

Intelligently. By default, merge commits are analyzed for their actual changes, not just the merge message. Use filter_commits = true and appropriate commit_parsers to control merge commit inclusion. The --topo-order flag ensures topological correctness for complex merge histories.

Q4: Is there a Node.js/JavaScript wrapper available?

Yes. The community maintains cliff-jumper, which combines git-cliff with conventional-recommended-bump for semantic versioning in Node.js projects. Additionally, git-cliff's Docker image integrates into any JavaScript CI pipeline.

Q5: Can I generate changelogs for specific date ranges instead of tags?

Yes. While tag-based generation is most common, you can use git cliff <commit-sha>..<commit-sha> for arbitrary ranges. For date-based filtering, combine with git log --since and pipe commit SHAs, or use the --from-context experimental feature.

Q6: How do I debug when my template isn't rendering correctly?

Use git cliff --context to output the raw JSON data structure available to your template. This reveals exact field names, types, and available data — essential for complex template development.

Q7: What's the story behind the mountain cliff logo?

The "cliff" represents the steep drop from chaotic, unstructured commit history to clean, organized release documentation. The mountain ascent symbolizes the journey of improving your development practices. Orhun's design captures the tool's transformative promise: the climb is worth the view from the top.


Conclusion: Your Release Process Deserves Better

Let's be brutally honest: if you're still writing changelogs manually in 2024, you're choosing to be a bottleneck. You're choosing inconsistency. You're choosing the 2 AM release nights that burn out good engineers and drive them to jobs with better tooling.

git-cliff isn't just a changelog generator. It's a statement that your team's time matters. That your users deserve accurate, comprehensive release documentation. That your commit history — the story of your software's evolution — should be celebrated, not buried.

From its Rust-powered performance to its regex-flexible parsing, from Tera templating to monorepo mastery, git-cliff represents the state of the art in release automation. The active community, comprehensive documentation, and relentless maintenance by Orhun Parmaksız ensure this tool will keep evolving with your needs.

Your action items:

  1. Install git-cliff today: cargo install git-cliff or brew install git-cliff
  2. Create your first cliff.toml using the examples above
  3. Run git cliff --unreleased and watch your commit history transform
  4. Integrate into CI/CD and never write a manual changelog again

The mountain awaits. Start climbing at github.com/orhun/git-cliff ⛰️

Comments (0)

Comments are moderated before appearing.

No comments yet. Be the first to share your thoughts!

Support us! ☕