Skip to content

feat(parser): allow commits to be parsed by multiple parsers - #1611

Merged
orhun merged 7 commits into
orhun:mainfrom
ChrisJr404:feat/multiple-commit-parsers
Sep 6, 2026
Merged

feat(parser): allow commits to be parsed by multiple parsers#1611
orhun merged 7 commits into
orhun:mainfrom
ChrisJr404:feat/multiple-commit-parsers

Conversation

@ChrisJr404

Copy link
Copy Markdown
Contributor

Closes #117. Adds an opt-in continue field on commit parsers so a commit can be processed by more than one parser instead of stopping at the first match.

Today the parser loop returns as soon as a commit matches a parser, so you can't have one parser set the scope and a later one set the group. The config from #117 ({ message = "(scope)", scope = "..." } followed by { message = "^feat", group = "..." }) only ever applies the first parser and the group is never set. The only workaround is spelling out every scope/group combination in a single parser, which gets tedious fast.

With this change you mark the parsers you want to compose with continue = true:

commit_parsers = [
  { message = '\(www\)', scope = "Application", continue = true },
  { message = "^feat", group = "Features" },
]

A parser with continue = true only writes the fields it actually sets (here just scope), then parsing moves on to the next parser, which fills in the group. Parsers without the flag behave exactly as before: first match wins and the loop short-circuits. So existing configs are untouched — the flag defaults to unset.

One thing worth a look: when several parsers should stack, each contributing one, they all need continue = true (a parser without it is treated as terminal and overwrites with its own fields, matching the current overwrite semantics). That felt like the least surprising reading of the issue, but I'm happy to change the shape if you had something else in mind — the thread left the design open ("needs some brainstorming").

Also handled the filtering edge case: a commit matched only by continue parsers (e.g. one that just sets a scope, no group) is kept when filter_commits is on, same as a normal match would be.

Tests in git-cliff-core/src/commit.rs (parse_commit_multiple_parsers) cover three things: the default first-match-wins path is unchanged, scope-then-group composition works with the flag, and a scope-only continue parser survives filtering. cargo test -p git-cliff-core is green (73 passed), cargo clippy --all-features clean, docs added under configuration/git.md.

Add an opt-in `continue` field to commit parsers. When set, parsing
keeps going after a parser matches, so a commit can be handled by more
than one parser in order (e.g. one sets the scope, the next sets the
group). Default behavior is unchanged: the first matching parser wins
and short-circuits.
@ChrisJr404
ChrisJr404 requested a review from orhun as a code owner August 18, 2026 05:14
@ChrisJr404

Copy link
Copy Markdown
Contributor Author

The Test suite red here isn't coming from this PR. All the tests pass; it's the codecov upload step failing on gpg signature verification (Can't check signature: No public key), the same failure that's red on main right now. Should sort itself out on a re-run.

@orhun

orhun commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Hey, thanks for the PR!

There are a couple of issues:

  1. Consider this case:

parser 1: scope = "Application", continue = true
parser 2: group = "Features", terminal

Doesscope become None because parser 2 overwrites every field? Can you confirm this?

  1. Can you add a test that matches this case? A fixture test would be also appreciated.
  2. I'd like to see some documentation update for showcasing this new feature too. We can already set scope and group in the same commit parser, so let's find another real-world use case.
  3. There seems to be some merge conflicts. Can you merge main?

A terminal parser (one without continue) used to overwrite group, scope
and default_scope wholesale, so a scope set by a preceding continue
parser was reset to None when a later parser only matched to set the
group. Make terminal parsers override just the fields they set, matching
the sha-based match path, so composing parsers augment instead of
clobbering each other.

Add a unit case and a test-multiple-commit-parsers fixture that derive a
scope from a git footer and then group by conventional type, and update
the docs with that real-world use case.
Resolve the parser loop conflict: keep the labeled 'parsers loop and the
missing-field fall-through from main alongside the continue flag and the
matched tracking from this branch. Keep both new tests.
@ChrisJr404

Copy link
Copy Markdown
Contributor Author

Good catch, and yes, I confirmed it. With the original version of this branch, parser 1: scope = "Application", continue = true followed by parser 2: group = "Features" (terminal) left the commit with scope = None. The terminal parser's branch was assigning all three fields unconditionally (self.scope = parser.scope.clone().map(...), etc.), so when parser 2 had no scope it reset the scope that parser 1 had set. I reproduced it with a quick test before changing anything.

I think that's surprising rather than intended, so I fixed it: a terminal parser now only overrides the fields it actually sets and leaves the rest alone, which is what the sha-based match path was already doing. So a later parser augments the commit instead of blanking out earlier fields. This is backward compatible, because without continue only one parser ever matches, so there was never a previously-set field to preserve in the first place. After the fix, the scope = "Application" + group = "Features" case keeps both.

Also done:

  • Added a unit case for exactly this scenario (scope from a continue parser, group from a following terminal parser, scope preserved), plus the existing composition cases.
  • Added a test-multiple-commit-parsers fixture and wired it into the workflow matrix.
  • Reworked the docs with a different real-world use case: reading a component out of a Component: git footer into the scope with continue = true, then grouping by conventional type in a separate parser. That keeps the component list in one place instead of writing out every component-and-type combination.
  • Merged latest main, so the conflict is cleared.

cargo test -p git-cliff-core is green apart from repo::test::git_upstream_remote, which fails the same way on a clean main checkout in my environment (it is tied to the local remote setup, not this change).

@orhun orhun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

I found another issue:

[git]
commit_parsers = [
  { footer = "^Component: Billing$", scope = "billing", continue = true },
  { field = "scope", pattern = "^billing$", group = "Billing changes" },
]

The second parser sees the scope from before parsing, not "billing".
So I think this should be either documented or behavior should be updated.

Comment thread website/docs/configuration/git.md Outdated
…caveat

Trim the git.md write-up to a short example and move the fuller
walkthrough to tips and tricks. Document that parser matching evaluates
against the original commit, so a later parser cannot match on a field
set by an earlier one.
@ChrisJr404

Copy link
Copy Markdown
Contributor Author

Shortened the git.md section down to a short paragraph plus a single example, and moved the fuller walkthrough into a new "Parsing commits with multiple parsers" entry in tips and tricks.

On the scope-field question: you are right, field/pattern matching runs against the original commit context (it is serialized once before the parser loop), so a later parser cannot match on a scope that an earlier parser just set. Rather than change the matching semantics in this PR, I documented the caveat in both places (the note steers users to match on the underlying commit data, e.g. the footer, instead). Happy to make matching see the in-progress fields instead if you would prefer the behavior change, but that felt like a separate decision.

@codecov-commenter

codecov-commenter commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.82%. Comparing base (854d892) to head (5acdb7e).

Files with missing lines Patch % Lines
git-cliff-core/src/commit.rs 85.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1611      +/-   ##
==========================================
+ Coverage   52.68%   52.82%   +0.14%     
==========================================
  Files          27       27              
  Lines        2597     2613      +16     
==========================================
+ Hits         1368     1380      +12     
- Misses       1229     1233       +4     
Flag Coverage Δ
unit-tests 52.82% <85.00%> (+0.14%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@orhun orhun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@orhun
orhun merged commit 4644e5f into orhun:main Sep 6, 2026
110 of 114 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow multiple parsers for skipped/non-skipped commits

3 participants