Skip to content
This repository was archived by the owner on Jan 14, 2026. It is now read-only.

Commit 3a020b8

Browse files
committed
feat(git): add generate_release_notes operation
- implement generate_release_notes for release notes generation between tags - support markdown, plain, and json output formats with Conventional Commits categorization - update documentation and usage examples for the new operation - expose generate_release_notes in git tool API and git_read schema
1 parent 1489047 commit 3a020b8

5 files changed

Lines changed: 254 additions & 5 deletions

File tree

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ A Neovim plugin extension for CodeCompanion that generates AI-powered Git commit
55
## ✨ Features
66

77
- 🤖 **AI Commit Generation** - Generate Conventional Commits compliant messages using CodeCompanion's LLM adapters
8-
- 🛠️ **Git Tool Integration** - Execute Git operations through `@{git_read}` (15 read operations) and `@{git_edit}` (17 write operations) tools in chat
8+
- 🛠️ **Git Tool Integration** - Execute Git operations through `@{git_read}` (16 read operations) and `@{git_edit}` (17 write operations) tools in chat
99
- 🤖 **Git Assistant** - Intelligent Git workflow assistance via `@{git_bot}` combining read/write operations
1010
- 🌍 **Multi-language Support** - Generate commit messages in multiple languages
1111
- 📝 **Smart Buffer Integration** - Auto-generate commit messages in gitcommit buffers with configurable keymaps
@@ -87,6 +87,8 @@ Use Git tools in CodeCompanion chat:
8787
@{git_read} branch # List all branches
8888
@{git_read} contributors --count 10 # Show top 10 contributors
8989
@{git_read} tags # List all tags
90+
@{git_read} generate_release_notes # Generate release notes between latest tags
91+
@{git_read} generate_release_notes --from_tag "v1.0.0" --to_tag "v1.1.0" # Generate release notes between specific tags
9092
@{git_read} gitignore_get # Get .gitignore content
9193
@{git_read} gitignore_check --gitignore_file "file.txt" # Check if file is ignored
9294
@{git_read} show --commit_hash "abc123" # Show commit details
@@ -149,6 +151,14 @@ Use a comprehensive Git assistant that combines read and write operations:
149151
/gitcommit # Select commit and insert its content for reference
150152
@{git_edit} commit --commit_message "feat: add new feature" # Commit
151153
@{git_edit} push --remote "origin" --branch "main" # Push changes
154+
@{git_read} generate_release_notes # Generate release notes between latest tags
155+
```
156+
157+
**4. Generate Release Notes:**
158+
```
159+
@{git_read} generate_release_notes # Auto-detect latest and previous tag
160+
@{git_read} generate_release_notes --from_tag "v1.0.0" --to_tag "v1.1.0" # Specific tags
161+
@{git_read} generate_release_notes --release_format "json" # JSON format output
152162
```
153163

154164
## ⚙️ Configuration Options
@@ -220,6 +230,15 @@ gitcommit.exports.git_tool.stage({"file1.txt", "file2.txt"})
220230

221231
-- Create and checkout branch
222232
gitcommit.exports.git_tool.create_branch("feature/new-feature", true)
233+
234+
-- Generate release notes
235+
local success, notes, user_msg, llm_msg = gitcommit.exports.git_tool.generate_release_notes()
236+
if success then
237+
print("Release notes:", notes)
238+
end
239+
240+
-- Generate release notes between specific tags
241+
local success, notes = gitcommit.exports.git_tool.generate_release_notes("v1.0.0", "v1.1.0", "markdown")
223242
```
224243

225244
## 📚 Documentation

doc/codecompanion-gitcommit.txt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Conventional Commits specification. It integrates seamlessly with
2828
CodeCompanion's LLM adapters to analyze your staged changes and create
2929
appropriate commit messages.
3030

31-
The extension provides comprehensive Git operations through @{git_read} (15
31+
The extension provides comprehensive Git operations through @{git_read} (16
3232
operations), @{git_edit} (17 operations), and @{git_bot} tools, offering a
3333
natural language interface for Git workflow management within CodeCompanion
3434
chat buffers.
@@ -45,7 +45,7 @@ chat buffers.
4545
• Commit history context for consistent styling and patterns
4646

4747
🛠️ Git Tool Integration
48-
• @{git_read} tool - 15 read-only Git operations (status, log, diff, etc.)
48+
• @{git_read} tool - 16 read-only Git operations (status, log, diff, release notes, etc.)
4949
• @{git_edit} tool - 17 write-access Git operations (stage, commit, push, etc.)
5050
• @{git_bot} tool - Comprehensive Git assistant combining all operations
5151
• Natural language interface for Git workflow control
@@ -140,6 +140,7 @@ Use Git tools in CodeCompanion chat: >
140140
/gitcommit # Generate commit message
141141
@{git_edit} commit --commit_message "feat: add feature" # Commit
142142
@{git_edit} push --remote "origin" --branch "main" # Push
143+
@{git_read} generate_release_notes # Generate release notes
143144
<
144145

145146
==============================================================================
@@ -168,6 +169,7 @@ Read-only operations (@{git_read}): *git_read*
168169
• contributors - Show contributors
169170
• search_commits - Search commit messages
170171
• tags - List all tags
172+
• generate_release_notes - Generate release notes between tags
171173
• gitignore_get - Get .gitignore content
172174
• gitignore_check - Check if a file is ignored
173175
• stash_list - List all stashes
@@ -203,6 +205,24 @@ Examples: >
203205
@{git_bot} Please help me create a new branch and commit current changes
204206
<
205207

208+
Release Notes Generation: *release_notes*
209+
210+
The generate_release_notes operation creates formatted release notes between two Git tags by analyzing commit messages. It supports multiple output formats and automatically categorizes commits using Conventional Commits specification.
211+
212+
Usage examples: >
213+
@{git_read} generate_release_notes
214+
@{git_read} generate_release_notes --from_tag "v1.0.0" --to_tag "v1.1.0"
215+
@{git_read} generate_release_notes --release_format "plain"
216+
@{git_read} generate_release_notes --release_format "json"
217+
<
218+
219+
Features:
220+
• Automatic tag detection (uses latest and second-latest if not specified)
221+
• Conventional Commits categorization (features, fixes, other changes)
222+
• Multiple output formats: markdown (default), plain, json
223+
• Contributors listing with commit counts
224+
• Chronological commit ordering
225+
206226
Safety features:
207227
• Read-only operations require no confirmation
208228
• Modifying operations require user confirmation

lua/codecompanion/_extensions/gitcommit/init.lua

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,19 @@ return {
481481
local GitTool = require("codecompanion._extensions.gitcommit.tools.git").GitTool
482482
return GitTool.merge(branch)
483483
end,
484+
485+
---Generate release notes between tags
486+
---@param from_tag? string Starting tag (if not provided, uses second latest tag)
487+
---@param to_tag? string Ending tag (if not provided, uses latest tag)
488+
---@param format? string Format for release notes (markdown, plain, json)
489+
---@return boolean success
490+
---@return string output
491+
---@return string user_msg
492+
---@return string llm_msg
493+
generate_release_notes = function(from_tag, to_tag, format)
494+
local GitTool = require("codecompanion._extensions.gitcommit.tools.git").GitTool
495+
return GitTool.generate_release_notes(from_tag, to_tag, format)
496+
end,
484497
},
485498
},
486499
}

lua/codecompanion/_extensions/gitcommit/tools/git.lua

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,5 +689,186 @@ function GitTool.merge(branch)
689689
end
690690
end
691691

692+
--- Generate release notes between two tags
693+
---@param from_tag string|nil Starting tag (if not provided, uses second latest tag)
694+
---@param to_tag string|nil Ending tag (if not provided, uses latest tag)
695+
---@param format string|nil Format for release notes (markdown, plain, json)
696+
---@return boolean success
697+
---@return string output
698+
---@return string user_msg
699+
---@return string llm_msg
700+
function GitTool.generate_release_notes(from_tag, to_tag, format)
701+
format = format or "markdown"
702+
703+
-- Get all tags sorted by version
704+
local success, tags_output = pcall(vim.fn.system, "git tag --sort=-version:refname")
705+
if not success or vim.v.shell_error ~= 0 then
706+
local msg = "Failed to get git tags: " .. (tags_output or "unknown error")
707+
local user_msg = msg
708+
local llm_msg = "<gitReleaseNotes>fail: " .. msg .. "</gitReleaseNotes>"
709+
return false, msg, user_msg, llm_msg
710+
end
711+
712+
local tags = {}
713+
for tag in tags_output:gmatch("[^\r\n]+") do
714+
if tag ~= "" then
715+
table.insert(tags, tag)
716+
end
717+
end
718+
719+
if #tags < 1 then
720+
local msg = "No tags found in repository"
721+
local user_msg = msg
722+
local llm_msg = "<gitReleaseNotes>fail: " .. msg .. "</gitReleaseNotes>"
723+
return false, msg, user_msg, llm_msg
724+
end
725+
726+
-- Determine tag range
727+
if not to_tag then
728+
to_tag = tags[1] -- Latest tag
729+
end
730+
731+
if not from_tag then
732+
if #tags < 2 then
733+
local msg = "Cannot generate release notes: only one tag found. Please specify from_tag parameter."
734+
local user_msg = msg
735+
local llm_msg = "<gitReleaseNotes>fail: " .. msg .. "</gitReleaseNotes>"
736+
return false, msg, user_msg, llm_msg
737+
end
738+
from_tag = tags[2] -- Second latest tag
739+
end
740+
741+
-- Get commit range between tags
742+
local range = from_tag .. ".." .. to_tag
743+
local commit_cmd = "git log --pretty=format:'%h|%s|%an|%ad' --date=short " .. range
744+
local success_commits, commits_output = pcall(vim.fn.system, commit_cmd)
745+
746+
if not success_commits or vim.v.shell_error ~= 0 then
747+
local msg = "Failed to get commits between " .. from_tag .. " and " .. to_tag .. ": " .. (commits_output or "unknown error")
748+
local user_msg = msg
749+
local llm_msg = "<gitReleaseNotes>fail: " .. msg .. "</gitReleaseNotes>"
750+
return false, msg, user_msg, llm_msg
751+
end
752+
753+
-- Parse commits
754+
local commits = {}
755+
for line in commits_output:gmatch("[^\r\n]+") do
756+
local hash, subject, author, date = line:match("([^|]+)|([^|]+)|([^|]+)|([^|]+)")
757+
if hash and subject then
758+
table.insert(commits, {
759+
hash = hash,
760+
subject = subject,
761+
author = author,
762+
date = date
763+
})
764+
end
765+
end
766+
767+
if #commits == 0 then
768+
local msg = "No commits found between " .. from_tag .. " and " .. to_tag
769+
local user_msg = msg
770+
local llm_msg = "<gitReleaseNotes>success: " .. msg .. "</gitReleaseNotes>"
771+
return true, msg, user_msg, llm_msg
772+
end
773+
774+
-- Generate release notes based on format
775+
local release_notes = ""
776+
local user_msg = ""
777+
local llm_msg = ""
778+
779+
if format == "markdown" then
780+
release_notes = "# Release Notes: " .. from_tag .. "" .. to_tag .. "\n\n"
781+
release_notes = release_notes .. "## Changes (" .. #commits .. " commits)\n\n"
782+
783+
-- Group commits by type (conventional commits)
784+
local features = {}
785+
local fixes = {}
786+
local others = {}
787+
788+
for _, commit in ipairs(commits) do
789+
local type_match = commit.subject:match("^(%w+):")
790+
if type_match then
791+
if type_match == "feat" or type_match == "feature" then
792+
table.insert(features, commit)
793+
elseif type_match == "fix" or type_match == "bugfix" then
794+
table.insert(fixes, commit)
795+
else
796+
table.insert(others, commit)
797+
end
798+
else
799+
table.insert(others, commit)
800+
end
801+
end
802+
803+
-- Add features
804+
if #features > 0 then
805+
release_notes = release_notes .. "### ✨ New Features\n\n"
806+
for _, commit in ipairs(features) do
807+
release_notes = release_notes .. "- " .. commit.subject .. " (" .. commit.hash .. ")\n"
808+
end
809+
release_notes = release_notes .. "\n"
810+
end
811+
812+
-- Add fixes
813+
if #fixes > 0 then
814+
release_notes = release_notes .. "### 🐛 Bug Fixes\n\n"
815+
for _, commit in ipairs(fixes) do
816+
release_notes = release_notes .. "- " .. commit.subject .. " (" .. commit.hash .. ")\n"
817+
end
818+
release_notes = release_notes .. "\n"
819+
end
820+
821+
-- Add other changes
822+
if #others > 0 then
823+
release_notes = release_notes .. "### 📝 Other Changes\n\n"
824+
for _, commit in ipairs(others) do
825+
release_notes = release_notes .. "- " .. commit.subject .. " (" .. commit.hash .. ")\n"
826+
end
827+
release_notes = release_notes .. "\n"
828+
end
829+
830+
-- Add contributors
831+
local contributors = {}
832+
for _, commit in ipairs(commits) do
833+
if not contributors[commit.author] then
834+
contributors[commit.author] = 0
835+
end
836+
contributors[commit.author] = contributors[commit.author] + 1
837+
end
838+
839+
release_notes = release_notes .. "### 👥 Contributors\n\n"
840+
for author, count in pairs(contributors) do
841+
release_notes = release_notes .. "- " .. author .. " (" .. count .. " commits)\n"
842+
end
843+
844+
elseif format == "plain" then
845+
release_notes = "Release Notes: " .. from_tag .. "" .. to_tag .. "\n"
846+
release_notes = release_notes .. "Changes (" .. #commits .. " commits):\n\n"
847+
for _, commit in ipairs(commits) do
848+
release_notes = release_notes .. "- " .. commit.subject .. " (" .. commit.hash .. " by " .. commit.author .. ")\n"
849+
end
850+
851+
elseif format == "json" then
852+
local json_data = {
853+
from_tag = from_tag,
854+
to_tag = to_tag,
855+
total_commits = #commits,
856+
commits = commits
857+
}
858+
release_notes = vim.fn.json_encode(json_data)
859+
860+
else
861+
local msg = "Unsupported format: " .. format .. ". Supported formats: markdown, plain, json"
862+
local user_msg = msg
863+
local llm_msg = "<gitReleaseNotes>fail: " .. msg .. "</gitReleaseNotes>"
864+
return false, msg, user_msg, llm_msg
865+
end
866+
867+
user_msg = "Generated release notes for " .. from_tag .. "" .. to_tag .. " (" .. #commits .. " commits)"
868+
llm_msg = "<gitReleaseNotes>success: " .. user_msg .. "\n\n" .. release_notes .. "</gitReleaseNotes>"
869+
870+
return true, release_notes, user_msg, llm_msg
871+
end
872+
692873
M.GitTool = GitTool
693874
return M

lua/codecompanion/_extensions/gitcommit/tools/git_read.lua

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ GitRead.schema = {
2929
"contributors",
3030
"search_commits",
3131
"tags",
32+
"generate_release_notes",
3233
"help",
3334
"gitignore_get",
3435
"gitignore_check",
@@ -86,6 +87,19 @@ GitRead.schema = {
8687
type = "string",
8788
description = "File to check if ignored",
8889
},
90+
from_tag = {
91+
type = "string",
92+
description = "Starting tag for release notes generation (if not provided, uses second latest tag)",
93+
},
94+
to_tag = {
95+
type = "string",
96+
description = "Ending tag for release notes generation (if not provided, uses latest tag)",
97+
},
98+
release_format = {
99+
type = "string",
100+
description = "Format for release notes (markdown, plain, json)",
101+
default = "markdown",
102+
},
89103
},
90104
additionalProperties = false,
91105
},
@@ -111,7 +125,7 @@ Best practices:
111125
• Avoid operations that modify repository state
112126
• Ensure operation args match expected parameters
113127
114-
Available operations: status, log, diff, branch, remotes, show, blame, stash_list, diff_commits, contributors, search_commits, tags, gitignore_get, gitignore_check, help]]
128+
Available operations: status, log, diff, branch, remotes, show, blame, stash_list, diff_commits, contributors, search_commits, tags, generate_release_notes, gitignore_get, gitignore_check, help]]
115129

116130
-- Helper function to validate required parameters
117131
local function validate_required_param(param_name, param_value, error_msg)
@@ -135,7 +149,7 @@ GitRead.cmds = {
135149

136150
if operation == "help" then
137151
local help_text =
138-
"\\\nAvailable read-only Git operations:\n• status: Show repository status\n• log: Show commit history\n• diff: Show file differences\n• branch: List branches\n• remotes: Show remote repositories\n• show: Show commit details\n• blame: Show file blame info\n• stash_list: List stashes\n• diff_commits: Compare commits\n• contributors: Show contributors\n• search_commits: Search commit messages\n• tags: List all tags\n• gitignore_get: Get .gitignore content\n• gitignore_check: Check if a file is ignored\n "
152+
"\\\nAvailable read-only Git operations:\n• status: Show repository status\n• log: Show commit history\n• diff: Show file differences\n• branch: List branches\n• remotes: Show remote repositories\n• show: Show commit details\n• blame: Show file blame info\n• stash_list: List stashes\n• diff_commits: Compare commits\n• contributors: Show contributors\n• search_commits: Search commit messages\n• tags: List all tags\ngenerate_release_notes: Generate release notes between tags\ngitignore_get: Get .gitignore content\n• gitignore_check: Check if a file is ignored\n "
139153
return { status = "success", data = help_text }
140154
end
141155

@@ -181,6 +195,8 @@ GitRead.cmds = {
181195
success, output, user_msg, llm_msg = GitTool.search_commits(op_args.pattern, op_args.count)
182196
elseif operation == "tags" then
183197
success, output, user_msg, llm_msg = GitTool.get_tags()
198+
elseif operation == "generate_release_notes" then
199+
success, output, user_msg, llm_msg = GitTool.generate_release_notes(op_args.from_tag, op_args.to_tag, op_args.release_format)
184200
elseif operation == "gitignore_get" then
185201
success, output, user_msg, llm_msg = GitTool.get_gitignore()
186202
elseif operation == "gitignore_check" then

0 commit comments

Comments
 (0)