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

Commit 4d810e6

Browse files
committed
feat(gitcommit): add merge conflict detection
1 parent 8b01d9b commit 4d810e6

2 files changed

Lines changed: 127 additions & 1 deletion

File tree

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

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,118 @@ function GitTool.merge_continue()
856856
end
857857
end
858858

859+
---Get list of files with merge conflicts
860+
---@return boolean success, string output, string user_msg, string llm_msg
861+
function GitTool.get_conflict_status()
862+
if not is_git_repo() then
863+
local msg = "Not in a git repository"
864+
return false, msg, "" .. msg, "<gitConflictStatus>fail: " .. msg .. "</gitConflictStatus>"
865+
end
866+
867+
-- git diff --name-only --diff-filter=U lists unmerged (conflicted) files
868+
local cmd = "git diff --name-only --diff-filter=U"
869+
local output = vim.fn.system(cmd)
870+
local exit_code = vim.v.shell_error
871+
872+
if exit_code ~= 0 then
873+
local msg = "Failed to get conflict status"
874+
return false, msg, "" .. msg, "<gitConflictStatus>fail: " .. msg .. "</gitConflictStatus>"
875+
end
876+
877+
local trimmed = vim.trim(output)
878+
if trimmed == "" then
879+
local msg = "No conflicts found"
880+
return true, msg, "" .. msg, "<gitConflictStatus>success: " .. msg .. "</gitConflictStatus>"
881+
end
882+
883+
local files = {}
884+
for file in trimmed:gmatch("[^\r\n]+") do
885+
if file ~= "" then
886+
table.insert(files, file)
887+
end
888+
end
889+
890+
local user_msg = string.format("⚠ %d file(s) with conflicts:\n", #files)
891+
for _, file in ipairs(files) do
892+
user_msg = user_msg .. "" .. file .. "\n"
893+
end
894+
895+
local llm_msg =
896+
string.format("<gitConflictStatus>success: %d conflicted file(s):\n%s</gitConflictStatus>", #files, trimmed)
897+
898+
return true, trimmed, user_msg, llm_msg
899+
end
900+
901+
---Show conflict markers in a specific file
902+
---@param file_path string Path to the file with conflicts
903+
---@return boolean success, string output, string user_msg, string llm_msg
904+
function GitTool.show_conflict(file_path)
905+
if not is_git_repo() then
906+
local msg = "Not in a git repository"
907+
return false, msg, "" .. msg, "<gitConflictShow>fail: " .. msg .. "</gitConflictShow>"
908+
end
909+
910+
if not file_path or vim.trim(file_path) == "" then
911+
local msg = "File path is required"
912+
return false, msg, "" .. msg, "<gitConflictShow>fail: " .. msg .. "</gitConflictShow>"
913+
end
914+
915+
local stat = vim.uv.fs_stat(file_path)
916+
if not stat then
917+
local msg = "File not found: " .. file_path
918+
return false, msg, "" .. msg, "<gitConflictShow>fail: " .. msg .. "</gitConflictShow>"
919+
end
920+
921+
local fd = vim.uv.fs_open(file_path, "r", 438)
922+
if not fd then
923+
local msg = "Failed to open file: " .. file_path
924+
return false, msg, "" .. msg, "<gitConflictShow>fail: " .. msg .. "</gitConflictShow>"
925+
end
926+
927+
local content = vim.uv.fs_read(fd, stat.size, 0)
928+
vim.uv.fs_close(fd)
929+
930+
if not content then
931+
local msg = "Failed to read file: " .. file_path
932+
return false, msg, "" .. msg, "<gitConflictShow>fail: " .. msg .. "</gitConflictShow>"
933+
end
934+
935+
if not content:match("<<<<<<< ") then
936+
local msg = "No conflict markers found in: " .. file_path
937+
return true, msg, "" .. msg, "<gitConflictShow>success: " .. msg .. "</gitConflictShow>"
938+
end
939+
940+
local conflicts = {}
941+
local conflict_num = 0
942+
943+
for conflict_block in content:gmatch("(<<<<<<<.->>>>>>>.-)\n?") do
944+
conflict_num = conflict_num + 1
945+
table.insert(conflicts, string.format("--- Conflict #%d ---\n%s", conflict_num, conflict_block))
946+
end
947+
948+
if #conflicts == 0 then
949+
local msg = "No conflict markers found in: " .. file_path
950+
return true, msg, "" .. msg, "<gitConflictShow>success: " .. msg .. "</gitConflictShow>"
951+
end
952+
953+
local conflict_output = table.concat(conflicts, "\n\n")
954+
local user_msg = string.format(
955+
"⚠ Found %d conflict(s) in %s:\n\n```\n%s\n```\n\nResolve conflicts manually, then use 'stage' followed by 'cherry_pick_continue' or 'merge_continue'.",
956+
#conflicts,
957+
file_path,
958+
conflict_output
959+
)
960+
961+
local llm_msg = string.format(
962+
"<gitConflictShow>success: %d conflict(s) in %s:\n%s</gitConflictShow>",
963+
#conflicts,
964+
file_path,
965+
conflict_output
966+
)
967+
968+
return true, conflict_output, user_msg, llm_msg
969+
end
970+
859971
--- Generate release notes between two tags
860972
---@param from_tag string|nil Starting tag (if not provided, uses second latest tag)
861973
---@param to_tag string|nil Ending tag (if not provided, uses latest tag)

lua/codecompanion/_extensions/gitcommit/tools/git_read.lua

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ GitRead.schema = {
3131
"search_commits",
3232
"tags",
3333
"generate_release_notes",
34+
"conflict_status",
35+
"conflict_show",
3436
"help",
3537
"gitignore_get",
3638
"gitignore_check",
@@ -140,6 +142,8 @@ GitRead.system_prompt = [[# Git Read Tool (`git_read`)
140142
| `search_commits` | Search commit messages | pattern (required) |
141143
| `tags` | List all tags | - |
142144
| `generate_release_notes` | Generate release notes | from_tag?, to_tag? |
145+
| `conflict_status` | List files with conflicts | - |
146+
| `conflict_show` | Show conflict markers in file | file_path (required) |
143147
| `gitignore_get` | Get .gitignore content | - |
144148
| `gitignore_check` | Check if file is ignored | gitignore_file (required) |
145149
| `help` | Show help information | - |
@@ -164,6 +168,8 @@ local VALID_OPERATIONS = {
164168
"search_commits",
165169
"tags",
166170
"generate_release_notes",
171+
"conflict_status",
172+
"conflict_show",
167173
"help",
168174
"gitignore_get",
169175
"gitignore_check",
@@ -191,7 +197,7 @@ GitRead.cmds = {
191197

192198
if operation == "help" then
193199
local help_text =
194-
"\\\nAvailable read-only Git operations:\n• status: Show repository status\n• log: Show commit history\n• diff: Show file differences\n• branch: List branches\n• remotes: Show remote repositories\n• show: Show commit details\n• blame: Show file blame info\n• stash_list: List stashes\n• diff_commits: Compare commits\n• contributors: Show contributors\n• search_commits: Search commit messages\n• tags: List all tags\n• generate_release_notes: Generate release notes between tags\n• gitignore_get: Get .gitignore content\n• gitignore_check: Check if a file is ignored\n "
200+
"\\\nAvailable read-only Git operations:\n• status: Show repository status\n• log: Show commit history\n• diff: Show file differences\n• branch: List branches\n• remotes: Show remote repositories\n• show: Show commit details\n• blame: Show file blame info\n• stash_list: List stashes\n• diff_commits: Compare commits\n• contributors: Show contributors\n• search_commits: Search commit messages\n• tags: List all tags\n• generate_release_notes: Generate release notes between tags\nconflict_status: List files with merge conflicts\n• conflict_show: Show conflict markers in a file\ngitignore_get: Get .gitignore content\n• gitignore_check: Check if a file is ignored\n "
195201
return { status = "success", data = help_text }
196202
end
197203

@@ -285,6 +291,14 @@ GitRead.cmds = {
285291
end
286292
success, output, user_msg, llm_msg =
287293
GitTool.generate_release_notes(op_args.from_tag, op_args.to_tag, op_args.release_format)
294+
elseif operation == "conflict_status" then
295+
success, output, user_msg, llm_msg = GitTool.get_conflict_status()
296+
elseif operation == "conflict_show" then
297+
param_err = validation.require_string(op_args.file_path, "file_path", TOOL_NAME)
298+
if param_err then
299+
return param_err
300+
end
301+
success, output, user_msg, llm_msg = GitTool.show_conflict(op_args.file_path)
288302
elseif operation == "gitignore_get" then
289303
success, output, user_msg, llm_msg = GitTool.get_gitignore()
290304
elseif operation == "gitignore_check" then

0 commit comments

Comments
 (0)