Skip to main content
Version: 1.1

ccme: the commit parser

github.com/yohimik/dispat/pkg/ccme is a Go parser for Conventional Commits, Monorepo Extension 1.0.0. This format is a strict superset of Conventional Commits 1.0.0 that adds scopes as packages, propagation depth, and prerelease channels. The package parses commit messages and nothing else: no git, no workspace, no versions.

The parser uses no regular expressions. It runs a single left-to-right index scan with one byte of lookahead, no backtracking, and no recursion. This gives O(n) time and O(1) working space, which matters when you feed it untrusted commit messages from a repository in CI.

Read the vendored specification at SPEC.md. Every section reference in the package points into this file.

go get github.com/yohimik/dispat/pkg/ccme

Parsing a message

p := ccme.DefaultParser()

res, err := p.Parse(message)
if err != nil {
// err is a *ccme.ParseError listing every error-severity diagnostic.
// res is still populated: an error invalidates only its own unit.
}

for _, u := range res.ValidUnits() {
fmt.Println(u.Header.Type, u.Scopes(), u.Bump, u.Directives.Depth)
}

A message can hold several units separated by ---, so the parser returns a list. An error in one unit never invalidates the others. Read both the result and the error returned by Parse.

Call ParseSubject to run commit-lint checks on a single subject line.

res, err := p.ParseSubject("feat(@acme/core)^^minor%beta!: streaming reader")
u := res.Units[0]
// u.Header.Type == "feat"
// u.Scopes().String() == "@acme/core"
// u.Breaking == true
// u.Bump == ccme.BumpMajor
// u.Directives.Propagate == ccme.PropagateMinor
// u.Directives.Depth == ccme.DepthAll
// u.Directives.Channel == ccme.ChannelValue{To: "beta"}

A Parser is immutable once constructed. You can safely share one parser across concurrent goroutines for a whole history sweep.

Configuring the parser

Set your custom rules in a Config struct. The zero value matches the specification default, so you only need to set the fields you want to change.

p, err := ccme.NewParser(ccme.Config{
Separator: "%%%",
StrictTypes: true,
MaxDescriptionLength: 72,
Propagation: ccme.PropagationConfig{
Bump: ccme.PropagateInherit,
Depth: ccme.DepthAll,
},
})

Call DefaultParser() as a shorthand for MustNewParser(Config{}). Call DefaultConfig() to get the same values spelled out when you want a populated struct.

A nil slice or map selects the default, but a non-nil empty one forbids all values. Both propagation depths default to a literal 0, which is the specification default rather than an unset marker. A unit reaches nobody until it says otherwise.

Diagnostics

Every finding carries a code, a severity, and an exact byte position. This lets you point directly at the offending character.

1:18: error E113: '+2' contradicts the depth of all asserted by '^^'

Errors use E codes and warnings use W codes. Call Result.Errors() and Result.Warnings() to read them. Both return nil rather than an empty slice on a clean parse, so a successful call allocates nothing.

The package emits only the codes decidable from a message on its own. Anything needing a workspace, a dependency graph, or git history belongs to the release engine. You can find the full numbered list of both sets in Diagnostic codes.

Handle W155 and W156 carefully, because they mean the message says something other than what its author meant, like a miscapitalised BREAKING CHANGE footer. You cannot suppress them. SilentFailureCodes() returns them so your commit-lint tooling can reject what the release engine tolerates.

Versions

The package includes a SemVer 2.0.0 parser. The parser uses this to validate an exact Release-As value while it reads the message.

v, err := ccme.ParseVersion("1.4.0-rc.2")
v.Compare(other)

Bulk parsing

The parser holds no mutable state, so you can scale it across goroutines. Each message produces a couple of kilobytes of short-lived garbage, making the garbage collector the limit past roughly a million messages per second. Raise GOGC for a tool that sweeps a history once and exits, as this costs nothing but peak memory.

Copy a description to keep it from a large message, because a Result retains the whole message string. Treat Directives.Kinds as read-only, because it aliases the parser configuration.

Further reading