Skip to content

Commit 706fc98

Browse files
authored
refac(git/config): Use an iterator for ListRegexp (#382)
TODO from when ListRegexp was originally implemented. Use an iter.Seq now that we're using Go 1.23 to build.
1 parent 6d8d795 commit 706fc98

File tree

6 files changed

+120
-63
lines changed

6 files changed

+120
-63
lines changed

internal/git/config.go

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"fmt"
88
"io"
9+
"iter"
910
"strings"
1011

1112
"github.com/charmbracelet/log"
@@ -133,10 +134,7 @@ type ConfigEntry struct {
133134

134135
// ListRegexp lists all configuration entries that match the given pattern.
135136
// If pattern is empty, '.' is used to match all entries.
136-
func (cfg *Config) ListRegexp(ctx context.Context, pattern string) (
137-
func(yield func(ConfigEntry, error) bool),
138-
error,
139-
) {
137+
func (cfg *Config) ListRegexp(ctx context.Context, pattern string) iter.Seq2[ConfigEntry, error] {
140138
if pattern == "" {
141139
pattern = "."
142140
}
@@ -145,26 +143,25 @@ func (cfg *Config) ListRegexp(ctx context.Context, pattern string) (
145143

146144
var _newline = []byte("\n")
147145

148-
func (cfg *Config) list(ctx context.Context, args ...string) (
149-
func(yield func(ConfigEntry, error) bool),
150-
error,
151-
) {
146+
func (cfg *Config) list(ctx context.Context, args ...string) iter.Seq2[ConfigEntry, error] {
147+
log := cfg.log
152148
args = append([]string{"config", "--null"}, args...)
153-
cmd := newGitCmd(ctx, cfg.log, args...).
154-
Dir(cfg.dir).
155-
AppendEnv(cfg.env...)
156-
157-
stdout, err := cmd.StdoutPipe()
158-
if err != nil {
159-
return nil, fmt.Errorf("stdout pipe: %w", err)
160-
}
149+
return func(yield func(ConfigEntry, error) bool) {
150+
cmd := newGitCmd(ctx, cfg.log, args...).
151+
Dir(cfg.dir).
152+
AppendEnv(cfg.env...)
153+
154+
stdout, err := cmd.StdoutPipe()
155+
if err != nil {
156+
yield(ConfigEntry{}, fmt.Errorf("stdout pipe: %w", err))
157+
return
158+
}
161159

162-
if err := cmd.Start(cfg.exec); err != nil {
163-
return nil, fmt.Errorf("start git-config: %w", err)
164-
}
160+
if err := cmd.Start(cfg.exec); err != nil {
161+
yield(ConfigEntry{}, fmt.Errorf("start git-config: %w", err))
162+
return
163+
}
165164

166-
log := cfg.log
167-
return func(yield func(ConfigEntry, error) bool) {
168165
// Always wait for the command to finish when this returns.
169166
// Ignore the error because git-config fails if there are no matches.
170167
// It's not an error for us if there are no matches.
@@ -195,9 +192,10 @@ func (cfg *Config) list(ctx context.Context, args ...string) (
195192
}
196193

197194
if err := scan.Err(); err != nil {
198-
_ = yield(ConfigEntry{}, fmt.Errorf("scan git-config output: %w", err))
195+
yield(ConfigEntry{}, fmt.Errorf("scan git-config output: %w", err))
196+
return
199197
}
200-
}, nil
198+
}
201199
}
202200

203201
// scanNullDelimited is a bufio.SplitFunc that splits on null bytes.

internal/git/config_test.go

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
1313
"go.abhg.dev/gs/internal/logtest"
14+
"go.abhg.dev/gs/internal/sliceutil"
1415
"go.uber.org/mock/gomock"
1516
)
1617

@@ -146,16 +147,8 @@ func TestConfigListRegexp(t *testing.T) {
146147
exec: execer,
147148
})
148149

149-
iter, err := cfg.ListRegexp(context.Background(), ".")
150+
got, err := sliceutil.CollectErr(cfg.ListRegexp(context.Background(), "."))
150151
require.NoError(t, err)
151-
152-
var got []ConfigEntry
153-
iter(func(entry ConfigEntry, err error) bool {
154-
require.NoError(t, err)
155-
got = append(got, entry)
156-
return true
157-
})
158-
159152
assert.Equal(t, tt.want, got)
160153
})
161154
}
@@ -251,15 +244,8 @@ func TestIntegrationConfigListRegexp(t *testing.T) {
251244
Log: log,
252245
})
253246

254-
var got []ConfigEntry
255-
iter, err := cfg.ListRegexp(ctx, tt.pattern)
247+
got, err := sliceutil.CollectErr(cfg.ListRegexp(ctx, tt.pattern))
256248
require.NoError(t, err)
257-
iter(func(entry ConfigEntry, err error) bool {
258-
require.NoError(t, err)
259-
got = append(got, entry)
260-
return true
261-
})
262-
263249
assert.ElementsMatch(t, tt.want, got)
264250
})
265251
}

internal/sliceutil/collect.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Package sliceutil contains utility functions for working with slices.
2+
// It's an extension of the std slices package.
3+
package sliceutil
4+
5+
import "iter"
6+
7+
// CollectErr collects items from a sequence of items and errors,
8+
// stopping at the first error and returning it.
9+
func CollectErr[T any](ents iter.Seq2[T, error]) ([]T, error) {
10+
var items []T
11+
for item, err := range ents {
12+
if err != nil {
13+
return nil, err
14+
}
15+
items = append(items, item)
16+
}
17+
return items, nil
18+
}

internal/sliceutil/collect_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package sliceutil_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
"go.abhg.dev/gs/internal/sliceutil"
9+
)
10+
11+
func TestCollectErr(t *testing.T) {
12+
type pair struct {
13+
val int
14+
err error
15+
}
16+
17+
tests := []struct {
18+
name string
19+
give []pair
20+
21+
want []int
22+
wantErr error
23+
}{
24+
{
25+
name: "Empty",
26+
give: nil,
27+
want: nil,
28+
},
29+
{
30+
name: "NoErrors",
31+
give: []pair{
32+
{val: 1},
33+
{val: 2},
34+
{val: 3},
35+
},
36+
want: []int{1, 2, 3},
37+
},
38+
{
39+
name: "Error",
40+
give: []pair{
41+
{val: 1},
42+
{err: assert.AnError},
43+
{val: 3},
44+
},
45+
wantErr: assert.AnError,
46+
},
47+
}
48+
49+
for _, tt := range tests {
50+
t.Run(tt.name, func(t *testing.T) {
51+
got, err := sliceutil.CollectErr(func(yield func(int, error) bool) {
52+
for _, p := range tt.give {
53+
if !yield(p.val, p.err) {
54+
break
55+
}
56+
}
57+
})
58+
59+
if tt.wantErr != nil {
60+
require.Error(t, err)
61+
assert.ErrorIs(t, err, tt.wantErr)
62+
} else {
63+
require.NoError(t, err)
64+
assert.Equal(t, tt.want, got)
65+
}
66+
})
67+
}
68+
}

internal/spice/config.go

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"iter"
78
"sort"
89

910
"github.com/alecthomas/kong"
@@ -21,10 +22,7 @@ const (
2122

2223
// GitConfigLister provides access to git-config output.
2324
type GitConfigLister interface {
24-
ListRegexp(context.Context, string) (
25-
func(yield func(git.ConfigEntry, error) bool),
26-
error,
27-
)
25+
ListRegexp(context.Context, string) iter.Seq2[git.ConfigEntry, error]
2826
}
2927

3028
var _ GitConfigLister = (*git.Config)(nil)
@@ -79,19 +77,12 @@ func LoadConfig(ctx context.Context, cfg GitConfigLister, opts ConfigOptions) (*
7977
opts.Log = log.New(io.Discard)
8078
}
8179

82-
entries, err := cfg.ListRegexp(ctx, `^`+_configSection+`\.`)
83-
if err != nil {
84-
return nil, fmt.Errorf("list configuration: %w", err)
85-
}
86-
8780
items := make(map[git.ConfigKey][]string)
8881
shorthands := make(map[string][]string)
8982

90-
err = nil // TODO: use a range loop after Go 1.23
91-
entries(func(entry git.ConfigEntry, iterErr error) bool {
92-
if iterErr != nil {
93-
err = iterErr
94-
return false
83+
for entry, err := range cfg.ListRegexp(ctx, `^`+_configSection+`\.`) {
84+
if err != nil {
85+
return nil, fmt.Errorf("list configuration: %w", err)
9586
}
9687

9788
key := entry.Key.Canonical()
@@ -100,7 +91,7 @@ func LoadConfig(ctx context.Context, cfg GitConfigLister, opts ConfigOptions) (*
10091
// Ignore keys that are not in the spice namespace.
10192
// This will never happen if git config --get-regexp
10293
// behaves correctly, but it's easy to handle.
103-
return true
94+
continue
10495
}
10596

10697
// Special-case: Everything under "spice.shorthand.*"
@@ -114,18 +105,14 @@ func LoadConfig(ctx context.Context, cfg GitConfigLister, opts ConfigOptions) (*
114105
"value", entry.Value,
115106
"error", err,
116107
)
117-
return true
108+
continue
118109
}
119110

120111
shorthands[short] = longform
121-
return true
112+
continue
122113
}
123114

124115
items[key] = append(items[key], entry.Value)
125-
return true
126-
})
127-
if err != nil {
128-
return nil, fmt.Errorf("read configuration: %w", err)
129116
}
130117

131118
return &Config{

internal/spice/stack_edit.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (s *Service) StackEdit(ctx context.Context, req *StackEditRequest) (*StackE
4444
must.NotContainf(req.Stack, s.store.Trunk(), "cannot edit trunk")
4545
must.NotBeBlankf(req.Editor, "editor is required")
4646

47-
branches, err := editStackFile(ctx, req.Editor, req.Stack)
47+
branches, err := editStackFile(req.Editor, req.Stack)
4848
if err != nil {
4949
return nil, err
5050
}
@@ -71,7 +71,7 @@ func (s *Service) StackEdit(ctx context.Context, req *StackEditRequest) (*StackE
7171
// The response list will be in the same order as the input list.
7272
//
7373
// Returns ErrStackEditAborted if the user aborts the edit operation.
74-
func editStackFile(ctx context.Context, editor string, branches []string) ([]string, error) {
74+
func editStackFile(editor string, branches []string) ([]string, error) {
7575
originals := make(map[string]struct{}, len(branches))
7676
for _, branch := range branches {
7777
originals[branch] = struct{}{}

0 commit comments

Comments
 (0)