+41
-6
cmd/generate.go
+41
-6
cmd/generate.go
···
30
30
sinceTag string
31
31
)
32
32
33
+
// TODO(determinism): Add deduplication logic using diff-based identity
34
+
//
35
+
// Currently generates duplicate .changes/*.md files when:
36
+
// 1. Running generate multiple times on the same range
37
+
// 2. History is rewritten (rebase/amend) but commit content unchanged
38
+
//
39
+
// Implementation:
40
+
//
41
+
// 1. Before processing commits, load existing entries:
42
+
// existingEntries := changeset.LoadExisting(".changes/data")
43
+
// // Returns map[diffHash]Metadata for O(1) lookups
44
+
//
45
+
// 2. For each selected commit:
46
+
// a. Compute diff hash: diffHash := changeset.ComputeDiffHash(repo, commit)
47
+
// b. Check if exists: if meta, exists := existingEntries[diffHash]; exists {
48
+
// - Same commit hash → true duplicate, skip
49
+
// - Different commit hash → rebased/cherry-picked
50
+
// * If --update-rebased: update metadata.CommitHash in JSON
51
+
// * If --skip-rebased: skip (default)
52
+
// * If --warn-rebased: print warning and skip
53
+
// }
54
+
// c. If not exists: create new entry with diff hash as filename
55
+
//
56
+
// 3. Report statistics:
57
+
// - N new entries created
58
+
// - M duplicates skipped (same commit)
59
+
// - K rebased commits detected (same diff, different commit)
60
+
//
61
+
// Flags to add:
62
+
// --update-rebased Update commit hash for rebased entries
63
+
// --skip-rebased Skip rebased commits (default)
64
+
// --warn-rebased Print warnings for rebased commits
65
+
// --force Regenerate all entries (ignore existing)
66
+
//
67
+
// Related: See internal/changeset/changeset.go TODO for implementation details
33
68
func generateCmd() *cobra.Command {
34
69
c := &cobra.Command{
35
70
Use: "generate [from] [to]",
···
65
100
}
66
101
67
102
if len(commits) == 0 {
68
-
style.Headline(fmt.Sprintf("No commits found between %s and %s", from, to))
103
+
style.Headlinef("No commits found between %s and %s", from, to)
69
104
return nil
70
105
}
71
106
···
98
133
return nil
99
134
}
100
135
101
-
style.Headline(fmt.Sprintf("Generating entries for %d selected commits", len(selectedItems)))
136
+
style.Headlinef("Generating entries for %d selected commits", len(selectedItems))
102
137
} else {
103
-
style.Headline(fmt.Sprintf("Found %d commits between %s and %s", len(commits), from, to))
138
+
style.Headlinef("Found %d commits between %s and %s", len(commits), from, to)
104
139
105
140
for _, commit := range commits {
106
141
subject := commit.Message
···
115
150
116
151
meta, err := parser.Parse(commit.Hash.String(), subject, body, commit.Author.When)
117
152
if err != nil {
118
-
fmt.Printf("Warning: failed to parse commit %s: %v\n", commit.Hash.String()[:7], err)
153
+
style.Println("Warning: failed to parse commit %s: %v", commit.Hash.String()[:gitlog.ShaLen], err)
119
154
continue
120
155
}
121
156
···
164
199
created++
165
200
}
166
201
167
-
fmt.Println()
168
-
style.Headline(fmt.Sprintf("Generated %d changelog entries", created))
202
+
style.Newline()
203
+
style.Headlinef("Generated %d changelog entries", created)
169
204
if skipped > 0 {
170
205
style.Println("Skipped %d commits (reverts or non-matching types)", skipped)
171
206
}
+2
-53
cmd/main.go
+2
-53
cmd/main.go
···
7
7
"github.com/charmbracelet/fang"
8
8
"github.com/charmbracelet/log"
9
9
"github.com/spf13/cobra"
10
+
"github.com/stormlightlabs/git-storm/internal/style"
10
11
)
11
12
12
13
var (
···
61
62
return c
62
63
}
63
64
64
-
func unreleasedCmd() *cobra.Command {
65
-
add := &cobra.Command{
66
-
Use: "add",
67
-
Short: "Add a new unreleased change entry",
68
-
Long: `Creates a new .changes/<date>-<summary>.md file with the specified type,
69
-
scope, and summary.`,
70
-
RunE: func(cmd *cobra.Command, args []string) error {
71
-
fmt.Println("unreleased add not implemented")
72
-
fmt.Printf("type=%q scope=%q summary=%q\n", changeType, scope, summary)
73
-
return nil
74
-
},
75
-
}
76
-
add.Flags().StringVar(&changeType, "type", "", "Type of change (added, changed, fixed, removed, security)")
77
-
add.Flags().StringVar(&scope, "scope", "", "Optional scope or subsystem name")
78
-
add.Flags().StringVar(&summary, "summary", "", "Short summary of the change")
79
-
add.MarkFlagRequired("type")
80
-
add.MarkFlagRequired("summary")
81
-
82
-
list := &cobra.Command{
83
-
Use: "list",
84
-
Short: "List all unreleased changes",
85
-
Long: "Prints all pending .changes entries to stdout. Supports JSON output.",
86
-
RunE: func(cmd *cobra.Command, args []string) error {
87
-
fmt.Println("unreleased list not implemented")
88
-
fmt.Printf("outputJSON=%v\n", outputJSON)
89
-
return nil
90
-
},
91
-
}
92
-
list.Flags().BoolVar(&outputJSON, "json", false, "Output results as JSON")
93
-
94
-
review := &cobra.Command{
95
-
Use: "review",
96
-
Short: "Review unreleased changes interactively",
97
-
Long: `Launches an interactive Bubble Tea TUI to review, edit, or categorize
98
-
unreleased entries before final release.`,
99
-
RunE: func(cmd *cobra.Command, args []string) error {
100
-
fmt.Println("unreleased review not implemented (TUI)")
101
-
return nil
102
-
},
103
-
}
104
-
105
-
root := &cobra.Command{
106
-
Use: "unreleased",
107
-
Short: "Manage unreleased changes (.changes directory)",
108
-
Long: `Work with unreleased change notes. Supports adding, listing,
109
-
and reviewing pending entries before release.`,
110
-
}
111
-
root.AddCommand(add, list, review)
112
-
113
-
return root
114
-
}
115
-
116
65
func main() {
117
66
ctx := context.Background()
118
67
root := &cobra.Command{
···
127
76
root.PersistentFlags().StringVarP(&output, "output", "o", "CHANGELOG.md", "Output changelog file path")
128
77
root.AddCommand(generateCmd(), unreleasedCmd(), releaseCmd(), diffCmd(), versionCmd())
129
78
130
-
if err := fang.Execute(ctx, root); err != nil {
79
+
if err := fang.Execute(ctx, root, fang.WithColorSchemeFunc(style.NewColorScheme)); err != nil {
131
80
log.Fatalf("Execution failed: %v", err)
132
81
}
133
82
}
+135
cmd/unreleased.go
+135
cmd/unreleased.go
···
39
39
--output <file> Optional file to export reviewed notes
40
40
*/
41
41
package main
42
+
43
+
import (
44
+
"encoding/json"
45
+
"fmt"
46
+
"slices"
47
+
"strings"
48
+
49
+
"github.com/spf13/cobra"
50
+
"github.com/stormlightlabs/git-storm/internal/changeset"
51
+
"github.com/stormlightlabs/git-storm/internal/style"
52
+
)
53
+
54
+
func unreleasedCmd() *cobra.Command {
55
+
add := &cobra.Command{
56
+
Use: "add",
57
+
Short: "Add a new unreleased change entry",
58
+
Long: `Creates a new .changes/<date>-<summary>.md file with the specified type,
59
+
scope, and summary.`,
60
+
RunE: func(cmd *cobra.Command, args []string) error {
61
+
validTypes := []string{"added", "changed", "fixed", "removed", "security"}
62
+
if !slices.Contains(validTypes, changeType) {
63
+
return fmt.Errorf("invalid type %q: must be one of %s", changeType, strings.Join(validTypes, ", "))
64
+
}
65
+
66
+
entry := changeset.Entry{
67
+
Type: changeType,
68
+
Scope: scope,
69
+
Summary: summary,
70
+
}
71
+
72
+
changesDir := ".changes"
73
+
filePath, err := changeset.Write(changesDir, entry)
74
+
if err != nil {
75
+
return fmt.Errorf("failed to create changelog entry: %w", err)
76
+
}
77
+
78
+
style.Addedf("Created %s", filePath)
79
+
return nil
80
+
},
81
+
}
82
+
add.Flags().StringVar(&changeType, "type", "", "Type of change (added, changed, fixed, removed, security)")
83
+
add.Flags().StringVar(&scope, "scope", "", "Optional scope or subsystem name")
84
+
add.Flags().StringVar(&summary, "summary", "", "Short summary of the change")
85
+
add.MarkFlagRequired("type")
86
+
add.MarkFlagRequired("summary")
87
+
88
+
list := &cobra.Command{
89
+
Use: "list",
90
+
Short: "List all unreleased changes",
91
+
Long: "Prints all pending .changes entries to stdout. Supports JSON output.",
92
+
RunE: func(cmd *cobra.Command, args []string) error {
93
+
changesDir := ".changes"
94
+
entries, err := changeset.List(changesDir)
95
+
if err != nil {
96
+
return fmt.Errorf("failed to list changelog entries: %w", err)
97
+
}
98
+
99
+
if len(entries) == 0 {
100
+
style.Println("No unreleased changes found")
101
+
return nil
102
+
}
103
+
104
+
if outputJSON {
105
+
jsonBytes, err := json.MarshalIndent(entries, "", " ")
106
+
if err != nil {
107
+
return fmt.Errorf("failed to marshal entries to JSON: %w", err)
108
+
}
109
+
fmt.Println(string(jsonBytes))
110
+
return nil
111
+
}
112
+
113
+
style.Headlinef("Found %d unreleased change(s):", len(entries))
114
+
style.Newline()
115
+
116
+
for _, e := range entries {
117
+
displayEntry(e)
118
+
}
119
+
120
+
return nil
121
+
},
122
+
}
123
+
list.Flags().BoolVar(&outputJSON, "json", false, "Output results as JSON")
124
+
125
+
review := &cobra.Command{
126
+
Use: "review",
127
+
Short: "Review unreleased changes interactively",
128
+
Long: `Launches an interactive Bubble Tea TUI to review, edit, or categorize
129
+
unreleased entries before final release.`,
130
+
RunE: func(cmd *cobra.Command, args []string) error {
131
+
fmt.Println("unreleased review not implemented (TUI)")
132
+
return nil
133
+
},
134
+
}
135
+
136
+
root := &cobra.Command{
137
+
Use: "unreleased",
138
+
Short: "Manage unreleased changes (.changes directory)",
139
+
Long: `Work with unreleased change notes. Supports adding, listing,
140
+
and reviewing pending entries before release.`,
141
+
}
142
+
root.AddCommand(add, list, review)
143
+
144
+
return root
145
+
}
146
+
147
+
// displayEntry formats and prints a single changelog entry with color-coded type.
148
+
func displayEntry(e changeset.EntryWithFile) {
149
+
var typeLabel string
150
+
switch e.Entry.Type {
151
+
case "added":
152
+
typeLabel = style.StyleAdded.Render(fmt.Sprintf("[%s]", e.Entry.Type))
153
+
case "changed":
154
+
typeLabel = style.StyleChanged.Render(fmt.Sprintf("[%s]", e.Entry.Type))
155
+
case "fixed":
156
+
typeLabel = style.StyleFixed.Render(fmt.Sprintf("[%s]", e.Entry.Type))
157
+
case "removed":
158
+
typeLabel = style.StyleRemoved.Render(fmt.Sprintf("[%s]", e.Entry.Type))
159
+
case "security":
160
+
typeLabel = style.StyleSecurity.Render(fmt.Sprintf("[%s]", e.Entry.Type))
161
+
default:
162
+
typeLabel = fmt.Sprintf("[%s]", e.Entry.Type)
163
+
}
164
+
165
+
var scopePart string
166
+
if e.Entry.Scope != "" {
167
+
scopePart = fmt.Sprintf("(%s) ", e.Entry.Scope)
168
+
}
169
+
170
+
style.Println("%s %s%s", typeLabel, scopePart, e.Entry.Summary)
171
+
style.Println(" File: %s", e.Filename)
172
+
if e.Entry.Breaking {
173
+
style.Println(" Breaking: %s\n", style.StyleRemoved.Render("YES"))
174
+
}
175
+
style.Newline()
176
+
}
+1
-1
go.mod
+1
-1
go.mod
···
27
27
github.com/aymanbagabas/go-udiff v0.3.1 // indirect
28
28
github.com/charmbracelet/bubbletea v1.3.10
29
29
github.com/charmbracelet/colorprofile v0.3.3 // indirect
30
-
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea // indirect
30
+
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea
31
31
github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef // indirect
32
32
github.com/charmbracelet/x/ansi v0.10.3 // indirect
33
33
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
+101
-3
internal/changeset/changeset.go
+101
-3
internal/changeset/changeset.go
···
1
+
// TODO(determinism): Make changeset file generation deterministic using diff-based identity
2
+
//
3
+
// Current implementation uses [time.Now] for filenames, causing duplicate entries
4
+
// when generate is run multiple times on the same commit range.
5
+
//
6
+
// Store commit metadata in .changes/data/<diff-hash>.json:
7
+
// - Compute hash of git diff content (not commit message)
8
+
// - Use diff hash as stable identifier across rebases
9
+
// - Store JSON metadata: {commit_hash, diff_hash, type, scope, summary, breaking, author, date}
10
+
// - Generate .changes/<diff-hash-7>-<slug>.md from metadata
11
+
//
12
+
// Implementation:
13
+
// 1. Add DiffHash field to Entry struct
14
+
// 2. Add CommitHash field for tracking source (optional, for reference)
15
+
// 3. Create ComputeDiffHash(commit) function:
16
+
// - Get commit.Tree() and parent.Tree()
17
+
// - Compute diff between trees
18
+
// - Hash the diff content (files changed + line changes)
19
+
// - Return hex string
20
+
//
21
+
// 4. Update Write() to:
22
+
// - Accept diff hash as parameter
23
+
// - Use format: .changes/<diff-hash-7>-<slug>.md
24
+
// - Write JSON to .changes/data/<diff-hash>.json
25
+
// - Check if diff hash exists before writing (deduplication)
26
+
//
27
+
// 5. Add Read() function to parse existing entries by diff hash
28
+
//
29
+
// Directory structure:
30
+
//
31
+
// .changes/
32
+
// a1b2c3d-add-authentication.md # Human-readable entry
33
+
// e5f6a7b-fix-memory-leak.md
34
+
// data/
35
+
// a1b2c3d4e5f6...json # Full metadata
36
+
// e5f6a7b8c9d0...json
37
+
//
38
+
// Related: See cmd/generate.go TODO for deduplication logic
1
39
package changeset
2
40
3
41
import (
42
+
"bytes"
4
43
"fmt"
5
44
"os"
6
45
"path/filepath"
···
36
75
if _, err := os.Stat(filePath); os.IsNotExist(err) {
37
76
break
38
77
}
39
-
// File exists, add counter
40
78
filename = fmt.Sprintf("%s-%s-%d.md", timestamp, slug, counter)
41
79
filePath = filepath.Join(dir, filename)
42
80
counter++
···
56
94
return filePath, nil
57
95
}
58
96
59
-
// slugify converts a string into a URL-friendly slug.
60
-
// Converts to lowercase, replaces spaces and special chars with hyphens.
97
+
// slugify converts a string into a URL-friendly slug by converting to lowercase,
98
+
// replaces spaces and special chars with hyphens.
61
99
func slugify(input string) string {
62
100
s := strings.ToLower(input)
63
101
reg := regexp.MustCompile(`[^a-z0-9]+`)
···
72
110
73
111
return s
74
112
}
113
+
114
+
// EntryWithFile pairs an Entry with its source filename for display/processing.
115
+
type EntryWithFile struct {
116
+
Entry Entry
117
+
Filename string
118
+
}
119
+
120
+
// List reads all .changes/*.md files and returns their parsed entries.
121
+
func List(dir string) ([]EntryWithFile, error) {
122
+
entries, err := os.ReadDir(dir)
123
+
if err != nil {
124
+
if os.IsNotExist(err) {
125
+
return []EntryWithFile{}, nil
126
+
}
127
+
return nil, fmt.Errorf("failed to read directory %s: %w", dir, err)
128
+
}
129
+
130
+
var results []EntryWithFile
131
+
132
+
for _, entry := range entries {
133
+
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
134
+
continue
135
+
}
136
+
137
+
filePath := filepath.Join(dir, entry.Name())
138
+
content, err := os.ReadFile(filePath)
139
+
if err != nil {
140
+
return nil, fmt.Errorf("failed to read file %s: %w", filePath, err)
141
+
}
142
+
143
+
parsed, err := parseEntry(content)
144
+
if err != nil {
145
+
return nil, fmt.Errorf("failed to parse %s: %w", entry.Name(), err)
146
+
}
147
+
148
+
results = append(results, EntryWithFile{
149
+
Entry: parsed,
150
+
Filename: entry.Name(),
151
+
})
152
+
}
153
+
154
+
return results, nil
155
+
}
156
+
157
+
// parseEntry extracts YAML frontmatter from a markdown file and unmarshals it into an Entry.
158
+
func parseEntry(content []byte) (Entry, error) {
159
+
var entry Entry
160
+
161
+
parts := bytes.Split(content, []byte("---"))
162
+
if len(parts) < 3 {
163
+
return entry, fmt.Errorf("invalid frontmatter format: expected ---...--- delimiters")
164
+
}
165
+
166
+
yamlContent := parts[1]
167
+
if err := yaml.Unmarshal(yamlContent, &entry); err != nil {
168
+
return entry, fmt.Errorf("failed to unmarshal YAML: %w", err)
169
+
}
170
+
171
+
return entry, nil
172
+
}
+10
-3
internal/gitlog/gitlog.go
+10
-3
internal/gitlog/gitlog.go
···
10
10
"github.com/go-git/go-git/v6/plumbing/object"
11
11
)
12
12
13
+
const ShaLen = 7
14
+
13
15
// CommitKind represents the kind of commit according to Conventional Commits.
14
16
type CommitKind int
15
17
···
91
93
type ConventionalParser struct{}
92
94
93
95
// Parse parses a conventional commit message into structured metadata.
94
-
// Format: type(scope): description or type(scope)!: description
96
+
//
97
+
// Format:
98
+
//
99
+
// type(scope): description or type(scope)!: description
100
+
//
95
101
// Breaking changes can also be indicated by BREAKING CHANGE: in footer.
96
102
func (p *ConventionalParser) Parse(hash, subject, body string, date time.Time) (CommitMeta, error) {
97
103
meta := CommitMeta{
···
239
245
case "docs", "style", "test", "build", "ci", "chore":
240
246
return "changed"
241
247
case "revert":
242
-
return "" // Skip reverts
248
+
return ""
243
249
default:
244
-
return "" // Unknown types are skipped
250
+
return ""
245
251
}
246
252
}
247
253
···
271
277
}
272
278
273
279
// ParseRefArgs parses command arguments to extract from/to refs.
280
+
//
274
281
// Supports both "from..to" and "from to" syntax.
275
282
// If only one arg, treats it as from with to=HEAD.
276
283
func ParseRefArgs(args []string) (from, to string) {
+79
internal/style/style.go
+79
internal/style/style.go
···
3
3
4
4
import (
5
5
"fmt"
6
+
"image/color"
6
7
8
+
"github.com/charmbracelet/fang"
7
9
"github.com/charmbracelet/lipgloss"
10
+
lg "github.com/charmbracelet/lipgloss/v2"
8
11
)
9
12
10
13
var (
···
38
41
fmt.Println(v)
39
42
}
40
43
44
+
func Headlinef(format string, args ...any) {
45
+
s := fmt.Sprintf(format, args...)
46
+
v := StyleHeadline.Render(s)
47
+
fmt.Println(v)
48
+
}
49
+
41
50
func Added(s string) {
42
51
v := StyleAdded.Render(s)
43
52
fmt.Println(v)
···
49
58
fmt.Println(v)
50
59
}
51
60
61
+
func Newline() { fmt.Println() }
62
+
52
63
func Fixed(s string) {
53
64
v := StyleFixed.Render(s)
54
65
fmt.Println(v)
···
67
78
msg := fmt.Sprintf(format, args...)
68
79
fmt.Println(msg)
69
80
}
81
+
82
+
var darkTheme = fang.ColorScheme{
83
+
Base: color.RGBA{25, 28, 35, 255},
84
+
Title: color.RGBA{129, 161, 193, 255},
85
+
Description: color.RGBA{180, 198, 211, 255},
86
+
Codeblock: color.RGBA{46, 52, 64, 255},
87
+
Program: color.RGBA{94, 129, 172, 255},
88
+
DimmedArgument: color.RGBA{110, 115, 125, 255},
89
+
Comment: color.RGBA{76, 86, 106, 255},
90
+
Flag: color.RGBA{143, 188, 187, 255},
91
+
FlagDefault: color.RGBA{163, 190, 140, 255},
92
+
Command: color.RGBA{208, 135, 112, 255},
93
+
QuotedString: color.RGBA{136, 192, 208, 255},
94
+
Argument: color.RGBA{191, 97, 106, 255},
95
+
Help: color.RGBA{143, 188, 187, 255},
96
+
Dash: color.RGBA{216, 222, 233, 255},
97
+
ErrorHeader: [2]color.Color{
98
+
color.RGBA{236, 239, 244, 255},
99
+
color.RGBA{191, 97, 106, 255},
100
+
},
101
+
ErrorDetails: color.RGBA{255, 203, 107, 255},
102
+
}
103
+
104
+
var lightTheme = fang.ColorScheme{
105
+
Base: color.RGBA{245, 247, 250, 255},
106
+
Title: color.RGBA{52, 73, 94, 255},
107
+
Description: color.RGBA{88, 110, 117, 255},
108
+
Codeblock: color.RGBA{230, 235, 240, 255},
109
+
Program: color.RGBA{70, 106, 145, 255},
110
+
DimmedArgument: color.RGBA{140, 145, 155, 255},
111
+
Comment: color.RGBA{150, 160, 170, 255},
112
+
Flag: color.RGBA{0, 114, 178, 255},
113
+
FlagDefault: color.RGBA{106, 153, 85, 255},
114
+
Command: color.RGBA{217, 95, 2, 255},
115
+
QuotedString: color.RGBA{38, 139, 210, 255},
116
+
Argument: color.RGBA{203, 75, 22, 255},
117
+
Help: color.RGBA{0, 114, 178, 255},
118
+
Dash: color.RGBA{120, 130, 140, 255},
119
+
ErrorHeader: [2]color.Color{
120
+
color.RGBA{255, 255, 255, 255},
121
+
color.RGBA{203, 75, 22, 255},
122
+
},
123
+
ErrorDetails: color.RGBA{230, 150, 50, 255},
124
+
}
125
+
126
+
func NewColorScheme(c lg.LightDarkFunc) fang.ColorScheme {
127
+
return fang.ColorScheme{
128
+
Base: c(lightTheme.Base, darkTheme.Base),
129
+
Title: c(lightTheme.Title, darkTheme.Title),
130
+
Description: c(lightTheme.Description, darkTheme.Description),
131
+
Codeblock: c(lightTheme.Codeblock, darkTheme.Codeblock),
132
+
Program: c(lightTheme.Program, darkTheme.Program),
133
+
DimmedArgument: c(lightTheme.DimmedArgument, darkTheme.DimmedArgument),
134
+
Comment: c(lightTheme.Comment, darkTheme.Comment),
135
+
Flag: c(lightTheme.Flag, darkTheme.Flag),
136
+
FlagDefault: c(lightTheme.FlagDefault, darkTheme.FlagDefault),
137
+
Command: c(lightTheme.Command, darkTheme.Command),
138
+
QuotedString: c(lightTheme.QuotedString, darkTheme.QuotedString),
139
+
Argument: c(lightTheme.Argument, darkTheme.Argument),
140
+
Help: c(lightTheme.Help, darkTheme.Help),
141
+
Dash: c(lightTheme.Dash, darkTheme.Dash),
142
+
ErrorHeader: [2]color.Color{
143
+
c(lightTheme.ErrorHeader[0], darkTheme.ErrorHeader[0]),
144
+
c(lightTheme.ErrorHeader[1], darkTheme.ErrorHeader[1]),
145
+
},
146
+
ErrorDetails: c(lightTheme.ErrorDetails, darkTheme.ErrorDetails),
147
+
}
148
+
}
+1
-1
internal/ui/commit_selector.go
+1
-1
internal/ui/commit_selector.go