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

gabyx / Githooks / 6711

20 Feb 2026 05:55PM UTC coverage: 78.823% (-0.2%) from 78.997%
6711

push

circleci

web-flow
chore: linting improvements (#184)

This change request performs comprehensive linting cleanup across the Githooks codebase, modernizing code style and fixing various linting issues. The changes standardize code patterns, improve error handling, and introduce a centralized linting configuration.

**Changes:**
- Added `run_docker` wrapper function to handle Docker commands with/without sudo based on CI environment + `podman`.
- Modernized Go code: replaced `interface{}` with `any`, improved defer patterns, standardized error handling, and cleaned up return statements
- Fixed spelling errors in comments and documentation.
- Added centralized `.golangci.yaml` configuration file and updated the pre-commit hook to use it.
- Run formatting over `treefmt` in the Nix shell.

282 of 426 new or added lines in 67 files covered. (66.2%)

29 existing lines in 20 files now uncovered.

9350 of 11862 relevant lines covered (78.82%)

1745.58 hits per line

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

86.36
/githooks/git/gitconfig-cache.go
1
package git
2

3
import (
4
        "bufio"
5
        "regexp"
6
        "strings"
7

8
        cm "github.com/gabyx/githooks/githooks/common"
9
        strs "github.com/gabyx/githooks/githooks/strings"
10
)
11

12
// ConfigScope Defines the scope of a config file, such as local, global or system.
13
type ConfigScope int
14

15
// Available ConfigScope's.
16
const (
17
        commandScope  ConfigScope = 0
18
        worktreeScope ConfigScope = 1
19

20
        LocalScope  ConfigScope = 2
21
        GlobalScope ConfigScope = 3
22
        SystemScope ConfigScope = 4
23

24
        Traverse ConfigScope = -1
25
)
26

27
type ConfigEntry struct {
28
        name    string
29
        values  []string
30
        changed bool
31
}
32

33
// ConfigMap holds all configs Git reads.
34
type ConfigMap map[string]*ConfigEntry
35

36
// GitConfigCache for faster read access.
37
type ConfigCache struct {
38
        scopes [5]ConfigMap
39
}
40

41
func (c *ConfigCache) getScopeMap(scope ConfigScope) ConfigMap {
106,624✔
42
        cm.DebugAssertF(int(scope) < len(c.scopes), "Wrong scope '%s'", scope)
106,624✔
43

106,624✔
44
        return c.scopes[int(scope)]
106,624✔
45
}
106,624✔
46

47
func toMapIdx(scope string) ConfigScope {
42,117✔
48
        switch scope {
42,117✔
49
        case "system":
7,589✔
50
                return SystemScope
7,589✔
51
        case "global":
26,575✔
52
                return GlobalScope
26,575✔
53
        case "local":
7,941✔
54
                return LocalScope
7,941✔
55
        case "worktree":
1✔
56
                return worktreeScope
1✔
57
        case "command":
11✔
58
                return commandScope
11✔
59
        default:
×
60
                return -1
×
61
        }
62
}
63

64
func toConfigArg(scope ConfigScope) string {
21,609✔
65
        if scope == Traverse {
21,609✔
66
                return ""
×
67
        }
×
68

69
        return "--" + ToConfigName(scope)
21,609✔
70
}
71

72
func ToConfigName(scope ConfigScope) string {
21,617✔
73
        switch scope {
21,617✔
74
        case SystemScope:
2✔
75
                return "system"
2✔
76
        case GlobalScope:
19,736✔
77
                return "global"
19,736✔
78
        case LocalScope:
1,879✔
79
                return "local"
1,879✔
80
        case worktreeScope:
×
81
                return "worktree"
×
82
        case commandScope:
×
83
                return "command"
×
84
        default:
×
85
                cm.PanicF("Wrong scope '%v'", scope)
×
86

×
87
                return ""
×
88
        }
89
}
90

91
func parseConfig(s string, filterFunc func(string) bool) (c ConfigCache, err error) {
1,517✔
92
        c.scopes = [5]ConfigMap{
1,517✔
93
                make(ConfigMap),
1,517✔
94
                make(ConfigMap),
1,517✔
95
                make(ConfigMap),
1,517✔
96
                make(ConfigMap),
1,517✔
97
                make(ConfigMap)}
1,517✔
98

1,517✔
99
        // Define a split function that separates on null-terminators.
1,517✔
100
        onNullTerminator := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
87,272✔
101
                for i := range data {
1,793,997✔
102
                        if data[i] == '\x00' {
1,792,479✔
103
                                return i + 1, data[:i], nil
84,237✔
104
                        }
84,237✔
105
                }
106

107
                if !atEOF {
1,519✔
108
                        return 0, nil, nil
1✔
109
                }
1✔
110
                // There is one final token to be delivered, which may be the empty string.
111
                // Returning bufio.ErrFinalToken here tells Scan there are no more tokens after this
112
                // but does not trigger an error to be returned from Scan itself.
113
                return 0, data, bufio.ErrFinalToken
1,517✔
114
        }
115

116
        addEntry := func(scope ConfigScope, keyValue []string) {
43,634✔
117
                cm.DebugAssert(len(keyValue) == 2) // nolint: mnd
42,117✔
118
                if len(keyValue) != 2 ||
42,117✔
119
                        (filterFunc != nil && !filterFunc(keyValue[0])) { // nolint: mnd
42,117✔
120
                        return
×
121
                }
×
122

123
                c.add(keyValue[0], keyValue[0], keyValue[1], scope, false)
42,117✔
124
        }
125

126
        scanner := bufio.NewScanner(strings.NewReader(s))
1,517✔
127
        scanner.Split(onNullTerminator)
1,517✔
128

1,517✔
129
        // Scan.
1,517✔
130
        i := -1
1,517✔
131
        var txt string
1,517✔
132
        var scope string
1,517✔
133
        for scanner.Scan() {
87,271✔
134
                i++
85,754✔
135

85,754✔
136
                txt = scanner.Text()
85,754✔
137

85,754✔
138
                if i%2 == 0 {
129,389✔
139
                        scope = txt
43,635✔
140
                } else {
85,754✔
141
                        if strs.IsEmpty(scope) { // Can happen, but shouldn't...
42,121✔
142
                                continue
2✔
143
                        }
144

145
                        idx := toMapIdx(scope)
42,117✔
146
                        if idx < 0 {
42,117✔
147
                                err = cm.ErrorF("Wrong Git config scope '%v' for value '%s'", scope, txt)
×
148

×
NEW
149
                                return c, err
×
150
                        }
×
151

152
                        addEntry(
42,117✔
153
                                idx,
42,117✔
154
                                strings.SplitN(txt, "\n", 2)) // nolint: mnd
42,117✔
155
                }
156
        }
157

158
        return c, err
1,517✔
159
}
160

161
func NewConfigCache(gitx Context, filterFunc func(string) bool) (ConfigCache, error) {
1,516✔
162
        conf, err := gitx.Get("config", "--includes", "--list", "--null", "--show-scope")
1,516✔
163
        if err != nil {
1,516✔
164
                return ConfigCache{}, err
×
165
        }
×
166

167
        return parseConfig(conf, filterFunc)
1,516✔
168
}
169

170
func (c *ConfigCache) SyncChangedValues() {}
×
171

172
// Get all config values for key `key` in the cache.
173
func (c *ConfigCache) getAll(key string, scope ConfigScope) (val []string, exists bool) {
11,135✔
174
        if scope == Traverse {
12,532✔
175
                for i := len(c.scopes) - 1; i >= 0; i-- {
8,382✔
176
                        res, _ := c.getAll(key, ConfigScope(i)) // This order is how Git config reports it.
6,985✔
177
                        val = append(val, res...)
6,985✔
178
                }
6,985✔
179
                exists = len(val) != 0
1,397✔
180

1,397✔
181
                return
1,397✔
182
        }
183

184
        m := c.getScopeMap(scope)
9,738✔
185
        v, inMap := m[key]
9,738✔
186
        if inMap && v.values != nil {
9,944✔
187
                val = append(val, v.values...) // dont return reference to internal slice.
206✔
188
                exists = true
206✔
189
        }
206✔
190

191
        return
9,738✔
192
}
193

194
// Get all config values for key `key` in the cache.
195
func (c *ConfigCache) GetAll(key string, scope ConfigScope) (val []string, exists bool) {
4,150✔
196
        return c.getAll(strings.ToLower(key), scope)
4,150✔
197
}
4,150✔
198

199
// Get all config values for regex key `key` in the cache.
200
func (c *ConfigCache) GetAllRegex(key *regexp.Regexp, scope ConfigScope) (vals []KeyValue) {
6✔
201
        if scope == Traverse {
7✔
202
                for i := len(c.scopes) - 1; i >= 0; i-- {
6✔
203
                        vals = append(vals, c.GetAllRegex(key, ConfigScope(i))...)
5✔
204
                }
5✔
205

206
                return
1✔
207
        }
208

209
        m := c.getScopeMap(scope)
5✔
210
        for k, v := range m {
16✔
211
                if key.MatchString(k) {
13✔
212
                        for i := range v.values {
4✔
213
                                vals = append(vals, KeyValue{k, v.values[i]})
2✔
214
                        }
2✔
215
                }
216
        }
217

218
        return
5✔
219
}
220

221
// Get a config value for key `key` in the cache.
222
func (c *ConfigCache) get(key string, scope ConfigScope) (val string, exists bool) {
64,947✔
223
        if scope == Traverse {
75,386✔
224
                for i := range len(c.scopes) {
62,489✔
225
                        val, exists = c.get(key, ConfigScope(i)) // This order is how Git config takes precedence over others.
52,050✔
226
                        if exists {
52,143✔
227
                                break
93✔
228
                        }
229
                }
230

231
                return
10,439✔
232
        }
233

234
        m := c.getScopeMap(scope)
54,508✔
235
        v, inMap := m[key]
54,508✔
236
        if inMap && v.values != nil {
56,768✔
237
                // Get always the last value defined.
2,260✔
238
                // Git config behavior for multiple values for one key.
2,260✔
239
                val = v.values[len(v.values)-1]
2,260✔
240
                exists = true
2,260✔
241
        }
2,260✔
242

243
        return
54,508✔
244
}
245

246
// Get a config value for key `key` in the cache.
247
func (c *ConfigCache) Get(key string, scope ConfigScope) (val string, exists bool) {
12,897✔
248
        return c.get(strings.ToLower(key), scope)
12,897✔
249
}
12,897✔
250

251
func (c *ConfigCache) add(key string, name string, value string, scope ConfigScope, changed bool) {
42,244✔
252
        m := c.getScopeMap(scope)
42,244✔
253

42,244✔
254
        val, exists := m[key]
42,244✔
255
        if !exists {
84,478✔
256
                val = &ConfigEntry{name: name}
42,234✔
257
                m[key] = val
42,234✔
258
        }
42,234✔
259

260
        val.values = append(val.values, value)
42,244✔
261
        val.changed = changed
42,244✔
262
}
263

264
// Set sets a config value `value` for `key` in the cache.
265
func (c *ConfigCache) Set(key string, value string, scope ConfigScope) (added bool) {
126✔
266
        m := c.getScopeMap(scope)
126✔
267

126✔
268
        k := strings.ToLower(key)
126✔
269
        val, inMap := m[k]
126✔
270
        cm.PanicIfF(inMap && len(val.values) > 1,
126✔
271
                "Cannot overwrite multiple values in '%v'.", key)
126✔
272

126✔
273
        if !inMap || len(val.values) == 0 {
250✔
274
                c.add(k, key, value, scope, true)
124✔
275
                added = true
124✔
276
        } else if val.values[0] != value {
125✔
277
                val.values[0] = value
×
278
                // Try to set the name to the upper case
×
279
                // version for better readibility
×
280
                val.name = key
×
281
                val.changed = true
×
282
        }
×
283

284
        return
125✔
285
}
286

287
// IsSet tells if a config value for `key` is set in the cache.
288
func (c *ConfigCache) IsSet(key string, scope ConfigScope) (exists bool) {
665✔
289
        _, exists = c.Get(key, scope)
665✔
290

665✔
291
        return
665✔
292
}
665✔
293

294
// Add a config value `value` to a `key` in the cache.
295
func (c *ConfigCache) Add(key string, value string, scope ConfigScope) {
3✔
296
        c.add(strings.ToLower(key), key, value, scope, true)
3✔
297
}
3✔
298

299
// Unset unsets all config values for `key` is set in the cache.
300
func (c *ConfigCache) Unset(key string, scope ConfigScope) bool {
3✔
301
        m := c.getScopeMap(scope)
3✔
302

3✔
303
        val, exists := m[strings.ToLower(key)]
3✔
304
        if !exists || len(val.values) == 0 {
4✔
305
                return false
1✔
306
        }
1✔
307

308
        val.changed = true
1✔
309
        val.values = nil
1✔
310

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

© 2026 Coveralls, Inc