11local GitTool = require (" codecompanion._extensions.gitcommit.tools.git" ).GitTool
2+ local validation = require (" codecompanion._extensions.gitcommit.tools.validation" )
23
34--- @class CodeCompanion.GitCommit.Tools.GitEdit
45local GitEdit = {}
@@ -150,40 +151,87 @@ GitEdit.schema = {
150151 },
151152}
152153
153- GitEdit .system_prompt = [[ Execute write-access Git repository operations
154+ GitEdit .system_prompt = [[ # Git Edit Tool (`git_edit`)
154155
155- When to use:
156- • When staging or unstaging file changes
157- • When creating or switching between branches
158- • When managing stashes and repository state
159- • When performing safe repository modifications
156+ ## CONTEXT
157+ - You have access to a write-access Git tool running within CodeCompanion, in Neovim.
158+ - Use this tool to modify repository state: staging, committing, branching, etc.
159+ - These operations can modify the repository, so use them carefully.
160160
161- Best practices:
162- • Must verify Git repository before operations
163- • Always specify files parameter for stage/unstage operations
164- • Use '.' to stage all modified files or specific file paths
165- • For commit operations, if no commit_message provided, automatically generate AI message
166- • Auto-generation analyzes staged changes and creates Conventional Commit compliant messages
167- • Use format: type(scope): description with lowercase type and imperative verb description
168- • Include body with bullet points for complex changes, keep description under 50 characters
169- • Use the `set_upstream` option to create and track a remote branch if it doesn't exist
170- • Avoid force push operations that rewrite history
171- • Ensure file paths and branch names are valid
161+ ## OBJECTIVE
162+ - Follow the tool's schema strictly.
163+ - Use the appropriate operation for the task.
164+ - For commits without a message, analyze staged changes and generate Conventional Commit format.
172165
173- Available operations: stage, unstage, commit, create_branch, checkout, stash, apply_stash, reset, gitignore_add, gitignore_remove, push, cherry_pick, revert, create_tag, delete_tag, merge, help]]
166+ ## AVAILABLE OPERATIONS
167+ | Operation | Description | Required Args |
168+ |-----------|-------------|---------------|
169+ | `stage` | Stage files for commit | files (required) |
170+ | `unstage` | Unstage files | files (required) |
171+ | `commit` | Commit staged changes | commit_message? (auto-generates if empty) |
172+ | `create_branch` | Create new branch | branch_name (required), checkout? |
173+ | `checkout` | Switch branch/commit | target (required) |
174+ | `stash` | Stash changes | message?, include_untracked? |
175+ | `apply_stash` | Apply stash | stash_ref? |
176+ | `reset` | Reset to commit | commit_hash (required), mode? |
177+ | `gitignore_add` | Add .gitignore rules | gitignore_rules (required) |
178+ | `gitignore_remove` | Remove .gitignore rules | gitignore_rule (required) |
179+ | `push` | Push to remote | remote?, branch?, set_upstream? |
180+ | `cherry_pick` | Apply commit | cherry_pick_commit_hash (required) |
181+ | `revert` | Revert commit | revert_commit_hash (required) |
182+ | `create_tag` | Create tag | tag_name (required), tag_message? |
183+ | `delete_tag` | Delete tag | tag_name (required) |
184+ | `merge` | Merge branch | branch (required) |
185+ | `help` | Show help | - |
174186
175- -- Helper function to validate required parameters
176- local function validate_required_param (param_name , param_value , error_msg )
177- if not param_value or (type (param_value ) == " table" and # param_value == 0 ) then
178- return { status = " error" , data = error_msg or (param_name .. " is required" ) }
179- end
180- return nil
181- end
187+ ## SAFETY RESTRICTIONS
188+ - Never use force push without explicit user confirmation.
189+ - Always verify staged changes before committing.
190+ - Warn users before destructive operations (reset --hard, delete).
191+
192+ ## RESPONSE
193+ - Only invoke this tool when modifying Git repository state.
194+ - For commit messages, use Conventional Commit format: type(scope): description.]]
195+
196+ local TOOL_NAME = " gitEdit"
197+ local VALID_OPERATIONS = {
198+ " stage" ,
199+ " unstage" ,
200+ " commit" ,
201+ " create_branch" ,
202+ " checkout" ,
203+ " stash" ,
204+ " apply_stash" ,
205+ " reset" ,
206+ " gitignore_add" ,
207+ " gitignore_remove" ,
208+ " push" ,
209+ " cherry_pick" ,
210+ " revert" ,
211+ " create_tag" ,
212+ " delete_tag" ,
213+ " merge" ,
214+ " help" ,
215+ }
216+ local VALID_RESET_MODES = { " soft" , " mixed" , " hard" }
182217
183218GitEdit .cmds = {
184219 function (self , args , input , output_handler )
220+ if args == nil or type (args ) ~= " table" then
221+ return validation .format_error (TOOL_NAME , " Invalid arguments: expected object" )
222+ end
223+
185224 local operation = args .operation
186- local op_args = args .args or {}
225+ local err = validation .require_enum (operation , " operation" , VALID_OPERATIONS , TOOL_NAME )
226+ if err then
227+ return err
228+ end
229+
230+ local op_args = args .args
231+ if op_args ~= nil and type (op_args ) ~= " table" then
232+ return validation .format_error (TOOL_NAME , " args must be an object" )
233+ end
234+ op_args = op_args or {}
187235
188236 if operation == " help" then
189237 local help_text = [[
@@ -207,6 +255,17 @@ Available write-access Git operations:
207255 end
208256
209257 if operation == " push" then
258+ local param_err = validation .first_error ({
259+ validation .optional_string (op_args .remote , " remote" , TOOL_NAME ),
260+ validation .optional_string (op_args .branch , " branch" , TOOL_NAME ),
261+ validation .optional_boolean (op_args .force , " force" , TOOL_NAME ),
262+ validation .optional_boolean (op_args .set_upstream , " set_upstream" , TOOL_NAME ),
263+ validation .optional_boolean (op_args .tags , " tags" , TOOL_NAME ),
264+ validation .optional_string (op_args .single_tag_name , " single_tag_name" , TOOL_NAME ),
265+ })
266+ if param_err then
267+ return param_err
268+ end
210269 -- If set_upstream is not explicitly specified, default to true for automatic remote tracking
211270 if op_args .set_upstream == nil then
212271 op_args .set_upstream = true
@@ -225,20 +284,29 @@ Available write-access Git operations:
225284 -- Safely execute operations through pcall to ensure there's always a response
226285 local ok , result = pcall (function ()
227286 local success , output
287+ local param_err
228288
229289 if operation == " stage" then
230- local validation_error = validate_required_param ( " files " , op_args .files , " No files specified for staging " )
231- if validation_error then
232- return validation_error
290+ param_err = validation . require_array ( op_args .files , " files" , TOOL_NAME )
291+ if param_err then
292+ return param_err
233293 end
234294 success , output = GitTool .stage_files (op_args .files )
235295 elseif operation == " unstage" then
236- local validation_error = validate_required_param ( " files " , op_args .files , " No files specified for unstaging " )
237- if validation_error then
238- return validation_error
296+ param_err = validation . require_array ( op_args .files , " files" , TOOL_NAME )
297+ if param_err then
298+ return param_err
239299 end
240300 success , output = GitTool .unstage_files (op_args .files )
241301 elseif operation == " commit" then
302+ param_err = validation .first_error ({
303+ validation .optional_string (op_args .commit_message , " commit_message" , TOOL_NAME ),
304+ validation .optional_string (op_args .message , " message" , TOOL_NAME ),
305+ validation .optional_boolean (op_args .amend , " amend" , TOOL_NAME ),
306+ })
307+ if param_err then
308+ return param_err
309+ end
242310 local message = op_args .commit_message or op_args .message
243311 if not message then
244312 -- Check if there are staged changes
@@ -258,81 +326,107 @@ Available write-access Git operations:
258326 end
259327 success , output = GitTool .commit (message , op_args .amend )
260328 elseif operation == " create_branch" then
261- if not op_args .branch_name then
262- return { status = " error" , data = " Branch name is required" }
329+ param_err = validation .first_error ({
330+ validation .require_string (op_args .branch_name , " branch_name" , TOOL_NAME ),
331+ validation .optional_boolean (op_args .checkout , " checkout" , TOOL_NAME ),
332+ })
333+ if param_err then
334+ return param_err
263335 end
264336 success , output = GitTool .create_branch (op_args .branch_name , op_args .checkout )
265337 elseif operation == " checkout" then
266- local validation_error =
267- validate_required_param (" target" , op_args .target , " Target branch or commit is required" )
268- if validation_error then
269- return validation_error
338+ param_err = validation .require_string (op_args .target , " target" , TOOL_NAME )
339+ if param_err then
340+ return param_err
270341 end
271342 success , output = GitTool .checkout (op_args .target )
272343 elseif operation == " stash" then
344+ param_err = validation .first_error ({
345+ validation .optional_string (op_args .message , " message" , TOOL_NAME ),
346+ validation .optional_boolean (op_args .include_untracked , " include_untracked" , TOOL_NAME ),
347+ })
348+ if param_err then
349+ return param_err
350+ end
273351 success , output = GitTool .stash (op_args .message , op_args .include_untracked )
274352 elseif operation == " apply_stash" then
353+ param_err = validation .optional_string (op_args .stash_ref , " stash_ref" , TOOL_NAME )
354+ if param_err then
355+ return param_err
356+ end
275357 success , output = GitTool .apply_stash (op_args .stash_ref )
276358 elseif operation == " reset" then
277- local validation_error =
278- validate_required_param (" commit_hash" , op_args .commit_hash , " Commit hash is required for reset" )
279- if validation_error then
280- return validation_error
359+ param_err = validation .first_error ({
360+ validation .require_string (op_args .commit_hash , " commit_hash" , TOOL_NAME ),
361+ op_args .mode and validation .require_enum (op_args .mode , " mode" , VALID_RESET_MODES , TOOL_NAME ) or nil ,
362+ })
363+ if param_err then
364+ return param_err
281365 end
282366 success , output = GitTool .reset (op_args .commit_hash , op_args .mode )
283367 elseif operation == " gitignore_add" then
284368 local rules = op_args .gitignore_rules or op_args .gitignore_rule
285- if not rules then
286- return { status = " error" , data = " No rule(s) specified for .gitignore add" }
369+ if rules == nil then
370+ return validation .format_error (TOOL_NAME , " gitignore_rules or gitignore_rule is required" )
371+ end
372+ if type (rules ) ~= " table" and type (rules ) ~= " string" then
373+ return validation .format_error (
374+ TOOL_NAME ,
375+ " gitignore_rules must be an array or gitignore_rule must be a string"
376+ )
287377 end
288378 success , output = GitTool .add_gitignore_rule (rules )
289379 elseif operation == " gitignore_remove" then
290380 local rules = op_args .gitignore_rules or op_args .gitignore_rule
291- if not rules then
292- return { status = " error" , data = " No rule(s) specified for .gitignore remove" }
381+ if rules == nil then
382+ return validation .format_error (TOOL_NAME , " gitignore_rules or gitignore_rule is required" )
383+ end
384+ if type (rules ) ~= " table" and type (rules ) ~= " string" then
385+ return validation .format_error (
386+ TOOL_NAME ,
387+ " gitignore_rules must be an array or gitignore_rule must be a string"
388+ )
293389 end
294390 success , output = GitTool .remove_gitignore_rule (rules )
295391 elseif operation == " cherry_pick" then
296- local validation_error = validate_required_param (
297- " cherry_pick_commit_hash" ,
298- op_args .cherry_pick_commit_hash ,
299- " Commit hash is required for cherry-pick"
300- )
301- if validation_error then
302- return validation_error
392+ param_err = validation .require_string (op_args .cherry_pick_commit_hash , " cherry_pick_commit_hash" , TOOL_NAME )
393+ if param_err then
394+ return param_err
303395 end
304396 success , output = GitTool .cherry_pick (op_args .cherry_pick_commit_hash )
305397 elseif operation == " revert" then
306- local validation_error = validate_required_param (
307- " revert_commit_hash" ,
308- op_args .revert_commit_hash ,
309- " Commit hash is required for revert"
310- )
311- if validation_error then
312- return validation_error
398+ param_err = validation .require_string (op_args .revert_commit_hash , " revert_commit_hash" , TOOL_NAME )
399+ if param_err then
400+ return param_err
313401 end
314402 success , output = GitTool .revert (op_args .revert_commit_hash )
315403 elseif operation == " create_tag" then
316- local validation_error = validate_required_param (" tag_name" , op_args .tag_name , " Tag name is required" )
317- if validation_error then
318- return validation_error
404+ param_err = validation .first_error ({
405+ validation .require_string (op_args .tag_name , " tag_name" , TOOL_NAME ),
406+ validation .optional_string (op_args .tag_message , " tag_message" , TOOL_NAME ),
407+ validation .optional_string (op_args .tag_commit_hash , " tag_commit_hash" , TOOL_NAME ),
408+ })
409+ if param_err then
410+ return param_err
319411 end
320412 success , output = GitTool .create_tag (op_args .tag_name , op_args .tag_message , op_args .tag_commit_hash )
321413 elseif operation == " delete_tag" then
322- local validation_error =
323- validate_required_param (" tag_name" , op_args .tag_name , " Tag name is required for deletion" )
324- if validation_error then
325- return validation_error
414+ param_err = validation .first_error ({
415+ validation .require_string (op_args .tag_name , " tag_name" , TOOL_NAME ),
416+ validation .optional_string (op_args .remote , " remote" , TOOL_NAME ),
417+ })
418+ if param_err then
419+ return param_err
326420 end
327421 success , output = GitTool .delete_tag (op_args .tag_name , op_args .remote )
328422 elseif operation == " merge" then
329- local validation_error = validate_required_param ( " branch " , op_args .branch , " Branch to merge is required " )
330- if validation_error then
331- return validation_error
423+ param_err = validation . require_string ( op_args .branch , " branch " , TOOL_NAME )
424+ if param_err then
425+ return param_err
332426 end
333427 success , output = GitTool .merge (op_args .branch )
334428 else
335- return { status = " error " , data = " Unknown Git edit operation: " .. operation }
429+ return validation . format_error ( TOOL_NAME , " Unknown Git edit operation: " .. tostring ( operation ))
336430 end
337431
338432 return { success = success , output = output }
@@ -368,19 +462,45 @@ GitEdit.handlers = {
368462}
369463
370464GitEdit .output = {
371- success = function (self , agent , cmd , stdout )
465+ prompt = function (self , _tools )
466+ local operation = self .args and self .args .operation or " unknown"
467+ local details = " "
468+ if operation == " stage" or operation == " unstage" then
469+ local files = self .args .args and self .args .args .files
470+ if files then
471+ details = string.format (" (%s)" , type (files ) == " table" and table.concat (files , " , " ) or files )
472+ end
473+ elseif operation == " commit" then
474+ local msg = self .args .args and self .args .args .commit_message
475+ details = msg and string.format (" with message: %s" , msg :sub (1 , 50 )) or " (auto-generate message)"
476+ elseif operation == " create_branch" then
477+ local branch = self .args .args and self .args .args .branch_name
478+ details = branch and string.format (" : %s" , branch ) or " "
479+ end
480+ return string.format (" Execute git %s%s?" , operation , details )
481+ end ,
482+
483+ success = function (self , agent , _cmd , stdout )
372484 local chat = agent .chat
373485 local operation = self .args .operation
374486 local user_msg = string.format (" Git edit operation [%s] executed successfully" , operation )
375487 return chat :add_tool_output (self , stdout [1 ], user_msg )
376488 end ,
377- error = function (self , agent , cmd , stderr , stdout )
489+
490+ error = function (self , agent , _cmd , stderr , stdout )
378491 local chat = agent .chat
379492 local operation = self .args .operation
380493 local error_msg = stderr and stderr [1 ] or (" Git edit operation [%s] failed" ):format (operation )
381494 local user_msg = string.format (" Git edit operation [%s] failed" , operation )
382495 return chat :add_tool_output (self , error_msg , user_msg )
383496 end ,
497+
498+ rejected = function (self , tools , _cmd , _opts )
499+ local chat = tools .chat
500+ local operation = self .args and self .args .operation or " unknown"
501+ local message = string.format (" User rejected the git %s operation" , operation )
502+ return chat :add_tool_output (self , message , message )
503+ end ,
384504}
385505
386506GitEdit .opts = {
0 commit comments