From dd5c45d9b94b525a741cf962b320440b3b2d25eb Mon Sep 17 00:00:00 2001 From: Meisterlala <6453306+Meisterlala@users.noreply.github.com> Date: Sun, 7 Jun 2026 16:07:06 +0200 Subject: [PATCH] feat(ai-commit): introduce adaptive context sizing with configurable line thresholds and remove hardcoded unified diff levels from configuration. Implement logic to dynamically adjust `git.diff` based on staged file changes using internal threshold rules (`small_changed_lines`, `medium_changed_lines`). Remove direct exposure of specific unified diff levels in config, centralizing behavior within `git.lua`. --- local/ai-commit/lua/ai-commit/config.lua | 4 + local/ai-commit/lua/ai-commit/generator.lua | 32 ++++- local/ai-commit/lua/ai-commit/git.lua | 126 ++++++++++++++++---- local/ai-commit/lua/ai-commit/prompts.lua | 5 + local/ai-commit/lua/ai-commit/providers.lua | 8 +- 5 files changed, 143 insertions(+), 32 deletions(-) diff --git a/local/ai-commit/lua/ai-commit/config.lua b/local/ai-commit/lua/ai-commit/config.lua index ed49b46..fcb6a46 100644 --- a/local/ai-commit/lua/ai-commit/config.lua +++ b/local/ai-commit/lua/ai-commit/config.lua @@ -16,6 +16,10 @@ M.values = { spinner_interval = 80, preview_lines = 5, max_diff_chars = 100000, + diff_context = { + small_changed_lines = 100, + medium_changed_lines = 500, + }, chat_timeout = 30000, model_highlight_group = 'Special', prompt_dump_path = vim.fn.stdpath 'log' .. '/ai-commit-last-prompt.md', diff --git a/local/ai-commit/lua/ai-commit/generator.lua b/local/ai-commit/lua/ai-commit/generator.lua index 73dd61c..be0bc8d 100644 --- a/local/ai-commit/lua/ai-commit/generator.lua +++ b/local/ai-commit/lua/ai-commit/generator.lua @@ -102,9 +102,9 @@ function M.insert() local done = false local aborted = false local http_jobs = {} - local context_total = 4 + local context_total = 5 local context_done = 0 - local pending = 4 + local pending = 5 local failed = false local last_status_line = nil local context = {} @@ -191,7 +191,7 @@ function M.insert() status(string.format('Preparing context (%d/%d)', context_done, context_total)) pending = pending - 1 if pending == 0 and not failed then - providers.generate_commit_message(context.branch, context.recent_commits, context.session_summary, context.diff, function(message) + providers.generate_commit_message(context.branch, context.recent_commits, context.session_summary, context.diff_stat, context.diff, function(message) finalize(message) end, status, vim.tbl_extend('force', request_context, { status_action = 'Generating commit message' })) elseif failed then @@ -228,7 +228,13 @@ function M.insert() mark_done() return end - logger.debug('Assistant session context loaded (provider=' .. tostring(session.label or session.provider) .. ' transcript_chars=' .. tostring(#(session.transcript or '')) .. ')') + logger.debug( + 'Assistant session context loaded (provider=' + .. tostring(session.label or session.provider) + .. ' transcript_chars=' + .. tostring(#(session.transcript or '')) + .. ')' + ) providers.summarize_session(session, function(summary) if done then return @@ -252,14 +258,30 @@ function M.insert() context.diff_meta = diff_meta logger.debug( string.format( - 'Staged diff context ready (chars=%d truncated=%s original_chars=%s)', + 'Staged diff context ready (chars=%d changed_lines=%s context_lines=%s truncated=%s original_chars=%s)', #diff, + tostring(diff_meta and diff_meta.changed_lines), + tostring(diff_meta and diff_meta.context_lines), tostring(diff_meta and diff_meta.truncated), tostring(diff_meta and diff_meta.original_chars) ) ) mark_done() end) + + git.staged_diff_stat(function(diff_stat) + if done then + return + end + if not diff_stat then + failed = true + finalize(nil) + return + end + context.diff_stat = diff_stat + logger.debug('Staged diff stat context ready (chars=' .. tostring(#diff_stat) .. ')') + mark_done() + end) end return M diff --git a/local/ai-commit/lua/ai-commit/git.lua b/local/ai-commit/lua/ai-commit/git.lua index bf10475..e140bb0 100644 --- a/local/ai-commit/lua/ai-commit/git.lua +++ b/local/ai-commit/lua/ai-commit/git.lua @@ -45,48 +45,126 @@ function M.recent_commits(count, callback) }):start() end +---@param numstat string[] +---@return integer +local function count_changed_lines(numstat) + local changed_lines = 0 + for _, line in ipairs(numstat) do + local additions, deletions = line:match '^(%S+)%s+(%S+)%s+' + additions = tonumber(additions) or 0 + deletions = tonumber(deletions) or 0 + changed_lines = changed_lines + additions + deletions + end + return changed_lines +end + +---@param changed_lines integer +---@return integer +local function diff_context_lines(changed_lines) + local diff_context = config.values.diff_context or {} + if changed_lines <= (diff_context.small_changed_lines or 100) then + return 2 + end + if changed_lines <= (diff_context.medium_changed_lines or 500) then + return 1 + end + return 0 +end + ---@param callback function(string|nil, table|nil) function M.staged_diff(callback) local logger = log() logger.debug 'Getting staged changes diff' + local Job = require 'plenary.job' + Job + :new({ + command = 'git', + args = { 'diff', '--cached', '--no-color', '--no-ext-diff', '--numstat' }, + on_exit = vim.schedule_wrap(function(numstat_job, numstat_code) + if numstat_code ~= 0 then + logger.error 'Failed to get staged changes numstat' + callback(nil, nil) + return + end + + local changed_lines = count_changed_lines(numstat_job:result()) + local unified = diff_context_lines(changed_lines) + logger.debug(string.format('Staged diff changed lines=%d using unified=%d', changed_lines, unified)) + + Job + :new({ + command = 'git', + args = { 'diff', '--cached', '--no-color', '--no-ext-diff', '--unified=' .. tostring(unified) }, + on_exit = vim.schedule_wrap(function(job, code) + if code ~= 0 then + logger.error 'Failed to get staged changes' + callback(nil, nil) + return + end + + local result = table.concat(job:result(), '\n') + if result == '' or result:match '^%s*$' then + logger.warn 'No staged changes found' + callback(nil, nil) + return + end + + local diff_meta = { + changed_lines = changed_lines, + context_lines = unified, + original_chars = #result, + sent_chars = #result, + truncated = false, + } + + if #result > config.values.max_diff_chars then + local head_len = math.floor(config.values.max_diff_chars * 0.7) + local tail_len = config.values.max_diff_chars - head_len + local tail_start = math.max(1, #result - tail_len + 1) + local marker = + string.format('\n\n[... diff truncated by ai_commit: original=%d chars, kept=%d chars ...]\n\n', #result, config.values.max_diff_chars) + result = result:sub(1, head_len) .. marker .. result:sub(tail_start) + diff_meta.truncated = true + diff_meta.sent_chars = #result + logger.warn(string.format('Staged diff exceeded max size, truncated to %d chars', config.values.max_diff_chars)) + end + + logger.info(string.format('Got staged diff (%d bytes, changed_lines=%d, unified=%d)', #result, changed_lines, unified)) + callback(result, diff_meta) + end), + }) + :start() + end), + }) + :start() +end + +---@param callback function(string|nil) +function M.staged_diff_stat(callback) + local logger = log() + logger.debug 'Getting staged changes diff stat' + local Job = require 'plenary.job' Job:new({ command = 'git', - args = { 'diff', '--cached', '--no-color', '--no-ext-diff' }, + args = { 'diff', '--cached', '--no-color', '--no-ext-diff', '--stat' }, on_exit = vim.schedule_wrap(function(job, code) if code ~= 0 then - logger.error 'Failed to get staged changes' - callback(nil, nil) + logger.error 'Failed to get staged changes diff stat' + callback(nil) return end local result = table.concat(job:result(), '\n') if result == '' or result:match '^%s*$' then - logger.warn 'No staged changes found' - callback(nil, nil) + logger.warn 'No staged changes diff stat found' + callback(nil) return end - local diff_meta = { - original_chars = #result, - sent_chars = #result, - truncated = false, - } - - if #result > config.values.max_diff_chars then - local head_len = math.floor(config.values.max_diff_chars * 0.7) - local tail_len = config.values.max_diff_chars - head_len - local tail_start = math.max(1, #result - tail_len + 1) - local marker = string.format('\n\n[... diff truncated by ai_commit: original=%d chars, kept=%d chars ...]\n\n', #result, config.values.max_diff_chars) - result = result:sub(1, head_len) .. marker .. result:sub(tail_start) - diff_meta.truncated = true - diff_meta.sent_chars = #result - logger.warn(string.format('Staged diff exceeded max size, truncated to %d chars', config.values.max_diff_chars)) - end - - logger.info(string.format('Got staged diff (%d bytes)', #result)) - callback(result, diff_meta) + logger.info(string.format('Got staged diff stat (%d bytes)', #result)) + callback(result) end), }):start() end diff --git a/local/ai-commit/lua/ai-commit/prompts.lua b/local/ai-commit/lua/ai-commit/prompts.lua index 1d6cfeb..fd7e31e 100644 --- a/local/ai-commit/lua/ai-commit/prompts.lua +++ b/local/ai-commit/lua/ai-commit/prompts.lua @@ -46,6 +46,11 @@ Recent commits: Recent assistant session context: %s +Staged Files: +``` +%s +``` + Staged changes: ``` %s diff --git a/local/ai-commit/lua/ai-commit/providers.lua b/local/ai-commit/lua/ai-commit/providers.lua index 2d89a2a..93322be 100644 --- a/local/ai-commit/lua/ai-commit/providers.lua +++ b/local/ai-commit/lua/ai-commit/providers.lua @@ -355,22 +355,24 @@ end ---@param branch string ---@param recent_commits string ---@param session_summary string|nil +---@param diff_stat string ---@param diff string ---@param callback function(string|nil, table|nil) ---@param status_callback function(string)|nil ---@param request_context table|nil -function M.generate_commit_message(branch, recent_commits, session_summary, diff, callback, status_callback, request_context) +function M.generate_commit_message(branch, recent_commits, session_summary, diff_stat, diff, callback, status_callback, request_context) local session_context = session_summary if type(session_context) ~= 'string' or session_context:match '^%s*$' then session_context = 'No recent assistant session context available.' end - local prompt = string.format(prompts.commit, branch, recent_commits, session_context, diff) + local prompt = string.format(prompts.commit, branch, recent_commits, session_context, diff_stat, diff) log().debug( string.format( - 'Commit prompt built (branch=%s commits_chars=%d session_context_chars=%d diff_chars=%d prompt_chars=%d has_session_context=%s)', + 'Commit prompt built (branch=%s commits_chars=%d session_context_chars=%d diff_stat_chars=%d diff_chars=%d prompt_chars=%d has_session_context=%s)', branch, #recent_commits, #session_context, + #diff_stat, #diff, #prompt, session_summary and 'yes' or 'no' -- 2.51.2