Give the panels showing a commit's diff one voice about it

Five panels show a commit's diff in the focused main view — one file of it
from the commit files panel, the whole of it from the commits, sub-commits,
stash and reflog panels — and each answered for that diff itself, in the
same words.

Have them share one object instead, told only which diff it is, so that what
follows has a single place to say what can be done to a commit's diff and
needs saying once rather than five times.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-10-05 11:20:32 +02:00
co-authored by Claude Opus 5
parent f74f20b746
commit f5577d16c3
4 changed files with 114 additions and 46 deletions
@@ -0,0 +1,45 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// CommitDiffActions implements what a panel showing a commit's diff offers on that diff
// in the focused main view. Five panels do: the commit files panel shows the diff of one
// file of a commit, and the commits, sub-commits, stash and reflog panels the whole diff
// of whatever they have selected. They all offer the same thing and differ only in which
// diff they show, so they share this, each saying which diff that is.
type CommitDiffActions struct {
c *ControllerCommon
// The panel this belongs to, and what it is showing the diff of — nil when it has
// nothing selected, and so no diff.
panel types.Context
target func() *commitDiffTarget
}
// commitDiffTarget is the diff a panel is showing: the two ends of it, and whether it
// belongs to a commit lazygit may rewrite.
type commitDiffTarget struct {
from string
to string
canRebase bool
}
var _ types.FocusedMainViewDiffSource = &CommitDiffActions{}
func NewCommitDiffActions(
c *ControllerCommon, panel types.Context, target func() *commitDiffTarget,
) *CommitDiffActions {
return &CommitDiffActions{c: c, panel: panel, target: target}
}
// PlainDiff hands out the diff the panel is showing, for the given files — the same
// diff as in the main view, only without the commit's message and stat above it.
func (self *CommitDiffActions) PlainDiff(_ types.DiffPaneContext, paths []string) string {
target := self.target()
if target == nil {
return ""
}
return self.c.Helpers().Diff.PlainDiffBetweenRefs(target.from, target.to, paths)
}
+17 -10
View File
@@ -23,6 +23,9 @@ type CommitFilesController struct {
baseController
*ListControllerTrait[*filetree.CommitFileNode]
c *ControllerCommon
// what this panel offers on the diff it shows in the focused main view
diffActions *CommitDiffActions
}
var _ types.IController = &CommitFilesController{}
@@ -30,7 +33,7 @@ var _ types.IController = &CommitFilesController{}
func NewCommitFilesController(
c *ControllerCommon,
) *CommitFilesController {
return &CommitFilesController{
controller := &CommitFilesController{
baseController: baseController{},
c: c,
ListControllerTrait: NewListControllerTrait(
@@ -40,6 +43,18 @@ func NewCommitFilesController(
c.Contexts().CommitFiles.GetSelectedItems,
),
}
controller.diffActions = NewCommitDiffActions(c, c.Contexts().CommitFiles, controller.diffTarget)
return controller
}
// diffTarget is the commit whose files this panel is showing. Its main view shows the
// diff of that commit.
func (self *CommitFilesController) diffTarget() *commitDiffTarget {
if self.context().GetRef() == nil && self.context().GetRefRange() == nil {
return nil
}
from, to := self.context().GetFromAndToForDiff()
return &commitDiffTarget{from: from, to: to, canRebase: self.context().GetCanRebase()}
}
func (self *CommitFilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
@@ -193,15 +208,7 @@ func (self *CommitFilesController) GetOnRenderToMain() func() {
}
func (self *CommitFilesController) GetFocusedMainViewDiffSource() types.FocusedMainViewDiffSource {
return self
}
// PlainDiff hands out the commit's diff for the given files. Both panes show the same
// diff here — the secondary one shows the custom patch built from it, which is not a
// diff of the commit — so which pane asks makes no difference.
func (self *CommitFilesController) PlainDiff(_ types.DiffPaneContext, paths []string) string {
from, to := self.context().GetFromAndToForDiff()
return self.c.Helpers().Diff.PlainDiffBetweenRefs(from, to, paths)
return self.diffActions
}
func (self *CommitFilesController) copyDiffToClipboard(paths []string, toastMessage string) error {
@@ -10,6 +10,9 @@ type ReflogCommitsController struct {
baseController
*ListControllerTrait[*models.Commit]
c *ControllerCommon
// what this panel offers on the diff it shows in the focused main view
diffActions *CommitDiffActions
}
var _ types.IController = &ReflogCommitsController{}
@@ -17,7 +20,7 @@ var _ types.IController = &ReflogCommitsController{}
func NewReflogCommitsController(
c *ControllerCommon,
) *ReflogCommitsController {
return &ReflogCommitsController{
controller := &ReflogCommitsController{
baseController: baseController{},
ListControllerTrait: NewListControllerTrait(
c,
@@ -27,6 +30,19 @@ func NewReflogCommitsController(
),
c: c,
}
controller.diffActions = NewCommitDiffActions(c, c.Contexts().ReflogCommits, controller.diffTarget)
return controller
}
// diffTarget is the reflog entry the panel has selected, whose diff its main view
// shows. A reflog entry is never a commit of the checked-out branch as far as we are
// concerned, so nothing here may be rewritten.
func (self *ReflogCommitsController) diffTarget() *commitDiffTarget {
commit := self.context().GetSelected()
if commit == nil {
return nil
}
return &commitDiffTarget{from: commit.ParentRefName(), to: commit.RefName()}
}
func (self *ReflogCommitsController) Context() types.Context {
@@ -38,17 +54,7 @@ func (self *ReflogCommitsController) context() *context.ReflogCommitsContext {
}
func (self *ReflogCommitsController) GetFocusedMainViewDiffSource() types.FocusedMainViewDiffSource {
return self
}
// PlainDiff hands out the reflog entry's diff for the given files — the same diff its
// main view shows, only without the entry's message and stat above it.
func (self *ReflogCommitsController) PlainDiff(_ types.DiffPaneContext, paths []string) string {
commit := self.context().GetSelected()
if commit == nil {
return ""
}
return self.c.Helpers().Diff.PlainDiffBetweenRefs(commit.ParentRefName(), commit.RefName(), paths)
return self.diffActions
}
func (self *ReflogCommitsController) GetOnRenderToMain() func() {
@@ -25,17 +25,48 @@ type SwitchToDiffFilesController struct {
baseController
c *ControllerCommon
context CanSwitchToDiffFiles
// what this panel offers on the diff it shows in the focused main view
diffActions *CommitDiffActions
}
func NewSwitchToDiffFilesController(
c *ControllerCommon,
context CanSwitchToDiffFiles,
) *SwitchToDiffFilesController {
return &SwitchToDiffFilesController{
controller := &SwitchToDiffFilesController{
baseController: baseController{},
c: c,
context: context,
}
controller.diffActions = NewCommitDiffActions(c, context, controller.diffTarget)
return controller
}
// diffTarget is the commit — or stash entry, or range of commits — the panel has
// selected, whose whole diff its main view shows.
func (self *SwitchToDiffFilesController) diffTarget() *commitDiffTarget {
ref := self.context.GetSelectedRef()
if ref == nil {
return nil
}
refRange := self.context.GetSelectedRefRangeForDiffFiles()
from, to := context.FromAndToForDiff(ref, refRange)
return &commitDiffTarget{from: from, to: to, canRebase: self.canRebase(ref, refRange)}
}
// canRebase reports whether the given selection is one lazygit may rewrite: the panel
// has to allow it in the first place, a range of commits can't be rewritten as one,
// and in diffing mode what the main view shows is a diff against another ref rather
// than the commit itself, unless that other ref is the selected commit.
func (self *SwitchToDiffFilesController) canRebase(ref models.Ref, refRange *types.RefRange) bool {
if !self.context.CanRebase() {
return false
}
if self.c.Modes().Diffing.Active() {
return self.c.Modes().Diffing.Ref == ref.RefName()
}
return refRange == nil
}
func (self *SwitchToDiffFilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
@@ -66,19 +97,7 @@ func (self *SwitchToDiffFilesController) GetOnDoubleClick() func() error {
}
func (self *SwitchToDiffFilesController) GetFocusedMainViewDiffSource() types.FocusedMainViewDiffSource {
return self
}
// PlainDiff hands out the diff of the panel's selected commit (or range of them) for
// the given files — the same diff its main view shows, only without the commit's
// message and stat above it.
func (self *SwitchToDiffFilesController) PlainDiff(_ types.DiffPaneContext, paths []string) string {
ref := self.context.GetSelectedRef()
if ref == nil {
return ""
}
from, to := context.FromAndToForDiff(ref, self.context.GetSelectedRefRangeForDiffFiles())
return self.c.Helpers().Diff.PlainDiffBetweenRefs(from, to, paths)
return self.diffActions
}
func (self *SwitchToDiffFilesController) enter() error {
@@ -86,16 +105,7 @@ func (self *SwitchToDiffFilesController) enter() error {
refsRange := self.context.GetSelectedRefRangeForDiffFiles()
commitFilesContext := self.c.Contexts().CommitFiles
canRebase := self.context.CanRebase()
if canRebase {
if self.c.Modes().Diffing.Active() {
if self.c.Modes().Diffing.Ref != ref.RefName() {
canRebase = false
}
} else if refsRange != nil {
canRebase = false
}
}
canRebase := self.canRebase(ref, refsRange)
commitFilesContext.ClearFilter()
commitFilesContext.ReInit(ref, refsRange)