Skip to content

Commit 601f9b1

Browse files
committed
fix: Addressing review feedback
1 parent 7fe6f35 commit 601f9b1

11 files changed

Lines changed: 111 additions & 68 deletions

File tree

internal/cli/commands/catalog/catalog_redesign.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package catalog
22

33
import (
44
"context"
5+
"fmt"
56
"runtime"
67

78
"golang.org/x/sync/errgroup"
@@ -35,7 +36,10 @@ func runRedesign(ctx context.Context, l log.Logger, opts *options.TerragruntOpti
3536
svc := catalog.NewCatalogService(opts)
3637

3738
onModule := func(mod *module.Module) {
38-
moduleCh <- mod
39+
select {
40+
case moduleCh <- mod:
41+
case <-ctx.Done():
42+
}
3943
}
4044

4145
urlCh := make(chan string, 10) //nolint:mnd
@@ -74,6 +78,8 @@ func runRedesign(ctx context.Context, l log.Logger, opts *options.TerragruntOpti
7478

7579
loaders.Go(func() error {
7680
if err := svc.LoadStreamingURL(loadCtx, l, repoURL, onModule); err != nil {
81+
// Individual repo failures are non-critical — warn and
82+
// continue so remaining repos can still load.
7783
l.Warnf("Error loading %s: %v", repoURL, err)
7884
}
7985

@@ -82,11 +88,11 @@ func runRedesign(ctx context.Context, l log.Logger, opts *options.TerragruntOpti
8288
}
8389

8490
if err := loaders.Wait(); err != nil {
85-
l.Warnf("Loader error: %v", err)
91+
return nil, fmt.Errorf("loading modules: %w", err)
8692
}
8793

8894
if err := g.Wait(); err != nil {
89-
l.Warnf("Discovery error: %v", err)
95+
return nil, fmt.Errorf("discovering sources: %w", err)
9096
}
9197

9298
if len(svc.Modules()) == 0 {

internal/cli/commands/catalog/tui/keys.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ type DelegateKeyMap struct {
7878
Scaffold key.Binding
7979
}
8080

81-
// Additional short help entries. This satisfies the help.KeyMap interface and
81+
// ShortHelp returns additional short help entries. This satisfies the help.KeyMap interface and
8282
// is entirely optional.
8383
func (d DelegateKeyMap) ShortHelp() []key.Binding { //nolint:gocritic
8484
return []key.Binding{
@@ -87,7 +87,7 @@ func (d DelegateKeyMap) ShortHelp() []key.Binding { //nolint:gocritic
8787
}
8888
}
8989

90-
// Additional full help entries. This satisfies the help.KeyMap interface and
90+
// FullHelp returns additional full help entries. This satisfies the help.KeyMap interface and
9191
// is entirely optional.
9292
func (d DelegateKeyMap) FullHelp() [][]key.Binding { //nolint:gocritic
9393
return [][]key.Binding{

internal/cli/commands/catalog/tui/redesign/model.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Package redesign implements the redesigned catalog TUI experience with
2+
// streaming module discovery and a welcome loading screen.
13
package redesign
24

35
import (
@@ -142,11 +144,14 @@ func newModelWithItems(l log.Logger, opts *options.TerragruntOptions, svc catalo
142144
// skipping duplicates. If the user has started navigating, the cursor stays
143145
// on the currently selected item. Otherwise it stays at the top of the list.
144146
func (m *Model) insertModuleSorted(mod *module.Module) tea.Cmd {
147+
if mod == nil {
148+
return nil
149+
}
150+
145151
items := m.List.Items()
146152
modTitle := mod.Title()
147153

148-
// Binary search finds the insertion point and doubles as a duplicate check:
149-
// if the item at insertIdx matches, the module is already in the list.
154+
// Binary search finds the insertion point by title for sort order.
150155
insertIdx := sort.Search(len(items), func(i int) bool {
151156
if existing, ok := items[i].(*module.Module); ok {
152157
return strings.ToLower(existing.Title()) >= strings.ToLower(modTitle)
@@ -155,7 +160,9 @@ func (m *Model) insertModuleSorted(mod *module.Module) tea.Cmd {
155160
return false
156161
})
157162

158-
if isDuplicate(items, insertIdx, modTitle) {
163+
// De-duplicate by source path, not title, so distinct modules that
164+
// share a display name are not collapsed.
165+
if isDuplicate(items, mod.TerraformSourcePath()) {
159166
return nil
160167
}
161168

@@ -177,19 +184,20 @@ func (m *Model) insertModuleSorted(mod *module.Module) tea.Cmd {
177184
return cmd
178185
}
179186

180-
// isDuplicate reports whether the item at idx in the sorted list has the
181-
// same title (case-insensitive) as modTitle.
182-
func isDuplicate(items []list.Item, idx int, modTitle string) bool {
183-
if idx >= len(items) {
184-
return false
185-
}
186-
187-
existing, ok := items[idx].(*module.Module)
188-
if !ok {
189-
return false
187+
// isDuplicate reports whether any item in the list has the same source path
188+
// as sourcePath. This uses the stable TerraformSourcePath identity rather
189+
// than the display title, so distinct modules that share a title are not
190+
// incorrectly collapsed.
191+
func isDuplicate(items []list.Item, sourcePath string) bool {
192+
for _, item := range items {
193+
if existing, ok := item.(*module.Module); ok {
194+
if existing.TerraformSourcePath() == sourcePath {
195+
return true
196+
}
197+
}
190198
}
191199

192-
return strings.EqualFold(existing.Title(), modTitle)
200+
return false
193201
}
194202

195203
func (m Model) listenForModule() tea.Cmd { //nolint:gocritic

internal/cli/commands/catalog/tui/redesign/model_test.go

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,11 @@ func createMockCatalogService(t *testing.T, opts *options.TerragruntOptions) cat
7777
repoURL := repoOpts.CloneURL
7878
dummyRepoDir := filepath.Join(helpers.TmpDirWOSymlinks(t), strings.ReplaceAll(repoURL, "github.com/gruntwork-io/", ""))
7979

80-
os.MkdirAll(dummyRepoDir, 0755)
80+
require.NoError(t, os.MkdirAll(dummyRepoDir, 0755), "MkdirAll %s", dummyRepoDir)
8181

8282
gitDir := filepath.Join(dummyRepoDir, ".git")
83-
os.MkdirAll(gitDir, 0755)
84-
os.WriteFile(filepath.Join(gitDir, "config"), fmt.Appendf(nil, `[core]
83+
require.NoError(t, os.MkdirAll(gitDir, 0755), "MkdirAll %s", gitDir)
84+
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "config"), fmt.Appendf(nil, `[core]
8585
repositoryformatversion = 0
8686
filemode = true
8787
bare = false
@@ -92,29 +92,33 @@ func createMockCatalogService(t *testing.T, opts *options.TerragruntOptions) cat
9292
[branch "main"]
9393
remote = origin
9494
merge = refs/heads/main
95-
`, repoURL), 0644)
96-
os.WriteFile(filepath.Join(gitDir, "HEAD"), []byte("ref: refs/heads/main\n"), 0644)
95+
`, repoURL), 0644), "WriteFile %s/config", gitDir)
96+
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "HEAD"), []byte("ref: refs/heads/main\n"), 0644), "WriteFile %s/HEAD", gitDir)
9797

9898
refsDir := filepath.Join(gitDir, "refs")
9999
headsDir := filepath.Join(refsDir, "heads")
100100
remotesDir := filepath.Join(refsDir, "remotes", "origin")
101101

102-
os.MkdirAll(headsDir, 0755)
103-
os.MkdirAll(remotesDir, 0755)
102+
require.NoError(t, os.MkdirAll(headsDir, 0755), "MkdirAll %s", headsDir)
103+
require.NoError(t, os.MkdirAll(remotesDir, 0755), "MkdirAll %s", remotesDir)
104104

105105
fakeCommitHash := "1234567890abcdef1234567890abcdef12345678"
106-
os.WriteFile(filepath.Join(headsDir, "main"), []byte(fakeCommitHash+"\n"), 0644)
107-
os.WriteFile(filepath.Join(remotesDir, "main"), []byte(fakeCommitHash+"\n"), 0644)
106+
require.NoError(t, os.WriteFile(filepath.Join(headsDir, "main"), []byte(fakeCommitHash+"\n"), 0644), "WriteFile %s/main", headsDir)
107+
require.NoError(t, os.WriteFile(filepath.Join(remotesDir, "main"), []byte(fakeCommitHash+"\n"), 0644), "WriteFile %s/main", remotesDir)
108108

109109
switch repoURL {
110110
case "github.com/gruntwork-io/test-repo-1":
111111
readme1Path := filepath.Join(dummyRepoDir, "README.md")
112-
os.WriteFile(readme1Path, []byte("# AWS VPC Module\nThis module creates a VPC in AWS with all the necessary components."), 0644)
113-
os.WriteFile(filepath.Join(dummyRepoDir, "main.tf"), []byte("# VPC terraform configuration"), 0644)
112+
require.NoError(t, os.WriteFile(readme1Path, []byte("# AWS VPC Module\nThis module creates a VPC in AWS with all the necessary components."), 0644), "WriteFile %s", readme1Path)
113+
114+
mainTF1 := filepath.Join(dummyRepoDir, "main.tf")
115+
require.NoError(t, os.WriteFile(mainTF1, []byte("# VPC terraform configuration"), 0644), "WriteFile %s", mainTF1)
114116
case "github.com/gruntwork-io/test-repo-2":
115117
readme2Path := filepath.Join(dummyRepoDir, "README.md")
116-
os.WriteFile(readme2Path, []byte("# AWS EKS Module\nThis module creates an EKS cluster in AWS."), 0644)
117-
os.WriteFile(filepath.Join(dummyRepoDir, "main.tf"), []byte("# EKS terraform configuration"), 0644)
118+
require.NoError(t, os.WriteFile(readme2Path, []byte("# AWS EKS Module\nThis module creates an EKS cluster in AWS."), 0644), "WriteFile %s", readme2Path)
119+
120+
mainTF2 := filepath.Join(dummyRepoDir, "main.tf")
121+
require.NoError(t, os.WriteFile(mainTF2, []byte("# EKS terraform configuration"), 0644), "WriteFile %s", mainTF2)
118122
default:
119123
return nil, fmt.Errorf("unexpected repoURL in mock: %s", repoURL)
120124
}
@@ -135,7 +139,7 @@ func createMockCatalogService(t *testing.T, opts *options.TerragruntOptions) cat
135139
require.NoError(t, err)
136140

137141
unitDir := filepath.Join(tmpDir, "unit")
138-
os.MkdirAll(unitDir, 0755)
142+
require.NoError(t, os.MkdirAll(unitDir, 0755), "MkdirAll %s", unitDir)
139143
opts.TerragruntConfigPath = filepath.Join(unitDir, "terragrunt.hcl")
140144
opts.ScaffoldRootFileName = config.RecommendedParentConfigName
141145

@@ -182,10 +186,10 @@ func TestModelStreamingInsertsSorted(t *testing.T) {
182186
items := finalModel.List.Items()
183187
assert.Len(t, items, len(modules), "all modules should be in the list")
184188

185-
// Verify sorted order
189+
// Verify sorted order (case-insensitive, matching the sort in model.go)
186190
for i := 1; i < len(items); i++ {
187-
prev := items[i-1].(*module.Module).Title()
188-
curr := items[i].(*module.Module).Title()
191+
prev := strings.ToLower(items[i-1].(*module.Module).Title())
192+
curr := strings.ToLower(items[i].(*module.Module).Title())
189193
assert.LessOrEqual(t, prev, curr, "modules should be in alphabetical order: %q should come before %q", prev, curr)
190194
}
191195
}

internal/cli/commands/catalog/tui/redesign/update.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func updateList(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { //nolint:gocritic
9292
m.State = PagerState
9393
case key.Matches(msg, m.delegateKeys.Scaffold):
9494
if m.SVC == nil {
95-
break
95+
return m, nil
9696
}
9797

9898
m.State = ScaffoldState
@@ -114,7 +114,7 @@ func updateList(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { //nolint:gocritic
114114

115115
// Append any commands from button bar initialization
116116
if len(cmds) > 0 {
117-
return m, tea.Batch(cmd, tea.Batch(cmds...))
117+
return m, tea.Batch(append([]tea.Cmd{cmd}, cmds...)...)
118118
}
119119

120120
return m, cmd
@@ -146,7 +146,7 @@ func updatePager(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { //nolint:gocritic
146146
switch currentAction {
147147
case scaffoldBtn:
148148
if m.SVC == nil {
149-
break
149+
return m, nil
150150
}
151151

152152
m.State = ScaffoldState
@@ -164,7 +164,7 @@ func updatePager(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { //nolint:gocritic
164164

165165
case key.Matches(msg, m.pagerKeys.Scaffold):
166166
if m.SVC == nil {
167-
break
167+
return m, nil
168168
}
169169

170170
m.State = ScaffoldState

internal/cli/commands/catalog/tui/redesign/view.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
var (
1212
AppStyle = lipgloss.NewStyle().Padding(1, 2) //nolint:mnd
1313
infoPositionStyle = lipgloss.NewStyle().Padding(0, 1).BorderStyle(lipgloss.HiddenBorder())
14-
infoLineStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#1D252"))
14+
infoLineStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#1D252F"))
1515
infoHelp = lipgloss.NewStyle().Padding(2, 0, 0, 2) //nolint:mnd
1616
)
1717

internal/cli/commands/catalog/tui/redesign/welcome.go

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type welcomeState int
3535
const (
3636
welcomeLoading welcomeState = iota
3737
welcomeNoSources
38+
welcomeDiscoveryError
3839
)
3940

4041
// DiscoveryCompleteMsg is sent when background discovery finishes.
@@ -79,18 +80,19 @@ var (
7980
// discovery runs in the background, then either transitions to the module
8081
// list TUI or settles into a "no sources found" help screen.
8182
type WelcomeModel struct {
82-
ctx context.Context
83-
logger log.Logger
84-
opts *options.TerragruntOptions
85-
loadFunc LoadFunc
86-
openURL OpenURLFunc
87-
statusCh chan string
88-
moduleCh chan *module.Module
89-
statusText string
90-
spinner spinner.Model
91-
state welcomeState
92-
width int
93-
height int
83+
ctx context.Context
84+
logger log.Logger
85+
lastDiscoveryErr error
86+
moduleCh chan *module.Module
87+
openURL OpenURLFunc
88+
statusCh chan string
89+
loadFunc LoadFunc
90+
opts *options.TerragruntOptions
91+
statusText string
92+
spinner spinner.Model
93+
state welcomeState
94+
width int
95+
height int
9496
}
9597

9698
// NewWelcomeModel creates a WelcomeModel that immediately begins discovery.
@@ -224,6 +226,10 @@ func (m WelcomeModel) handleModuleMsg(msg moduleMsg) (tea.Model, tea.Cmd) { //no
224226
func (m WelcomeModel) handleDiscoveryComplete(msg DiscoveryCompleteMsg) (tea.Model, tea.Cmd) { //nolint:gocritic
225227
if msg.Err != nil {
226228
m.logger.Warnf("Discovery error: %v", msg.Err)
229+
m.lastDiscoveryErr = msg.Err
230+
m.state = welcomeDiscoveryError
231+
232+
return m, nil
227233
}
228234

229235
// Defensive: if we're still on the loading screen but the service has
@@ -256,6 +262,8 @@ func (m WelcomeModel) View() tea.View { //nolint:gocritic
256262
content = m.loadingView()
257263
case welcomeNoSources:
258264
content = m.noSourcesView()
265+
case welcomeDiscoveryError:
266+
content = m.discoveryErrorView()
259267
}
260268

261269
if m.width > 0 && m.height > 0 {
@@ -303,6 +311,29 @@ func (m WelcomeModel) noSourcesView() string { //nolint:gocritic
303311
return lipgloss.JoinVertical(lipgloss.Center, title, body)
304312
}
305313

314+
func (m WelcomeModel) discoveryErrorView() string { //nolint:gocritic
315+
title := welcomeTitleStyle.Render(" Terragrunt Catalog ")
316+
317+
errMsg := "unknown error"
318+
if m.lastDiscoveryErr != nil {
319+
errMsg = m.lastDiscoveryErr.Error()
320+
}
321+
322+
body := welcomeBodyStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
323+
"",
324+
"An error occurred while discovering catalog sources:",
325+
"",
326+
welcomeCodeStyle.Render(" "+errMsg),
327+
"",
328+
"Please check your network connection, authentication, and",
329+
"catalog configuration, then try again.",
330+
"",
331+
welcomeHintStyle.Render("q/esc: exit"),
332+
))
333+
334+
return lipgloss.JoinVertical(lipgloss.Center, title, body)
335+
}
336+
306337
// RunRedesign launches the redesigned catalog experience. It shows a loading
307338
// screen immediately while discovery runs in the background, then transitions
308339
// to the module list if modules are found.

internal/cli/commands/catalog/tui/redesign/welcome_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"os"
7+
"strings"
78
"testing"
89
"time"
910

@@ -270,11 +271,11 @@ func TestWelcomeStreamingModules(t *testing.T) {
270271
assert.Equal(t, redesign.ListState, listModel.State)
271272
assert.Len(t, listModel.List.Items(), len(modules), "all streamed modules should appear in list")
272273

273-
// Verify alphabetical order
274+
// Verify alphabetical order (case-insensitive, matching the sort in model.go)
274275
items := listModel.List.Items()
275276
for i := 1; i < len(items); i++ {
276-
prev := items[i-1].(*module.Module).Title()
277-
curr := items[i].(*module.Module).Title()
277+
prev := strings.ToLower(items[i-1].(*module.Module).Title())
278+
curr := strings.ToLower(items[i].(*module.Module).Title())
278279
assert.LessOrEqual(t, prev, curr, "modules should be in alphabetical order")
279280
}
280281
}

internal/cli/commands/catalog/tui/update.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func updateList(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { //nolint:gocritic
9090
m.State = PagerState
9191
case key.Matches(msg, m.delegateKeys.Scaffold):
9292
if m.SVC == nil {
93-
break
93+
return m, nil
9494
}
9595

9696
m.State = ScaffoldState
@@ -144,7 +144,7 @@ func updatePager(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { //nolint:gocritic
144144
switch currentAction {
145145
case scaffoldBtn:
146146
if m.SVC == nil {
147-
break
147+
return m, nil
148148
}
149149

150150
m.State = ScaffoldState
@@ -162,7 +162,7 @@ func updatePager(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { //nolint:gocritic
162162

163163
case key.Matches(msg, m.pagerKeys.Scaffold):
164164
if m.SVC == nil {
165-
break
165+
return m, nil
166166
}
167167

168168
m.State = ScaffoldState

0 commit comments

Comments
 (0)