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
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ opts = {
adapter = "openai", -- LLM adapter
model = "gpt-4", -- Model name
languages = { "English", "Chinese", "Japanese", "French" }, -- Supported languages list
prompt_template = nil, -- Custom prompt template (see below)
exclude_files = { -- Excluded file patterns
"*.pb.go", "*.min.js", "*.min.css",
"package-lock.json", "yarn.lock", "*.log",
Expand Down Expand Up @@ -209,6 +210,36 @@ opts = {

</details>

### Custom Prompt Template

You can customize the commit message generation prompt using the `prompt_template` option. The template supports the following placeholders:

| Placeholder | Description |
|-------------|-------------|
| `%{language}` | Target language for the commit message |
| `%{diff}` | Git diff content |
| `%{history_context}` | Recent commit history (if enabled) |

**Example:**

```lua
opts = {
prompt_template = [[Generate a commit message for this diff.
Language: %{language}

Rules:
1. Use conventional commits format
2. Be concise

Diff:
%{diff}

%{history_context}]],
}
```

To see the default template, check `lua/codecompanion/_extensions/gitcommit/config.lua`.

## 🔌 Programmatic API

The extension provides a comprehensive API for external integrations:
Expand Down
17 changes: 17 additions & 0 deletions doc/codecompanion-gitcommit.txt
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,23 @@ Marketing Style:
The specific model to use. If not specified, defaults to the model
configured for CodeCompanion's chat strategy.

*prompt_template* Type: string
Custom prompt template for commit message generation. When specified,
replaces the default prompt. Supports the following placeholders:
• %{language} - Target language for the commit message
• %{diff} - Git diff content
• %{history_context} - Recent commit history (if enabled)

Example: >
prompt_template = [[Generate a commit message.
Language: %{language}
Diff:
%{diff}
%{history_context}]]
<
See lua/codecompanion/_extensions/gitcommit/config.lua for the default
template.

*languages* Type: table
A list of languages for generating commit messages. When specified,
the extension will prompt you to select a language before generating.
Expand Down
30 changes: 30 additions & 0 deletions lua/codecompanion/_extensions/gitcommit/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,36 @@ M.default_opts = {
adapter = nil, -- Inherit from global config
model = nil, -- Inherit from global config
languages = { "English", "Chinese", "Japanese", "French" },
prompt_template = [[You are a commit message generator. Produce exactly ONE Conventional Commit message for the provided git diff.

FORMAT:
type(scope): concise, imperative description of WHAT changed

Optional body (only if needed for non-obvious changes)

Allowed types: feat, fix, docs, style, refactor, perf, test, chore
Language: %{language} (type/scope stay in English)

CRITICAL RULES:
1. Output ONLY the commit message; no markdown, no quotes, no extra text
2. Subject is imperative, present tense, no trailing period
3. Be specific about WHAT changed; avoid WHY or impact
4. Avoid vague verbs: "update", "improve", "clarify", "adjust", "enhance", "fix issues"
Prefer concrete verbs: "add", "remove", "rename", "move", "replace", "extract", "inline"
5. Scope is optional; include only if clearly implied by the diff
6. Subject <= 50 chars; body lines <= 72 chars
7. Add body only when the subject alone is not enough
8. If the diff introduces a breaking change, mark with "!" and add "BREAKING CHANGE:" in body
9. Do not invent issue references, ticket IDs, or files not in the diff
10. If the diff includes multiple unrelated changes, pick the single most important one
11. When body is present, reference concrete entities from the diff (module, file, function, setting)

DIFF (source of truth):
```diff
%{diff}
```
END DIFF
%{history_context}]],
exclude_files = {
"*.pb.go",
"*.min.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ local fmt = string.format
M.schema = {
adapter = { "string", "nil" },
model = { "string", "nil" },
prompt_template = { "string", "nil" },
languages = { type = "array", items = "string" },
exclude_files = { type = "array", items = "string" },
buffer = {
Expand Down
8 changes: 6 additions & 2 deletions lua/codecompanion/_extensions/gitcommit/generator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ local Generator = {}
local _adapter_name = nil
--- @type string? Model name
local _model_name = nil
--- @type string? Prompt template
local _prompt_template = nil

local CONSTANTS = {
STATUS_ERROR = "error",
Expand All @@ -17,9 +19,11 @@ local CONSTANTS = {

--- @param adapter string? The adapter to use for generation
--- @param model string? The model of the adapter to use for generation
function Generator.setup(adapter, model)
--- @param prompt_template string? Custom prompt template
function Generator.setup(adapter, model, prompt_template)
_adapter_name = adapter
_model_name = model
_prompt_template = prompt_template
end

---Create a client for both HTTP and ACP adapters
Expand Down Expand Up @@ -226,7 +230,7 @@ end
---@param diff string The git diff to include in prompt
---@param commit_history? string[] Recent commit messages for context (optional)
function Generator._create_prompt(diff, lang, commit_history)
return git_utils.build_commit_prompt(diff, lang, commit_history)
return git_utils.build_commit_prompt(diff, lang, commit_history, _prompt_template)
end

return Generator
56 changes: 20 additions & 36 deletions lua/codecompanion/_extensions/gitcommit/git_utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,9 @@ end
---@param diff string The git diff content
---@param lang string The target language for the commit message
---@param commit_history? string[] Recent commit messages for context
---@param prompt_template? string Custom prompt template with placeholders
---@return string prompt The formatted prompt
function M.build_commit_prompt(diff, lang, commit_history)
function M.build_commit_prompt(diff, lang, commit_history, prompt_template)
local history_context = ""
if commit_history and #commit_history > 0 then
history_context = "BEGIN HISTORY (style reference only):\n"
Expand All @@ -338,41 +339,24 @@ function M.build_commit_prompt(diff, lang, commit_history)
.. "END HISTORY\nStyle reference only. Do not copy content or topics; base the message ONLY on the diff.\n"
end

return string.format(
[[You are a commit message generator. Produce exactly ONE Conventional Commit message for the provided git diff.

FORMAT:
type(scope): concise, imperative description of WHAT changed

Optional body (only if needed for non-obvious changes)

Allowed types: feat, fix, docs, style, refactor, perf, test, chore
Language: %s (type/scope stay in English)

CRITICAL RULES:
1. Output ONLY the commit message; no markdown, no quotes, no extra text
2. Subject is imperative, present tense, no trailing period
3. Be specific about WHAT changed; avoid WHY or impact
4. Avoid vague verbs: "update", "improve", "clarify", "adjust", "enhance", "fix issues"
Prefer concrete verbs: "add", "remove", "rename", "move", "replace", "extract", "inline"
5. Scope is optional; include only if clearly implied by the diff
6. Subject <= 50 chars; body lines <= 72 chars
7. Add body only when the subject alone is not enough
8. If the diff introduces a breaking change, mark with "!" and add "BREAKING CHANGE:" in body
9. Do not invent issue references, ticket IDs, or files not in the diff
10. If the diff includes multiple unrelated changes, pick the single most important one
11. When body is present, reference concrete entities from the diff (module, file, function, setting)

DIFF (source of truth):
```diff
%s
```
END DIFF
%s]],
lang or "English",
diff,
history_context
)
local template = prompt_template
if template == nil or template == "" then
local Config = require("codecompanion._extensions.gitcommit.config")
template = Config.default_opts.prompt_template
end

local prompt = template
prompt = prompt:gsub("%%{language}", function()
return lang or "English"
end)
prompt = prompt:gsub("%%{diff}", function()
return diff
end)
prompt = prompt:gsub("%%{history_context}", function()
return history_context
end)

return prompt
end

---Parse git conflict markers from file content
Expand Down
2 changes: 1 addition & 1 deletion lua/codecompanion/_extensions/gitcommit/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ return
use_commit_history = opts.use_commit_history,
commit_history_count = opts.commit_history_count,
})
Generator.setup(opts.adapter, opts.model)
Generator.setup(opts.adapter, opts.model, opts.prompt_template)
Buffer.setup(opts.buffer)
Langs.setup(opts.languages)

Expand Down
94 changes: 94 additions & 0 deletions tests/test_git_utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -953,4 +953,98 @@ T["conflict_markers"]["parses conflict blocks"] = function()
h.expect_match(">>>>>>>", result.block)
end

T["build_commit_prompt"] = new_set()

T["build_commit_prompt"]["uses default template when no custom template"] = function()
local result = child.lua([[
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
local prompt = GitUtils.build_commit_prompt("test diff", "English", nil, nil)
return prompt:find("commit message generator") ~= nil
]])
h.eq(true, result)
end

T["build_commit_prompt"]["uses default template when empty template"] = function()
local result = child.lua([[
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
local prompt = GitUtils.build_commit_prompt("test diff", "English", nil, "")
return prompt:find("commit message generator") ~= nil
]])
h.eq(true, result)
end

T["build_commit_prompt"]["replaces language placeholder"] = function()
local result = child.lua([[
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
local template = "Generate in %{language}"
local prompt = GitUtils.build_commit_prompt("diff", "Chinese", nil, template)
return prompt == "Generate in Chinese"
]])
h.eq(true, result)
end

T["build_commit_prompt"]["replaces diff placeholder"] = function()
local result = child.lua([[
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
local template = "Diff: %{diff}"
local prompt = GitUtils.build_commit_prompt("my changes", "English", nil, template)
return prompt == "Diff: my changes"
]])
h.eq(true, result)
end

T["build_commit_prompt"]["replaces history_context placeholder"] = function()
local result = child.lua([[
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
local template = "History: %{history_context}"
local prompt = GitUtils.build_commit_prompt("diff", "English", {"commit 1", "commit 2"}, template)
return prompt:find("BEGIN HISTORY") ~= nil and prompt:find("commit 1") ~= nil
]])
h.eq(true, result)
end

T["build_commit_prompt"]["handles empty history_context"] = function()
local result = child.lua([[
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
local template = "History: [%{history_context}]"
local prompt = GitUtils.build_commit_prompt("diff", "English", nil, template)
return prompt == "History: []"
]])
h.eq(true, result)
end

T["build_commit_prompt"]["replaces all placeholders in custom template"] = function()
local result = child.lua([[
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
local template = "Lang: %{language}, Diff: %{diff}, Ctx: %{history_context}"
local prompt = GitUtils.build_commit_prompt("my diff", "Japanese", {"hist1"}, template)
local has_lang = prompt:find("Lang: Japanese") ~= nil
local has_diff = prompt:find("Diff: my diff") ~= nil
local has_ctx = prompt:find("Ctx: BEGIN HISTORY") ~= nil
return has_lang and has_diff and has_ctx
]])
h.eq(true, result)
end

T["build_commit_prompt"]["handles diff with special characters"] = function()
local result = child.lua([[
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
local template = "Diff: %{diff}"
local diff_content = "+hello %{language} world"
local prompt = GitUtils.build_commit_prompt(diff_content, "English", nil, template)
return prompt:find("%%{language}") ~= nil
]])
h.eq(true, result)
end

T["build_commit_prompt"]["defaults language to English"] = function()
local result = child.lua([[
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
local template = "Lang: %{language}"
local prompt = GitUtils.build_commit_prompt("diff", nil, nil, template)
return prompt == "Lang: English"
]])
h.eq(true, result)
end

return T