• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

thumbrise / commitlint-scope / 27126538804

08 Jun 2026 08:52AM UTC coverage: 59.466% (+5.3%) from 54.213%
27126538804

push

github

thumbrise
fix: Sort outsiders, harden config test, clarify intersection in docs

- Sort findOutsiders result by File (non-deterministic map iteration)
- Assert ScopeSeparator unconditionally in config tests
- Rewrite README multi-scope section with explicit intersection semantics

3 of 3 new or added lines in 1 file covered. (100.0%)

25 existing lines in 4 files now uncovered.

245 of 412 relevant lines covered (59.47%)

0.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.15
/pkg/validator/validator.go
1
package validator
2

3
import (
4
        "cmp"
5
        "context"
6
        "errors"
7
        "fmt"
8
        "log/slog"
9
        "slices"
10
)
11

12
var (
13
        ErrGetMessage      = errors.New("get commit message")
14
        ErrGetChangedFiles = errors.New("get changed files")
15
        ErrShaLength       = errors.New("sha length must be greater than 0")
16
        ErrSkip            = errors.New("skip commit")
17
)
18

19
type Violation struct {
20
        SHA       string     `json:"sha"`
21
        Header    string     `json:"header"`
22
        Outsiders []Outsider `json:"outsiders"`
23
}
24
type Git interface {
25
        SHA(ctx context.Context, from, to string) ([]string, error)
26
        Message(ctx context.Context, sha string) (string, error)
27
        FilesChanged(ctx context.Context, sha string) ([]string, error)
28
}
29
type ScopeParser interface {
30
        Parse(message string) []string
31
}
32
type OutsiderFinder interface {
33
        Find(scope string, files []string) []Outsider
34
}
35

36
type Options struct {
37
        Logger         *slog.Logger
38
        SHALength      int
39
        Git            Git
40
        OutsiderFinder OutsiderFinder
41
        ScopeParser    ScopeParser
42
}
43
type Validator struct {
44
        logger         *slog.Logger
45
        git            Git
46
        outsiderFinder OutsiderFinder
47
        scopeParser    ScopeParser
48
        shaLength      int
49
}
50

51
func NewValidator(cfg Config, options Options) (*Validator, error) {
1✔
52
        logger := options.Logger
1✔
53
        shaLength := options.SHALength
1✔
54
        scopeParser := options.ScopeParser
1✔
55
        outsiderFinder := options.OutsiderFinder
1✔
56
        git := options.Git
1✔
57

1✔
58
        if logger == nil {
1✔
59
                logger = slog.Default()
×
UNCOV
60
        }
×
61

62
        if git == nil {
1✔
63
                git = NewDefaultGit("")
×
UNCOV
64
        }
×
65

66
        if outsiderFinder == nil {
1✔
67
                var err error
×
68

×
69
                ofPatterns := make([]OutsiderFinderPattern, len(cfg.Patterns))
×
70
                for i, cfgPattern := range cfg.Patterns {
×
71
                        ofPatterns[i] = OutsiderFinderPattern(cfgPattern)
×
UNCOV
72
                }
×
73

74
                outsiderFinder, err = NewDefaultOutsiderFinder(ofPatterns)
×
75
                if err != nil {
×
76
                        return nil, err
×
UNCOV
77
                }
×
78
        }
79

80
        if scopeParser == nil {
1✔
81
                scopeParser = NewDefaultScopeParser(cfg.ScopeRegex, cfg.ScopeSeparator)
×
UNCOV
82
        }
×
83

84
        if shaLength == 0 {
1✔
85
                shaLength = 7
×
UNCOV
86
        }
×
87

88
        if shaLength < 0 {
1✔
89
                return nil, fmt.Errorf("%w, got %d", ErrShaLength, shaLength)
×
UNCOV
90
        }
×
91

92
        return &Validator{
1✔
93
                logger:         logger,
1✔
94
                git:            git,
1✔
95
                outsiderFinder: outsiderFinder,
1✔
96
                scopeParser:    scopeParser,
1✔
97
                shaLength:      shaLength,
1✔
98
        }, nil
1✔
99
}
100

101
func (v *Validator) Validate(ctx context.Context, from, to string) ([]Violation, error) {
1✔
102
        shas, err := v.git.SHA(ctx, from, to)
1✔
103
        if err != nil {
1✔
104
                return nil, fmt.Errorf("git sha: %w", err)
×
UNCOV
105
        }
×
106

107
        var violations []Violation
1✔
108

1✔
109
        for _, sha := range shas {
2✔
110
                violation, err := v.checkCommit(ctx, sha)
1✔
111
                if errors.Is(err, ErrSkip) {
2✔
112
                        continue
1✔
113
                }
114

115
                if err != nil {
2✔
116
                        return nil, err
1✔
117
                }
1✔
118

119
                violations = append(violations, *violation)
1✔
120
        }
121

122
        return violations, nil
1✔
123
}
124

125
func (v *Validator) checkCommit(ctx context.Context, sha string) (*Violation, error) {
1✔
126
        message, err := v.git.Message(ctx, sha)
1✔
127
        if err != nil {
2✔
128
                return nil, fmt.Errorf("%w sha=%s: %w", ErrGetMessage, sha, err)
1✔
129
        }
1✔
130

131
        if message == "" {
2✔
132
                v.logger.Debug("no message, skip", "sha", sha)
1✔
133

1✔
134
                return nil, fmt.Errorf("%w: empty message", ErrSkip)
1✔
135
        }
1✔
136

137
        scopes := v.scopeParser.Parse(message)
1✔
138
        if len(scopes) == 0 {
2✔
139
                v.logger.Debug("no scope, skip", "sha", sha, "message", message)
1✔
140

1✔
141
                return nil, fmt.Errorf("%w: no scope", ErrSkip)
1✔
142
        }
1✔
143

144
        files, err := v.git.FilesChanged(ctx, sha)
1✔
145
        if err != nil {
1✔
UNCOV
146
                return nil, fmt.Errorf("%w sha=%s, commit=%s: %w", ErrGetChangedFiles, sha, message, err)
×
UNCOV
147
        }
×
148

149
        if len(files) == 0 {
2✔
150
                v.logger.Debug("no files changed, skip", "sha", sha)
1✔
151

1✔
152
                return nil, fmt.Errorf("%w: no files", ErrSkip)
1✔
153
        }
1✔
154

155
        allOutsiders := findOutsiders(v.outsiderFinder, scopes, files)
1✔
156
        if len(allOutsiders) == 0 {
2✔
157
                return nil, fmt.Errorf("%w: no outsiders", ErrSkip)
1✔
158
        }
1✔
159

160
        truncatedSHA := sha
1✔
161
        if len(truncatedSHA) > v.shaLength {
2✔
162
                truncatedSHA = truncatedSHA[:v.shaLength]
1✔
163
        }
1✔
164

165
        return &Violation{
1✔
166
                SHA:       truncatedSHA,
1✔
167
                Header:    message,
1✔
168
                Outsiders: allOutsiders,
1✔
169
        }, nil
1✔
170
}
171

172
func findOutsiders(finder OutsiderFinder, scopes []string, files []string) []Outsider {
1✔
173
        if len(scopes) == 0 {
1✔
UNCOV
174
                return nil
×
UNCOV
175
        }
×
176

177
        outsidersByFile := make(map[string]Outsider)
1✔
178
        for _, o := range finder.Find(scopes[0], files) {
2✔
179
                outsidersByFile[o.File] = o
1✔
180
        }
1✔
181

182
        for _, scope := range scopes[1:] {
2✔
183
                validForScope := make(map[string]bool)
1✔
184
                for _, o := range finder.Find(scope, files) {
2✔
185
                        validForScope[o.File] = true
1✔
186
                }
1✔
187

188
                for file := range outsidersByFile {
2✔
189
                        if !validForScope[file] {
2✔
190
                                delete(outsidersByFile, file)
1✔
191
                        }
1✔
192
                }
193
        }
194

195
        result := make([]Outsider, 0, len(outsidersByFile))
1✔
196
        for _, o := range outsidersByFile {
2✔
197
                result = append(result, o)
1✔
198
        }
1✔
199

200
        slices.SortFunc(result, func(a, b Outsider) int {
2✔
201
                return cmp.Compare(a.File, b.File)
1✔
202
        })
1✔
203

204
        return result
1✔
205
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc