detached e194240

fix(cli): show renamed/added/deprecated settings separately in config migrate

commit
e1942406339e65804237e96959bc1e846d345af5
author
brice · · 2025-12-28 07:46 UTC
parent
5fe539f
signature
unsigned
2 files · +132 -23
M internal/cli/cmd/config.go +24 -23
@@ -117,22 +117,23 @@ func runConfigMigrate(_ *cobra.Command, _ []string) error {
117117 return nil
118118 }
119119
120- // Check for migration
120+ // Detect changes (includes renames, additions, and removals)
121121 ctx := context.Background()
122- result, err := uc.Check(ctx, usecase.CheckConfigMigrationInput{})
122+ detectResult, err := uc.DetectChanges(ctx, usecase.DetectChangesInput{})
123123 if err != nil {
124124 fmt.Println(renderer.RenderError(err))
125125 return nil
126126 }
127127
128- if !result.NeedsMigration {
128+ if !detectResult.HasChanges {
129129 fmt.Println(renderer.RenderUpToDate(configFile))
130130 return nil
131131 }
132132
133- // Show config info and missing keys
134- fmt.Println(renderer.RenderConfigInfo(configFile, len(result.MissingKeys)))
135- fmt.Println(renderer.RenderMissingKeys(result.MissingKeys))
133+ // Show config info with change summary
134+ fmt.Println(renderer.RenderConfigInfo(configFile, len(detectResult.Changes)))
135+ fmt.Println(renderer.RenderChangesSummary(detectResult.Changes))
136+ fmt.Println(renderer.RenderChanges(detectResult.Changes))
136137
137138 // If --yes flag, proceed without confirmation
138139 if configYes {
@@ -140,7 +141,7 @@ func runConfigMigrate(_ *cobra.Command, _ []string) error {
140141 }
141142
142143 // Show confirmation dialog
143- return runMigrateWithConfirmation(ctx, uc, renderer, app.Theme, result.MissingKeys)
144+ return runMigrateWithConfirmation(ctx, uc, renderer, app.Theme, detectResult.Changes)
144145 }
145146
146147 // executeMigration performs the actual migration.
@@ -168,12 +169,12 @@ const (
168169
169170 // migrateModel is the bubbletea model for the migrate confirmation.
170171 type migrateModel struct {
171- spinner spinner.Model
172- renderer *styles.ConfigRenderer
173- confirm styles.ConfirmModel
174- state migrateState
175- uc *usecase.MigrateConfigUseCase
176- missingKeys []port.KeyInfo
172+ spinner spinner.Model
173+ renderer *styles.ConfigRenderer
174+ confirm styles.ConfirmModel
175+ state migrateState
176+ uc *usecase.MigrateConfigUseCase
177+ changes []port.KeyChange
177178
178179 result string
179180 err error
@@ -190,21 +191,21 @@ func newMigrateModel(
190191 renderer *styles.ConfigRenderer,
191192 theme *styles.Theme,
192193 uc *usecase.MigrateConfigUseCase,
193- missingKeys []port.KeyInfo,
194+ changes []port.KeyChange,
194195 ) migrateModel {
195196 s := spinner.New()
196197 s.Spinner = spinner.Dot
197198 s.Style = lipgloss.NewStyle().Foreground(theme.Accent)
198199
199- confirm := styles.NewConfirm(theme, "Add these settings with default values?")
200+ confirm := styles.NewConfirm(theme, "Apply these changes?")
200201
201202 return migrateModel{
202- spinner: s,
203- renderer: renderer,
204- confirm: confirm,
205- state: migrateStateConfirm,
206- uc: uc,
207- missingKeys: missingKeys,
203+ spinner: s,
204+ renderer: renderer,
205+ confirm: confirm,
206+ state: migrateStateConfirm,
207+ uc: uc,
208+ changes: changes,
208209 }
209210 }
210211
@@ -291,12 +292,12 @@ func runMigrateWithConfirmation(
291292 uc *usecase.MigrateConfigUseCase,
292293 renderer *styles.ConfigRenderer,
293294 theme *styles.Theme,
294- missingKeys []port.KeyInfo,
295+ changes []port.KeyChange,
295296 ) error {
296297 // Suppress unused ctx warning - kept for consistency with other run functions
297298 _ = ctx
298299
299- m := newMigrateModel(renderer, theme, uc, missingKeys)
300+ m := newMigrateModel(renderer, theme, uc, changes)
300301 p := tea.NewProgram(m)
301302
302303 _, err := p.Run()
@@ -117,22 +117,23 @@ func runConfigMigrate(_ *cobra.Command, _ []string) error {
117 return nil117 return nil
118 }118 }
119 119
120- // Check for migration120+ // Detect changes (includes renames, additions, and removals)
121 ctx := context.Background()121 ctx := context.Background()
122- result, err := uc.Check(ctx, usecase.CheckConfigMigrationInput{})122+ detectResult, err := uc.DetectChanges(ctx, usecase.DetectChangesInput{})
123 if err != nil {123 if err != nil {
124 fmt.Println(renderer.RenderError(err))124 fmt.Println(renderer.RenderError(err))
125 return nil125 return nil
126 }126 }
127 127
128- if !result.NeedsMigration {128+ if !detectResult.HasChanges {
129 fmt.Println(renderer.RenderUpToDate(configFile))129 fmt.Println(renderer.RenderUpToDate(configFile))
130 return nil130 return nil
131 }131 }
132 132
133- // Show config info and missing keys133+ // Show config info with change summary
134- fmt.Println(renderer.RenderConfigInfo(configFile, len(result.MissingKeys)))134+ fmt.Println(renderer.RenderConfigInfo(configFile, len(detectResult.Changes)))
135- fmt.Println(renderer.RenderMissingKeys(result.MissingKeys))135+ fmt.Println(renderer.RenderChangesSummary(detectResult.Changes))
136+ fmt.Println(renderer.RenderChanges(detectResult.Changes))
136 137
137 // If --yes flag, proceed without confirmation138 // If --yes flag, proceed without confirmation
138 if configYes {139 if configYes {
@@ -140,7 +141,7 @@ func runConfigMigrate(_ *cobra.Command, _ []string) error {
140 }141 }
141 142
142 // Show confirmation dialog143 // Show confirmation dialog
143- return runMigrateWithConfirmation(ctx, uc, renderer, app.Theme, result.MissingKeys)144+ return runMigrateWithConfirmation(ctx, uc, renderer, app.Theme, detectResult.Changes)
144 }145 }
145 146
146 // executeMigration performs the actual migration.147 // executeMigration performs the actual migration.
@@ -168,12 +169,12 @@ const (
168 169
169 // migrateModel is the bubbletea model for the migrate confirmation.170 // migrateModel is the bubbletea model for the migrate confirmation.
170 type migrateModel struct {171 type migrateModel struct {
171- spinner spinner.Model172+ spinner spinner.Model
172- renderer *styles.ConfigRenderer173+ renderer *styles.ConfigRenderer
173- confirm styles.ConfirmModel174+ confirm styles.ConfirmModel
174- state migrateState175+ state migrateState
175- uc *usecase.MigrateConfigUseCase176+ uc *usecase.MigrateConfigUseCase
176- missingKeys []port.KeyInfo177+ changes []port.KeyChange
177 178
178 result string179 result string
179 err error180 err error
@@ -190,21 +191,21 @@ func newMigrateModel(
190 renderer *styles.ConfigRenderer,191 renderer *styles.ConfigRenderer,
191 theme *styles.Theme,192 theme *styles.Theme,
192 uc *usecase.MigrateConfigUseCase,193 uc *usecase.MigrateConfigUseCase,
193- missingKeys []port.KeyInfo,194+ changes []port.KeyChange,
194 ) migrateModel {195 ) migrateModel {
195 s := spinner.New()196 s := spinner.New()
196 s.Spinner = spinner.Dot197 s.Spinner = spinner.Dot
197 s.Style = lipgloss.NewStyle().Foreground(theme.Accent)198 s.Style = lipgloss.NewStyle().Foreground(theme.Accent)
198 199
199- confirm := styles.NewConfirm(theme, "Add these settings with default values?")200+ confirm := styles.NewConfirm(theme, "Apply these changes?")
200 201
201 return migrateModel{202 return migrateModel{
202- spinner: s,203+ spinner: s,
203- renderer: renderer,204+ renderer: renderer,
204- confirm: confirm,205+ confirm: confirm,
205- state: migrateStateConfirm,206+ state: migrateStateConfirm,
206- uc: uc,207+ uc: uc,
207- missingKeys: missingKeys,208+ changes: changes,
208 }209 }
209 }210 }
210 211
@@ -291,12 +292,12 @@ func runMigrateWithConfirmation(
291 uc *usecase.MigrateConfigUseCase,292 uc *usecase.MigrateConfigUseCase,
292 renderer *styles.ConfigRenderer,293 renderer *styles.ConfigRenderer,
293 theme *styles.Theme,294 theme *styles.Theme,
294- missingKeys []port.KeyInfo,295+ changes []port.KeyChange,
295 ) error {296 ) error {
296 // Suppress unused ctx warning - kept for consistency with other run functions297 // Suppress unused ctx warning - kept for consistency with other run functions
297 _ = ctx298 _ = ctx
298 299
299- m := newMigrateModel(renderer, theme, uc, missingKeys)300+ m := newMigrateModel(renderer, theme, uc, changes)
300 p := tea.NewProgram(m)301 p := tea.NewProgram(m)
301 302
302 _, err := p.Run()303 _, err := p.Run()
M internal/cli/styles/config.go +108 -0
@@ -69,6 +69,114 @@ func (r *ConfigRenderer) RenderMissingKeys(keys []port.KeyInfo) string {
6969 return sb.String()
7070 }
7171
72+// RenderChanges renders the list of detected changes (added, renamed, removed).
73+func (r *ConfigRenderer) RenderChanges(changes []port.KeyChange) string {
74+ if len(changes) == 0 {
75+ return ""
76+ }
77+
78+ addStyle := lipgloss.NewStyle().Foreground(r.theme.Success)
79+ renameStyle := lipgloss.NewStyle().Foreground(r.theme.Warning)
80+ removeStyle := lipgloss.NewStyle().Foreground(r.theme.Error)
81+ keyStyle := r.theme.Highlight
82+ valueStyle := r.theme.Subtle
83+
84+ // Group changes by type
85+ var added, renamed, removed []port.KeyChange
86+ for _, c := range changes {
87+ switch c.Type {
88+ case port.KeyChangeAdded:
89+ added = append(added, c)
90+ case port.KeyChangeRenamed:
91+ renamed = append(renamed, c)
92+ case port.KeyChangeRemoved:
93+ removed = append(removed, c)
94+ }
95+ }
96+
97+ var sb strings.Builder
98+
99+ // Show renamed keys first (most important for user to understand)
100+ if len(renamed) > 0 {
101+ sb.WriteString(fmt.Sprintf("\n Renamed settings (%d):\n", len(renamed)))
102+ for _, c := range renamed {
103+ sb.WriteString(fmt.Sprintf(
104+ " %s %s %s %s\n Value: %s\n",
105+ renameStyle.Render("~"),
106+ valueStyle.Render(c.OldKey),
107+ renameStyle.Render("→"),
108+ keyStyle.Render(c.NewKey),
109+ valueStyle.Render(c.OldValue),
110+ ))
111+ }
112+ }
113+
114+ // Show new settings
115+ if len(added) > 0 {
116+ sb.WriteString(fmt.Sprintf("\n New settings (%d):\n", len(added)))
117+ for _, c := range added {
118+ sb.WriteString(fmt.Sprintf(
119+ " %s %s\n Default: %s\n",
120+ addStyle.Render("+"),
121+ keyStyle.Render(c.NewKey),
122+ valueStyle.Render(c.NewValue),
123+ ))
124+ }
125+ }
126+
127+ // Show deprecated settings (will be removed)
128+ if len(removed) > 0 {
129+ sb.WriteString(fmt.Sprintf("\n Deprecated settings (%d):\n", len(removed)))
130+ for _, c := range removed {
131+ sb.WriteString(fmt.Sprintf(
132+ " %s %s %s\n",
133+ removeStyle.Render("-"),
134+ valueStyle.Render(c.OldKey),
135+ removeStyle.Render("(will be ignored)"),
136+ ))
137+ }
138+ }
139+
140+ return sb.String()
141+}
142+
143+// RenderChangesSummary renders a summary of changes for the config info header.
144+func (r *ConfigRenderer) RenderChangesSummary(changes []port.KeyChange) string {
145+ var added, renamed, removed int
146+ for _, c := range changes {
147+ switch c.Type {
148+ case port.KeyChangeAdded:
149+ added++
150+ case port.KeyChangeRenamed:
151+ renamed++
152+ case port.KeyChangeRemoved:
153+ removed++
154+ }
155+ }
156+
157+ iconStyle := lipgloss.NewStyle().Foreground(r.theme.Accent)
158+ addStyle := lipgloss.NewStyle().Foreground(r.theme.Success)
159+ renameStyle := lipgloss.NewStyle().Foreground(r.theme.Warning)
160+ removeStyle := lipgloss.NewStyle().Foreground(r.theme.Error)
161+
162+ var parts []string
163+ if renamed > 0 {
164+ parts = append(parts, renameStyle.Render(fmt.Sprintf("%d renamed", renamed)))
165+ }
166+ if added > 0 {
167+ parts = append(parts, addStyle.Render(fmt.Sprintf("%d new", added)))
168+ }
169+ if removed > 0 {
170+ parts = append(parts, removeStyle.Render(fmt.Sprintf("%d deprecated", removed)))
171+ }
172+
173+ if len(parts) == 0 {
174+ return ""
175+ }
176+
177+ return fmt.Sprintf("\n %s %s", iconStyle.Render(IconInfo), strings.Join(parts, ", "))
178+}
179+
72180 // RenderMigrationSuccess renders the success message after migration.
73181 func (r *ConfigRenderer) RenderMigrationSuccess(count int, path string) string {
74182 iconStyle := lipgloss.NewStyle().Foreground(r.theme.Success)
@@ -69,6 +69,114 @@ func (r *ConfigRenderer) RenderMissingKeys(keys []port.KeyInfo) string {
69 return sb.String()69 return sb.String()
70 }70 }
71 71
72+// RenderChanges renders the list of detected changes (added, renamed, removed).
73+func (r *ConfigRenderer) RenderChanges(changes []port.KeyChange) string {
74+ if len(changes) == 0 {
75+ return ""
76+ }
77+
78+ addStyle := lipgloss.NewStyle().Foreground(r.theme.Success)
79+ renameStyle := lipgloss.NewStyle().Foreground(r.theme.Warning)
80+ removeStyle := lipgloss.NewStyle().Foreground(r.theme.Error)
81+ keyStyle := r.theme.Highlight
82+ valueStyle := r.theme.Subtle
83+
84+ // Group changes by type
85+ var added, renamed, removed []port.KeyChange
86+ for _, c := range changes {
87+ switch c.Type {
88+ case port.KeyChangeAdded:
89+ added = append(added, c)
90+ case port.KeyChangeRenamed:
91+ renamed = append(renamed, c)
92+ case port.KeyChangeRemoved:
93+ removed = append(removed, c)
94+ }
95+ }
96+
97+ var sb strings.Builder
98+
99+ // Show renamed keys first (most important for user to understand)
100+ if len(renamed) > 0 {
101+ sb.WriteString(fmt.Sprintf("\n Renamed settings (%d):\n", len(renamed)))
102+ for _, c := range renamed {
103+ sb.WriteString(fmt.Sprintf(
104+ " %s %s %s %s\n Value: %s\n",
105+ renameStyle.Render("~"),
106+ valueStyle.Render(c.OldKey),
107+ renameStyle.Render("→"),
108+ keyStyle.Render(c.NewKey),
109+ valueStyle.Render(c.OldValue),
110+ ))
111+ }
112+ }
113+
114+ // Show new settings
115+ if len(added) > 0 {
116+ sb.WriteString(fmt.Sprintf("\n New settings (%d):\n", len(added)))
117+ for _, c := range added {
118+ sb.WriteString(fmt.Sprintf(
119+ " %s %s\n Default: %s\n",
120+ addStyle.Render("+"),
121+ keyStyle.Render(c.NewKey),
122+ valueStyle.Render(c.NewValue),
123+ ))
124+ }
125+ }
126+
127+ // Show deprecated settings (will be removed)
128+ if len(removed) > 0 {
129+ sb.WriteString(fmt.Sprintf("\n Deprecated settings (%d):\n", len(removed)))
130+ for _, c := range removed {
131+ sb.WriteString(fmt.Sprintf(
132+ " %s %s %s\n",
133+ removeStyle.Render("-"),
134+ valueStyle.Render(c.OldKey),
135+ removeStyle.Render("(will be ignored)"),
136+ ))
137+ }
138+ }
139+
140+ return sb.String()
141+}
142+
143+// RenderChangesSummary renders a summary of changes for the config info header.
144+func (r *ConfigRenderer) RenderChangesSummary(changes []port.KeyChange) string {
145+ var added, renamed, removed int
146+ for _, c := range changes {
147+ switch c.Type {
148+ case port.KeyChangeAdded:
149+ added++
150+ case port.KeyChangeRenamed:
151+ renamed++
152+ case port.KeyChangeRemoved:
153+ removed++
154+ }
155+ }
156+
157+ iconStyle := lipgloss.NewStyle().Foreground(r.theme.Accent)
158+ addStyle := lipgloss.NewStyle().Foreground(r.theme.Success)
159+ renameStyle := lipgloss.NewStyle().Foreground(r.theme.Warning)
160+ removeStyle := lipgloss.NewStyle().Foreground(r.theme.Error)
161+
162+ var parts []string
163+ if renamed > 0 {
164+ parts = append(parts, renameStyle.Render(fmt.Sprintf("%d renamed", renamed)))
165+ }
166+ if added > 0 {
167+ parts = append(parts, addStyle.Render(fmt.Sprintf("%d new", added)))
168+ }
169+ if removed > 0 {
170+ parts = append(parts, removeStyle.Render(fmt.Sprintf("%d deprecated", removed)))
171+ }
172+
173+ if len(parts) == 0 {
174+ return ""
175+ }
176+
177+ return fmt.Sprintf("\n %s %s", iconStyle.Render(IconInfo), strings.Join(parts, ", "))
178+}
179+
72 // RenderMigrationSuccess renders the success message after migration.180 // RenderMigrationSuccess renders the success message after migration.
73 func (r *ConfigRenderer) RenderMigrationSuccess(count int, path string) string {181 func (r *ConfigRenderer) RenderMigrationSuccess(count int, path string) string {
74 iconStyle := lipgloss.NewStyle().Foreground(r.theme.Success)182 iconStyle := lipgloss.NewStyle().Foreground(r.theme.Success)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close