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

Commit 69eef7a

Browse files
committed
feat(gitcommit): add .gitignore management operations
- implement get, add, remove, and check functions for .gitignore in GitTool - extend GitBot schema and handlers to support gitignore operations - update output handlers for simplified user messages
1 parent 0dd976b commit 69eef7a

2 files changed

Lines changed: 179 additions & 30 deletions

File tree

lua/codecompanion/_extensions/gitcommit/tools/git.lua

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,135 @@ local GitTool = {
88
description = "Execute git operations and commands",
99
}
1010

11+
--- Get the path to the .gitignore file in the current git repo root
12+
local function get_gitignore_path()
13+
local git_dir = vim.fn.system("git rev-parse --show-toplevel"):gsub("\n", "")
14+
if vim.v.shell_error ~= 0 or not git_dir or git_dir == "" then
15+
return nil
16+
end
17+
local sep = package.config:sub(1, 1)
18+
return git_dir .. sep .. ".gitignore"
19+
end
20+
21+
--- Read .gitignore content
22+
function GitTool.get_gitignore()
23+
local path = get_gitignore_path()
24+
if not path then
25+
return false, ".gitignore not found (not in a git repo)"
26+
end
27+
local stat = vim.uv.fs_stat(path)
28+
if not stat then
29+
return true, "" -- treat as empty if not exists
30+
end
31+
local fd = vim.uv.fs_open(path, "r", 438)
32+
if not fd then
33+
return false, "Failed to open .gitignore for reading"
34+
end
35+
local data = vim.uv.fs_read(fd, stat.size, 0)
36+
vim.uv.fs_close(fd)
37+
return true, data or ""
38+
end
39+
40+
--- Add rule(s) to .gitignore (no duplicates)
41+
function GitTool.add_gitignore_rule(rule)
42+
local path = get_gitignore_path()
43+
if not path then
44+
return false, ".gitignore not found (not in a git repo)"
45+
end
46+
local rules = type(rule) == "table" and rule or { rule }
47+
local stat = vim.uv.fs_stat(path)
48+
local lines = {}
49+
if stat then
50+
local fd = vim.uv.fs_open(path, "r", 438)
51+
if fd then
52+
local data = vim.uv.fs_read(fd, stat.size, 0)
53+
vim.uv.fs_close(fd)
54+
if data then
55+
for line in data:gmatch("([^\r\n]+)") do
56+
table.insert(lines, line)
57+
end
58+
end
59+
end
60+
end
61+
local set = {}
62+
for _, l in ipairs(lines) do set[l] = true end
63+
local added = {}
64+
for _, r in ipairs(rules) do
65+
if not set[r] then
66+
table.insert(lines, r)
67+
set[r] = true
68+
table.insert(added, r)
69+
end
70+
end
71+
local fdw = vim.uv.fs_open(path, "w", 420)
72+
if not fdw then
73+
return false, "Failed to open .gitignore for writing"
74+
end
75+
vim.uv.fs_write(fdw, table.concat(lines, "\n") .. "\n", 0)
76+
vim.uv.fs_close(fdw)
77+
if #added == 0 then
78+
return true, "No new rule added (already present)"
79+
end
80+
return true, "Added rule(s): " .. table.concat(added, ", ")
81+
end
82+
83+
--- Remove rule(s) from .gitignore
84+
function GitTool.remove_gitignore_rule(rule)
85+
local path = get_gitignore_path()
86+
if not path then
87+
return false, ".gitignore not found (not in a git repo)"
88+
end
89+
local rules = type(rule) == "table" and rule or { rule }
90+
local stat = vim.uv.fs_stat(path)
91+
if not stat then
92+
return false, ".gitignore does not exist"
93+
end
94+
local fd = vim.uv.fs_open(path, "r", 438)
95+
if not fd then
96+
return false, "Failed to open .gitignore for reading"
97+
end
98+
local data = vim.uv.fs_read(fd, stat.size, 0)
99+
vim.uv.fs_close(fd)
100+
if not data then
101+
return false, "Failed to read .gitignore"
102+
end
103+
local lines = {}
104+
local removed = {}
105+
local rule_set = {}
106+
for _, r in ipairs(rules) do rule_set[r] = true end
107+
for line in data:gmatch("([^\r\n]+)") do
108+
if rule_set[line] then
109+
table.insert(removed, line)
110+
else
111+
table.insert(lines, line)
112+
end
113+
end
114+
local fdw = vim.uv.fs_open(path, "w", 420)
115+
if not fdw then
116+
return false, "Failed to open .gitignore for writing"
117+
end
118+
vim.uv.fs_write(fdw, table.concat(lines, "\n") .. "\n", 0)
119+
vim.uv.fs_close(fdw)
120+
if #removed == 0 then
121+
return true, "No rule removed (not present)"
122+
end
123+
return true, "Removed rule(s): " .. table.concat(removed, ", ")
124+
end
125+
126+
--- Check if a file is ignored by .gitignore
127+
function GitTool.is_ignored(file)
128+
if not file or file == "" then
129+
return false, "No file specified"
130+
end
131+
local ok, result = pcall(function()
132+
return vim.fn.system({"git", "check-ignore", file})
133+
end)
134+
if not ok or vim.v.shell_error ~= 0 then
135+
return false, "File is not ignored or not in a git repo"
136+
end
137+
return true, vim.trim(result)
138+
end
139+
11140
---Check if we're in a git repository
12141
---@return boolean
13142
local function is_git_repo()

lua/codecompanion/_extensions/gitcommit/tools/git_bot.lua

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ GitBot.schema = {
3838
"contributors",
3939
"search_commits",
4040
"help",
41+
"gitignore_get",
42+
"gitignore_add",
43+
"gitignore_remove",
44+
"gitignore_check",
4145
},
4246
description = "The Git operation to perform",
4347
},
@@ -122,6 +126,19 @@ GitBot.schema = {
122126
type = "string",
123127
description = "Stash reference (e.g., stash@{0})",
124128
},
129+
gitignore_rule = {
130+
type = "string",
131+
description = "Rule to add or remove from .gitignore",
132+
},
133+
gitignore_rules = {
134+
type = "array",
135+
items = { type = "string" },
136+
description = "Multiple rules to add or remove from .gitignore",
137+
},
138+
gitignore_file = {
139+
type = "string",
140+
description = "File to check if ignored",
141+
},
125142
},
126143
additionalProperties = false,
127144
},
@@ -267,6 +284,26 @@ Available Git operations:
267284
return { status = "error", data = "Search pattern is required" }
268285
end
269286
success, output = GitTool.search_commits(op_args.pattern, op_args.count)
287+
elseif operation == "gitignore_get" then
288+
success, output = GitTool.get_gitignore()
289+
elseif operation == "gitignore_add" then
290+
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 add" }
293+
end
294+
success, output = GitTool.add_gitignore_rule(rules)
295+
elseif operation == "gitignore_remove" then
296+
local rules = op_args.gitignore_rules or op_args.gitignore_rule
297+
if not rules then
298+
return { status = "error", data = "No rule(s) specified for .gitignore remove" }
299+
end
300+
success, output = GitTool.remove_gitignore_rule(rules)
301+
elseif operation == "gitignore_check" then
302+
local file = op_args.gitignore_file
303+
if not file then
304+
return { status = "error", data = "No file specified for .gitignore check" }
305+
end
306+
success, output = GitTool.is_ignored(file)
270307
else
271308
return { status = "error", data = "Unknown Git operation: " .. operation }
272309
end
@@ -293,37 +330,20 @@ GitBot.handlers = {
293330

294331
-- Output handlers
295332
GitBot.output = {
296-
success = function(self, agent, cmd, stdout)
297-
local chat = agent.chat
298-
local operation = self.args.operation
299-
local result = stdout[1]
333+
success = function(self, agent, cmd, stdout)
334+
local chat = agent.chat
335+
local operation = self.args.operation
336+
-- Show a simple English message for user, highlight can be added later
337+
local user_msg = string.format("Git operation [%s] completed", operation)
338+
return chat:add_tool_output(self, stdout[1], user_msg)
339+
end,
300340

301-
-- Format the output based on operation type
302-
local formatted_output
303-
if operation == "status" then
304-
formatted_output = "📊 **Git Status:**\n```\n" .. result .. "\n```"
305-
elseif operation == "log" then
306-
formatted_output = "📜 **Git Log:**\n```\n" .. result .. "\n```"
307-
elseif operation == "diff" then
308-
formatted_output = "🔍 **Git Diff:**\n```diff\n" .. result .. "\n```"
309-
elseif operation == "branch" then
310-
formatted_output = "🌿 **Git Branches:**\n```\n" .. result .. "\n```"
311-
elseif operation == "blame" then
312-
formatted_output = "👤 **Git Blame:**\n```\n" .. result .. "\n```"
313-
else
314-
formatted_output = "✅ **Git " .. operation .. ":**\n```\n" .. result .. "\n```"
315-
end
316-
317-
return chat:add_tool_output(self, result, formatted_output)
318-
end,
319-
320-
error = function(self, agent, cmd, stderr, stdout)
321-
local chat = agent.chat
322-
local error_msg = stderr[1] or "Unknown Git error occurred"
323-
local formatted_error = "❌ **Git Error:**\n```\n" .. error_msg .. "\n```"
324-
325-
return chat:add_tool_output(self, error_msg, formatted_error)
326-
end,
341+
error = function(self, agent, cmd, stderr, stdout)
342+
local chat = agent.chat
343+
local error_msg = stderr[1] or "Git operation failed"
344+
local user_msg = "Git operation failed"
345+
return chat:add_tool_output(self, error_msg, user_msg)
346+
end,
327347
}
328348

329349
-- Optional: No approval required for read-only operations

0 commit comments

Comments
 (0)