11local M = {}
22
3+ --- @alias ReleaseNotesStyle " detailed" | " concise" | " changelog" | " marketing"
4+
5+ --- @class CommitInfo
6+ --- @field hash string Full commit hash
7+ --- @field subject string Commit subject line
8+ --- @field body string | nil Commit body (optional )
9+ --- @field author string | nil Author name (optional )
10+ --- @field type string | nil Commit type from subject (feat , fix , etc. )
11+
12+ --- @class CommitAnalysis
13+ --- @field features CommitInfo[]
14+ --- @field fixes CommitInfo[]
15+ --- @field breaking_changes CommitInfo[]
16+ --- @field performance CommitInfo[]
17+ --- @field documentation CommitInfo[]
18+ --- @field refactoring CommitInfo[]
19+ --- @field tests CommitInfo[]
20+ --- @field chore CommitInfo[]
21+ --- @field other CommitInfo[]
22+ --- @field contributors table<string , number> Author -> commit count
23+
324M .style_guides = {
425 detailed = [[ You are creating comprehensive release notes for developers.
526Write thorough explanations of changes with technical context.
@@ -37,7 +58,14 @@ WRITING GUIDELINES:
3758- Merge similar commits into single entries when appropriate
3859]]
3960
61+ --- @param commit CommitInfo Commit information to format
62+ --- @param include_hash boolean Whether to include commit hash
63+ --- @return string Formatted commit entry
4064local function format_commit (commit , include_hash )
65+ if not commit or not commit .subject then
66+ return " - (invalid commit data)"
67+ end
68+
4169 local parts = { " - " , commit .subject }
4270 if include_hash and commit .hash then
4371 table.insert (parts , string.format (" (%s)" , commit .hash :sub (1 , 7 )))
@@ -51,8 +79,12 @@ local function format_commit(commit, include_hash)
5179 return table.concat (parts )
5280end
5381
82+ --- @param name string Category name
83+ --- @param commits CommitInfo[] List of commits in this category
84+ --- @param include_hash boolean Whether to include commit hashes
85+ --- @return string | nil Formatted category section or nil if empty
5486local function format_category (name , commits , include_hash )
55- if # commits == 0 then
87+ if not commits or # commits == 0 then
5688 return nil
5789 end
5890 local lines = { string.format (" \n ### %s" , name ) }
@@ -62,7 +94,24 @@ local function format_category(name, commits, include_hash)
6294 return table.concat (lines , " \n " )
6395end
6496
97+ --- @param commits CommitInfo[] List of commits to analyze
98+ --- @return CommitAnalysis Analysis results with categorized commits and contributor counts
6599function M .analyze_commits (commits )
100+ if type (commits ) ~= " table" then
101+ return {
102+ features = {},
103+ fixes = {},
104+ breaking_changes = {},
105+ performance = {},
106+ documentation = {},
107+ refactoring = {},
108+ tests = {},
109+ chore = {},
110+ other = {},
111+ contributors = {},
112+ }
113+ end
114+
66115 local analysis = {
67116 features = {},
68117 fixes = {},
@@ -77,11 +126,18 @@ function M.analyze_commits(commits)
77126 }
78127
79128 for _ , commit in ipairs (commits ) do
80- analysis .contributors [commit .author ] = (analysis .contributors [commit .author ] or 0 ) + 1
129+ -- Track contributors (skip nil authors)
130+ if commit .author then
131+ analysis .contributors [commit .author ] = (analysis .contributors [commit .author ] or 0 ) + 1
132+ end
81133
82- local is_breaking = commit .subject :match (" ^%w+!:" ) -- feat!: xxx
83- or commit .subject :match (" ^%w+%b()!:" ) -- feat(scope)!: xxx
84- or commit .subject :upper ():match (" BREAKING" )
134+ -- Check for breaking changes (nil-safe)
135+ local is_breaking = false
136+ if commit .subject then
137+ is_breaking = commit .subject :match (" ^%w+!:" ) -- feat!: xxx
138+ or commit .subject :match (" ^%w+%b()!:" ) -- feat(scope)!: xxx
139+ or commit .subject :upper ():match (" BREAKING" )
140+ end
85141
86142 if is_breaking then
87143 table.insert (analysis .breaking_changes , commit )
@@ -107,7 +163,20 @@ function M.analyze_commits(commits)
107163 return analysis
108164end
109165
166+ --- @param commits CommitInfo[] List of commits to analyze
167+ --- @param style ReleaseNotesStyle Style of release notes to generate
168+ --- @param version_info table { from : string , to : string } Version range information
169+ --- @return string Generated prompt for AI release notes generation
110170function M .create_smart_prompt (commits , style , version_info )
171+ -- Input validation
172+ if type (commits ) ~= " table" then
173+ commits = {}
174+ end
175+
176+ if not version_info or type (version_info ) ~= " table" or not version_info .from or not version_info .to then
177+ version_info = { from = " unknown" , to = " unknown" }
178+ end
179+
111180 local analysis = M .analyze_commits (commits )
112181 local guide = M .style_guides [style ] or M .style_guides .detailed
113182
0 commit comments