diff --git a/README.md b/README.md index 771efc5..be1b5b1 100644 --- a/README.md +++ b/README.md @@ -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", @@ -209,6 +210,36 @@ opts = { +### 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: diff --git a/doc/codecompanion-gitcommit.txt b/doc/codecompanion-gitcommit.txt index a232047..d05135d 100644 --- a/doc/codecompanion-gitcommit.txt +++ b/doc/codecompanion-gitcommit.txt @@ -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. diff --git a/lua/codecompanion/_extensions/gitcommit/config.lua b/lua/codecompanion/_extensions/gitcommit/config.lua index 2ce4214..e63f8b3 100644 --- a/lua/codecompanion/_extensions/gitcommit/config.lua +++ b/lua/codecompanion/_extensions/gitcommit/config.lua @@ -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", diff --git a/lua/codecompanion/_extensions/gitcommit/config_validation.lua b/lua/codecompanion/_extensions/gitcommit/config_validation.lua index 466af93..4b700f2 100644 --- a/lua/codecompanion/_extensions/gitcommit/config_validation.lua +++ b/lua/codecompanion/_extensions/gitcommit/config_validation.lua @@ -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 = { diff --git a/lua/codecompanion/_extensions/gitcommit/generator.lua b/lua/codecompanion/_extensions/gitcommit/generator.lua index 1217b07..d20edda 100644 --- a/lua/codecompanion/_extensions/gitcommit/generator.lua +++ b/lua/codecompanion/_extensions/gitcommit/generator.lua @@ -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", @@ -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 @@ -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 diff --git a/lua/codecompanion/_extensions/gitcommit/git_utils.lua b/lua/codecompanion/_extensions/gitcommit/git_utils.lua index 027aa38..869af76 100644 --- a/lua/codecompanion/_extensions/gitcommit/git_utils.lua +++ b/lua/codecompanion/_extensions/gitcommit/git_utils.lua @@ -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" @@ -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 diff --git a/lua/codecompanion/_extensions/gitcommit/init.lua b/lua/codecompanion/_extensions/gitcommit/init.lua index 2c42f48..1736e21 100644 --- a/lua/codecompanion/_extensions/gitcommit/init.lua +++ b/lua/codecompanion/_extensions/gitcommit/init.lua @@ -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) diff --git a/tests/test_git_utils.lua b/tests/test_git_utils.lua index e0b3c75..ed20ec8 100644 --- a/tests/test_git_utils.lua +++ b/tests/test_git_utils.lua @@ -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