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

Commit 56f4c53

Browse files
committed
feat(gitcommit): support custom prompt templates
Add the prompt_template setting to allow customization of the prompt. Supports %{language}, %{diff}, and %{history_context} placeholders. Move the default prompt from git_utils.lua to config.lua.
1 parent 7ee9f27 commit 56f4c53

8 files changed

Lines changed: 198 additions & 39 deletions

File tree

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ opts = {
180180
adapter = "openai", -- LLM adapter
181181
model = "gpt-4", -- Model name
182182
languages = { "English", "Chinese", "Japanese", "French" }, -- Supported languages list
183+
prompt_template = nil, -- Custom prompt template (see below)
183184
exclude_files = { -- Excluded file patterns
184185
"*.pb.go", "*.min.js", "*.min.css",
185186
"package-lock.json", "yarn.lock", "*.log",
@@ -209,6 +210,36 @@ opts = {
209210

210211
</details>
211212

213+
### Custom Prompt Template
214+
215+
You can customize the commit message generation prompt using the `prompt_template` option. The template supports the following placeholders:
216+
217+
| Placeholder | Description |
218+
|-------------|-------------|
219+
| `%{language}` | Target language for the commit message |
220+
| `%{diff}` | Git diff content |
221+
| `%{history_context}` | Recent commit history (if enabled) |
222+
223+
**Example:**
224+
225+
```lua
226+
opts = {
227+
prompt_template = [[Generate a commit message for this diff.
228+
Language: %{language}
229+
230+
Rules:
231+
1. Use conventional commits format
232+
2. Be concise
233+
234+
Diff:
235+
%{diff}
236+
237+
%{history_context}]],
238+
}
239+
```
240+
241+
To see the default template, check `lua/codecompanion/_extensions/gitcommit/config.lua`.
242+
212243
## 🔌 Programmatic API
213244

214245
The extension provides a comprehensive API for external integrations:

doc/codecompanion-gitcommit.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,23 @@ Marketing Style:
338338
The specific model to use. If not specified, defaults to the model
339339
configured for CodeCompanion's chat strategy.
340340

341+
*prompt_template* Type: string
342+
Custom prompt template for commit message generation. When specified,
343+
replaces the default prompt. Supports the following placeholders:
344+
• %{language} - Target language for the commit message
345+
• %{diff} - Git diff content
346+
• %{history_context} - Recent commit history (if enabled)
347+
348+
Example: >
349+
prompt_template = [[Generate a commit message.
350+
Language: %{language}
351+
Diff:
352+
%{diff}
353+
%{history_context}]]
354+
<
355+
See lua/codecompanion/_extensions/gitcommit/config.lua for the default
356+
template.
357+
341358
*languages* Type: table
342359
A list of languages for generating commit messages. When specified,
343360
the extension will prompt you to select a language before generating.

lua/codecompanion/_extensions/gitcommit/config.lua

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@ M.default_opts = {
55
adapter = nil, -- Inherit from global config
66
model = nil, -- Inherit from global config
77
languages = { "English", "Chinese", "Japanese", "French" },
8+
prompt_template = [[You are a commit message generator. Produce exactly ONE Conventional Commit message for the provided git diff.
9+
10+
FORMAT:
11+
type(scope): concise, imperative description of WHAT changed
12+
13+
Optional body (only if needed for non-obvious changes)
14+
15+
Allowed types: feat, fix, docs, style, refactor, perf, test, chore
16+
Language: %{language} (type/scope stay in English)
17+
18+
CRITICAL RULES:
19+
1. Output ONLY the commit message; no markdown, no quotes, no extra text
20+
2. Subject is imperative, present tense, no trailing period
21+
3. Be specific about WHAT changed; avoid WHY or impact
22+
4. Avoid vague verbs: "update", "improve", "clarify", "adjust", "enhance", "fix issues"
23+
Prefer concrete verbs: "add", "remove", "rename", "move", "replace", "extract", "inline"
24+
5. Scope is optional; include only if clearly implied by the diff
25+
6. Subject <= 50 chars; body lines <= 72 chars
26+
7. Add body only when the subject alone is not enough
27+
8. If the diff introduces a breaking change, mark with "!" and add "BREAKING CHANGE:" in body
28+
9. Do not invent issue references, ticket IDs, or files not in the diff
29+
10. If the diff includes multiple unrelated changes, pick the single most important one
30+
11. When body is present, reference concrete entities from the diff (module, file, function, setting)
31+
32+
DIFF (source of truth):
33+
```diff
34+
%{diff}
35+
```
36+
END DIFF
37+
%{history_context}]],
838
exclude_files = {
939
"*.pb.go",
1040
"*.min.js",

lua/codecompanion/_extensions/gitcommit/config_validation.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ local fmt = string.format
2525
M.schema = {
2626
adapter = { "string", "nil" },
2727
model = { "string", "nil" },
28+
prompt_template = { "string", "nil" },
2829
languages = { type = "array", items = "string" },
2930
exclude_files = { type = "array", items = "string" },
3031
buffer = {

lua/codecompanion/_extensions/gitcommit/generator.lua

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ local Generator = {}
99
local _adapter_name = nil
1010
--- @type string? Model name
1111
local _model_name = nil
12+
--- @type string? Prompt template
13+
local _prompt_template = nil
1214

1315
local CONSTANTS = {
1416
STATUS_ERROR = "error",
@@ -17,9 +19,11 @@ local CONSTANTS = {
1719

1820
--- @param adapter string? The adapter to use for generation
1921
--- @param model string? The model of the adapter to use for generation
20-
function Generator.setup(adapter, model)
22+
--- @param prompt_template string? Custom prompt template
23+
function Generator.setup(adapter, model, prompt_template)
2124
_adapter_name = adapter
2225
_model_name = model
26+
_prompt_template = prompt_template
2327
end
2428

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

232236
return Generator

lua/codecompanion/_extensions/gitcommit/git_utils.lua

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,9 @@ end
326326
---@param diff string The git diff content
327327
---@param lang string The target language for the commit message
328328
---@param commit_history? string[] Recent commit messages for context
329+
---@param prompt_template? string Custom prompt template with placeholders
329330
---@return string prompt The formatted prompt
330-
function M.build_commit_prompt(diff, lang, commit_history)
331+
function M.build_commit_prompt(diff, lang, commit_history, prompt_template)
331332
local history_context = ""
332333
if commit_history and #commit_history > 0 then
333334
history_context = "BEGIN HISTORY (style reference only):\n"
@@ -338,41 +339,22 @@ function M.build_commit_prompt(diff, lang, commit_history)
338339
.. "END HISTORY\nStyle reference only. Do not copy content or topics; base the message ONLY on the diff.\n"
339340
end
340341

341-
return string.format(
342-
[[You are a commit message generator. Produce exactly ONE Conventional Commit message for the provided git diff.
343-
344-
FORMAT:
345-
type(scope): concise, imperative description of WHAT changed
346-
347-
Optional body (only if needed for non-obvious changes)
348-
349-
Allowed types: feat, fix, docs, style, refactor, perf, test, chore
350-
Language: %s (type/scope stay in English)
351-
352-
CRITICAL RULES:
353-
1. Output ONLY the commit message; no markdown, no quotes, no extra text
354-
2. Subject is imperative, present tense, no trailing period
355-
3. Be specific about WHAT changed; avoid WHY or impact
356-
4. Avoid vague verbs: "update", "improve", "clarify", "adjust", "enhance", "fix issues"
357-
Prefer concrete verbs: "add", "remove", "rename", "move", "replace", "extract", "inline"
358-
5. Scope is optional; include only if clearly implied by the diff
359-
6. Subject <= 50 chars; body lines <= 72 chars
360-
7. Add body only when the subject alone is not enough
361-
8. If the diff introduces a breaking change, mark with "!" and add "BREAKING CHANGE:" in body
362-
9. Do not invent issue references, ticket IDs, or files not in the diff
363-
10. If the diff includes multiple unrelated changes, pick the single most important one
364-
11. When body is present, reference concrete entities from the diff (module, file, function, setting)
365-
366-
DIFF (source of truth):
367-
```diff
368-
%s
369-
```
370-
END DIFF
371-
%s]],
372-
lang or "English",
373-
diff,
374-
history_context
375-
)
342+
local template = prompt_template
343+
if template == nil or template == "" then
344+
local Config = require("codecompanion._extensions.gitcommit.config")
345+
template = Config.default_opts.prompt_template
346+
end
347+
348+
local prompt = template
349+
prompt = prompt:gsub("%%{language}", lang or "English")
350+
prompt = prompt:gsub("%%{diff}", function()
351+
return diff
352+
end)
353+
prompt = prompt:gsub("%%{history_context}", function()
354+
return history_context
355+
end)
356+
357+
return prompt
376358
end
377359

378360
---Parse git conflict markers from file content

lua/codecompanion/_extensions/gitcommit/init.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ return
349349
use_commit_history = opts.use_commit_history,
350350
commit_history_count = opts.commit_history_count,
351351
})
352-
Generator.setup(opts.adapter, opts.model)
352+
Generator.setup(opts.adapter, opts.model, opts.prompt_template)
353353
Buffer.setup(opts.buffer)
354354
Langs.setup(opts.languages)
355355

tests/test_git_utils.lua

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,4 +953,98 @@ T["conflict_markers"]["parses conflict blocks"] = function()
953953
h.expect_match(">>>>>>>", result.block)
954954
end
955955

956+
T["build_commit_prompt"] = new_set()
957+
958+
T["build_commit_prompt"]["uses default template when no custom template"] = function()
959+
local result = child.lua([[
960+
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
961+
local prompt = GitUtils.build_commit_prompt("test diff", "English", nil, nil)
962+
return prompt:find("commit message generator") ~= nil
963+
]])
964+
h.eq(true, result)
965+
end
966+
967+
T["build_commit_prompt"]["uses default template when empty template"] = function()
968+
local result = child.lua([[
969+
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
970+
local prompt = GitUtils.build_commit_prompt("test diff", "English", nil, "")
971+
return prompt:find("commit message generator") ~= nil
972+
]])
973+
h.eq(true, result)
974+
end
975+
976+
T["build_commit_prompt"]["replaces language placeholder"] = function()
977+
local result = child.lua([[
978+
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
979+
local template = "Generate in %{language}"
980+
local prompt = GitUtils.build_commit_prompt("diff", "Chinese", nil, template)
981+
return prompt == "Generate in Chinese"
982+
]])
983+
h.eq(true, result)
984+
end
985+
986+
T["build_commit_prompt"]["replaces diff placeholder"] = function()
987+
local result = child.lua([[
988+
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
989+
local template = "Diff: %{diff}"
990+
local prompt = GitUtils.build_commit_prompt("my changes", "English", nil, template)
991+
return prompt == "Diff: my changes"
992+
]])
993+
h.eq(true, result)
994+
end
995+
996+
T["build_commit_prompt"]["replaces history_context placeholder"] = function()
997+
local result = child.lua([[
998+
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
999+
local template = "History: %{history_context}"
1000+
local prompt = GitUtils.build_commit_prompt("diff", "English", {"commit 1", "commit 2"}, template)
1001+
return prompt:find("BEGIN HISTORY") ~= nil and prompt:find("commit 1") ~= nil
1002+
]])
1003+
h.eq(true, result)
1004+
end
1005+
1006+
T["build_commit_prompt"]["handles empty history_context"] = function()
1007+
local result = child.lua([[
1008+
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
1009+
local template = "History: [%{history_context}]"
1010+
local prompt = GitUtils.build_commit_prompt("diff", "English", nil, template)
1011+
return prompt == "History: []"
1012+
]])
1013+
h.eq(true, result)
1014+
end
1015+
1016+
T["build_commit_prompt"]["replaces all placeholders in custom template"] = function()
1017+
local result = child.lua([[
1018+
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
1019+
local template = "Lang: %{language}, Diff: %{diff}, Ctx: %{history_context}"
1020+
local prompt = GitUtils.build_commit_prompt("my diff", "Japanese", {"hist1"}, template)
1021+
local has_lang = prompt:find("Lang: Japanese") ~= nil
1022+
local has_diff = prompt:find("Diff: my diff") ~= nil
1023+
local has_ctx = prompt:find("Ctx: BEGIN HISTORY") ~= nil
1024+
return has_lang and has_diff and has_ctx
1025+
]])
1026+
h.eq(true, result)
1027+
end
1028+
1029+
T["build_commit_prompt"]["handles diff with special characters"] = function()
1030+
local result = child.lua([[
1031+
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
1032+
local template = "Diff: %{diff}"
1033+
local diff_content = "+hello %{language} world"
1034+
local prompt = GitUtils.build_commit_prompt(diff_content, "English", nil, template)
1035+
return prompt:find("%%{language}") ~= nil
1036+
]])
1037+
h.eq(true, result)
1038+
end
1039+
1040+
T["build_commit_prompt"]["defaults language to English"] = function()
1041+
local result = child.lua([[
1042+
local GitUtils = require("codecompanion._extensions.gitcommit.git_utils")
1043+
local template = "Lang: %{language}"
1044+
local prompt = GitUtils.build_commit_prompt("diff", nil, nil, template)
1045+
return prompt == "Lang: English"
1046+
]])
1047+
h.eq(true, result)
1048+
end
1049+
9561050
return T

0 commit comments

Comments
 (0)