@@ -551,6 +551,193 @@ function DiffView:is_valid()
551
551
return self .valid
552
552
end
553
553
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
+
554
741
M .DiffView = DiffView
555
742
556
743
return M
0 commit comments