Skip to content

Commit 7f009f7

Browse files
authored
feat: add view history command (#11)
* get history command added * use vim.ui.input to support telescope and other pickers etc * stylua
1 parent 6b9b3a4 commit 7f009f7

13 files changed

Lines changed: 637 additions & 209 deletions

File tree

lua/coderabbit/history.lua

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
local M = {}
2+
3+
local function format_time(timestamp)
4+
if not timestamp then
5+
return "unknown"
6+
end
7+
return os.date("%Y-%m-%d %H:%M", timestamp)
8+
end
9+
10+
--- Format a storage entry into a display string for the picker.
11+
--- @param entry table { id, timestamp, context, finding_count }
12+
--- @return string
13+
function M.format_entry(entry)
14+
local ctx = entry.context or {}
15+
local parts = { string.format("#%d", entry.id) }
16+
table.insert(parts, format_time(entry.timestamp))
17+
if ctx.current_branch then
18+
table.insert(parts, ctx.current_branch)
19+
end
20+
if ctx.review_type then
21+
table.insert(parts, ctx.review_type)
22+
end
23+
table.insert(parts, string.format("%d finding%s", entry.finding_count, entry.finding_count == 1 and "" or "s"))
24+
return table.concat(parts, "")
25+
end
26+
27+
function M.open()
28+
local storage = require("coderabbit.storage")
29+
local entries = storage.list()
30+
31+
if #entries == 0 then
32+
vim.notify("CodeRabbit: No saved reviews yet", vim.log.levels.INFO)
33+
return
34+
end
35+
36+
vim.ui.select(entries, {
37+
prompt = "CodeRabbit Review History",
38+
format_item = M.format_entry,
39+
}, function(entry)
40+
if entry then
41+
require("coderabbit.show").open(entry.id)
42+
end
43+
end)
44+
end
45+
46+
return M

lua/coderabbit/init.lua

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ function M.clear()
2020
require("coderabbit.review").clear()
2121
end
2222

23-
function M.show()
24-
require("coderabbit.show").open()
23+
function M.show(id)
24+
require("coderabbit.show").open(id)
25+
end
26+
27+
function M.history()
28+
require("coderabbit.history").open()
2529
end
2630

2731
function M.status()

lua/coderabbit/review.lua

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ local state = {
2121
base_commit = nil,
2222
}
2323

24+
local storage = require("coderabbit.storage")
25+
2426
local function spinner()
2527
local idx = math.floor(vim.uv.hrtime() / (1e6 * FRAME_MS)) % #spinner_frames + 1
2628
return spinner_frames[idx]
@@ -74,6 +76,14 @@ function M.get_context()
7476
}
7577
end
7678

79+
function M.get_history()
80+
return storage.list()
81+
end
82+
83+
function M.get_review(id)
84+
return storage.load(id)
85+
end
86+
7787
--- Return a short status string for statusline integration.
7888
--- Returns nil when no review is running.
7989
function M.status()
@@ -199,6 +209,8 @@ function M.run(opts)
199209
fidget_finish("done (with errors)")
200210
end
201211

212+
storage.save(state.findings, M.get_context())
213+
202214
if cfg.on_review_complete then
203215
cfg.on_review_complete(state.findings)
204216
end

lua/coderabbit/show.lua

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,24 @@ function M.render(findings, context, opts)
128128
return lines
129129
end
130130

131-
function M.open()
131+
function M.open(id)
132132
local review = require("coderabbit.review")
133-
local findings = review.get_results()
134-
local context = review.get_context()
135-
local running = review.is_running()
133+
local findings, context, running
134+
135+
if id then
136+
local entry = review.get_review(id)
137+
if not entry then
138+
vim.notify("CodeRabbit: Review #" .. id .. " not found", vim.log.levels.WARN)
139+
return
140+
end
141+
findings = entry.findings
142+
context = entry.context
143+
running = false
144+
else
145+
findings = review.get_results()
146+
context = review.get_context()
147+
running = review.is_running()
148+
end
136149

137150
if #findings == 0 and not running and not context then
138151
vim.notify("CodeRabbit: No review results. Run :CodeRabbitReview first", vim.log.levels.WARN)

lua/coderabbit/storage.lua

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
local M = {}
2+
3+
local base_dir = vim.fn.stdpath("data") .. "/coderabbit"
4+
5+
--- Override the base directory (for testing only).
6+
function M._set_base_dir(dir)
7+
base_dir = dir
8+
end
9+
10+
--- Resolve the git root for the current repo.
11+
--- @return string|nil
12+
local function git_root()
13+
local out = vim.fn.systemlist({ "git", "rev-parse", "--show-toplevel" })
14+
if vim.v.shell_error ~= 0 or not out[1] or out[1] == "" then
15+
return nil
16+
end
17+
return out[1]
18+
end
19+
20+
--- Turn a git root path into a safe directory name.
21+
--- e.g. /Users/sam/projects/my-repo -> Users-sam-projects-my-repo
22+
--- C:\Users\me\repo -> Users-me-repo
23+
local function repo_key(root)
24+
local key = root
25+
-- Strip Windows drive letter (e.g. "C:")
26+
key = key:gsub("^%a:", "")
27+
-- Strip leading separators
28+
key = key:gsub("^[\\/]+", "")
29+
-- Replace all path separators with dashes
30+
key = key:gsub("[\\/]+", "-")
31+
return key
32+
end
33+
34+
--- Return the per-repo review storage directory.
35+
--- @return string
36+
local function repo_dir()
37+
local root = git_root()
38+
if not root then
39+
return base_dir .. "/_unknown"
40+
end
41+
return base_dir .. "/" .. repo_key(root)
42+
end
43+
44+
local function ensure_dir(dir)
45+
vim.fn.mkdir(dir, "p")
46+
end
47+
48+
--- Format a timestamp for use as a filename.
49+
--- @param ts number epoch seconds
50+
--- @return string e.g. "2026-04-14_15-30-00"
51+
local function ts_filename(ts)
52+
return os.date("%Y-%m-%d_%H-%M-%S", ts)
53+
end
54+
55+
--- Save a completed review to disk.
56+
--- @param findings table[] Array of { diagnostic, filepath }
57+
--- @param context table|nil Review context metadata
58+
--- @return string|nil filename The review filename (without path), or nil if nothing saved
59+
function M.save(findings, context)
60+
if #findings == 0 then
61+
return nil
62+
end
63+
local dir = repo_dir()
64+
ensure_dir(dir)
65+
local ts = os.time()
66+
local entry = {
67+
findings = findings,
68+
context = context,
69+
timestamp = ts,
70+
}
71+
local json = vim.json.encode(entry)
72+
-- Avoid collisions when multiple reviews finish in the same second
73+
local base = ts_filename(ts)
74+
local filename = base .. ".json"
75+
local path = dir .. "/" .. filename
76+
local suffix = 1
77+
while vim.fn.filereadable(path) == 1 do
78+
filename = base .. "_" .. suffix .. ".json"
79+
path = dir .. "/" .. filename
80+
suffix = suffix + 1
81+
end
82+
local file = io.open(path, "w")
83+
if not file then
84+
return nil
85+
end
86+
local ok = file:write(json)
87+
file:close()
88+
if not ok then
89+
return nil
90+
end
91+
return filename
92+
end
93+
94+
--- List all saved reviews for the current repo (summary only, no findings).
95+
--- Returns entries sorted chronologically (oldest first), each with an
96+
--- ordinal `id` field for display / lookup.
97+
--- @return table[] Array of { id, timestamp, context, finding_count, filename }
98+
function M.list()
99+
local dir = repo_dir()
100+
ensure_dir(dir)
101+
local files = vim.fn.glob(dir .. "/*.json", false, true)
102+
table.sort(files)
103+
104+
local entries = {}
105+
for i, path in ipairs(files) do
106+
local file = io.open(path, "r")
107+
if file then
108+
local content = file:read("*a")
109+
file:close()
110+
local ok, data = pcall(vim.json.decode, content)
111+
if ok and type(data) == "table" then
112+
table.insert(entries, {
113+
id = i,
114+
timestamp = data.timestamp,
115+
context = data.context,
116+
finding_count = data.findings and #data.findings or 0,
117+
filename = vim.fn.fnamemodify(path, ":t"),
118+
})
119+
end
120+
end
121+
end
122+
return entries
123+
end
124+
125+
--- Load a review by ordinal ID (1-indexed position in chronological order).
126+
--- @param id number
127+
--- @return table|nil
128+
function M.load(id)
129+
local dir = repo_dir()
130+
local files = vim.fn.glob(dir .. "/*.json", false, true)
131+
table.sort(files)
132+
133+
local path = files[id]
134+
if not path then
135+
return nil
136+
end
137+
local file = io.open(path, "r")
138+
if not file then
139+
return nil
140+
end
141+
local content = file:read("*a")
142+
file:close()
143+
local ok, data = pcall(vim.json.decode, content)
144+
if not ok or type(data) ~= "table" then
145+
return nil
146+
end
147+
data.id = id
148+
return data
149+
end
150+
151+
--- Return list of existing review IDs as strings (for command completion).
152+
--- @return string[]
153+
function M.ids()
154+
local dir = repo_dir()
155+
ensure_dir(dir)
156+
local files = vim.fn.glob(dir .. "/*.json", false, true)
157+
local ids = {}
158+
for i = 1, #files do
159+
table.insert(ids, tostring(i))
160+
end
161+
return ids
162+
end
163+
164+
return M

plugin/coderabbit.lua

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,21 @@ end, {
3636
desc = "Clear CodeRabbit diagnostics",
3737
})
3838

39-
vim.api.nvim_create_user_command("CodeRabbitShow", function()
39+
vim.api.nvim_create_user_command("CodeRabbitShow", function(args)
4040
ensure_setup()
41-
require("coderabbit").show()
41+
local id = args.fargs[1] and tonumber(args.fargs[1]) or nil
42+
require("coderabbit").show(id)
4243
end, {
43-
desc = "Show CodeRabbit review results in a buffer",
44+
nargs = "?",
45+
complete = function()
46+
return require("coderabbit.storage").ids()
47+
end,
48+
desc = "Show CodeRabbit review results in a buffer (optional: review ID)",
49+
})
50+
51+
vim.api.nvim_create_user_command("CodeRabbitHistory", function()
52+
ensure_setup()
53+
require("coderabbit").history()
54+
end, {
55+
desc = "Browse CodeRabbit review history",
4456
})

0 commit comments

Comments
 (0)