ccme

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 16, 2026 License: MIT Imports: 5 Imported by: 0

README

ccme

A Go parser for Conventional Commits, Monorepo Extension (CCME) 1.0.0, a strict superset of Conventional Commits 1.0.0.

No regular expressions. The parser is the single left-to-right index scan described in §20 of the specification: one byte of lookahead, no backtracking, no recursion, O (n) time and O (1) working space. That is the property that matters when the parser runs over untrusted commit messages in CI.

The specification is vendored as SPEC.md, and every §n.m in the code and in this file refers to it. Note that the parsing chapter is §20; §17 is Conformance.

Install

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

Use

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)
}

ParseSubject is the narrow entry point for commit-lint checks; it takes the subject line alone:

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"}
Two propagation axes

§5.3 splits propagation into two independent axes, each with its own value and its own depth. Both depths default to 0, so a unit reaches nobody on either axis until it says otherwise:

Axis Value Depth Footers
bump ^, ^^ +N Propagate, Propagate-Depth, Propagate-Scope
channel %% ++N Propagate-Channel, Propagate-Channel-Depth, Propagate-Channel-Scope

% sits on neither axis: it sets the unit's own channel.

res, _ := p.ParseSubject("feat(core)^^minor%%beta++2: x")
d := res.Units[0].Directives
// d.Propagate        == ccme.PropagateMinor
// d.Depth            == ccme.DepthAll        // "^^" asserts all
// d.PropagateChannel == ccme.ChannelValue{To: "beta"}
// d.ChannelDepth     == ccme.Depth(2)        // "++2" overrides the 1 that "%%" implies

The doubled sigils are fixed two-character tokens, never a repetition count: ^^^, %%% and +++ are all E110. A bare ^ is legal and means "propagate the default bump one level"; %%, + and ++ all require a value, because a channel with no name and a depth with no number carry nothing worth guessing (E111).

A caret implies a depth of 1 and an explicit +N silently overrides it; ^^ asserts all, so a disagreeing +N is E113 and a restating +* is W110. The same shape applies to %% and ++N, except that %% only implies, never asserts. Every combination is order-independent.

Channel transitions

A channel value may be a transition, from>to (§11.2):

p.ParseSubject("release(core)%%*>stable++*: promote the whole train")
// d.PropagateChannel == ccme.ChannelValue{From: "*", To: "stable"}
// d.ChannelDepth     == ccme.DepthAll

* matches any prerelease and is a source only. %%beta>* is E111, because "move them to some prerelease or other" is not a releasable instruction. inherit and none are whole values that only Propagate-Channel accepts; they are never a side of a transition. A transition whose sides are equal is inert and warns with W207.

A Parser is immutable after construction and safe for concurrent use.

Configuration

Everything lives in one Config struct that mirrors §14. The zero value is the specification default, so only the fields you actually want to change need setting:

p, err := ccme.NewParser(ccme.Config{
Separator:            "%%%", // for repos that use format-patch / am
StrictTypes:          true,  // unknown type -> E140 instead of W140
Lenient:              true, // downgrade selected errors to warnings
MaxDescriptionLength: 72,   // 0 = the default 100, negative = no check
Types:                types,   // nil = DefaultTypes(); a non-nil map replaces it
Propagation: ccme.PropagationConfig{
Bump:    ccme.PropagateInherit,
Depth:        ccme.DepthAll,
ChannelDepth: 1,
Kinds:        []ccme.DependencyKind{ccme.KindDependencies, ccme.KindPeerDependencies},
Channel:      ccme.ChannelInherit,
},
Limits: ccme.Limits{           // §14.1 parser bounds; 0 = default, negative = off
UnitsPerMessage:   64,
ScopeTermsPerUnit: 256,
MessageBytes:      1 << 20,
},
AllowedChannels:      []string{"beta", "rc"}, // nil = unrestricted
MessageLevelTrailers: []string{"Signed-off-by", "Change-Id"},
IssueTrailers:        []string{"Closes", "Fixes"},
})

ccme.DefaultParser() is shorthand for ccme.MustNewParser(ccme.Config{}), and ccme.DefaultConfig() returns the same values fully spelled out when you'd rather start from a populated struct:

cfg := ccme.DefaultConfig()
cfg.Types["deps"] = ccme.BumpPatch
p := ccme.MustNewParser(cfg)

Conventions worth knowing:

  • nil vs empty slice. A nil Types, Propagation.Kinds, AllowedChannels, MessageLevelTrailers or IssueTrailers selects the default; a non-nil empty one means none.
  • Both depths default to 0. Propagation.Depth and Propagation.ChannelDepth mean exactly what they say: a literal 0 is the spec default, not "unset", so there is no ambiguity to resolve. Repositories that bundle rather than declare their dependencies should set Depth: 1; use ccme.DepthAll for the full transitive closure.
  • Propagation.Kinds is configuration only. §8.4 has no per-unit override, so this list applies to every unit of every message.

Anything not stated by the author falls back to this configuration, then to the spec default, and Directives.*Set tells you which it was.

Performance

The package is built for sweeping large histories: one parser, many messages, often in parallel. Parsing is O (n) in message length with no backtracking, and the hot path avoids copying the input.

  • Normalisation is a no-op when it can be. A message that already has LF endings, no trailing whitespace and no trailing blank lines (what git hands you) is returned unchanged, with zero allocations. The check is driven by strings.IndexByte rather than a byte-at-a-time loop, since it runs over every message in a history. The rewrite path, when needed, is a single pass into one buffer.
  • A single-unit message needs one allocation for its object graph. Result, the Unit and the []*Unit come out of one backing struct rather than three separate allocations; multi-unit messages still get one array for all units.
  • Text is sliced, not rebuilt. Unit.Raw, Unit.Body, Header.Raw, Header.Description and every scope term are substrings of the normalised message. The one exception is a unit containing an escaped separator (\---), which is not contiguous and so is reassembled.
  • One allocation for all units, not one per unit, and none at all for the per-unit checks: scope-overlap, propagation-redundancy and Release-As scope checks are all scans over a handful of terms rather than temporary maps or filtered slices.
  • Footer keys are matched with an ASCII fold-compare over the nine-entry registry instead of lowercasing the key into a fresh string for a map lookup. BREAKING CHANGE sits outside it, since §8.1.1 makes it the one key compared exactly.
  • Clean parses do not allocate diagnostics. Errors() and Warnings() return nil rather than an empty slice, so a successful Parse allocates nothing for the diagnostic path.

Two consequences follow from the zero-copy design and are worth knowing:

  • A Result retains the message string. Holding a Result holds the whole message alive; if you keep only a description from a large message, copy it.
  • Directives.Kinds always aliases the parser configuration (§8.4 has no per-unit override). Treat it as read-only. Everything else a unit exposes is either a value or freshly allocated.

bench_test.go covers subject-only, body, directive-heavy, multi-unit, CRLF and error inputs, plus a parallel benchmark and a -race test that hammers one shared parser from sixteen goroutines:

go test -bench . -benchmem ./...

Measured on an Apple M5 Pro, Go 1.26:

Benchmark ns/op B/op allocs/op
ParseSubject (63 B header only) 137 640 2
ParseSimple (217 B, header + body) 331 1008 5
ParseDirectives (375 B, sigils + 5 footers) 876 2152 15
ParseMultiUnit (178 B, 4 units) 818 3200 12
NormalizeFastPath 19.8 0 0
NormalizeRewrite 104 240 1

These figures predate the two-axis grammar, which widened InlineDirectives and Directives by a few words each. The shape of the numbers is unchanged (the work per message is still one pass with no backtracking) but B/op will have moved. Re-run go test -bench . -benchmem on your own hardware before quoting them; the ns/op column is machine-specific anyway.

Roughly 1.8M simple messages per second on one core. B/op is dominated by the Unit struct itself (Header and Directives are wide value types), not by copies of the input; a message ten times longer costs the same allocations.

ParseSimple pays one of its five allocations for normalisation because its input ends in a newline, as git log --format=%B output does. A message already stripped of its trailing newline takes the zero-allocation fast path.

Tune GOGC for bulk sweeps

A Parser holds no mutable state, so parsing scales across goroutines, but past a certain rate the limit is the garbage collector, not the parser. Each message produces a couple of kilobytes of short-lived garbage, and at a million-plus messages per second that is gigabytes per second for the collector to sweep. On the same machine as the table above:

BenchmarkParseParallel ns/op speedup vs serial
default GOGC=100 641 1.4x
GOGC=800 232 3.5x

Allocation volume is identical in both runs (2152 B/op, 15 allocs/op), so the difference is collection cost alone. A tool that sweeps a history once and exits should raise GOGC (or set a debug.SetMemoryLimit and turn GOGC off entirely); it is the single highest-value knob here, and it costs nothing but peak RSS.

What it covers

Everything in a commit message: normalisation (§4.1), unit splitting and the escaped separator (§4.2), the header grammar (§5) including scope-sets, the ^ / ^^ / + / ++ / % / %% sigils, channel transitions (§11.2) and the breaking marker, the body/footer split (§4.4, §20.5), the eleven-entry footer registry (§8.1), inline-versus-footer reconciliation (§5.3), type-to-bump mapping (§7), the cancel / release control rules (§7.2, §10.2), and the correction footers Edits: and Deletes: (§7.4), whose values are shape-validated here and resolved against history by the release engine. Reverts: values are shape-validated too (W214 when the value is not a commit sha), because §7.3 makes the footer suppress reverted changelog entries.

Diagnostics carry a code, a severity and an exact position, so a caller can point a caret at the offending byte:

1:18: error E113: '+2' contradicts the depth of all asserted by '^^'
BREAKING CHANGE is case-sensitive

It is the one key in the format that is, and getting it wrong is the most dangerous thing a message can do: it parses cleanly and ships a major change as a minor one. The package refuses to let that happen quietly (§8.1.1):

Written Result
BREAKING CHANGE: x / BREAKING-CHANGE: x breaking
Breaking change: x not breaking, not even a footer: W155
breaking-change: x not breaking, an unknown footer key: W155
BREAKING CHANGE: x in the body, not the last paragraph no effect: W156
BREAKING CHANGE: with no value breaking: W157
BREAKING CHANGE: x as the header line E100, with a message saying so

W155 and W156 are exposed as ccme.SilentFailureCodes(): they mean the message says something other than what its author meant, and commit-lint tooling should reject them even though the release engine tolerates them.

Inert directives: W152 vs W201

§8.3b draws the line by what the author actually asked for, and never emits both for one axis:

Written Diagnostic Why
^none, +0, ^^none, %%none, ++0 W152 the whole directive resolves to nothing; deleting it changes nothing
^minor+0, ^inherit+0, %%beta++0 W201 a value was named and the depth throws it away
release(core)^minor neither the directive is fine; what silences it is the type's bump of none

"Supplied" means written by the unit, in the header or a footer. A value inherited from Config never triggers W201, because a repository that raises Propagation.Depth makes the same footer meaningful.

What it does not cover

This package parses messages. It does not read git, load a workspace, walk a dependency graph, or compute versions, so the diagnostics that need any of those are never emitted:

E130, E153, E156, E157, E182, E185, E191, E195, E196, E197, E198, E199, E200, E210, E211, E212, E213, W130, W131, W134, W135, W153, W154, W158, W159, W160, W170, W171, W172, W185, W186, W190, W192, W193, W194, W195, W196, W197, W199, W200, W202, W203, W204, W205, W206, W208, W209, W210, W211, W212, W213, W215.

The three W2xx codes that are decidable from a message alone are emitted: W201 for a propagation value on an axis whose depth is 0, W207 for a channel transition whose sides are equal, and W214 for a Reverts value that is not a commit sha. The correction footers follow the same split: their shape is validated here (E151, E173) and their targets are resolved against history by the engine (§13.4b).

E154 is enforced for the cases decidable from the message alone: two or more explicit include terms, or an include term addressing the whole workspace.

The hold machinery of §8.6.1 is split the same way. Release-As values are parsed and classified (4.0.0 is a pin, none a hold and auto a resume, all three package-level) but resolving which directive wins over a window belongs to the engine.

Release-As has no bump form: Release-As: minor is E151, with a diagnostic that says why (§8.6). How large a change is, is declared by the type; if a category of commit should release in your repository, say so once in Types rather than on every commit. And Release-As: none does not suppress its own unit's bump: a hold retains the pending work, which is what distinguishes it from cancel (§8.6.2, §13.6).

Parser bounds are always enforced

A commit message is untrusted input (§18), so the §14.1 caps are on by default and exceeding one is E158, which is message-scoped: the whole commit contributes nothing.

Config.Limits field Default
UnitsPerMessage 64
ScopeTermsPerUnit 256
MessageBytes 1 MiB

A zero field takes the default; a negative one disables that bound, which is only appropriate for input you control.

A SemVer 2.0.0 parser (ParseVersion, Version.Compare) is included because an exact Release-As value has to be validated at parse time; it is also the type a release engine needs on top.

Test

go vet ./...
go test -race -cover ./...          # correctness, including the fuzz seed corpus
go test -bench . -benchmem ./...    # throughput and the allocation budget

Fuzzing runs one target at a time, and -fuzz takes a regexp. Anchor it, or FuzzParse will also match FuzzParseSubject and the run is refused:

go test -fuzz '^FuzzParse$'        -fuzztime 5m
go test -fuzz '^FuzzParseSubject$' -fuzztime 2m
go test -fuzz '^FuzzNormalize$'    -fuzztime 2m

The suite reproduces every vector of Appendix B.1 and B.2, and gates six things a release depends on:

  • Every diagnostic code is reachable. TestEveryDiagnosticCodeIsReachable maps all 40 codes to an input that produces it, and fails if a code is declared without a test or produced without being declared.
  • No input can panic. Three fuzz targets cover Parse, ParseSubject and Normalize, checking that diagnostic positions stay inside the message, that Valid matches the diagnostics attached to a unit, that re-parsing the normalised message is indistinguishable from parsing the original, and that the zero-copy substrings really are substrings. The seed corpus alone runs under plain go test.
  • Normalisation is a fixed point. FuzzNormalize asserts idempotence and that the fast-path predicate agrees with the rewriter; a disagreement there would let Parse and Normalize see different text.
  • Output is deterministic (§17.2). TestDiagnosticsAreDeterministic parses one message fifty times and requires byte-identical diagnostics in the same order; nothing may depend on map-iteration order.
  • W155 and W156 cannot be switched off (§14.2). There is no suppression mechanism, and TestSilentFailureWarningsCannotBeSuppressed checks the most permissive configuration the API allows still emits both.
  • Allocations do not regress. alloc_test.go pins the per-message allocation counts and asserts that a body a hundred times larger costs no extra allocations. It carries a !race build tag, since the race detector allocates on its own.
The fuzz corpus

go test -fuzz writes only failing inputs to testdata/fuzz/<Target>/. Those are regression cases (Go replays them on every plain go test), so commit them. Two are checked in, both defects the fuzzer found here:

File Input Defect
FuzzParse/ca2afcdbb1727c84 "---" W001 reported one line past the end of the message
FuzzParse/d03d5667d745b3ab "\ufeff\ufeff" Normalize stripped one BOM, so it was not idempotent

The much larger coverage-guided corpus (several hundred generated inputs per target) lives in the build cache ($GOCACHE/fuzz), not in the repository. It is regenerated by each run and is not something to commit.

Requirements

Go 1.21 or later. No dependencies.

Licence

MIT. See LICENSE.

Documentation

Overview

Package ccme implements a parser for Conventional Commits: Monorepo Extension (CCME) 1.0.0, a strict superset of Conventional Commits 1.0.0.

The parser is a single left-to-right index scan with one byte of lookahead: no regular-expression engine, no backtracking, no recursion, and therefore no input that can trigger superlinear behaviour (§20). Every diagnostic is raised at a known position, so a caller can render a caret under the offending character.

Usage

p := ccme.DefaultParser()
res, err := p.Parse(message)
if err != nil {
	// err lists the error-severity diagnostics; res is still populated
	// with the units that parsed cleanly.
}
for _, u := range res.ValidUnits() {
	fmt.Println(u.Header.Type, u.Scopes(), u.Bump)
}

All configuration lives in a single Config struct whose zero value is the specification default, so only the fields you care about need setting:

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

A Parser is immutable after construction and safe for concurrent use, so a single package-level parser can serve every goroutine in a service.

Propagation has two axes

§5.3 gives propagation two independent axes, each with a value and a depth, and each expressible inline or as a footer:

axis     value        depth   footers
bump     "^", "^^"    "+N"    Propagate, Propagate-Depth, Propagate-Scope
channel  "%%"         "++N"   Propagate-Channel, Propagate-Channel-Depth,
                              Propagate-Channel-Scope

"%" is on neither axis: it sets the unit's own channel. Both depths default to 0, so a unit reaches nobody until it opts in. The doubled sigils are fixed two-character tokens rather than repetition counts, so "^^^", "%%%" and "+++" are all E110.

A channel value may also be a transition, "from>to", where "*" on the left matches any prerelease (§11.2).

Performance

The package is built for sweeping large histories. Parsing is O(n) in message length with no backtracking, and the hot path is allocation-lean: Header.Raw, Unit.Raw, Unit.Body, Header.Description and every scope term are substrings of the input rather than copies, and a message that is already normalised is not rewritten at all. A clean single-unit message costs a handful of small allocations, none of them proportional to the body size.

Two consequences are worth knowing. Result and its Units retain a reference to the message string, so holding a Result alive holds the message alive. And Directives.Kinds always aliases the parser's configuration, since §8.4 has no per-unit override, so it must be treated as read-only.

Scope

This package parses messages. It does not read git, load a workspace, walk a dependency graph, or compute versions, so the diagnostics that require any of those are out of scope and are never emitted: E001 is raised only for invalid UTF-8 in the message itself, and E130, E153, E156, E157, E182, E185, E191, E195, E196, E210, E211, E212, E213, W130, W131, W134, W135, W153, W154, W158, W160, W170, W171, W172, W185, W186, W190, W192, W209, W210, W211, W212, W213 and W215 belong to the release engine. E154 is enforced for the cases that are decidable from the message alone, and the correction footers of §7.4 are shape-validated here (E151, E173, W214) while their targets are resolved by the engine (§13.4b).

The parser bounds of §14.1 are always enforced, because a commit message is untrusted input (§18): exceeding limits.unitsPerMessage, limits.scopeTermsPerUnit or limits.messageBytes is E158, which is message-scoped: the commit contributes nothing.

The hold machinery of §8.6.1 is split the same way: this package parses and classifies Release-As values, all three of which are package-level since §8.6 has no bump form, but resolving which directive wins over a window, and what that means for a release plan, belongs to the engine.

Section references in the documentation are to the CCME specification.

Index

Examples

Constants

View Source
const (
	DefaultSeparator            = "---"
	DefaultMaxDescriptionLength = 100
	DefaultPropagate            = PropagatePatch
	DefaultDepth                = Depth(0)
	DefaultChannelDepth         = Depth(0)
	DefaultPropagateChannel     = ChannelInherit
)

Default configuration values (§14).

View Source
const (
	DefaultUnitsPerMessage   = 64
	DefaultScopeTermsPerUnit = 256
	DefaultMessageBytes      = 1 << 20
)

Default parser bounds (§14.1). Unlike the rest of §14's safety limits, which gate a release run and are opt-in, these are always enforced: a hostile commit message is untrusted input, and exceeding a bound must be a diagnostic rather than unbounded work (§18.3).

View Source
const (
	CodeE001 = "E001" // message is not valid UTF-8
	CodeE002 = "E002" // message is empty
	CodeE100 = "E100" // unit header does not match the grammar
	CodeE101 = "E101" // type contains uppercase or illegal characters
	CodeE102 = "E102" // whitespace inside a scope-set other than after a comma
	CodeE103 = "E103" // unbalanced or nested parentheses
	CodeE104 = "E104" // empty scope-set
	CodeE110 = "E110" // duplicate inline directive sigil
	CodeE111 = "E111" // unknown inline directive value, or an illegally empty value
	CodeE112 = "E112" // inline and footer set the same key to different values
	CodeE113 = "E113" // "^^" combined with an explicit "+N" where N is not all
	CodeE120 = "E120" // missing or malformed ": " separator
	CodeE121 = "E121" // empty description
	CodeE140 = "E140" // unknown type under strictTypes
	CodeE141 = "E141" // release unit with "!"
	CodeE151 = "E151" // footer value is not valid for its key
	CodeE154 = "E154" // exact Release-As on a multi-package scope-set
	CodeE158 = "E158" // a limits.* cap was exceeded; message-scoped
	CodeE170 = "E170" // cancel unit with "!"
	CodeE171 = "E171" // cancel unit with directives or footers
	CodeE173 = "E173" // correction footer on a release unit
	CodeE180 = "E180" // reserved channel name "latest"
	CodeE181 = "E181" // channel name contains uppercase or illegal characters

	CodeW001 = "W001" // empty unit discarded
	CodeW101 = "W101" // type lowercased under lenient mode
	CodeW110 = "W110" // redundant restatement of a directive
	CodeW112 = "W112" // footer overrode inline under lenient mode
	CodeW120 = "W120" // description exceeds maxDescriptionLength
	CodeW121 = "W121" // missing space after ": " accepted under lenient mode
	CodeW132 = "W132" // multi-unit commit with unscoped units
	CodeW133 = "W133" // package both included and excluded
	CodeW140 = "W140" // unknown type mapped to none
	CodeW141 = "W141" // release unit with no directives
	CodeW150 = "W150" // unknown footer key ignored
	CodeW151 = "W151" // trailing paragraph nearly footer-shaped but treated as body
	CodeW152 = "W152" // redundant no-op propagation pairing
	CodeW155 = "W155" // footer key matches BREAKING CHANGE only case-insensitively
	CodeW156 = "W156" // a BREAKING CHANGE line sits in the body, not the footer block
	CodeW157 = "W157" // BREAKING CHANGE with an empty value
	CodeW201 = "W201" // a propagation value was supplied while its axis's depth is 0
	CodeW207 = "W207" // a channel transition whose from equals its to; inert
	CodeW214 = "W214" // Reverts value is not a commit sha; footer is informational
)

Diagnostic codes from §16 (Diagnostics registry).

Codes that require a workspace, a dependency graph or git history to detect are deliberately absent: this package parses messages, it does not compute releases. See the package documentation for the exact list.

View Source
const (
	FooterBreakingChange   = "BREAKING CHANGE"
	FooterPropagate        = "Propagate"
	FooterPropagateDepth   = "Propagate-Depth"
	FooterPropagateScope   = "Propagate-Scope"
	FooterPropagateChannel = "Propagate-Channel"
	// FooterPropagateChannelDepth and FooterPropagateChannelScope are the
	// channel axis's counterparts of Propagate-Depth and Propagate-Scope
	// (§8.3a, §9.3).
	FooterPropagateChannelDepth = "Propagate-Channel-Depth"
	FooterPropagateChannelScope = "Propagate-Channel-Scope"
	FooterChannel               = "Channel"
	FooterReleaseAs             = "Release-As"
	FooterReverts               = "Reverts"
	// FooterEdits and FooterDeletes are the correction footers of §7.4: an
	// ordinary unit carrying one restates or discards the pending records it
	// names.
	FooterEdits   = "Edits"
	FooterDeletes = "Deletes"
)

Canonical footer keys from the registry in §8.1.

View Source
const (
	// ChannelStable is the non-prerelease line. Legal on either side of a
	// transition, and as a whole value.
	ChannelStable = "stable"
	// ChannelInherit means "the origin's channel". A Propagate-Channel value
	// only, never a side of a transition.
	ChannelInherit = "inherit"
	// ChannelNone disables channel propagation. A Propagate-Channel value
	// only, never a side of a transition.
	ChannelNone = "none"
	// ChannelAnyPrerelease is "*", legal only as a transition's from-side,
	// where it matches any prerelease channel and never matches stable.
	ChannelAnyPrerelease = "*"
)

Reserved channel values (§11.2). None of them may be used as a channel name, and a package may not be named after them.

View Source
const (
	TypeCancel  = "cancel"
	TypeRelease = "release"
)

Reserved type names that carry control semantics rather than a bump (§7).

Variables

View Source
var ErrInvalidVersion = errors.New("ccme: invalid semantic version")

ErrInvalidVersion is returned by ParseVersion for any malformed input.

Functions

func DefaultIssueTrailers

func DefaultIssueTrailers() []string

DefaultIssueTrailers returns the issue-reference trailer keys, which are ignored for versioning but may be surfaced in a changelog (§4.5).

func DefaultMessageLevelTrailers

func DefaultMessageLevelTrailers() []string

DefaultMessageLevelTrailers returns the trailer keys that describe authorship or review rather than release intent (§4.5). They are ignored wherever they appear and never prevent a paragraph from being a footer block.

func DefaultTypes

func DefaultTypes() map[string]Bump

DefaultTypes returns a fresh copy of the type-to-bump table of §7.1.

func IsDiagnosticCode

func IsDiagnosticCode(code string) bool

IsDiagnosticCode reports whether code is one this package emits: a finding about a commit message itself, as opposed to one about the workspace, the graph or the git history, which are the caller's to produce. A tool presenting both kinds together uses it to tell them apart: to group them, to count them, or to let an operator silence the authoring findings of a history that predates the convention.

func Normalize

func Normalize(message string) string

Normalize applies the input normalisation of §4.1 to a cleaned commit message, i.e. the output of `git log --format=%B`:

  1. strip a leading UTF-8 BOM;
  2. normalise CRLF and CR line terminators to LF;
  3. strip trailing spaces and tabs from the end of each line, preserving leading whitespace, which is significant for footer continuations;
  4. strip trailing blank lines from the end of the message.

Nothing else is altered. Normalize is idempotent, and returns the input string unchanged, with no allocation, when it is already normalised, which is the common case for messages read straight from git.

func SilentFailureCodes

func SilentFailureCodes() []string

SilentFailureCodes returns the warnings §16 singles out as silent-wrong-answer warnings rather than style notes: each one means the message says something different from what its author meant, with no error to stop it. The returned slice is a copy; callers may modify it freely.

Commit-lint implementations SHOULD reject a commit carrying any of them. W172 belongs to this set too but requires git history, so it is not emitted by this package.

Types

type Bump

type Bump int

Bump is a semantic-version increment level, ordered none < patch < minor < major (§2).

const (
	BumpNone Bump = iota
	BumpPatch
	BumpMinor
	BumpMajor
)

Bump levels.

func MaxBump

func MaxBump(a, b Bump) Bump

MaxBump returns the higher of two bumps (§2).

func ParseBump

func ParseBump(s string) (Bump, bool)

ParseBump maps a spelled-out bump level to a Bump.

func (Bump) String

func (b Bump) String() string

String implements fmt.Stringer.

type ChannelValue

type ChannelValue struct {
	// Word is ChannelInherit or ChannelNone when the entire value is one of
	// those. From and To are then empty.
	Word string
	// From is the transition's source, or empty when the value names only a
	// target. ChannelAnyPrerelease matches any prerelease channel.
	From string
	// To is the target: a channel name or ChannelStable. Empty when Word is
	// set.
	To string
}

ChannelValue is a parsed channel directive value (§11.2):

channel-value = [ from ">" ] to
from          = channel-name / "stable" / "*"
to            = channel-name / "stable"

Propagate-Channel additionally accepts the whole-value words "inherit" and "none", which are reported in Word.

func (ChannelValue) IsTransition

func (c ChannelValue) IsTransition() bool

IsTransition reports whether the value is a <from>><to> form.

func (ChannelValue) IsWord

func (c ChannelValue) IsWord() bool

IsWord reports whether the whole value was inherit or none.

func (ChannelValue) IsZero

func (c ChannelValue) IsZero() bool

IsZero reports whether no channel value was given at all.

func (ChannelValue) String

func (c ChannelValue) String() string

String renders the value as it would be written.

type Config

type Config struct {
	// Separator is the unit separator line (§4.2, §4.3). It must be at least
	// three ASCII-printable characters, must contain no whitespace, and must
	// not begin with a character that can begin a type. Zero value: "---".
	// Repositories that exchange patches by mail should set "%%%".
	Separator string

	// Types maps a type to its default direct bump (§7.1). A nil map selects
	// DefaultTypes; a non-nil map replaces the table wholesale, so add to a
	// copy of DefaultTypes rather than starting from scratch unless you mean
	// to drop the standard types. Type names must consist of a-z only.
	Types map[string]Bump

	// StrictTypes turns an unknown type into E140 instead of W140.
	StrictTypes bool

	// Lenient downgrades selected errors to warnings (§16): an uppercase type
	// is lowercased with W101, a missing space after ':' is accepted with
	// W121, and a footer contradicting an inline directive wins with W112
	// instead of raising E112. Two or more spaces after ':' remain E120 even
	// here, because the extra space is indistinguishable from a description
	// that begins with one (§5.5).
	Lenient bool

	// MaxDescriptionLength is the W120 threshold, counted in Unicode scalar
	// values. Zero value: 100. A negative value disables the check.
	MaxDescriptionLength int

	// Propagation holds the propagation defaults.
	Propagation PropagationConfig

	// Limits are the always-enforced parser bounds of §14.1.
	Limits Limits

	// AllowedChannels restricts channel names (channels.allowed in §14). A nil
	// slice is unrestricted; a non-nil slice rejects any channel name outside
	// it with E181. "stable" is always accepted, because it is a graduation
	// directive rather than a channel name (§11.5).
	AllowedChannels []string

	// MessageLevelTrailers are the authorship and review trailers ignored
	// wherever they appear (§4.5). A nil slice selects
	// DefaultMessageLevelTrailers; a non-nil empty slice disables the
	// behaviour. Matching is case-insensitive.
	MessageLevelTrailers []string

	// IssueTrailers are the issue-reference trailers, ignored for versioning
	// but surfaced on Footer.IssueReference for changelog use (§4.5). A nil
	// slice selects DefaultIssueTrailers. Matching is case-insensitive.
	IssueTrailers []string
}

Config is the complete parser configuration: every option in one struct.

The zero value is valid and means "the specification defaults", so Config{Lenient: true} changes exactly one thing. Any field left at its zero value is filled in from §14, which is why DefaultConfig is only needed when you want to read the defaults rather than set them.

Keys of §14 that affect release computation rather than parsing (tagFormat, initialVersion, preserveMajorZero, rangeStrategy, rootPathMap, ignoredPaths, branchChannels, propagation.respectRanges) are deliberately absent: this package parses messages.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns the specification defaults, fully populated. It is equivalent to Config{} once NewParser has filled in the zero values, and is the convenient starting point when you want to adjust one field:

cfg := ccme.DefaultConfig()
cfg.Types["deps"] = ccme.BumpPatch
p, err := ccme.NewParser(cfg)

func (Config) Clone

func (c Config) Clone() Config

Clone returns a deep copy, so that mutating the result cannot affect the original or any parser built from it.

The nil-versus-empty distinction is preserved exactly, because it is load-bearing: a nil slice selects the default, a non-nil empty one means "none".

func (Config) Validate

func (c Config) Validate() error

Validate reports the first problem with a configuration. NewParser calls it after filling in defaults, so Config{}.Validate() checking a partially filled struct may report a zero value that NewParser would have accepted; prefer letting NewParser do the validation.

type CorrectionTarget

type CorrectionTarget struct {
	// SHA is the full or abbreviated commit id, 7 to 64 lowercase hexadecimal
	// characters. Empty when All is set.
	SHA string
	// UnitSelector is the 1-based unit index into the target commit, or 0 when
	// no selector was written. A bare SHA is legal only for single-unit
	// targets; the engine enforces that (E211).
	UnitSelector int
	// All reports the wildcard "*": every record pending for the carrying
	// unit's resolved scope-set (§7.4.2).
	All bool
	// Raw is the value as written.
	Raw string
}

CorrectionTarget is a parsed Edits or Deletes footer value (§7.4.1):

correction-value = "*" / sha [ "#" unit-no ]

Resolving the sha, the selector, and the correction's effect requires git history and is the release engine's §13.4b; the parser validates shape.

func (CorrectionTarget) IsWildcard

func (t CorrectionTarget) IsWildcard() bool

IsWildcard reports whether the target is the "*" form.

func (CorrectionTarget) String

func (t CorrectionTarget) String() string

String implements fmt.Stringer.

type DependencyKind

type DependencyKind string

DependencyKind is a manifest dependency field traversed as a graph edge (§8.4).

const (
	KindDependencies         DependencyKind = "dependencies"
	KindDevDependencies      DependencyKind = "devDependencies"
	KindPeerDependencies     DependencyKind = "peerDependencies"
	KindOptionalDependencies DependencyKind = "optionalDependencies"
	KindAll                  DependencyKind = "*"
)

Dependency kinds. KindAll is the wildcard "*", reusing the scope-set selector of §5.2: every kind is traversed, devDependencies included.

func DefaultPropagateKinds

func DefaultPropagateKinds() []DependencyKind

DefaultPropagateKinds returns the manifest fields traversed by default (§8.4). devDependencies is deliberately absent.

func ParseDependencyKind

func ParseDependencyKind(s string) (DependencyKind, bool)

ParseDependencyKind validates a single dependency-edge kind (§8.4).

type Depth

type Depth int

Depth is a Propagate-Depth value (§8.3): the number of graph edges a propagation travels. DepthAll denotes the full transitive closure.

const DepthAll Depth = -1

DepthAll is the "+*" / "all" depth.

func (Depth) IsAll

func (d Depth) IsAll() bool

IsAll reports whether d is the transitive closure.

func (Depth) String

func (d Depth) String() string

String implements fmt.Stringer.

type Diagnostic

type Diagnostic struct {
	Code      string
	Severity  Severity
	Message   string
	Position  Position
	UnitIndex int // index into Result.Units, or -1 for message-level diagnostics
}

Diagnostic is a single entry produced by a parse. Every diagnostic carries the exact position at which it was raised, so callers can render a caret under the offending character (§20.7).

func (Diagnostic) IsError

func (d Diagnostic) IsError() bool

IsError reports whether the diagnostic invalidates its unit.

func (Diagnostic) String

func (d Diagnostic) String() string

String implements fmt.Stringer.

type Directives

type Directives struct {
	Propagate    Propagate
	PropagateSet bool

	Depth    Depth
	DepthSet bool

	PropagateScope    ScopeSet
	PropagateScopeSet bool

	PropagateChannel    ChannelValue
	PropagateChannelSet bool

	ChannelDepth    Depth
	ChannelDepthSet bool

	PropagateChannelScope    ScopeSet
	PropagateChannelScopeSet bool

	Channel    ChannelValue
	ChannelSet bool

	ReleaseAs *ReleaseAs

	// Edits holds the targets this unit restates: each named record is
	// discarded and this unit's type, breaking marker and description stand in
	// its place. Applying them is the engine's §13.4b.
	Edits []CorrectionTarget
	// Deletes holds the targets this unit discards without restatement.
	Deletes []CorrectionTarget

	// Kinds are the manifest fields traversed as propagation edges. They come
	// from configuration alone: §8.4 defines no per-unit override, because
	// which dependency fields imply "must be republished" is a fact about the
	// repository, not about any one commit.
	//
	// The slice aliases the parser's configuration and must be treated as
	// read-only.
	Kinds []DependencyKind

	// Reverts holds the values of any Reverts footers, which are informational
	// (§8.1).
	Reverts []string

	// BreakingChange is the text of a BREAKING CHANGE footer, if present.
	BreakingChange string
}

Directives is the reconciled directive state of a unit: inline sigils and footers merged, then filled in from configuration and the spec defaults. The *Set fields report whether the value was stated by the author rather than inherited from a default.

type Footer struct {
	// Key is the key exactly as written.
	Key string
	// CanonicalKey is the registry spelling for a known key, or Key otherwise.
	CanonicalKey string
	// Value is the trailer value, with continuation lines already joined.
	Value string
	// Separator is ": " or " #" (§20.5).
	Separator string
	// Position is where the key begins.
	Position Position
	// Known reports whether the key is in the §8.1 registry.
	Known bool
	// MiscasedBreaking reports a key that equals BREAKING CHANGE or
	// BREAKING-CHANGE case-insensitively but not exactly. Such a footer is
	// NOT breaking (it is an unknown key) and carries W155 (§8.1.1).
	MiscasedBreaking bool
	// MessageLevel reports an authorship or review trailer, which the release
	// engine ignores wherever it appears (§4.5).
	MessageLevel bool
	// IssueReference reports an issue-reference trailer, ignored for
	// versioning but available to a changelog (§4.5).
	IssueReference bool
}

Footer is one git trailer in a unit's final paragraph (§8.1).

func (Footer) IsBreakingChange

func (f Footer) IsBreakingChange() bool

IsBreakingChange reports whether this footer is a BREAKING CHANGE trailer.

type Header struct {
	// Raw is the header line exactly as it appeared after normalisation.
	Raw string
	// Type is the lowercase type.
	Type string
	// HasScopeSet reports whether parentheses were written at all. It is what
	// distinguishes "feat: x" (derived scope, §6.2) from "feat(*): x".
	HasScopeSet bool
	// Scopes holds the scope terms in written order.
	Scopes ScopeSet
	// Inline holds the directives written with sigils.
	Inline InlineDirectives
	// Breaking reports whether "!" preceded the colon.
	Breaking bool
	// Description is the remainder of the line after ": ".
	Description string
	// Position is the start of the header in the message.
	Position Position
}

Header is a parsed unit header (§5):

<type>[(<scope-set>)][<inline-directives>][!]: <description>

type InlineDirectives

type InlineDirectives struct {
	// Propagate is set by "^x" or a non-empty "^^x".
	Propagate *Propagate
	// Depth is set by "+N", by a bare or valued "^" (as 1), or by "^^" (as all).
	Depth *Depth
	// Channel is set by "%x": the unit's own channel.
	Channel *ChannelValue
	// PropagateChannel is set by "%%x": the channel given to dependents.
	PropagateChannel *ChannelValue
	// ChannelDepth is set by "++N", or implied as 1 by "%%".
	ChannelDepth *Depth
	// contains filtered or unexported fields
}

InlineDirectives holds the directives written in a header with sigils (§5.3). A nil field means the sigil was absent; it does not mean "default".

func (InlineDirectives) IsEmpty

func (d InlineDirectives) IsEmpty() bool

IsEmpty reports whether the header carried no inline directive at all.

type Limits

type Limits struct {
	// UnitsPerMessage caps the number of units in one message. Zero value: 64.
	UnitsPerMessage int
	// ScopeTermsPerUnit caps the terms in one scope-set. Zero value: 256.
	ScopeTermsPerUnit int
	// MessageBytes caps the message length. Zero value: 1 MiB.
	MessageBytes int
}

Limits are the parser bounds of §14.1. Exceeding any of them is E158, which is message-scoped: the commit contributes nothing.

The zero value of each field selects the default. The bounds cannot be disabled: a commit message is untrusted input (§18.3), so a negative value is rejected by Validate. Raise the numbers instead.

type ParseError

type ParseError struct {
	Diagnostics []Diagnostic
}

ParseError aggregates every error-severity diagnostic produced by a parse. A non-nil ParseError does not mean the Result is unusable: units without errors are still populated and still apply (§16).

func (*ParseError) Codes

func (e *ParseError) Codes() []string

Codes returns the diagnostic codes in order, which is convenient in tests.

func (*ParseError) Error

func (e *ParseError) Error() string

Error implements error.

type Parser

type Parser struct {
	// contains filtered or unexported fields
}

Parser parses CCME commit messages. Construct one with NewParser, MustNewParser or DefaultParser; the zero value is not usable.

A Parser holds no mutable state and is safe for concurrent use.

func DefaultParser

func DefaultParser() *Parser

DefaultParser returns a parser configured entirely from the specification defaults. It is shorthand for MustNewParser(Config{}).

func MustNewParser

func MustNewParser(cfg Config) *Parser

MustNewParser is NewParser for configurations known to be valid, such as package-level defaults. It panics on an invalid configuration.

func NewParser

func NewParser(cfg Config) (*Parser, error)

NewParser builds a Parser from a single configuration struct. Every zero-valued field is filled in from the specification defaults (§14), so NewParser(ccme.Config{}) is the fully default parser and NewParser(ccme.Config{Lenient: true}) changes exactly one thing.

The configuration is copied, so the caller may reuse or mutate the struct afterwards. An invalid configuration is returned as an error.

Example
package main

import (
	"fmt"

	"github.com/yohimik/dispat/pkg/ccme"
)

func main() {
	types := ccme.DefaultTypes()
	types["deps"] = ccme.BumpPatch

	p, err := ccme.NewParser(ccme.Config{
		Separator:   "%%%",
		StrictTypes: true,
		Types:       types,
		Propagation: ccme.PropagationConfig{Depth: ccme.DepthAll},
	})
	if err != nil {
		fmt.Println("config error:", err)
		return
	}

	res, err := p.Parse("deps(core): bump lockfile\n%%%\nfix(cli): guard nil")
	fmt.Println("units:", len(res.Units), "err:", err)
	fmt.Println("unit 0 bump:", res.Units[0].Bump)
	fmt.Println("unit 0 depth:", res.Units[0].Directives.Depth)

}
Output:
units: 2 err: <nil>
unit 0 bump: patch
unit 0 depth: all

func (*Parser) Config

func (p *Parser) Config() Config

Config returns a deep copy of the parser's effective configuration, with every default already filled in.

func (*Parser) Parse

func (p *Parser) Parse(message string) (*Result, error)

Parse parses a complete commit message: normalisation (§4.1), splitting into units (§4.2), then header, body, footers and semantics for each unit.

The returned Result is always non-nil. A non-nil error means at least one error-severity diagnostic was raised; the units that parsed cleanly are still present and still apply, because an error invalidates only the offending unit (§16).

Example
package main

import (
	"fmt"

	"github.com/yohimik/dispat/pkg/ccme"
)

func main() {
	const message = `feat(@acme/api): add cursor pagination

---

fix(@acme/api): reject negative page sizes

---

docs(docs-site): document pagination`

	p := ccme.DefaultParser()
	res, _ := p.Parse(message)

	for _, u := range res.ValidUnits() {
		fmt.Printf("%d: %-5s %-12s %s\n", u.Index, u.Header.Type, u.Scopes(), u.Bump)
	}
	fmt.Println("message bump:", res.Bump())

}
Output:
0: feat  @acme/api    minor
1: fix   @acme/api    patch
2: docs  docs-site    none
message bump: minor
Example (Diagnostics)
package main

import (
	"fmt"

	"github.com/yohimik/dispat/pkg/ccme"
)

func main() {
	p := ccme.DefaultParser()

	res, err := p.Parse("feat(core)^^minor+2: broken directive")
	fmt.Println("err != nil:", err != nil)
	for _, d := range res.Diagnostics {
		fmt.Printf("%s %s at %s\n", d.Severity, d.Code, d.Position)
	}

}
Output:
err != nil: true
error E113 at 1:18

func (*Parser) ParseSubject

func (p *Parser) ParseSubject(subject string) (*Result, error)

ParseSubject parses a single commit subject, the header line on its own. It is the narrow entry point for commit-lint style checks; the message is normalised first, and a subject containing a line break is rejected.

Example
package main

import (
	"fmt"

	"github.com/yohimik/dispat/pkg/ccme"
)

func main() {
	p := ccme.DefaultParser()

	res, err := p.ParseSubject("feat(@acme/core)^^minor%beta!: streaming reader")
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	u := res.Units[0]
	fmt.Println("type:       ", u.Header.Type)
	fmt.Println("scopes:     ", u.Scopes())
	fmt.Println("breaking:   ", u.Breaking)
	fmt.Println("bump:       ", u.Bump)
	fmt.Println("propagate:  ", u.Directives.Propagate)
	fmt.Println("depth:      ", u.Directives.Depth)
	fmt.Println("channel:    ", u.Directives.Channel)
	fmt.Println("description:", u.Header.Description)

}
Output:
type:        feat
scopes:      @acme/core
breaking:    true
bump:        major
propagate:   minor
depth:       all
channel:     beta
description: streaming reader

type Position

type Position struct {
	Line   int
	Column int
}

Position is a location inside the normalised message. Both fields are 1-based; Column counts bytes, which is what a caret-pointing renderer needs.

func (Position) String

func (p Position) String() string

String implements fmt.Stringer, rendering "line:column". Like every other String in the package it avoids fmt, so rendering diagnostics stays off the allocator's hot path.

type Propagate

type Propagate string

Propagate is the bump handed to dependents of a unit's packages (§8.2).

const (
	PropagateNone    Propagate = "none"
	PropagatePatch   Propagate = "patch"
	PropagateMinor   Propagate = "minor"
	PropagateMajor   Propagate = "major"
	PropagateInherit Propagate = "inherit"
)

Propagate values.

func ParsePropagate

func ParsePropagate(s string) (Propagate, bool)

ParsePropagate validates a Propagate value byte-for-byte. Abbreviations are rejected (§5.3).

func (Propagate) Bump

func (p Propagate) Bump(unitBump Bump) Bump

Bump returns the concrete bump a Propagate value denotes, given the bump the originating unit itself produces. inherit copies that bump (§8.2).

type PropagationConfig

type PropagationConfig struct {
	// Bump is the default Propagate value. Zero value: PropagatePatch.
	Bump Propagate

	// Depth is the default Propagate-Depth (§8.3). Zero value: 0, which is
	// also the specification default: a unit does not propagate unless it
	// says so, so there is no ambiguity between "unset" and "no propagation".
	//
	// Repositories that bundle rather than declare their dependencies should
	// set 1; use DepthAll for the full transitive closure.
	Depth Depth

	// ChannelDepth is the default Propagate-Channel-Depth (§8.3a). Zero value:
	// 0: a unit moves nobody else's channel unless it says so. Set 1 or
	// DepthAll to carry release trains along by default.
	ChannelDepth Depth

	// Kinds is the set of dependency edges propagation follows (§8.4). It is
	// configuration only, since the spec has no per-unit override, so every unit
	// of every message sees this list. A nil slice selects
	// DefaultPropagateKinds; a non-nil empty slice traverses no edges at all.
	Kinds []DependencyKind

	// Channel is the default Propagate-Channel value. Zero value:
	// ChannelInherit.
	Channel string
}

PropagationConfig holds the configurable propagation defaults (§14). Every field is overridden by a directive written on the unit itself; the precedence chain is footer → inline sigil → this configuration → the specification default (§8.3). A footer wins over the sigil it contradicts, with E112, or with W112 under Config.Lenient.

Propagation has two independent axes (§5.3). The bump axis is Bump plus Depth, written "^", "^^" and "+N"; the channel axis is Channel plus ChannelDepth, written "%%" and "++N". Both depths default to 0, so neither axis reaches anybody until a unit or this configuration opts in.

type ReleaseAs

type ReleaseAs struct {
	Kind    ReleaseAsKind
	Version Version // valid when Kind is ReleaseAsExact
	Raw     string
}

ReleaseAs is a parsed Release-As footer value (§8.6). It never carries a bump: Release-As acts on the release, not on the size of the change.

func (ReleaseAs) String

func (r ReleaseAs) String() string

String implements fmt.Stringer.

type ReleaseAsKind

type ReleaseAsKind int

ReleaseAsKind discriminates the three forms of a Release-As value (§8.6).

const (
	// ReleaseAsExact pins a specific version.
	ReleaseAsExact ReleaseAsKind = iota
	// ReleaseAsNone holds the package: it is not released in this window, but
	// its pending units are retained rather than discarded, and accumulate
	// until the hold is lifted (§8.6.1). This is what distinguishes it from
	// cancel, which erases.
	ReleaseAsNone
	// ReleaseAsAuto lifts an active hold and returns to normal computation,
	// releasing everything that accumulated at the max() of all of it.
	ReleaseAsAuto
)

Release-As forms. All three operate at the same level, the package for the current window, because Release-As decides whether and at what version a package is released, never how large a change is (§8.6).

There is deliberately no bump form: how large a change is, is a property of the change, and the type already declares it. Release-As: minor is E151.

func (ReleaseAsKind) IsHold

func (k ReleaseAsKind) IsHold() bool

IsHold reports whether the directive pauses publishing (§8.6.1).

func (ReleaseAsKind) String

func (k ReleaseAsKind) String() string

String implements fmt.Stringer.

type Result

type Result struct {
	// Message is the normalised message (§4.1).
	Message string
	// Units are every unit in written order, including units that failed to
	// parse. Check Unit.Valid, or use ValidUnits.
	Units []*Unit
	// Diagnostics are every diagnostic raised.
	//
	// The order is deterministic and is a pure function of the input, as §17.2
	// requires: unit-level diagnostics are grouped by unit in unit order, and
	// within a unit they follow the order the parser raises them. Nothing here
	// depends on map-iteration order.
	Diagnostics []Diagnostic
}

Result is the outcome of a parse.

A Result keeps a reference to the normalised message: Units, bodies and descriptions are substrings of it, so retaining a Result retains the message.

func (*Result) Bump

func (r *Result) Bump() Bump

Bump returns the highest direct bump over the valid units. It is the message's contribution before scope resolution and propagation (§13.6).

func (*Result) Codes

func (r *Result) Codes() []string

Codes returns every diagnostic code in order, which is convenient in tests.

func (*Result) Errors

func (r *Result) Errors() []Diagnostic

Errors returns the error-severity diagnostics.

func (*Result) HasErrors

func (r *Result) HasErrors() bool

HasErrors reports whether any error-severity diagnostic was raised.

func (*Result) ValidUnits

func (r *Result) ValidUnits() []*Unit

ValidUnits returns the units with no error-severity diagnostic.

When every unit is valid, the overwhelmingly common case, it returns Units itself rather than a copy, so treat the result as read-only.

func (*Result) Warnings

func (r *Result) Warnings() []Diagnostic

Warnings returns the warning-severity diagnostics.

type ScopeSet

type ScopeSet []ScopeTerm

ScopeSet is an ordered list of scope terms.

func (ScopeSet) Excludes

func (s ScopeSet) Excludes() ScopeSet

Excludes returns the terms with an exclusion marker, markers stripped.

func (ScopeSet) Includes

func (s ScopeSet) Includes() ScopeSet

Includes returns the terms without an exclusion marker.

func (ScopeSet) Names

func (s ScopeSet) Names() []string

Names returns every term's Name in order.

func (ScopeSet) String

func (s ScopeSet) String() string

String renders the set as it would be written inside parentheses.

type ScopeTerm

type ScopeTerm struct {
	// Raw is the term as written, including any leading "-".
	Raw string
	// Name is the term without its exclusion marker.
	Name string
	// Exclude reports whether the term was written with a leading "-".
	Exclude bool
	// Position is where the term begins in the message.
	Position Position
}

ScopeTerm is one comma-separated term of a scope-set (§5.2). Resolution of a term to concrete packages requires a workspace and is out of scope for this package; the term is reported exactly as written, with the leading "-" of an exclusion stripped from Name.

func (ScopeTerm) IsAll

func (t ScopeTerm) IsAll() bool

IsAll reports whether the term addresses every package in the workspace,

func (ScopeTerm) IsDerived

func (t ScopeTerm) IsDerived() bool

IsDerived reports whether the term is ".", the file-derived set (§6.2).

func (ScopeTerm) IsGlob

func (t ScopeTerm) IsGlob() bool

IsGlob reports whether the term contains the "*" wildcard.

func (ScopeTerm) String

func (t ScopeTerm) String() string

String implements fmt.Stringer.

type Severity

type Severity int

Severity classifies a Diagnostic. Errors make the offending unit's contribution undefined; warnings never block a release (§16).

const (
	// SeverityWarning never invalidates a unit.
	SeverityWarning Severity = iota
	// SeverityError invalidates the unit it is attached to. Other units in the
	// same message still apply.
	SeverityError
)

func (Severity) String

func (s Severity) String() string

String implements fmt.Stringer.

type Unit

type Unit struct {
	// Index is the unit's position in the message, starting at 0.
	Index int
	// Start is where the unit's header begins in the normalised message.
	Start Position
	// Raw is the unit's text.
	Raw string
	// Header is the parsed header. It is zero-valued when the header itself
	// failed to parse.
	Header Header
	// Body is the free-form body, without the trailing footer block.
	Body string
	// Footers are the trailers of the unit's final paragraph.
	Footers []Footer
	// Directives is the reconciled directive state: inline sigils and footers
	// merged, then filled in from configuration and the spec defaults.
	Directives Directives
	// Breaking reports "!" on the header or a BREAKING CHANGE footer (§5.4).
	Breaking bool
	// TypeBump is the bump the type alone maps to (§7.1).
	TypeBump Bump
	// Bump is the unit's direct bump: TypeBump, raised to major by a breaking
	// marker, then overridden by Release-As (§13.6).
	Bump Bump
	// Valid reports that no error-severity diagnostic is attached to the unit.
	// An invalid unit contributes nothing, but its siblings still apply (§16).
	Valid bool
	// Diagnostics are the diagnostics raised for this unit.
	Diagnostics []Diagnostic
}

Unit is one <header>[body][footers] block of a commit message (§2, §4.4).

func (*Unit) BreakingDescription

func (u *Unit) BreakingDescription() string

BreakingDescription returns the text of a BREAKING CHANGE footer, if any.

func (*Unit) HasExplicitScope

func (u *Unit) HasExplicitScope() bool

HasExplicitScope reports whether the header carried a scope-set. When it did not, the unit's packages are derived from the commit's changed files (§6.2).

func (*Unit) IsCancel

func (u *Unit) IsCancel() bool

IsCancel reports whether the unit is a cancel barrier (§10).

func (*Unit) IsControl

func (u *Unit) IsControl() bool

IsControl reports whether the unit is a control unit, which never produces a bump of its own (§7.1).

func (*Unit) IsRelease

func (u *Unit) IsRelease() bool

IsRelease reports whether the unit is a directive-only release unit (§7.2).

func (*Unit) Scopes

func (u *Unit) Scopes() ScopeSet

Scopes returns the unit's scope terms.

type Version

type Version struct {
	Major      uint64
	Minor      uint64
	Patch      uint64
	Prerelease []string
	Build      []string
	Raw        string
}

Version is a parsed SemVer 2.0.0 version. It exists so that an exact Release-As value can be validated at parse time; the release engine needs the same type when it compares against a baseline.

func ParseVersion

func ParseVersion(s string) (Version, error)

ParseVersion parses a SemVer 2.0.0 version with a single left-to-right scan and no regular expression (§20.6). A leading "v" is rejected, as are leading zeros in numeric identifiers.

func (Version) Bumped

func (v Version) Bumped(b Bump) Version

Bumped returns the version incremented by b: the next major, minor or patch core above v's, with prerelease and build dropped. Bumping a *prerelease* baseline therefore both graduates it and moves the core: 1.2.0-beta.1 bumped minor is 1.3.0, not 1.2.0; whether the train should instead graduate to its own target is the release engine's §11 decision, made against the stable baseline, never through this helper. BumpNone returns v unchanged, Raw and build metadata included.

func (Version) Compare

func (v Version) Compare(o Version) int

Compare orders two versions by SemVer precedence. Build metadata is ignored. It returns -1, 0 or 1.

func (Version) Core

func (v Version) Core() Version

Core strips the prerelease and build components, leaving the MAJOR.MINOR.PATCH triple: 1.0.1-beta.4 -> 1.0.1.

func (Version) IsPrerelease

func (v Version) IsPrerelease() bool

IsPrerelease reports whether the version carries a prerelease component.

func (Version) MarshalText

func (v Version) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler, rendering the version the way String does (build metadata dropped), so a Version crossing a module boundary, say a config model's initial versions, serialises as "1.2.3" rather than as a struct.

func (Version) String

func (v Version) String() string

String renders the version without its build metadata, which is never carried into a computed version (§12.1).

func (*Version) UnmarshalText

func (v *Version) UnmarshalText(text []byte) error

UnmarshalText implements encoding.TextUnmarshaler via ParseVersion.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL