Skip to content

Commit cbc379c

Browse files
feat: expose registry to define custom rules, formatters
1 parent 26c58b2 commit cbc379c

File tree

8 files changed

+301
-40
lines changed

8 files changed

+301
-40
lines changed

README.md

Lines changed: 233 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ commitlint checks if your commit message meets the [conventional commit format](
5050
- [Trailer / sign-off rules](#trailer--sign-off-rules)
5151
- [Breaking change rules](#breaking-change-rules)
5252
- [Available Formatters](#available-formatters)
53-
- [Extensibility](#extensibility)
53+
- [Programmatic Usage](#programmatic-usage)
54+
- [One-liner with default config](#one-liner-with-default-config)
55+
- [Full control with default config](#full-control-with-default-config)
56+
- [Lint with a config file](#lint-with-a-config-file)
57+
- [Custom rules](#custom-rules)
58+
- [Custom formatters](#custom-formatters)
5459
- [FAQ](#faq)
5560
- [License](#license)
5661

@@ -223,7 +228,7 @@ ignores: []
223228
224229
### Commit Types
225230
226-
Commonly used commit types from [Conventional Commit Types](https://github.com/commitizen/conventional-commit-types)
231+
Commonly used commit types
227232
228233
| Type | Description |
229234
|:---------|:---------------------------------------------------------------------------------|
@@ -411,13 +416,237 @@ Total 1 errors, 0 warnings, 0 other severities
411416
{"input":"fear: do not fear for commit message","issues":[{"description":"type 'fear' is not allowed, you can use one of [build chore ci docs feat fix perf refactor revert style test]","name":"type-enum","severity":"error"}]}
412417
```
413418

414-
## Extensibility
419+
## Programmatic Usage
420+
421+
All public packages are importable. The module path is `github.com/conventionalcommit/commitlint`.
422+
423+
```bash
424+
go get github.com/conventionalcommit/commitlint@latest
425+
```
426+
427+
Key packages:
428+
429+
| Package | Purpose |
430+
|:--------|:--------|
431+
| `config` | Parse config files, build a `Linter`, access defaults |
432+
| `lint` | Core types: `Linter`, `Rule`, `Formatter`, `Config`, `Result`, `Issue` |
433+
| `registry` | Register and look up custom rules / formatters |
434+
| `rule` | Built-in rule implementations |
435+
| `formatter` | Built-in formatters (`default`, `json`) |
436+
437+
### One-liner with default config
438+
439+
The simplest entry point — no config file required:
440+
441+
```go
442+
package main
443+
444+
import (
445+
"fmt"
446+
"github.com/conventionalcommit/commitlint/config"
447+
)
448+
449+
func main() {
450+
result, err := config.LintMessage("feat: add login page")
451+
if err != nil {
452+
panic(err)
453+
}
454+
455+
for _, issue := range result.Issues() {
456+
fmt.Printf("%s: %s: %s\n", issue.Severity(), issue.RuleName(), issue.Description())
457+
}
458+
459+
if len(result.Issues()) == 0 {
460+
fmt.Println("commit message is valid")
461+
}
462+
}
463+
```
464+
465+
### Full control with default config
466+
467+
Build the linter yourself for more control (e.g. to swap the formatter):
468+
469+
```go
470+
package main
471+
472+
import (
473+
"fmt"
474+
"github.com/conventionalcommit/commitlint/config"
475+
"github.com/conventionalcommit/commitlint/formatter"
476+
)
477+
478+
func main() {
479+
conf := config.NewDefault()
480+
// optionally customise conf here
481+
482+
linter, err := config.NewLinter(conf)
483+
if err != nil {
484+
panic(err)
485+
}
486+
487+
result, err := linter.ParseAndLint("feat: add login page")
488+
if err != nil {
489+
panic(err)
490+
}
491+
492+
out, err := (&formatter.JSONFormatter{}).Format(result)
493+
if err != nil {
494+
panic(err)
495+
}
496+
fmt.Println(out)
497+
}
498+
```
499+
500+
### Lint with a config file
501+
502+
Load a `.commitlint.yaml` and lint against it:
503+
504+
```go
505+
package main
506+
507+
import (
508+
"fmt"
509+
"github.com/conventionalcommit/commitlint/config"
510+
)
511+
512+
func main() {
513+
conf, err := config.Parse(".commitlint.yaml")
514+
if err != nil {
515+
panic(err)
516+
}
517+
518+
linter, err := config.NewLinter(conf)
519+
if err != nil {
520+
panic(err)
521+
}
522+
523+
result, err := linter.ParseAndLint("feat: add login page")
524+
if err != nil {
525+
panic(err)
526+
}
527+
528+
for _, issue := range result.Issues() {
529+
fmt.Printf("%s: %s\n", issue.RuleName(), issue.Description())
530+
}
531+
}
532+
```
533+
534+
### Custom rules
535+
536+
Implement the `lint.Rule` interface and register it before building a linter:
537+
538+
```go
539+
package main
540+
541+
import (
542+
"fmt"
543+
"github.com/conventionalcommit/commitlint/config"
544+
"github.com/conventionalcommit/commitlint/lint"
545+
"github.com/conventionalcommit/commitlint/registry"
546+
)
547+
548+
// NoWIPRule rejects commit messages whose description starts with "WIP".
549+
type NoWIPRule struct{}
550+
551+
func (r *NoWIPRule) Name() string { return "no-wip" }
552+
func (r *NoWIPRule) Apply(setting lint.RuleSetting) error { return nil }
553+
func (r *NoWIPRule) Validate(commit lint.Commit) (*lint.Issue, error) {
554+
if len(commit.Description()) >= 3 && commit.Description()[:3] == "WIP" {
555+
return lint.NewIssue("description must not start with WIP"), nil
556+
}
557+
return nil, nil
558+
}
559+
560+
func main() {
561+
if err := registry.RegisterRule(&NoWIPRule{}); err != nil {
562+
panic(err)
563+
}
564+
565+
conf := config.NewDefault()
566+
conf.Rules = append(conf.Rules, "no-wip")
567+
conf.Settings["no-wip"] = lint.RuleSetting{}
568+
569+
linter, err := config.NewLinter(conf)
570+
if err != nil {
571+
panic(err)
572+
}
573+
574+
result, err := linter.ParseAndLint("feat: WIP do not merge")
575+
if err != nil {
576+
panic(err)
577+
}
578+
579+
for _, issue := range result.Issues() {
580+
fmt.Printf("%s: %s\n", issue.RuleName(), issue.Description())
581+
}
582+
}
583+
```
584+
585+
### Custom formatters
586+
587+
Implement `lint.Formatter` and register it:
588+
589+
```go
590+
package main
591+
592+
import (
593+
"fmt"
594+
"strings"
595+
"github.com/conventionalcommit/commitlint/config"
596+
"github.com/conventionalcommit/commitlint/lint"
597+
"github.com/conventionalcommit/commitlint/registry"
598+
)
599+
600+
type SimpleFormatter struct{}
601+
602+
func (f *SimpleFormatter) Name() string { return "simple" }
603+
func (f *SimpleFormatter) Format(result *lint.Result) (string, error) {
604+
if len(result.Issues()) == 0 {
605+
return "ok", nil
606+
}
607+
var sb strings.Builder
608+
for _, issue := range result.Issues() {
609+
fmt.Fprintf(&sb, "[%s] %s: %s\n", issue.Severity(), issue.RuleName(), issue.Description())
610+
}
611+
return sb.String(), nil
612+
}
613+
614+
func main() {
615+
if err := registry.RegisterFormatter(&SimpleFormatter{}); err != nil {
616+
panic(err)
617+
}
618+
619+
conf := config.NewDefault()
620+
conf.Formatter = "simple"
621+
622+
format, err := config.GetFormatter(conf)
623+
if err != nil {
624+
panic(err)
625+
}
626+
627+
linter, err := config.NewLinter(conf)
628+
if err != nil {
629+
panic(err)
630+
}
631+
632+
result, err := linter.ParseAndLint("bad message")
633+
if err != nil {
634+
panic(err)
635+
}
636+
637+
out, err := format.Format(result)
638+
if err != nil {
639+
panic(err)
640+
}
641+
fmt.Print(out)
642+
}
643+
```
415644

416645
## FAQ
417646

418647
- How to have custom config for each repository?
419648

420-
Place `.commitlint.yaml` file in repo root directory. linter follows [config precedence](#precedence).
649+
Place `.commitlint.yaml` file in repo root directory. linter follows [config precedence](#config-precedence).
421650

422651
To create a sample config, run `commitlint config create` (or `commitlint config create --all` to include all available settings)
423652

config/api.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package config
2+
3+
import "github.com/conventionalcommit/commitlint/lint"
4+
5+
// LintMessage lints commitMsg using the default configuration.
6+
// It is the simplest entry point for programmatic use: no config file is needed.
7+
//
8+
// For custom configuration use Parse or NewDefault, then NewLinter.
9+
func LintMessage(commitMsg string) (*lint.Result, error) {
10+
linter, err := NewLinter(NewDefault())
11+
if err != nil {
12+
return nil, err
13+
}
14+
return linter.ParseAndLint(commitMsg)
15+
}

config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import (
1414

1515
"github.com/conventionalcommit/commitlint/formatter"
1616
"github.com/conventionalcommit/commitlint/internal"
17-
"github.com/conventionalcommit/commitlint/internal/registry"
1817
"github.com/conventionalcommit/commitlint/lint"
18+
"github.com/conventionalcommit/commitlint/registry"
1919
)
2020

2121
// Parse parse given file in confPath, and return Config instance, error if any

config/default_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package config
33
import (
44
"testing"
55

6-
"github.com/conventionalcommit/commitlint/internal/registry"
6+
"github.com/conventionalcommit/commitlint/registry"
77
)
88

99
func TestDefaultLint(t *testing.T) {

config/lint.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package config
33
import (
44
"fmt"
55

6-
"github.com/conventionalcommit/commitlint/internal/registry"
76
"github.com/conventionalcommit/commitlint/lint"
7+
"github.com/conventionalcommit/commitlint/registry"
88
)
99

1010
// NewLinter returns Linter for given confFilePath

internal/registry/registry_test.go

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
// Package registry contains registered rules and formatters
1+
// Package registry holds the global registry of rules and formatters.
2+
// External packages can call RegisterRule and RegisterFormatter to extend
3+
// commitlint with custom rules or formatters before building a linter.
24
package registry
35

46
import (
@@ -12,33 +14,38 @@ import (
1214

1315
var globalRegistry = newRegistry()
1416

15-
// RegisterRule registers a custom rule
16-
// if rule already exists, returns error
17+
// RegisterRule registers a custom rule.
18+
// Returns an error if a rule with the same name is already registered.
1719
func RegisterRule(r lint.Rule) error {
1820
return globalRegistry.RegisterRule(r)
1921
}
2022

21-
// RegisterFormatter registers a custom formatter
22-
// if formatter already exists, returns error
23+
// RegisterFormatter registers a custom formatter.
24+
// Returns an error if a formatter with the same name is already registered.
2325
func RegisterFormatter(format lint.Formatter) error {
2426
return globalRegistry.RegisterFormatter(format)
2527
}
2628

27-
// GetRule returns Rule with given name
29+
// GetRule returns the Rule registered under name, and whether it was found.
2830
func GetRule(name string) (lint.Rule, bool) {
2931
return globalRegistry.GetRule(name)
3032
}
3133

32-
// GetFormatter returns Formatter with given name
34+
// GetFormatter returns the Formatter registered under name, and whether it was found.
3335
func GetFormatter(name string) (lint.Formatter, bool) {
3436
return globalRegistry.GetFormatter(name)
3537
}
3638

37-
// Rules returns all registered rules
39+
// Rules returns all registered rules.
3840
func Rules() []lint.Rule {
3941
return globalRegistry.Rules()
4042
}
4143

44+
// Formatters returns all registered formatters.
45+
func Formatters() []lint.Formatter {
46+
return globalRegistry.Formatters()
47+
}
48+
4249
type registry struct {
4350
mut *sync.Mutex
4451

0 commit comments

Comments
 (0)