diff --git a/README.md b/README.md index c2ae6b3..e2c3571 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,9 @@ require("coderabbit").setup({ quickfix = { auto = false, -- populate on review complete }, + history = { + max_entries = 50, -- keep the most recent saved reviews per repo + }, on_review_complete = nil, }) ``` diff --git a/doc/coderabbit.txt b/doc/coderabbit.txt index 38543ce..bbdec5c 100644 --- a/doc/coderabbit.txt +++ b/doc/coderabbit.txt @@ -65,6 +65,9 @@ All options are optional. Defaults: >lua quickfix = { auto = false, }, + history = { + max_entries = 50, + }, on_review_complete = nil, }) < @@ -93,6 +96,9 @@ show.float.border Border style for the floating window. Default: `"rounded quickfix.auto Populate the quickfix list automatically when a review completes. Default: `false`. +history.max_entries Maximum saved reviews kept per repo. Default: `50`. + Set to `0` to keep all saved reviews. + on_review_complete Callback receiving the findings table when a review finishes. diff --git a/lua/coderabbit/config.lua b/lua/coderabbit/config.lua index d96b53f..355bd9e 100644 --- a/lua/coderabbit/config.lua +++ b/lua/coderabbit/config.lua @@ -33,6 +33,9 @@ M.defaults = { quickfix = { auto = false, }, + history = { + max_entries = 50, + }, on_review_complete = nil, } diff --git a/lua/coderabbit/storage.lua b/lua/coderabbit/storage.lua index 7debd70..dfdf719 100644 --- a/lua/coderabbit/storage.lua +++ b/lua/coderabbit/storage.lua @@ -1,5 +1,6 @@ local M = {} +local config = require("coderabbit.config") local utils = require("coderabbit.utils") local base_dir = vim.fn.stdpath("data") .. "/coderabbit" @@ -53,6 +54,85 @@ local function ts_filename(ts) return os.date("%Y-%m-%d_%H-%M-%S", ts) end +local function review_file_parts(path) + local name = vim.fn.fnamemodify(path, ":t") + local stamp, suffix = name:match("^(%d%d%d%d%-%d%d%-%d%d_%d%d%-%d%d%-%d%d)_?(%d*)%.json$") + if not stamp then + return name, 0, name + end + return stamp, tonumber(suffix) or 0, name +end + +local function sorted_review_files(dir) + local files = vim.fn.glob(dir .. "/*.json", false, true) + table.sort(files, function(a, b) + local a_stamp, a_suffix, a_name = review_file_parts(a) + local b_stamp, b_suffix, b_name = review_file_parts(b) + + if a_stamp ~= b_stamp then + return a_stamp < b_stamp + end + if a_suffix ~= b_suffix then + return a_suffix < b_suffix + end + return a_name < b_name + end) + return files +end + +local function max_entries() + local history_cfg = config.get().history + if type(history_cfg) ~= "table" then + return nil + end + + local raw_max = history_cfg.max_entries + if type(raw_max) ~= "number" and type(raw_max) ~= "string" then + return nil + end + + local max = tonumber(raw_max) + if not max or max < 1 then + return nil + end + return math.floor(max) +end + +local function prune_old_reviews(dir) + local max = max_entries() + if not max then + return + end + + local files = sorted_review_files(dir) + local remove_count = #files - max + for i = 1, remove_count do + vim.fn.delete(files[i]) + end +end + +local function next_filename(dir, base) + local prefix = base .. "_" + local max_suffix = -1 + + for _, path in ipairs(vim.fn.glob(dir .. "/" .. base .. "*.json", false, true)) do + local name = vim.fn.fnamemodify(path, ":t") + if name == base .. ".json" then + max_suffix = math.max(max_suffix, 0) + elseif name:sub(1, #prefix) == prefix and name:sub(-5) == ".json" then + local suffix = tonumber(name:sub(#prefix + 1, -6)) + if suffix then + max_suffix = math.max(max_suffix, suffix) + end + end + end + + if max_suffix < 0 then + return base .. ".json" + end + return base .. "_" .. (max_suffix + 1) .. ".json" +end + --- Save a completed review to disk. --- @param findings table[] Array of { diagnostic, filepath } --- @param context table|nil Review context metadata @@ -72,17 +152,12 @@ function M.save(findings, context) local json = vim.json.encode(entry) -- Avoid collisions when multiple reviews finish in the same second local base = ts_filename(ts) - local filename = base .. ".json" + local filename = next_filename(dir, base) local path = dir .. "/" .. filename - local suffix = 1 - while vim.fn.filereadable(path) == 1 do - filename = base .. "_" .. suffix .. ".json" - path = dir .. "/" .. filename - suffix = suffix + 1 - end if not utils.write_file(path, json) then return nil end + prune_old_reviews(dir) return filename end @@ -93,8 +168,7 @@ end function M.list() local dir = repo_dir() ensure_dir(dir) - local files = vim.fn.glob(dir .. "/*.json", false, true) - table.sort(files) + local files = sorted_review_files(dir) local entries = {} for i, path in ipairs(files) do @@ -120,8 +194,7 @@ end --- @return table|nil function M.load(id) local dir = repo_dir() - local files = vim.fn.glob(dir .. "/*.json", false, true) - table.sort(files) + local files = sorted_review_files(dir) local path = files[id] if not path then @@ -144,7 +217,7 @@ end function M.ids() local dir = repo_dir() ensure_dir(dir) - local files = vim.fn.glob(dir .. "/*.json", false, true) + local files = sorted_review_files(dir) local ids = {} for i = 1, #files do table.insert(ids, tostring(i)) diff --git a/tests/coderabbit/storage_spec.lua b/tests/coderabbit/storage_spec.lua index ea54ad9..7bad890 100644 --- a/tests/coderabbit/storage_spec.lua +++ b/tests/coderabbit/storage_spec.lua @@ -1,4 +1,5 @@ local storage = require("coderabbit.storage") +local config = require("coderabbit.config") local h = require("tests.helpers") local test, eq = h.test, h.eq @@ -17,6 +18,15 @@ local function make_findings(n) return findings end +local function with_history_max(max, fn) + config.setup({ history = { max_entries = max } }) + local ok, err = pcall(fn) + config.setup({}) + if not ok then + error(err, 2) + end +end + -- ────────────────────────────────────────────────────────── -- Tests (table-driven where possible) -- ────────────────────────────────────────────────────────── @@ -80,5 +90,20 @@ test("ids: returns empty when no reviews", function() eq(#storage.ids(), 0) end) +test("save: prunes oldest reviews according to history.max_entries", function() + cleanup() + with_history_max(3, function() + for i = 1, 5 do + storage.save(make_findings(1), h.context("review-" .. i)) + end + + local entries = storage.list() + eq(#entries, 3) + eq(entries[1].context.current_branch, "review-3") + eq(entries[2].context.current_branch, "review-4") + eq(entries[3].context.current_branch, "review-5") + end) +end) + cleanup() h.summary()