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 all 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
25 changes: 22 additions & 3 deletions 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 All @@ -103,7 +105,7 @@ Use Git tools in CodeCompanion chat:
```
@{git_edit} stage --files ["src/main.lua", "README.md"]
@{git_edit} unstage --files ["src/main.lua"]
@{git_edit} commit --commit_message "feat: add new feature"
@{git_edit} commit --commit_message "feat(api): add new feature"
@{git_edit} commit # Auto-generate AI commit message
@{git_edit} create_branch --branch_name "feature/new-ui" --checkout true
@{git_edit} checkout --target "main"
Expand Down Expand Up @@ -147,8 +149,16 @@ Use a comprehensive Git assistant that combines read and write operations:
@{git_read} status # Check repository status
@{git_edit} stage --files ["file1.txt", "file2.txt"] # Stage files
/gitcommit # Select commit and insert its content for reference
@{git_edit} commit --commit_message "feat: add new feature" # Commit
@{git_edit} commit --commit_message "feat(api): 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 between specific tags (with all parameters)
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 (auto-detect latest two tags)
local success, notes = gitcommit.exports.git_tool.generate_release_notes()
```

## 📚 Documentation
Expand Down
30 changes: 26 additions & 4 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 @@ -138,8 +138,9 @@ Use Git tools in CodeCompanion chat: >
@{git_read} status # Check repository status
@{git_edit} stage --files ["file.txt"] # Stage files
/gitcommit # Generate commit message
@{git_edit} commit --commit_message "feat: add feature" # Commit
@{git_edit} commit --commit_message "feat(api): 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 @@ -199,10 +201,30 @@ Examples: >
@{git_read} log --count 5
@{git_edit} stage --files ["src/main.lua"]
@{git_edit} create_branch --branch_name "feature/new"
@{git_edit} commit --commit_message "feat: add new feature"
@{git_edit} commit --commit_message "feat(api): add new feature"
@{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)
• Enhanced Conventional Commits support (supports scope format like "feat(api):")
• Smart commit categorization (features, fixes, other changes)
• Multiple output formats: markdown (default), plain, json
• Contributors listing with commit counts (sorted by contribution)
• Secure parameter handling with proper escaping
• Performance optimized string operations

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,
},
},
}
202 changes: 202 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,207 @@ 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 (markdown, plain, json)
---@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 escaped_range = vim.fn.shellescape(range)
if not escaped_range or escaped_range == "" then
local msg = "Failed to escape tag range: " .. range
local user_msg = msg
local llm_msg = "<gitReleaseNotes>fail: " .. msg .. "</gitReleaseNotes>"
return false, msg, user_msg, llm_msg
end
local commit_cmd = "git log --pretty=format:'%h\x01%s\x01%an\x01%ad' --date=short " .. escaped_range
local success_commits, commits_output = pcall(vim.fn.system, commit_cmd)

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

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+)%(.*%):") or commit.subject:match("^(%w+):")
if type_match then
if type_match == "feat" then
table.insert(features, commit)
elseif type_match == "fix" then
table.insert(fixes, commit)
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.
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
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
Loading