From fccfdf7635c317e7f03de752ea761f184d90c43f Mon Sep 17 00:00:00 2001 From: Radu Alexe Date: Wed, 22 Jul 2026 22:45:20 +0300 Subject: [PATCH] Add commitDisplayFormat option with normal/comfortable/spacious modes Add a git.log.commitDisplayFormat user config controlling how commits are rendered in the commits and sub-commits panels, switchable at runtime from the log menu (ctrl+l): - normal: the existing one-line-per-commit layout - comfortable: two lines per commit, with author and date on an indented second line, freeing the full width for the subject - spacious: like comfortable, plus an empty graph row between commits The extra lines are implemented as non-model items (like section headers), so commit-level navigation, search and selection are unaffected. Each inserted line starts with the graph's connector characters (rendered from the same cached pipe sets as the commit rows) so branch lanes remain continuous, including the selection highlight of the selected commit's path. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MacRoM2ci19SpyNzGnpr9S --- pkg/config/user_config.go | 12 +++- pkg/gui/context/local_commits_context.go | 60 +++++++++++++++++++ pkg/gui/context/sub_commits_context.go | 9 +++ .../controllers/local_commits_controller.go | 36 +++++++++++ pkg/gui/presentation/commits.go | 29 +++++++++ pkg/gui/presentation/commits_test.go | 1 + pkg/gui/presentation/graph/graph.go | 38 ++++++++++++ pkg/i18n/english.go | 4 ++ 8 files changed, 186 insertions(+), 3 deletions(-) diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 30ce0377d5a..fe2e8b8f648 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -417,6 +417,11 @@ type LogConfig struct { ShowGraph string `yaml:"showGraph" jsonschema:"enum=always,enum=never,enum=when-maximised"` // displays the whole git graph by default in the commits view (equivalent to passing the `--all` argument to `git log`) ShowWholeGraph bool `yaml:"showWholeGraph"` + // Controls how much information is shown per commit in the commits panel. + // One of 'normal' | 'comfortable' | 'spacious' + // + // Can be changed from within lazygit with `Log menu -> Commit display format` (`` in the commits window by default). + CommitDisplayFormat string `yaml:"commitDisplayFormat" jsonschema:"enum=normal,enum=comfortable,enum=spacious"` } type CommitPrefixConfig struct { @@ -942,9 +947,10 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig { SquashMergeMessage: "Squash merge {{selectedRef}} into {{currentBranch}}", }, Log: LogConfig{ - Order: "topo-order", - ShowGraph: "always", - ShowWholeGraph: false, + Order: "topo-order", + ShowGraph: "always", + ShowWholeGraph: false, + CommitDisplayFormat: "normal", }, LocalBranchSortOrder: "date", RemoteBranchSortOrder: "date", diff --git a/pkg/gui/context/local_commits_context.go b/pkg/gui/context/local_commits_context.go index a66c720c9ad..364dac45c15 100644 --- a/pkg/gui/context/local_commits_context.go +++ b/pkg/gui/context/local_commits_context.go @@ -3,6 +3,7 @@ package context import ( "fmt" "log" + "sort" "strings" "sync/atomic" "time" @@ -10,7 +11,9 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/presentation" + "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -63,6 +66,7 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext { endIdx, shouldShowGraph(c), c.Model().BisectInfo, + c.UserConfig().Git.Log.CommitDisplayFormat, ) } @@ -108,6 +112,14 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext { }) } + var selectedCommitHashPtr *string + if c.Context().Current().GetKey() == LOCAL_COMMITS_CONTEXT_KEY { + if selectedCommit := viewModel.GetSelected(); selectedCommit != nil { + selectedCommitHashPtr = selectedCommit.HashPtr() + } + } + result = appendCommitDetailItems(c, c.Model().Commits, selectedCommitHashPtr, result) + return result } @@ -247,6 +259,54 @@ func (self *LocalCommitsViewModel) GetCommits() []*models.Commit { return self.getModel() } +// In the 'comfortable' and 'spacious' display formats, insert an extra +// (unselectable) line below each commit showing author and date, similar to +// `git log`'s two-line layouts. The line starts with the graph's connector +// characters so the graph stays continuous, and is rendered from the +// graph/subject column so it aligns with the commit subject. 'spacious' +// additionally inserts an empty connector-only row between commits. The +// resulting list must stay sorted by Index, which is what the list renderer +// assumes, so we re-sort after appending. +func appendCommitDetailItems(c *ContextCommon, commits []*models.Commit, selectedCommitHashPtr *string, result []*NonModelItem) []*NonModelItem { + format := c.UserConfig().Git.Log.CommitDisplayFormat + if format != "comfortable" && format != "spacious" { + return result + } + + var connectorLines []string + if shouldShowGraph(c) { + connectorLines = presentation.GetCommitGraphConnectorLines(commits, selectedCommitHashPtr) + } + + now := time.Now() + timeFormat := c.UserConfig().Gui.TimeFormat + shortTimeFormat := c.UserConfig().Gui.ShortTimeFormat + for i, commit := range commits { + if commit.IsTODO() { + continue + } + connector := "" + if connectorLines != nil { + connector = connectorLines[i] + } + result = append(result, &NonModelItem{ + Index: i + 1, + Column: 6, // the graph+subject column produced by displayCommit + Content: connector + style.FgCyan.Sprint(commit.AuthorName) + " " + + style.FgBlue.Sprint(utils.UnixToDateSmart(now, commit.UnixTimestamp, timeFormat, shortTimeFormat)), + }) + if format == "spacious" && i < len(commits)-1 { + result = append(result, &NonModelItem{ + Index: i + 1, + Column: 6, + Content: connector, + }) + } + } + sort.SliceStable(result, func(a, b int) bool { return result[a].Index < result[b].Index }) + return result +} + func shouldShowGraph(c *ContextCommon) bool { if c.Modes().Filtering.Active() { return false diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go index 4e05c9594a2..3c200dc9750 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -77,6 +77,7 @@ func NewSubCommitsContext( endIdx, shouldShowGraph(c), git_commands.NewNullBisectInfo(), + c.UserConfig().Git.Log.CommitDisplayFormat, ) } @@ -104,6 +105,14 @@ func NewSubCommitsContext( }) } + var selectedCommitHashPtr *string + if c.Context().Current().GetKey() == SUB_COMMITS_CONTEXT_KEY { + if selectedCommit := viewModel.GetSelected(); selectedCommit != nil { + selectedCommitHashPtr = selectedCommit.HashPtr() + } + } + result = appendCommitDetailItems(c, c.Model().SubCommits, selectedCommitHashPtr, result) + return result } diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 2da502b79c5..49e89e8dc6b 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -1302,6 +1302,42 @@ func (self *LocalCommitsController) handleOpenLogMenu() error { }) }, }, + { + Label: self.c.Tr.CommitDisplayFormat, + Tooltip: self.c.Tr.CommitDisplayFormatTooltip, + OpensMenu: true, + OnPress: func() error { + currentValue := self.c.UserConfig().Git.Log.CommitDisplayFormat + onPress := func(value string) func() error { + return func() error { + self.c.UserConfig().Git.Log.CommitDisplayFormat = value + self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits) + self.c.PostRefreshUpdate(self.c.Contexts().SubCommits) + return nil + } + } + return self.c.Menu(types.CreateMenuOptions{ + Title: self.c.Tr.CommitDisplayFormat, + Items: []*types.MenuItem{ + { + Label: "normal", + OnPress: onPress("normal"), + Widget: types.MakeMenuRadioButton(currentValue == "normal"), + }, + { + Label: "comfortable", + OnPress: onPress("comfortable"), + Widget: types.MakeMenuRadioButton(currentValue == "comfortable"), + }, + { + Label: "spacious", + OnPress: onPress("spacious"), + Widget: types.MakeMenuRadioButton(currentValue == "spacious"), + }, + }, + }) + }, + }, { Label: self.c.Tr.SortCommits, Tooltip: self.c.Tr.SortCommitsTooltip, diff --git a/pkg/gui/presentation/commits.go b/pkg/gui/presentation/commits.go index 67fa62ac82c..3b03346c171 100644 --- a/pkg/gui/presentation/commits.go +++ b/pkg/gui/presentation/commits.go @@ -56,6 +56,7 @@ func GetCommitListDisplayStrings( endIdx int, showGraph bool, bisectInfo *git_commands.BisectInfo, + commitDisplayFormat string, ) [][]string { mutex.Lock() defer mutex.Unlock() @@ -204,11 +205,34 @@ func GetCommitListDisplayStrings( fullDescription, bisectStatus, bisectInfo, + commitDisplayFormat, )) } return lines } +// GetCommitGraphConnectorLines returns, for each commit, the graph connector +// line to draw below it. Used by the two-line display formats ('comfortable' +// and 'spacious') for their inserted author/date rows, so that the graph stays +// continuous. Entries are empty for TODO commits and in divergence views +// (where no single graph spans the list). +func GetCommitGraphConnectorLines(commits []*models.Commit, selectedCommitHashPtr *string) []string { + mutex.Lock() + defer mutex.Unlock() + + result := make([]string, len(commits)) + if len(commits) == 0 || commits[0].Divergence != models.DivergenceNone { + return result + } + + rebaseOffset := indexOfFirstNonTODOCommit(commits) + pipeSets := loadPipesets(commits[rebaseOffset:]) + for i, pipeSet := range pipeSets { + result[rebaseOffset+i] = graph.RenderConnectorRow(pipeSet, selectedCommitHashPtr) + } + return result +} + func getbisectBounds(commits []*models.Commit, bisectInfo *git_commands.BisectInfo) *bisectBounds { if !bisectInfo.Bisecting() { return nil @@ -355,6 +379,7 @@ func displayCommit( fullDescription bool, bisectStatus BisectStatus, bisectInfo *git_commands.BisectInfo, + commitDisplayFormat string, ) []string { bisectString := getBisectStatusText(bisectStatus, bisectInfo) @@ -438,6 +463,10 @@ func displayCommit( authorLength = common.UserConfig().Gui.CommitAuthorLongLength } author := authors.AuthorWithLength(commit.AuthorName, authorLength) + // The two-line formats show the author on the second line instead + if commitDisplayFormat == "comfortable" || commitDisplayFormat == "spacious" { + author = "" + } cols := make([]string, 0, 7) cols = append( diff --git a/pkg/gui/presentation/commits_test.go b/pkg/gui/presentation/commits_test.go index 12e0fc1d661..e2e57bfc466 100644 --- a/pkg/gui/presentation/commits_test.go +++ b/pkg/gui/presentation/commits_test.go @@ -566,6 +566,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) { s.endIdx, s.showGraph, s.bisectInfo, + "normal", ) renderedLines, _ := utils.RenderDisplayStrings(result, nil) diff --git a/pkg/gui/presentation/graph/graph.go b/pkg/gui/presentation/graph/graph.go index 1639a62e62e..0d3b364142d 100644 --- a/pkg/gui/presentation/graph/graph.go +++ b/pkg/gui/presentation/graph/graph.go @@ -106,6 +106,44 @@ func RenderAux(pipeSets [][]Pipe, commits []*models.Commit, selectedCommitHashPt return lo.Flatten(chunks) } +// RenderConnectorRow renders the row of vertical connectors to draw below the +// commit row rendered from the same pipe set. The width matches the commit +// row's graph width, so text following the connectors aligns with the commit's +// subject. Pipes sourced from the selected commit get the same highlight style +// as in the commit rows, so the highlighted path stays unbroken. +func RenderConnectorRow(pipes []Pipe, selectedCommitHashPtr *string) string { + maxPos := int16(0) + for _, pipe := range pipes { + if pipe.right() > maxPos { + maxPos = pipe.right() + } + } + + styles := make([]*style.TextStyle, maxPos+1) + for _, pipe := range pipes { + if pipe.kind != TERMINATES && styles[pipe.toPos] == nil { + styles[pipe.toPos] = pipe.style + } + } + for _, pipe := range pipes { + if pipe.kind != TERMINATES && equalHashes(pipe.fromHash, selectedCommitHashPtr) { + styles[pipe.toPos] = &highlightStyle + } + } + + writer := &strings.Builder{} + writer.Grow(len(styles) * 2) + for _, s := range styles { + if s != nil { + writer.WriteString(s.Sprint("│")) + } else { + writer.WriteString(" ") + } + writer.WriteString(" ") + } + return writer.String() +} + func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *models.Commit) *style.TextStyle) []Pipe { maxPos := int16(0) for _, pipe := range prevPipes { diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 2e83fed9bde..a5c11e83c11 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -821,6 +821,8 @@ type TranslationSet struct { SortOrderPrompt string SortCommits string SortCommitsTooltip string + CommitDisplayFormat string + CommitDisplayFormatTooltip string CantChangeContextSizeError string CantChangeRenameThresholdError string OpenCommitInBrowser string @@ -1967,6 +1969,8 @@ func EnglishTranslationSet() *TranslationSet { SortBasedOnReflog: "(based on reflog)", SortCommits: "Commit sort order", SortCommitsTooltip: "Change the sort order of the commits in the commit log.\n\nThe default can be changed in the config file with the key 'git.log.sortOrder'.", + CommitDisplayFormat: "Commit display format", + CommitDisplayFormatTooltip: "Change how commits are displayed in the commit log.\n\nnormal: one line per commit with hash, author, and subject\ncomfortable: two lines per commit, author and date on the second line\nspacious: like comfortable, plus an empty graph row between commits\n\nThe default can be changed in the config file with the key 'git.log.commitDisplayFormat'.", CantChangeContextSizeError: "Cannot change context while in patch building mode because we were too lazy to support it when releasing the feature. If you really want it, please let us know!", CantChangeRenameThresholdError: "Cannot change the rename similarity threshold while in patch building mode, because the custom patch can't cope with a rename turning into a delete and add underneath it.", OpenCommitInBrowser: "Open commit in browser",