Skip to content

Commit 8fe38d6

Browse files
committed
feat(diff_view): add select_(next|previous)_commit
Other than some minor stylistic changes this commit was entirely produced by an LLM through the use of [Amp](https://ampcode.com/).
1 parent 4516612 commit 8fe38d6

File tree

3 files changed

+197
-2
lines changed

3 files changed

+197
-2
lines changed

doc/diffview.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,12 +1186,12 @@ select_last_entry *diffview-actions-select_last_en
11861186
Select the last entry.
11871187

11881188
[count] select_next_commit *diffview-actions-select_next_commit*
1189-
Contexts: `file_history_view`, `file_history_panel`
1189+
Contexts: `diff_view`, `file_history_view`, `file_history_panel`
11901190

11911191
Select the commit following the subject.
11921192

11931193
[count] select_prev_commit *diffview-actions-select_prev_commit*
1194-
Contexts: `file_history_view`, `file_history_panel`
1194+
Contexts: `diff_view`, `file_history_view`, `file_history_panel`
11951195

11961196
Select the commit preceding the subject.
11971197

lua/diffview/scene/views/diff/diff_view.lua

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,193 @@ function DiffView:is_valid()
551551
return self.valid
552552
end
553553

554+
---Helper function to navigate commit history
555+
---@param direction "next"|"prev" # Direction to navigate in commit history
556+
---@return string|nil # Commit hash or nil if none available
557+
DiffView._get_commit_in_direction = async.wrap(function(self, direction, callback)
558+
if not self._commit_history then
559+
-- Prevent race conditions by checking if we're already building
560+
if self._building_commit_history then
561+
callback(nil)
562+
return
563+
end
564+
565+
self._building_commit_history = true
566+
local err = await(self:_build_commit_history())
567+
self._building_commit_history = false
568+
569+
if err then
570+
callback(nil)
571+
return
572+
end
573+
end
574+
575+
local current_commit = self:_get_current_commit_hash()
576+
if not current_commit then
577+
callback(nil)
578+
return
579+
end
580+
581+
local current_idx = nil
582+
for i, commit_hash in ipairs(self._commit_history) do
583+
if commit_hash == current_commit then
584+
current_idx = i
585+
break
586+
end
587+
end
588+
589+
if not current_idx then
590+
callback(nil)
591+
return
592+
end
593+
594+
if direction == "next" then
595+
if current_idx >= #self._commit_history then
596+
callback(nil)
597+
else
598+
callback(self._commit_history[current_idx + 1])
599+
end
600+
elseif direction == "prev" then
601+
if current_idx <= 1 then
602+
callback(nil)
603+
else
604+
callback(self._commit_history[current_idx - 1])
605+
end
606+
else
607+
callback(nil)
608+
end
609+
end)
610+
611+
---Get the next commit in the commit history
612+
---@return string|nil # Next commit hash or nil if none available
613+
DiffView.get_older_commit = async.wrap(function(self, callback)
614+
local result = await(self:_get_commit_in_direction("next"))
615+
callback(result)
616+
end)
617+
618+
---Get the previous commit in the commit history
619+
---@return string|nil # Previous commit hash or nil if none available
620+
DiffView.get_newer_commit = async.wrap(function(self, callback)
621+
local result = await(self:_get_commit_in_direction("prev"))
622+
callback(result)
623+
end)
624+
625+
---Build commit history for navigation
626+
---@private
627+
---@return string|nil # Error message if failed
628+
DiffView._build_commit_history = async.wrap(function(self, callback)
629+
local Job = require("diffview.job").Job
630+
631+
-- Build git log arguments based on the diff view context
632+
local args = { "log", "--pretty=format:%H", "--no-merges", "--first-parent" }
633+
634+
-- Always use HEAD to get the full commit history for navigation
635+
-- We need the complete history to navigate forward/backward through commits
636+
table.insert(args, "HEAD")
637+
638+
-- Add path arguments if any
639+
if self.path_args and #self.path_args > 0 then
640+
table.insert(args, "--")
641+
for _, path in ipairs(self.path_args) do
642+
table.insert(args, path)
643+
end
644+
end
645+
646+
local job = Job({
647+
command = "git",
648+
args = args,
649+
cwd = self.adapter.ctx.toplevel,
650+
})
651+
652+
local ok = await(job)
653+
if not ok then
654+
callback("Failed to get commit history: " .. table.concat(job.stderr or {}, "\n"))
655+
return
656+
end
657+
658+
local raw_output = table.concat(job.stdout or {}, "\n")
659+
self._commit_history = vim.split(raw_output, "\n", { trimempty = true })
660+
callback(nil)
661+
end)
662+
663+
---Get current commit hash being viewed
664+
---@private
665+
---@return string|nil
666+
function DiffView:_get_current_commit_hash()
667+
if self.right.commit then
668+
-- Handle both cases: commit object with .hash property, or commit being the hash itself
669+
return type(self.right.commit) == "table" and self.right.commit.hash or self.right.commit
670+
elseif self.left.commit then
671+
-- Handle both cases: commit object with .hash property, or commit being the hash itself
672+
return type(self.left.commit) == "table" and self.left.commit.hash or self.left.commit
673+
end
674+
return nil
675+
end
676+
677+
---Set the current commit being viewed
678+
---@param commit_hash string
679+
DiffView.set_commit = async.void(function(self, commit_hash)
680+
local RevType = require("diffview.vcs.rev").RevType
681+
local Job = require("diffview.job").Job
682+
683+
-- Resolve the parent commit hash using git rev-parse
684+
local parent_job = Job({
685+
command = "git",
686+
args = { "rev-parse", commit_hash .. "^" },
687+
cwd = self.adapter.ctx.toplevel,
688+
})
689+
690+
local ok = await(parent_job)
691+
local new_left, new_right
692+
693+
if not ok or not parent_job.stdout or #parent_job.stdout == 0 then
694+
-- Fallback: use the string reference if we can't resolve it
695+
new_left = self.adapter.Rev(RevType.COMMIT, commit_hash .. "~1")
696+
new_right = self.adapter.Rev(RevType.COMMIT, commit_hash)
697+
else
698+
-- Use the resolved parent commit hash
699+
local parent_hash = vim.trim(parent_job.stdout[1])
700+
new_left = self.adapter.Rev(RevType.COMMIT, parent_hash)
701+
new_right = self.adapter.Rev(RevType.COMMIT, commit_hash)
702+
end
703+
704+
-- Update the view's revisions
705+
self.left = new_left
706+
self.right = new_right
707+
708+
-- Update the panel's pretty name to reflect the new commit
709+
-- For single commits, show the conventional git format: commit_hash^..commit_hash
710+
local right_abbrev = new_right:abbrev()
711+
self.panel.rev_pretty_name = right_abbrev .. "^.." .. right_abbrev
712+
713+
-- Update files and refresh the view
714+
self:update_files()
715+
716+
-- Update panel to show current commit info and refresh diff content
717+
vim.schedule(function()
718+
self.panel:render()
719+
self.panel:redraw()
720+
721+
-- If there's a currently selected file, update its revisions and refresh
722+
-- This needs to be scheduled to avoid fast event context issues
723+
if self.cur_entry then
724+
-- Update the current entry's file revisions to match the new commit
725+
if self.cur_entry.layout and self.cur_entry.layout.a and self.cur_entry.layout.b then
726+
-- Dispose old buffers to prevent cached stale content
727+
self.cur_entry.layout.a.file:dispose_buffer()
728+
self.cur_entry.layout.b.file:dispose_buffer()
729+
730+
-- Update the file objects with new revisions
731+
self.cur_entry.layout.a.file.rev = new_left
732+
self.cur_entry.layout.b.file.rev = new_right
733+
734+
-- Force refresh by calling use_entry again
735+
self:use_entry(self.cur_entry)
736+
end
737+
end
738+
end)
739+
end)
740+
554741
M.DiffView = DiffView
555742

556743
return M

lua/diffview/scene/views/diff/listeners.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,5 +329,13 @@ return function(view)
329329
local dir = view.panel:get_dir_at_cursor()
330330
if dir then view.panel:toggle_item_fold(dir) end
331331
end,
332+
select_next_commit = async.void(function()
333+
local commit = await(view:get_newer_commit())
334+
if commit then await(view:set_commit(commit)) end
335+
end),
336+
select_prev_commit = async.void(function()
337+
local commit = await(view:get_older_commit())
338+
if commit then await(view:set_commit(commit)) end
339+
end),
332340
}
333341
end

0 commit comments

Comments
 (0)