Skip to content

Commit 539c4cd

Browse files
committed
feat(origin): add support for github issue lookup
for #2
1 parent fed2a7b commit 539c4cd

5 files changed

Lines changed: 209 additions & 53 deletions

File tree

changelog/changelog.go

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import (
44
"bufio"
55
"fmt"
66
"regexp"
7-
"slices"
7+
"strconv"
88
"strings"
99
"time"
1010

1111
"cl-parse/git"
12+
"cl-parse/origin"
1213
)
1314

1415
const (
@@ -18,23 +19,25 @@ const (
1819
)
1920

2021
type ChangelogEntry struct {
21-
Version string `json:"version" yaml:"version" toml:"version"`
22-
Date time.Time `json:"date" yaml:"date" toml:"date"`
22+
Version string `json:"version" yaml:"version" toml:"version"`
23+
Date time.Time `json:"date" yaml:"date" toml:"date"`
2324
CompareURL string `json:"compareUrl" yaml:"compareUrl" toml:"compareUrl"`
24-
Changes map[string][]Change `json:"changes" yaml:"changes" toml:"changes"`
25+
Changes map[string][]Change `json:"changes" yaml:"changes" toml:"changes"`
2526
}
2627

2728
type Change struct {
28-
Scope string `json:"scope,omitempty" yaml:"scope,omitempty" toml:"scope,omitempty"`
29-
Description string `json:"description" yaml:"description" toml:"description"`
30-
Commit string `json:"commit,omitempty" yaml:"commit,omitempty" toml:"commit,omitempty"`
31-
CommitBody string `json:"commitBody,omitempty" yaml:"commitBody,omitempty" toml:"commitBody,omitempty"`
32-
RelatedItems []string `json:"relatedItems,omitempty" yaml:"relatedItems,omitempty" toml:"relatedItems,omitempty"`
29+
Scope string `json:"scope,omitempty" yaml:"scope,omitempty" toml:"scope,omitempty"`
30+
Description string `json:"description" yaml:"description" toml:"description"`
31+
Commit string `json:"commit,omitempty" yaml:"commit,omitempty" toml:"commit,omitempty"`
32+
CommitBody string `json:"commitBody,omitempty" yaml:"commitBody,omitempty" toml:"commitBody,omitempty"`
33+
RelatedItems []*origin.Issue `json:"relatedItems,omitempty" yaml:"relatedItems,omitempty" toml:"relatedItems,omitempty"`
3334
}
3435

3536
type Parser struct {
36-
entries []ChangelogEntry
37-
IncludeBody bool
37+
entries []ChangelogEntry
38+
originUrl string
39+
IncludeBody bool
40+
FetchItemDetails bool
3841
}
3942

4043
func NewParser() *Parser {
@@ -63,10 +66,18 @@ func (p *Parser) Parse(content string) ([]ChangelogEntry, error) {
6366
scanner := bufio.NewScanner(strings.NewReader(content))
6467
var currentEntry *ChangelogEntry
6568
var currentSection string
69+
var err error
6670

6771
versionRegex := regexp.MustCompile(versionPattern)
6872
changeRegex := regexp.MustCompile(changePattern)
6973

74+
if p.FetchItemDetails {
75+
p.originUrl, err = git.GetOriginURL(".")
76+
if err != nil {
77+
return nil, fmt.Errorf("failed to get origin URL: %w", err)
78+
}
79+
}
80+
7081
for scanner.Scan() {
7182
line := strings.TrimSpace(scanner.Text())
7283

@@ -79,7 +90,6 @@ func (p *Parser) Parse(content string) ([]ChangelogEntry, error) {
7990
p.entries = append(p.entries, *currentEntry)
8091
}
8192

82-
var err error
8393
currentEntry, err = p.createNewEntry(matches)
8494
if err != nil {
8595
return nil, err
@@ -120,28 +130,40 @@ func (p *Parser) createNewEntry(matches []string) (*ChangelogEntry, error) {
120130
}, nil
121131
}
122132

123-
func (p *Parser) parseChange(line string, changeRegex *regexp.Regexp, currentSection string, currentEntry *ChangelogEntry) error {
133+
func (p *Parser) parseChange(
134+
line string,
135+
changeRegex *regexp.Regexp,
136+
currentSection string,
137+
currentEntry *ChangelogEntry,
138+
) error {
124139
matches := changeRegex.FindStringSubmatch(line)
125140
if matches == nil {
126141
return nil
127142
}
128143

144+
relatedItems, err := extractRelatedItems(matches[2], p.originUrl)
145+
if err != nil {
146+
return err
147+
}
148+
129149
change := Change{
130150
Scope: matches[1],
131151
Description: matches[2],
132-
RelatedItems: extractRelatedItems(matches[2]), // Extract from description
152+
RelatedItems: relatedItems,
133153
}
134154

135155
if matches[3] != "" {
136156
change.Commit = parseCommitHashFromLink(matches[3])
137157
if err := p.addCommitBody(&change); err != nil {
138158
return err
139159
}
140-
// Extract related items from commit body if available
141160
if change.CommitBody != "" {
142-
bodyItems := extractRelatedItems(change.CommitBody)
161+
bodyItems, err := extractRelatedItems(change.CommitBody, p.originUrl)
162+
if err != nil {
163+
return err
164+
}
143165
for _, item := range bodyItems {
144-
if !slices.Contains(change.RelatedItems, item) {
166+
if !containsIssue(change.RelatedItems, item) {
145167
change.RelatedItems = append(change.RelatedItems, item)
146168
}
147169
}
@@ -185,19 +207,37 @@ func parseCommitHashFromLink(link string) string {
185207
return ""
186208
}
187209

188-
func extractRelatedItems(text string) []string {
210+
func extractRelatedItems(text string, repoUrl string) ([]*origin.Issue, error) {
189211
regex := regexp.MustCompile(`#(\d+)`)
190212
matches := regex.FindAllStringSubmatch(text, -1)
191213

192214
seen := make(map[string]bool)
193-
var items []string
215+
var items []*origin.Issue
194216

195217
for _, match := range matches {
196218
if !seen[match[1]] {
197-
items = append(items, match[1])
219+
number, _ := strconv.Atoi(match[1])
220+
issue := &origin.Issue{
221+
Number: number,
222+
}
223+
if repoUrl != "" {
224+
if err := origin.GetIssueDetails(issue, repoUrl, match[1]); err != nil {
225+
return nil, fmt.Errorf("failed to get issue details for #%s: %w", match[1], err)
226+
}
227+
}
228+
items = append(items, issue)
198229
seen[match[1]] = true
199230
}
200231
}
201232

202-
return items
233+
return items, nil
234+
}
235+
236+
func containsIssue(items []*origin.Issue, item *origin.Issue) bool {
237+
for _, existing := range items {
238+
if existing.Number == item.Number {
239+
return true
240+
}
241+
}
242+
return false
203243
}

changelog/changelog_test.go

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ import (
44
"reflect"
55
"testing"
66
"time"
7+
8+
"cl-parse/origin"
79
)
810

911
// Test helpers
10-
func createTestEntry(version, date string, compareURL string, changes map[string][]Change) ChangelogEntry {
12+
func createTestEntry(
13+
version, date string,
14+
compareURL string,
15+
changes map[string][]Change,
16+
) ChangelogEntry {
1117
return ChangelogEntry{
1218
Version: version,
1319
Date: mustParseTime(date),
@@ -16,7 +22,12 @@ func createTestEntry(version, date string, compareURL string, changes map[string
1622
}
1723
}
1824

19-
func createTestChange(description string, scope string, commit string, relatedItems []string) Change {
25+
func createTestChange(
26+
description string,
27+
scope string,
28+
commit string,
29+
relatedItems []*origin.Issue,
30+
) Change {
2031
return Change{
2132
Description: description,
2233
Scope: scope,
@@ -48,15 +59,25 @@ func TestParse(t *testing.T) {
4859
* **ui**: fix button alignment
4960
`,
5061
want: []ChangelogEntry{
51-
createTestEntry("1.0.0", "2025-01-01", "https://github.com/user/repo/compare/v0.1.0...v1.0.0", map[string][]Change{
52-
"Features": {
53-
createTestChange("add new endpoint", "api", "", nil),
54-
createTestChange("basic feature", "", "1a196c09283903991da080552e3aa980ac64fec9", nil),
62+
createTestEntry(
63+
"1.0.0",
64+
"2025-01-01",
65+
"https://github.com/user/repo/compare/v0.1.0...v1.0.0",
66+
map[string][]Change{
67+
"Features": {
68+
createTestChange("add new endpoint", "api", "", nil),
69+
createTestChange(
70+
"basic feature",
71+
"",
72+
"1a196c09283903991da080552e3aa980ac64fec9",
73+
nil,
74+
),
75+
},
76+
"Bug Fixes": {
77+
createTestChange("fix button alignment", "ui", "", nil),
78+
},
5579
},
56-
"Bug Fixes": {
57-
createTestChange("fix button alignment", "ui", "", nil),
58-
},
59-
}),
80+
),
6081
},
6182
},
6283
{
@@ -69,11 +90,16 @@ func TestParse(t *testing.T) {
6990
* basic feature
7091
`,
7192
want: []ChangelogEntry{
72-
createTestEntry("1.0.0-alpha.1", "2025-01-01", "https://github.com/user/repo/compare/v0.1.0...v1.0.0-alpha.1", map[string][]Change{
73-
"Features": {
74-
createTestChange("basic feature", "", "", nil),
93+
createTestEntry(
94+
"1.0.0-alpha.1",
95+
"2025-01-01",
96+
"https://github.com/user/repo/compare/v0.1.0...v1.0.0-alpha.1",
97+
map[string][]Change{
98+
"Features": {
99+
createTestChange("basic feature", "", "", nil),
100+
},
75101
},
76-
}),
102+
),
77103
},
78104
},
79105
{
@@ -88,7 +114,12 @@ func TestParse(t *testing.T) {
88114
want: []ChangelogEntry{
89115
createTestEntry("1.0.0", "2025-01-01", "", map[string][]Change{
90116
"Features": {
91-
createTestChange("basic feature #456", "", "", []string{"456"}),
117+
createTestChange(
118+
"basic feature #456",
119+
"",
120+
"",
121+
[]*origin.Issue{{Number: 456}},
122+
),
92123
},
93124
}),
94125
},
@@ -105,13 +136,28 @@ func TestParse(t *testing.T) {
105136
* some docs ([docs link text](https://example.com/docs))
106137
`,
107138
want: []ChangelogEntry{
108-
createTestEntry("1.0.0", "2025-01-01", "https://github.com/user/repo/compare/v0.1.0...v1.0.0", map[string][]Change{
109-
"Features": {
110-
createTestChange("basic feature", "", "8f5b75c6ba6c525e29463e2a96fec119e426e283", nil),
111-
createTestChange("another feature", "", "22822a9f19442b51d952b550e73ad3c229583371", nil),
112-
createTestChange("some docs", "", "", nil),
139+
createTestEntry(
140+
"1.0.0",
141+
"2025-01-01",
142+
"https://github.com/user/repo/compare/v0.1.0...v1.0.0",
143+
map[string][]Change{
144+
"Features": {
145+
createTestChange(
146+
"basic feature",
147+
"",
148+
"8f5b75c6ba6c525e29463e2a96fec119e426e283",
149+
nil,
150+
),
151+
createTestChange(
152+
"another feature",
153+
"",
154+
"22822a9f19442b51d952b550e73ad3c229583371",
155+
nil,
156+
),
157+
createTestChange("some docs", "", "", nil),
158+
},
113159
},
114-
}),
160+
),
115161
},
116162
},
117163
}

cmd/cmd.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ import (
1717
const VERSION = "0.3.0" // x-release-please-version
1818

1919
type options struct {
20-
version bool
21-
latest bool
22-
release string
23-
includeBody bool
24-
format string
20+
version bool
21+
latest bool
22+
release string
23+
includeBody bool
24+
fetchItemDetails bool
25+
format string
2526
}
2627

2728
var cmd = &cobra.Command{
@@ -48,7 +49,8 @@ var cmd = &cobra.Command{
4849

4950
parser := changelog.NewParser()
5051
parser.IncludeBody = opts.includeBody
51-
if parser.IncludeBody && !git.IsGitRepo(".") {
52+
parser.FetchItemDetails = opts.fetchItemDetails
53+
if (parser.IncludeBody || parser.FetchItemDetails) && !git.IsGitRepo(".") {
5254
fmt.Println("Cannot fetch commits: Not a git repository")
5355
os.Exit(1)
5456
}
@@ -87,6 +89,8 @@ func init() {
8789
cmd.Flags().BoolP("latest", "l", false, "display the most recent version from the changelog")
8890
cmd.Flags().StringP("release", "r", "", "display the changelog entry for a specific release")
8991
cmd.Flags().Bool("include-body", false, "include the full commit body in changelog entry")
92+
cmd.Flags().
93+
Bool("fetch-item-details", false, "fetch details for related items (e.g. GitHub issues & PRs)")
9094
cmd.Flags().StringP("format", "f", "json", "output format (json, yaml, or toml)")
9195
}
9296

@@ -95,14 +99,16 @@ func getOptions(cmd *cobra.Command) options {
9599
latest, _ := cmd.Flags().GetBool("latest")
96100
release, _ := cmd.Flags().GetString("release")
97101
includeBody, _ := cmd.Flags().GetBool("include-body")
102+
fetchItemDetails, _ := cmd.Flags().GetBool("fetch-item-details")
98103
format, _ := cmd.Flags().GetString("format")
99104

100105
return options{
101-
version: version,
102-
latest: latest,
103-
release: release,
104-
includeBody: includeBody,
105-
format: format,
106+
version: version,
107+
latest: latest,
108+
release: release,
109+
includeBody: includeBody,
110+
fetchItemDetails: fetchItemDetails,
111+
format: format,
106112
}
107113
}
108114

git/git.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,18 @@ func IsValidSha(sha string) bool {
5454

5555
return true
5656
}
57+
58+
// GetOriginURL retrieves the origin URL for a git repository
59+
func GetOriginURL(path string) (string, error) {
60+
repo, err := git.PlainOpen(path)
61+
if err != nil {
62+
return "", fmt.Errorf("failed to open repository: %w", err)
63+
}
64+
65+
remote, err := repo.Remote("origin")
66+
if err != nil {
67+
return "", fmt.Errorf("failed to get remote: %w", err)
68+
}
69+
70+
return remote.Config().URLs[0], nil
71+
}

0 commit comments

Comments
 (0)