A parser for Conventional Commits v1.0 commit messages.
Fu powers to parse your commits!
This repository provides a library to parse your commit messages according to the Conventional Commits v1.0 specification.
go get github.com/leodido/go-conventionalcommitsThe parser/docs directory contains .dot and .png files representing the finite-state machines (FSMs) implementing the parser.
Your code base uses only single line commit messages like this one?
feat: awesomenessNo problem at all since the body and the footer parts are not mandatory:
m, _ := parser.NewMachine().Parse([]byte(`feat: awesomeness`))Imagine you have a commit message like this:
docs: correct minor typos
see the issue for details
on docs edits.
Reviewed-by: Z
Refs #133Go with this:
opts := []conventionalcommits.MachineOption{
WithTypes(conventionalcommits.TypesConventional),
}
res, err := parser.NewMachine(opts...).Parse(i)Or, more simpler:
res, err := parser.NewMachine(WithTypes(conventionalcommits.TypesConventional)).Parse(i)This library provides support for different types sets:
- minimal: fix, feat
- conventional: build, ci, chore, docs, feat, fix, perf, refactor, revert, style, test
- falco: build, ci, chore, docs, feat, fix, perf, new, revert, update, test, rule
At the moment, those types are at build time. Which means users can't configure them at runtime.
Anyway, there's also a free-form types set that accepts any combination of printable characters (before the separator after which the commit description starts) as a valid type.
You can choose the type set passing the WithTypes(conventionalcommits.TypesConventional) option as shown above.
A parser behaviour is configurable by using options.
You can set them calling a function on the parser machine.
p := parser.NewMachine()
p.WithBestEffort()
res, err := p.Parse(i)Or you can provide options to NewMachine(...) directly.
p := parser.NewMachine(WithBestEffort())
res, err := p.Parse(i)The best effort mode will make the parser return what it found until the point it errored out, if it found (at least) a valid type and a valid description.
Let's make an example.
Suppose this input commit message:
fix: description
a blank line is mandatory to start the body part of the commit message!The input does not respect the Conventional Commits v1 specification because it lacks a blank line after the description (before the body).
Anyways, if the parser you're using has the best effort mode enabled, you can still obtain some structured data since at least a valid type and description have been found!
res, err := parser.NewMachine(WithBestEffort()).Parse(i)The result will contain a ConventionalCommit struct instance with the Type and the Description fields populated and ignore the rest after the error column.
The parser will still return the error (with the position information), so that you can eventually use it.
The parser implements Conventional Commits v1.0 clauses 8 to 10 for trailers (also called footers). A few behaviors are worth calling out.
A line in the body that looks like a trailer (say, Fixes #123) is not picked up as one. The trailer block is only the run of trailer lines at the end of the message, with a blank line between it and the body.
Given this commit message:
fix: x
This paragraph mentions
Fixes #123 in passing.
Reviewed-by: Xin := []byte("fix: x\n\nThis paragraph mentions\nFixes #123 in passing.\n\nReviewed-by: X")
m, _ := parser.NewMachine(parser.WithTypes(conventionalcommits.TypesFreeForm)).Parse(in)
// m.Body == "This paragraph mentions\nFixes #123 in passing."
// m.Footers == map[reviewed-by:[X]]A message whose body ends with a trailer-shaped line but has no real trailer block keeps the line as body content:
fix: x
This paragraph mentions
Fixes #123 in passing.in := []byte("fix: x\n\nThis paragraph mentions\nFixes #123 in passing.")
m, _ := parser.NewMachine(parser.WithTypes(conventionalcommits.TypesFreeForm)).Parse(in)
// m.Body == "This paragraph mentions\nFixes #123 in passing."
// m.Footers == map[]Clause 10 says a trailer's value can contain spaces and newlines. The value keeps going until the parser sees the next trailer (a Token: value line) or a blank line.
Given this commit message:
fix: x
BREAKING CHANGE: this is a long
explanation that wraps
across several lines
Reviewed-by: Xin := []byte("fix: x\n\nBREAKING CHANGE: this is a long\n explanation that wraps\n across several lines\nReviewed-by: X")
m, _ := parser.NewMachine(parser.WithTypes(conventionalcommits.TypesFreeForm)).Parse(in)
// m.Footers == map[
// breaking-change: ["this is a long\n explanation that wraps\n across several lines"]
// reviewed-by: ["X"]
// ]Clause 8 lets you put a blank line between trailers. Both layouts parse the same way.
Blank-separated:
feat: x
Fixes #1
Reviewed-by: XAdjacent:
feat: x
Fixes #1
Reviewed-by: Xparser.NewMachine(...).Parse([]byte("feat: x\n\nFixes #1\n\nReviewed-by: X"))
parser.NewMachine(...).Parse([]byte("feat: x\n\nFixes #1\nReviewed-by: X"))
// Both yield: footers == map[fixes:[1] reviewed-by:[X]]In strict mode (best-effort off), Parse returns one of:
(*ConventionalCommit, nil)on success(nil, error)on failure
It never returns (nil, nil), so a nil first value always means the second value is the real error.
In best-effort mode you can also get (*ConventionalCommit, error): a partial commit recovered before the parser ran into the problem, plus the error telling you where it stopped.
To run the benchmark suite execute the following command.
make benchAll the parsers have the best effort mode on.
On my machine1, these are the results for the slim parser with the default - ie., minimal, commit message types.
[ok]_minimal______________________________________-12 4876018 242 ns/op 147 B/op 5 allocs/op
[ok]_minimal_with_scope___________________________-12 4258562 284 ns/op 163 B/op 6 allocs/op
[ok]_minimal_breaking_with_scope__________________-12 4176747 288 ns/op 163 B/op 6 allocs/op
[ok]_full_with_50_characters_long_description_____-12 1661618 700 ns/op 288 B/op 10 allocs/op
[no]_empty________________________________________-12 4059327 292 ns/op 112 B/op 3 allocs/op
[no]_type_but_missing_colon_______________________-12 2701904 444 ns/op 200 B/op 6 allocs/op
[no]_type_but_missing_description_________________-12 2207985 539 ns/op 288 B/op 8 allocs/op
[no]_type_and_scope_but_missing_description_______-12 1969390 605 ns/op 312 B/op 10 allocs/op
[no]_breaking_with_type_and_scope_but_missing_desc-12 1978302 606 ns/op 312 B/op 10 allocs/op
[~~]_newline_in_description_______________________-12 2115649 563 ns/op 232 B/op 11 allocs/op
[no]_missing_whitespace_in_description____________-12 1997863 595 ns/op 312 B/op 10 allocs/opUsing another set of commit message types, for example the conventional one, does not have any noticeable impact on performances, as you can see below.
[ok]_minimal______________________________________-12 5297486 228 ns/op 147 B/op 5 allocs/op
[ok]_minimal_with_scope___________________________-12 4498694 267 ns/op 163 B/op 6 allocs/op
[ok]_minimal_breaking_with_scope__________________-12 4431040 273 ns/op 163 B/op 6 allocs/op
[ok]_full_with_50_characters_long_description_____-12 1750111 692 ns/op 288 B/op 10 allocs/op
[no]_empty________________________________________-12 3996532 294 ns/op 112 B/op 3 allocs/op
[no]_type_but_missing_colon_______________________-12 2657913 451 ns/op 200 B/op 6 allocs/op
[no]_type_but_missing_description_________________-12 2172524 553 ns/op 288 B/op 8 allocs/op
[no]_type_and_scope_but_missing_description_______-12 1880526 637 ns/op 312 B/op 10 allocs/op
[no]_breaking_with_type_and_scope_but_missing_desc-12 1879779 635 ns/op 312 B/op 10 allocs/op
[~~]_newline_in_description_______________________-12 2023514 592 ns/op 232 B/op 11 allocs/op
[no]_missing_whitespace_in_description____________-12 1883124 623 ns/op 312 B/op 10 allocs/opAs you may notice, this library is very fast at what it does.
Parsing a commit goes from taking about the same amount of time (~299ns) the half-life of polonium-212 takes2 to less than a microsecond.
To build this Go library locally just run:
makeYou'd need Ragel 6.10 to be present into your machine.
- [1]: Intel Core i7-8850H CPU @ 2.60GHz
- [2]: https://en.wikipedia.org/wiki/nanosecond