Skip to content

Commit 8f134a1

Browse files
committed
Add section header support for changelog output
- Parse ## and ### headers from GitHub release bodies - Group changes under sections (TUI, Desktop, New Features, Bug Fixes, etc.) - Display sections in plain text as [Section Name] - Include sections array in JSON output - Falls back gracefully for sources without sections (Claude, Copilot)
1 parent bc0d510 commit 8f134a1

1 file changed

Lines changed: 73 additions & 9 deletions

File tree

main.go

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ import (
1212

1313
var version = "dev"
1414

15-
type ChangelogEntry struct {
16-
Version string `json:"version"`
15+
type Section struct {
16+
Name string `json:"name"`
1717
Changes []string `json:"changes"`
1818
}
1919

20+
type ChangelogEntry struct {
21+
Version string `json:"version"`
22+
Sections []Section `json:"sections,omitempty"`
23+
Changes []string `json:"changes,omitempty"`
24+
}
25+
2026
type Source struct {
2127
Name string
2228
DisplayName string
@@ -233,31 +239,65 @@ func fetchGitHubReleases(owner, repo string) ([]ChangelogEntry, error) {
233239
ver = strings.TrimPrefix(ver, "v")
234240
ver = strings.TrimPrefix(ver, "rust-v")
235241

236-
changes := parseReleaseBody(rel.Body)
242+
sections, ungroupedChanges := parseReleaseBody(rel.Body)
237243

238244
entries = append(entries, ChangelogEntry{
239-
Version: ver,
240-
Changes: changes,
245+
Version: ver,
246+
Sections: sections,
247+
Changes: ungroupedChanges,
241248
})
242249
}
243250

244251
return entries, nil
245252
}
246253

247-
func parseReleaseBody(body string) []string {
248-
var changes []string
254+
func parseReleaseBody(body string) ([]Section, []string) {
255+
var sections []Section
256+
var ungroupedChanges []string
257+
258+
headerRegex := regexp.MustCompile(`^#{1,3}\s+(.+)$`)
249259
lines := strings.Split(body, "\n")
260+
261+
var currentSection *Section
262+
250263
for _, line := range lines {
251264
trimmed := strings.TrimSpace(line)
265+
266+
// Check for section header (# ## or ###)
267+
if match := headerRegex.FindStringSubmatch(trimmed); match != nil {
268+
headerName := strings.TrimSpace(match[1])
269+
// Skip "What's Changed" as it's just a wrapper, not a real category
270+
if headerName == "What's Changed" {
271+
continue
272+
}
273+
// Save previous section if exists
274+
if currentSection != nil && len(currentSection.Changes) > 0 {
275+
sections = append(sections, *currentSection)
276+
}
277+
currentSection = &Section{Name: headerName}
278+
continue
279+
}
280+
281+
// Check for list item
252282
if strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(trimmed, "* ") {
253283
change := strings.TrimPrefix(trimmed, "- ")
254284
change = strings.TrimPrefix(change, "* ")
255285
if change != "" && !strings.HasPrefix(change, "@") {
256-
changes = append(changes, change)
286+
if currentSection != nil {
287+
currentSection.Changes = append(currentSection.Changes, change)
288+
} else {
289+
ungroupedChanges = append(ungroupedChanges, change)
290+
}
257291
}
258292
}
259293
}
260-
return changes
294+
295+
// Don't forget the last section
296+
if currentSection != nil && len(currentSection.Changes) > 0 {
297+
sections = append(sections, *currentSection)
298+
}
299+
300+
return sections, ungroupedChanges
261301
}
262302

263303
func parseMarkdownChangelog(content, versionPattern string) []ChangelogEntry {
@@ -332,6 +372,17 @@ func outputJSON(entry *ChangelogEntry) {
332372

333373
func outputMarkdown(entry *ChangelogEntry) {
334374
fmt.Printf("## %s\n\n", entry.Version)
375+
376+
// Output sectioned changes
377+
for _, section := range entry.Sections {
378+
fmt.Printf("### %s\n\n", section.Name)
379+
for _, change := range section.Changes {
380+
fmt.Printf("- %s\n", change)
381+
}
382+
fmt.Println()
383+
}
384+
385+
// Output ungrouped changes
335386
for _, change := range entry.Changes {
336387
fmt.Printf("- %s\n", change)
337388
}
@@ -340,6 +391,19 @@ func outputMarkdown(entry *ChangelogEntry) {
340391
func outputPlainText(displayName string, entry *ChangelogEntry) {
341392
fmt.Printf("%s %s\n", displayName, entry.Version)
342393
fmt.Println(strings.Repeat("-", 40))
394+
395+
// Output sectioned changes
396+
for _, section := range entry.Sections {
397+
fmt.Printf("\n[%s]\n", section.Name)
398+
for _, change := range section.Changes {
399+
fmt.Printf(" * %s\n", change)
400+
}
401+
}
402+
403+
// Output ungrouped changes
404+
if len(entry.Sections) > 0 && len(entry.Changes) > 0 {
405+
fmt.Println()
406+
}
343407
for _, change := range entry.Changes {
344408
fmt.Printf(" * %s\n", change)
345409
}

0 commit comments

Comments
 (0)