Skip to content
This repository was archived by the owner on Jan 14, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ A Neovim plugin extension for CodeCompanion that generates AI-powered Git commit
## ✨ Features

- 🤖 **AI Commit Generation** - Generate Conventional Commits compliant messages using CodeCompanion's LLM adapters
- 🛠️ **Git Tool Integration** - Execute Git operations through `@{git_read}` (15 read operations) and `@{git_edit}` (17 write operations) tools in chat
- 🛠️ **Git Tool Integration** - Execute Git operations through `@{git_read}` (16 read operations) and `@{git_edit}` (17 write operations) tools in chat
- 🤖 **Git Assistant** - Intelligent Git workflow assistance via `@{git_bot}` combining read/write operations
- 🌍 **Multi-language Support** - Generate commit messages in multiple languages
- 📝 **Smart Buffer Integration** - Auto-generate commit messages in gitcommit buffers with configurable keymaps
Expand Down Expand Up @@ -87,6 +87,8 @@ Use Git tools in CodeCompanion chat:
@{git_read} branch # List all branches
@{git_read} contributors --count 10 # Show top 10 contributors
@{git_read} tags # List all tags
@{git_read} generate_release_notes # Generate release notes between latest tags
@{git_read} generate_release_notes --from_tag "v1.0.0" --to_tag "v1.1.0" # Generate release notes between specific tags
@{git_read} gitignore_get # Get .gitignore content
@{git_read} gitignore_check --gitignore_file "file.txt" # Check if file is ignored
@{git_read} show --commit_hash "abc123" # Show commit details
Expand Down Expand Up @@ -149,6 +151,14 @@ Use a comprehensive Git assistant that combines read and write operations:
/gitcommit # Select commit and insert its content for reference
@{git_edit} commit --commit_message "feat: add new feature" # Commit
@{git_edit} push --remote "origin" --branch "main" # Push changes
@{git_read} generate_release_notes # Generate release notes between latest tags
```

**4. Generate Release Notes:**
```
@{git_read} generate_release_notes # Auto-detect latest and previous tag
@{git_read} generate_release_notes --from_tag "v1.0.0" --to_tag "v1.1.0" # Specific tags
@{git_read} generate_release_notes --release_format "json" # JSON format output
Comment thread
jinzhongjia marked this conversation as resolved.
```

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

-- Create and checkout branch
gitcommit.exports.git_tool.create_branch("feature/new-feature", true)

-- Generate release notes
local success, notes, user_msg, llm_msg = gitcommit.exports.git_tool.generate_release_notes("v1.0.0", "v1.1.0", "markdown")
if success then
print("Release notes:", notes)
end

-- Generate release notes between specific tags
local success, notes = gitcommit.exports.git_tool.generate_release_notes("v1.0.0", "v1.1.0", "markdown")
Comment thread
jinzhongjia marked this conversation as resolved.
Outdated
```

## 📚 Documentation
Expand Down
24 changes: 22 additions & 2 deletions doc/codecompanion-gitcommit.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Conventional Commits specification. It integrates seamlessly with
CodeCompanion's LLM adapters to analyze your staged changes and create
appropriate commit messages.

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

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

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

Release Notes Generation: *release_notes*

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.

Usage examples: >
@{git_read} generate_release_notes
@{git_read} generate_release_notes --from_tag "v1.0.0" --to_tag "v1.1.0"
@{git_read} generate_release_notes --release_format "plain"
@{git_read} generate_release_notes --release_format "json"
<

Features:
• Automatic tag detection (uses latest and second-latest if not specified)
• Conventional Commits categorization (features, fixes, other changes)
• Multiple output formats: markdown (default), plain, json
• Contributors listing with commit counts
• Chronological commit ordering

Safety features:
• Read-only operations require no confirmation
• Modifying operations require user confirmation
Expand Down
13 changes: 13 additions & 0 deletions lua/codecompanion/_extensions/gitcommit/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,19 @@ return {
local GitTool = require("codecompanion._extensions.gitcommit.tools.git").GitTool
return GitTool.merge(branch)
end,

---Generate release notes between tags
---@param from_tag? string Starting tag (if not provided, uses second latest tag)
---@param to_tag? string Ending tag (if not provided, uses latest tag)
---@param format? string Format for release notes (markdown, plain, json)
---@return boolean success
---@return string output
---@return string user_msg
---@return string llm_msg
generate_release_notes = function(from_tag, to_tag, format)
local GitTool = require("codecompanion._extensions.gitcommit.tools.git").GitTool
return GitTool.generate_release_notes(from_tag, to_tag, format)
end,
},
},
}
195 changes: 195 additions & 0 deletions lua/codecompanion/_extensions/gitcommit/tools/git.lua
Original file line number Diff line number Diff line change
Expand Up @@ -689,5 +689,200 @@ function GitTool.merge(branch)
end
end

--- Generate release notes between two tags
---@param from_tag string|nil Starting tag (if not provided, uses second latest tag)
---@param to_tag string|nil Ending tag (if not provided, uses latest tag)
---@param format string|nil Format for release notes (markdown, plain, json)
Comment thread
jinzhongjia marked this conversation as resolved.
Outdated
---@return boolean success
---@return string output
---@return string user_msg
---@return string llm_msg
function GitTool.generate_release_notes(from_tag, to_tag, format)
Comment thread
jinzhongjia marked this conversation as resolved.
format = format or "markdown"

-- Get all tags sorted by version
local success, tags_output = pcall(vim.fn.system, "git tag --sort=-version:refname")
if not success or vim.v.shell_error ~= 0 then
local msg = "Failed to get git tags: " .. (tags_output or "unknown error")
local user_msg = msg
local llm_msg = "<gitReleaseNotes>fail: " .. msg .. "</gitReleaseNotes>"
return false, msg, user_msg, llm_msg
end

local tags = {}
for tag in tags_output:gmatch("[^\r\n]+") do
if tag ~= "" then
table.insert(tags, tag)
end
end

if #tags < 1 then
local msg = "No tags found in repository"
local user_msg = msg
local llm_msg = "<gitReleaseNotes>fail: " .. msg .. "</gitReleaseNotes>"
return false, msg, user_msg, llm_msg
end

-- Determine tag range
if not to_tag then
to_tag = tags[1] -- Latest tag
end

if not from_tag then
if #tags < 2 then
local msg = "Cannot generate release notes: only one tag found. Please specify from_tag parameter."
local user_msg = msg
local llm_msg = "<gitReleaseNotes>fail: " .. msg .. "</gitReleaseNotes>"
return false, msg, user_msg, llm_msg
end
from_tag = tags[2] -- Second latest tag
end

-- Get commit range between tags
local range = from_tag .. ".." .. to_tag
local commit_cmd = "git log --pretty=format:'%h\x01%s\x01%an\x01%ad' --date=short " .. vim.fn.shellescape(range)
local success_commits, commits_output = pcall(vim.fn.system, commit_cmd)
Comment thread
jinzhongjia marked this conversation as resolved.
Outdated

if not success_commits or vim.v.shell_error ~= 0 then
local msg = "Failed to get commits between "
.. from_tag
.. " and "
.. to_tag
.. ": "
.. (commits_output or "unknown error")
local user_msg = msg
local llm_msg = "<gitReleaseNotes>fail: " .. msg .. "</gitReleaseNotes>"
return false, msg, user_msg, llm_msg
end

-- Parse commits
local commits = {}
for line in commits_output:gmatch("[^\r\n]+") do
local parts = vim.split(line, "\x01")
if #parts == 4 then
table.insert(commits, {
hash = parts[1],
subject = parts[2],
author = parts[3],
date = parts[4],
})
end
end
Comment thread
jinzhongjia marked this conversation as resolved.
Comment thread
jinzhongjia marked this conversation as resolved.

if #commits == 0 then
local msg = "No commits found between " .. from_tag .. " and " .. to_tag
local user_msg = msg
local llm_msg = "<gitReleaseNotes>success: " .. msg .. "</gitReleaseNotes>"
return true, msg, user_msg, llm_msg
end

-- Generate release notes based on format
local release_notes = ""
local user_msg = ""
local llm_msg = ""

if format == "markdown" then
local parts = { "# Release Notes: " .. from_tag .. " → " .. to_tag .. "\n\n" }
table.insert(parts, "## Changes (" .. #commits .. " commits)\n\n")

-- Group commits by type (conventional commits)
local features = {}
local fixes = {}
local others = {}

for _, commit in ipairs(commits) do
local type_match = commit.subject:match("^(%w+):")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current regex ^(%w+): for parsing the Conventional Commit type does not handle commits with scopes (e.g., feat(parser): ...). This will cause such commits to be miscategorized under 'Other Changes'. To correctly extract the type, the regex should be changed to ^(%w+) to capture the type part before any optional scope.

      local type_match = commit.subject:match("^(%w+)")

if type_match then
if type_match == "feat" or type_match == "feature" then
table.insert(features, commit)
elseif type_match == "fix" or type_match == "bugfix" then
table.insert(fixes, commit)
Comment thread
jinzhongjia marked this conversation as resolved.
Outdated
else
table.insert(others, commit)
end
else
table.insert(others, commit)
end
end

-- Add features
if #features > 0 then
table.insert(parts, "### ✨ New Features\n\n")
for _, commit in ipairs(features) do
table.insert(parts, "- " .. commit.subject .. " (" .. commit.hash .. ")\n")
end
table.insert(parts, "\n")
end

-- Add fixes
if #fixes > 0 then
table.insert(parts, "### 🐛 Bug Fixes\n\n")
for _, commit in ipairs(fixes) do
table.insert(parts, "- " .. commit.subject .. " (" .. commit.hash .. ")\n")
end
table.insert(parts, "\n")
end

-- Add other changes
if #others > 0 then
table.insert(parts, "### 📝 Other Changes\n\n")
for _, commit in ipairs(others) do
table.insert(parts, "- " .. commit.subject .. " (" .. commit.hash .. ")\n")
end
table.insert(parts, "\n")
end

-- Add contributors
local contributors = {}
for _, commit in ipairs(commits) do
if not contributors[commit.author] then
contributors[commit.author] = 0
end
contributors[commit.author] = contributors[commit.author] + 1
end
Comment thread
jinzhongjia marked this conversation as resolved.

table.insert(parts, "### 👥 Contributors\n\n")
local sorted_authors = {}
for author in pairs(contributors) do
table.insert(sorted_authors, author)
end
table.sort(sorted_authors, function(a, b)
if contributors[a] == contributors[b] then
return a < b
end
return contributors[a] > contributors[b]
end)
Comment thread
jinzhongjia marked this conversation as resolved.
for _, author in ipairs(sorted_authors) do
table.insert(parts, "- " .. author .. " (" .. contributors[author] .. " commits)\n")
end
Comment thread
jinzhongjia marked this conversation as resolved.
Outdated
Comment thread
jinzhongjia marked this conversation as resolved.
release_notes = table.concat(parts)
elseif format == "plain" then
local parts = { "Release Notes: " .. from_tag .. " → " .. to_tag .. "\n" }
table.insert(parts, "Changes (" .. #commits .. " commits):\n\n")
for _, commit in ipairs(commits) do
table.insert(parts, "- " .. commit.subject .. " (" .. commit.hash .. " by " .. commit.author .. ")\n")
Comment thread
jinzhongjia marked this conversation as resolved.
end
Comment thread
jinzhongjia marked this conversation as resolved.
release_notes = table.concat(parts)
elseif format == "json" then
local json_data = {
from_tag = from_tag,
to_tag = to_tag,
total_commits = #commits,
commits = commits,
}
release_notes = vim.fn.json_encode(json_data)
else
local msg = "Unsupported format: " .. format .. ". Supported formats: markdown, plain, json"
local user_msg = msg
local llm_msg = "<gitReleaseNotes>fail: " .. msg .. "</gitReleaseNotes>"
return false, msg, user_msg, llm_msg
end

user_msg = "Generated release notes for " .. from_tag .. " → " .. to_tag .. " (" .. #commits .. " commits)"
llm_msg = "<gitReleaseNotes>success: " .. user_msg .. "\n\n" .. release_notes .. "</gitReleaseNotes>"

return true, release_notes, user_msg, llm_msg
end

M.GitTool = GitTool
return M
21 changes: 19 additions & 2 deletions lua/codecompanion/_extensions/gitcommit/tools/git_read.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ GitRead.schema = {
"contributors",
"search_commits",
"tags",
"generate_release_notes",
"help",
"gitignore_get",
"gitignore_check",
Expand Down Expand Up @@ -86,6 +87,19 @@ GitRead.schema = {
type = "string",
description = "File to check if ignored",
},
from_tag = {
type = "string",
description = "Starting tag for release notes generation (if not provided, uses second latest tag)",
},
to_tag = {
type = "string",
description = "Ending tag for release notes generation (if not provided, uses latest tag)",
},
release_format = {
type = "string",
description = "Format for release notes (markdown, plain, json)",
default = "markdown",
},
},
additionalProperties = false,
},
Expand All @@ -111,7 +125,7 @@ Best practices:
• Avoid operations that modify repository state
• Ensure operation args match expected parameters

Available operations: status, log, diff, branch, remotes, show, blame, stash_list, diff_commits, contributors, search_commits, tags, gitignore_get, gitignore_check, help]]
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]]

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

if operation == "help" then
local help_text =
"\\\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 "
"\\\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• generate_release_notes: Generate release notes between tags\n• gitignore_get: Get .gitignore content\n• gitignore_check: Check if a file is ignored\n "
return { status = "success", data = help_text }
end

Expand Down Expand Up @@ -181,6 +195,9 @@ GitRead.cmds = {
success, output, user_msg, llm_msg = GitTool.search_commits(op_args.pattern, op_args.count)
elseif operation == "tags" then
success, output, user_msg, llm_msg = GitTool.get_tags()
elseif operation == "generate_release_notes" then
success, output, user_msg, llm_msg =
GitTool.generate_release_notes(op_args.from_tag, op_args.to_tag, op_args.release_format)
elseif operation == "gitignore_get" then
success, output, user_msg, llm_msg = GitTool.get_gitignore()
elseif operation == "gitignore_check" then
Expand Down