@@ -551,6 +551,193 @@ function DiffView:is_valid()
551551 return self.valid
552552end
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+
554741M.DiffView = DiffView
555742
556743return M
0 commit comments