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",