@@ -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