Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
```
Expand Down
6 changes: 6 additions & 0 deletions doc/coderabbit.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ All options are optional. Defaults: >lua
quickfix = {
auto = false,
},
history = {
max_entries = 50,
},
on_review_complete = nil,
})
<
Expand Down Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions lua/coderabbit/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ M.defaults = {
quickfix = {
auto = false,
},
history = {
max_entries = 50,
},
on_review_complete = nil,
}

Expand Down
97 changes: 85 additions & 12 deletions lua/coderabbit/storage.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local M = {}

local config = require("coderabbit.config")
local utils = require("coderabbit.utils")
local base_dir = vim.fn.stdpath("data") .. "/coderabbit"

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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))
Expand Down
25 changes: 25 additions & 0 deletions tests/coderabbit/storage_spec.lua
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
-- ──────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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()
Loading