Skip to content

Commit

Permalink
feat(origin): add support for github issue lookup
Browse files Browse the repository at this point in the history
for #2
  • Loading branch information
scottmckendry committed Jan 13, 2025
1 parent fed2a7b commit 539c4cd
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 53 deletions.
82 changes: 61 additions & 21 deletions changelog/changelog.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import (
"bufio"
"fmt"
"regexp"
"slices"
"strconv"
"strings"
"time"

"cl-parse/git"
"cl-parse/origin"
)

const (
Expand All @@ -18,23 +19,25 @@ const (
)

type ChangelogEntry struct {
Version string `json:"version" yaml:"version" toml:"version"`
Date time.Time `json:"date" yaml:"date" toml:"date"`
Version string `json:"version" yaml:"version" toml:"version"`
Date time.Time `json:"date" yaml:"date" toml:"date"`
CompareURL string `json:"compareUrl" yaml:"compareUrl" toml:"compareUrl"`
Changes map[string][]Change `json:"changes" yaml:"changes" toml:"changes"`
Changes map[string][]Change `json:"changes" yaml:"changes" toml:"changes"`
}

type Change struct {
Scope string `json:"scope,omitempty" yaml:"scope,omitempty" toml:"scope,omitempty"`
Description string `json:"description" yaml:"description" toml:"description"`
Commit string `json:"commit,omitempty" yaml:"commit,omitempty" toml:"commit,omitempty"`
CommitBody string `json:"commitBody,omitempty" yaml:"commitBody,omitempty" toml:"commitBody,omitempty"`
RelatedItems []string `json:"relatedItems,omitempty" yaml:"relatedItems,omitempty" toml:"relatedItems,omitempty"`
Scope string `json:"scope,omitempty" yaml:"scope,omitempty" toml:"scope,omitempty"`
Description string `json:"description" yaml:"description" toml:"description"`
Commit string `json:"commit,omitempty" yaml:"commit,omitempty" toml:"commit,omitempty"`
CommitBody string `json:"commitBody,omitempty" yaml:"commitBody,omitempty" toml:"commitBody,omitempty"`
RelatedItems []*origin.Issue `json:"relatedItems,omitempty" yaml:"relatedItems,omitempty" toml:"relatedItems,omitempty"`
}

type Parser struct {
entries []ChangelogEntry
IncludeBody bool
entries []ChangelogEntry
originUrl string
IncludeBody bool
FetchItemDetails bool
}

func NewParser() *Parser {
Expand Down Expand Up @@ -63,10 +66,18 @@ func (p *Parser) Parse(content string) ([]ChangelogEntry, error) {
scanner := bufio.NewScanner(strings.NewReader(content))
var currentEntry *ChangelogEntry
var currentSection string
var err error

versionRegex := regexp.MustCompile(versionPattern)
changeRegex := regexp.MustCompile(changePattern)

if p.FetchItemDetails {
p.originUrl, err = git.GetOriginURL(".")
if err != nil {
return nil, fmt.Errorf("failed to get origin URL: %w", err)
}
}

for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())

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

var err error
currentEntry, err = p.createNewEntry(matches)
if err != nil {
return nil, err
Expand Down Expand Up @@ -120,28 +130,40 @@ func (p *Parser) createNewEntry(matches []string) (*ChangelogEntry, error) {
}, nil
}

func (p *Parser) parseChange(line string, changeRegex *regexp.Regexp, currentSection string, currentEntry *ChangelogEntry) error {
func (p *Parser) parseChange(
line string,
changeRegex *regexp.Regexp,
currentSection string,
currentEntry *ChangelogEntry,
) error {
matches := changeRegex.FindStringSubmatch(line)
if matches == nil {
return nil
}

relatedItems, err := extractRelatedItems(matches[2], p.originUrl)
if err != nil {
return err
}

change := Change{
Scope: matches[1],
Description: matches[2],
RelatedItems: extractRelatedItems(matches[2]), // Extract from description
RelatedItems: relatedItems,
}

if matches[3] != "" {
change.Commit = parseCommitHashFromLink(matches[3])
if err := p.addCommitBody(&change); err != nil {
return err
}
// Extract related items from commit body if available
if change.CommitBody != "" {
bodyItems := extractRelatedItems(change.CommitBody)
bodyItems, err := extractRelatedItems(change.CommitBody, p.originUrl)
if err != nil {
return err
}
for _, item := range bodyItems {
if !slices.Contains(change.RelatedItems, item) {
if !containsIssue(change.RelatedItems, item) {
change.RelatedItems = append(change.RelatedItems, item)
}
}
Expand Down Expand Up @@ -185,19 +207,37 @@ func parseCommitHashFromLink(link string) string {
return ""
}

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

seen := make(map[string]bool)
var items []string
var items []*origin.Issue

for _, match := range matches {
if !seen[match[1]] {
items = append(items, match[1])
number, _ := strconv.Atoi(match[1])
issue := &origin.Issue{
Number: number,
}
if repoUrl != "" {
if err := origin.GetIssueDetails(issue, repoUrl, match[1]); err != nil {
return nil, fmt.Errorf("failed to get issue details for #%s: %w", match[1], err)
}
}
items = append(items, issue)
seen[match[1]] = true
}
}

return items
return items, nil
}

func containsIssue(items []*origin.Issue, item *origin.Issue) bool {
for _, existing := range items {
if existing.Number == item.Number {
return true
}
}
return false
}
88 changes: 67 additions & 21 deletions changelog/changelog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ import (
"reflect"
"testing"
"time"

"cl-parse/origin"
)

// Test helpers
func createTestEntry(version, date string, compareURL string, changes map[string][]Change) ChangelogEntry {
func createTestEntry(
version, date string,
compareURL string,
changes map[string][]Change,
) ChangelogEntry {
return ChangelogEntry{
Version: version,
Date: mustParseTime(date),
Expand All @@ -16,7 +22,12 @@ func createTestEntry(version, date string, compareURL string, changes map[string
}
}

func createTestChange(description string, scope string, commit string, relatedItems []string) Change {
func createTestChange(
description string,
scope string,
commit string,
relatedItems []*origin.Issue,
) Change {
return Change{
Description: description,
Scope: scope,
Expand Down Expand Up @@ -48,15 +59,25 @@ func TestParse(t *testing.T) {
* **ui**: fix button alignment
`,
want: []ChangelogEntry{
createTestEntry("1.0.0", "2025-01-01", "https://github.com/user/repo/compare/v0.1.0...v1.0.0", map[string][]Change{
"Features": {
createTestChange("add new endpoint", "api", "", nil),
createTestChange("basic feature", "", "1a196c09283903991da080552e3aa980ac64fec9", nil),
createTestEntry(
"1.0.0",
"2025-01-01",
"https://github.com/user/repo/compare/v0.1.0...v1.0.0",
map[string][]Change{
"Features": {
createTestChange("add new endpoint", "api", "", nil),
createTestChange(
"basic feature",
"",
"1a196c09283903991da080552e3aa980ac64fec9",
nil,
),
},
"Bug Fixes": {
createTestChange("fix button alignment", "ui", "", nil),
},
},
"Bug Fixes": {
createTestChange("fix button alignment", "ui", "", nil),
},
}),
),
},
},
{
Expand All @@ -69,11 +90,16 @@ func TestParse(t *testing.T) {
* basic feature
`,
want: []ChangelogEntry{
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{
"Features": {
createTestChange("basic feature", "", "", nil),
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{
"Features": {
createTestChange("basic feature", "", "", nil),
},
},
}),
),
},
},
{
Expand All @@ -88,7 +114,12 @@ func TestParse(t *testing.T) {
want: []ChangelogEntry{
createTestEntry("1.0.0", "2025-01-01", "", map[string][]Change{
"Features": {
createTestChange("basic feature #456", "", "", []string{"456"}),
createTestChange(
"basic feature #456",
"",
"",
[]*origin.Issue{{Number: 456}},
),
},
}),
},
Expand All @@ -105,13 +136,28 @@ func TestParse(t *testing.T) {
* some docs ([docs link text](https://example.com/docs))
`,
want: []ChangelogEntry{
createTestEntry("1.0.0", "2025-01-01", "https://github.com/user/repo/compare/v0.1.0...v1.0.0", map[string][]Change{
"Features": {
createTestChange("basic feature", "", "8f5b75c6ba6c525e29463e2a96fec119e426e283", nil),
createTestChange("another feature", "", "22822a9f19442b51d952b550e73ad3c229583371", nil),
createTestChange("some docs", "", "", nil),
createTestEntry(
"1.0.0",
"2025-01-01",
"https://github.com/user/repo/compare/v0.1.0...v1.0.0",
map[string][]Change{
"Features": {
createTestChange(
"basic feature",
"",
"8f5b75c6ba6c525e29463e2a96fec119e426e283",
nil,
),
createTestChange(
"another feature",
"",
"22822a9f19442b51d952b550e73ad3c229583371",
nil,
),
createTestChange("some docs", "", "", nil),
},
},
}),
),
},
},
}
Expand Down
28 changes: 17 additions & 11 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import (
const VERSION = "0.3.0" // x-release-please-version

type options struct {
version bool
latest bool
release string
includeBody bool
format string
version bool
latest bool
release string
includeBody bool
fetchItemDetails bool
format string
}

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

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

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

return options{
version: version,
latest: latest,
release: release,
includeBody: includeBody,
format: format,
version: version,
latest: latest,
release: release,
includeBody: includeBody,
fetchItemDetails: fetchItemDetails,
format: format,
}
}

Expand Down
15 changes: 15 additions & 0 deletions git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,18 @@ func IsValidSha(sha string) bool {

return true
}

// GetOriginURL retrieves the origin URL for a git repository
func GetOriginURL(path string) (string, error) {
repo, err := git.PlainOpen(path)
if err != nil {
return "", fmt.Errorf("failed to open repository: %w", err)
}

remote, err := repo.Remote("origin")
if err != nil {
return "", fmt.Errorf("failed to get remote: %w", err)
}

return remote.Config().URLs[0], nil
}
Loading

0 comments on commit 539c4cd

Please sign in to comment.