diff --git a/docs-master/Config.md b/docs-master/Config.md index 1d101be1863..ae4776eddf2 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -799,6 +799,7 @@ keybinding: resetCherryPick: copyCommitAttributeToClipboard: "y" openLogMenu: + toggleOverviewMode: "~" openInBrowser: o openPullRequestInBrowser: G viewBisectOptions: b diff --git a/docs-master/keybindings/Keybindings_en.md b/docs-master/keybindings/Keybindings_en.md index 4aa202740d0..b2d0e2285bf 100644 --- a/docs-master/keybindings/Keybindings_en.md +++ b/docs-master/keybindings/Keybindings_en.md @@ -106,6 +106,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` t `` | Revert | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. | | `` T `` | Tag commit | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. | | `` `` | View log options | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. | +| `` ~ `` | Toggle overview mode | Only show merge commits, and commits that a local or remote branch, a tag, or HEAD points to; hide all other commits. Useful for getting a high-level view of the history. | | `` G `` | Open pull request in browser | | | `` `` | Checkout | Checkout the selected commit as a detached HEAD. | | `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). | diff --git a/docs-master/keybindings/Keybindings_ja.md b/docs-master/keybindings/Keybindings_ja.md index 5b9c798a1e8..043e9c140da 100644 --- a/docs-master/keybindings/Keybindings_ja.md +++ b/docs-master/keybindings/Keybindings_ja.md @@ -86,6 +86,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` t `` | リバート | 選択したコミットの変更を逆に適用する、リバートコミットを作成します。 | | `` T `` | コミットにタグを付ける | 選択したコミットを指すタグを新規作成します。タグ名とオプションの説明を入力するよう促されます。 | | `` `` | ログオプションを表示 | コミットログのオプションを表示します(例:並び順の変更、Gitグラフの非表示、Gitグラフ全体の表示)。 | +| `` ~ `` | Toggle overview mode | Only show merge commits, and commits that a local or remote branch, a tag, or HEAD points to; hide all other commits. Useful for getting a high-level view of the history. | | `` G `` | Open pull request in browser | | | `` `` | チェックアウト(ブランチの切り替え) | 選択したコミットをデタッチドヘッド(特定のブランチに属さない状態)としてチェックアウトします。 | | `` y `` | コミット属性をクリップボードにコピー | コミット属性をクリップボードにコピーします(例:ハッシュ、URL、差分、メッセージ、作者)。 | diff --git a/docs-master/keybindings/Keybindings_ko.md b/docs-master/keybindings/Keybindings_ko.md index d1eb3afb1cb..a28e1b86ddc 100644 --- a/docs-master/keybindings/Keybindings_ko.md +++ b/docs-master/keybindings/Keybindings_ko.md @@ -317,6 +317,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` t `` | Revert | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. | | `` T `` | Tag commit | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. | | `` `` | 로그 메뉴 열기 | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. | +| `` ~ `` | Toggle overview mode | Only show merge commits, and commits that a local or remote branch, a tag, or HEAD points to; hide all other commits. Useful for getting a high-level view of the history. | | `` G `` | Open pull request in browser | | | `` `` | 체크아웃 | Checkout the selected commit as a detached HEAD. | | `` y `` | 커밋 attribute 복사 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). | diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index 0eb61729b5e..3d9c9cf2789 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -178,6 +178,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` t `` | Revert | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. | | `` T `` | Tag commit | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. | | `` `` | View log options | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. | +| `` ~ `` | Toggle overview mode | Only show merge commits, and commits that a local or remote branch, a tag, or HEAD points to; hide all other commits. Useful for getting a high-level view of the history. | | `` G `` | Open pull request in browser | | | `` `` | Uitchecken | Checkout the selected commit as a detached HEAD. | | `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). | diff --git a/docs-master/keybindings/Keybindings_pl.md b/docs-master/keybindings/Keybindings_pl.md index 719a7e6c4fe..24b17cc352c 100644 --- a/docs-master/keybindings/Keybindings_pl.md +++ b/docs-master/keybindings/Keybindings_pl.md @@ -79,6 +79,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` t `` | Cofnij | Utwórz commit cofający dla wybranego commita, który stosuje zmiany wybranego commita w odwrotnej kolejności. | | `` T `` | Otaguj commit | Utwórz nowy tag wskazujący na wybrany commit. Zostaniesz poproszony o wprowadzenie nazwy tagu i opcjonalnego opisu. | | `` `` | Zobacz opcje logów | Zobacz opcje dla logów commitów, np. zmiana kolejności sortowania, ukrywanie grafu gita, pokazywanie całego grafu gita. | +| `` ~ `` | Toggle overview mode | Only show merge commits, and commits that a local or remote branch, a tag, or HEAD points to; hide all other commits. Useful for getting a high-level view of the history. | | `` G `` | Otwórz żądanie ściągnięcia w przeglądarce | | | `` `` | Przełącz | Przełącz wybrany commit jako odłączoną HEAD. | | `` y `` | Kopiuj atrybut commita do schowka | Kopiuj atrybut commita do schowka (np. hash, URL, różnice, wiadomość, autor). | diff --git a/docs-master/keybindings/Keybindings_pt.md b/docs-master/keybindings/Keybindings_pt.md index 5fcba268804..397d1c3d8b1 100644 --- a/docs-master/keybindings/Keybindings_pt.md +++ b/docs-master/keybindings/Keybindings_pt.md @@ -182,6 +182,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` t `` | Reverter | Crie um commit reverter para o commit selecionado, que aplica as alterações do commit selecionado em reverso. | | `` T `` | Etiquetar commit | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. | | `` `` | View log options | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. | +| `` ~ `` | Toggle overview mode | Only show merge commits, and commits that a local or remote branch, a tag, or HEAD points to; hide all other commits. Useful for getting a high-level view of the history. | | `` G `` | Open pull request in browser | | | `` `` | Verificar | Checkout the selected commit as a detached HEAD. | | `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). | diff --git a/docs-master/keybindings/Keybindings_ru.md b/docs-master/keybindings/Keybindings_ru.md index 6831354480b..e23d2e954a8 100644 --- a/docs-master/keybindings/Keybindings_ru.md +++ b/docs-master/keybindings/Keybindings_ru.md @@ -188,6 +188,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` t `` | Revert | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. | | `` T `` | Пометить коммит тегом | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. | | `` `` | Открыть меню журнала | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. | +| `` ~ `` | Toggle overview mode | Only show merge commits, and commits that a local or remote branch, a tag, or HEAD points to; hide all other commits. Useful for getting a high-level view of the history. | | `` G `` | Open pull request in browser | | | `` `` | Переключить | Checkout the selected commit as a detached HEAD. | | `` y `` | Скопировать атрибут коммита | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). | diff --git a/docs-master/keybindings/Keybindings_zh-CN.md b/docs-master/keybindings/Keybindings_zh-CN.md index b4f1134ecfe..cde4cf4884b 100644 --- a/docs-master/keybindings/Keybindings_zh-CN.md +++ b/docs-master/keybindings/Keybindings_zh-CN.md @@ -143,6 +143,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` t `` | 撤销(Revert) | 为所选提交创建还原提交,这会反向应用所选提交的更改。 | | `` T `` | 标签提交 | 创建一个新标签指向所选提交。您可以在弹窗中输入标签名称和描述(可选)。 | | `` `` | 打开日志菜单 | 查看提交日志的选项,例如更改排序顺序、隐藏 git graph、显示整个 git graph。 | +| `` ~ `` | Toggle overview mode | Only show merge commits, and commits that a local or remote branch, a tag, or HEAD points to; hide all other commits. Useful for getting a high-level view of the history. | | `` G `` | 在浏览器中打开拉取请求 | | | `` `` | 检出 | 检出所选择的提交作为分离HEAD。 | | `` y `` | 复制提交属性到剪贴板 | 复制提交属性到剪贴板(如hash、URL、diff、消息、作者)。 | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index b50e25d354e..5b0decc816a 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -202,6 +202,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` t `` | 還原 | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. | | `` T `` | 打標籤到提交 | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. | | `` `` | 開啟記錄選單 | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. | +| `` ~ `` | Toggle overview mode | Only show merge commits, and commits that a local or remote branch, a tag, or HEAD points to; hide all other commits. Useful for getting a high-level view of the history. | | `` G `` | Open pull request in browser | | | `` `` | 檢出 | Checkout the selected commit as a detached HEAD. | | `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). | diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 30ce0377d5a..149ef4b7790 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -630,6 +630,7 @@ type KeybindingCommitsConfig struct { ResetCherryPick Keybinding `yaml:"resetCherryPick"` CopyCommitAttributeToClipboard Keybinding `yaml:"copyCommitAttributeToClipboard"` OpenLogMenu Keybinding `yaml:"openLogMenu"` + ToggleOverviewMode Keybinding `yaml:"toggleOverviewMode"` OpenInBrowser Keybinding `yaml:"openInBrowser"` OpenPullRequestInBrowser Keybinding `yaml:"openPullRequestInBrowser"` ViewBisectOptions Keybinding `yaml:"viewBisectOptions"` @@ -1144,6 +1145,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig { ResetCherryPick: Keybinding{""}, CopyCommitAttributeToClipboard: Keybinding{"y"}, OpenLogMenu: Keybinding{""}, + ToggleOverviewMode: Keybinding{"~"}, OpenInBrowser: Keybinding{"o"}, OpenPullRequestInBrowser: Keybinding{"G"}, ViewBisectOptions: Keybinding{"b"}, diff --git a/pkg/gui/context/local_commits_context.go b/pkg/gui/context/local_commits_context.go index a66c720c9ad..a2a8b5393a8 100644 --- a/pkg/gui/context/local_commits_context.go +++ b/pkg/gui/context/local_commits_context.go @@ -44,9 +44,18 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext { hasRebaseUpdateRefsConfig := c.Git().Config.GetRebaseUpdateRefs() + // In overview mode the model only holds the displayed subset of the + // commits; the graph is rendered from the full list so that hiding + // rows doesn't change its shape. + commits := c.Model().Commits + displayIndices := overviewDisplayIndices(c, startIdx, endIdx) + if displayIndices != nil { + commits = c.Model().OverviewCommitsSource + } + return presentation.GetCommitListDisplayStrings( c.Common, - c.Model().Commits, + commits, c.Model().Branches, c.Model().CheckedOutBranch, hasRebaseUpdateRefsConfig, @@ -61,6 +70,7 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext { selectedCommitHashPtr, startIdx, endIdx, + displayIndices, shouldShowGraph(c), c.Model().BisectInfo, ) @@ -149,6 +159,17 @@ type LocalCommitsViewModel struct { // If this is true we'll use git log --all when fetching the commits. showWholeGitGraph bool + + // If this is true we only show merge commits and commits that a local or + // remote branch, a tag, or HEAD points to. + overviewMode bool + + // When enabling overview mode hides the selected commit, we remember it + // here, along with the commit the selection was moved to, so that + // disabling the mode can jump back to it if the selection hasn't moved + // in the meantime. + overviewReturnHash string + overviewAutoSelectedHash string } func NewLocalCommitsViewModel(getModel func() []*models.Commit, c *ContextCommon) *LocalCommitsViewModel { @@ -243,10 +264,60 @@ func (self *LocalCommitsViewModel) GetShowWholeGitGraph() bool { return self.showWholeGitGraph } +func (self *LocalCommitsViewModel) SetOverviewMode(value bool) { + self.overviewMode = value +} + +func (self *LocalCommitsViewModel) GetOverviewMode() bool { + return self.overviewMode +} + +func (self *LocalCommitsViewModel) SetOverviewReturnHashes(returnHash string, autoSelectedHash string) { + self.overviewReturnHash = returnHash + self.overviewAutoSelectedHash = autoSelectedHash +} + +func (self *LocalCommitsViewModel) GetOverviewReturnHashes() (string, string) { + return self.overviewReturnHash, self.overviewAutoSelectedHash +} + func (self *LocalCommitsViewModel) GetCommits() []*models.Commit { return self.getModel() } +// overviewDisplayIndices returns, for the given window of displayed rows, the +// indices of those rows in Model().OverviewCommitsSource, or nil when overview +// mode is off. It validates that the mapping matches the displayed commits and +// returns nil on a mismatch (which can happen transiently when something other +// than a commits refresh replaces Model().Commits, e.g. the mid-rebase todo +// refresh), so the caller falls back to rendering from the displayed list. +func overviewDisplayIndices(c *ContextCommon, startIdx int, endIdx int) []int { + indices := c.Model().OverviewCommitIndices + if indices == nil { + return nil + } + + displayed := c.Model().Commits + source := c.Model().OverviewCommitsSource + if len(indices) != len(displayed) { + return nil + } + + endIdx = min(endIdx, len(displayed)) + if startIdx >= endIdx { + return []int{} + } + + window := indices[startIdx:endIdx] + for i, modelIdx := range window { + if modelIdx >= len(source) || source[modelIdx] != displayed[startIdx+i] { + return nil + } + } + + return window +} + 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..092b0ecedd8 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -75,6 +75,7 @@ func NewSubCommitsContext( selectedCommitHashPtr, startIdx, endIdx, + nil, shouldShowGraph(c), git_commands.NewNullBisectInfo(), ) diff --git a/pkg/gui/controllers/helpers/mode_helper.go b/pkg/gui/controllers/helpers/mode_helper.go index 4947e42d1c8..7a457988392 100644 --- a/pkg/gui/controllers/helpers/mode_helper.go +++ b/pkg/gui/controllers/helpers/mode_helper.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/samber/lo" @@ -92,6 +93,16 @@ func (self *ModeHelper) Statuses() []ModeStatus { }, Reset: self.ExitFilterMode, }, + { + IsActive: self.c.Contexts().LocalCommits.GetOverviewMode, + InfoLabel: func() string { + return self.withResetButton(self.c.Tr.OverviewModeStatus, style.FgBlue) + }, + CancelLabel: func() string { + return self.c.Tr.ExitOverviewMode + }, + Reset: self.ExitOverviewMode, + }, { IsActive: self.c.Modes().MarkedBaseCommit.Active, InfoLabel: func() string { @@ -182,6 +193,94 @@ func (self *ModeHelper) ExitFilterMode() error { return self.ClearFiltering() } +// updateCommitsViewTitle gives the commits view's title and tab label an +// "(overview)" suffix while overview mode is on. The tab strip is shared by +// all views of the panel, so updating the commits view's own tab entry is +// enough for the label to show whichever tab is selected. +func (self *ModeHelper) updateCommitsViewTitle() { + view := self.c.Views().Commits + title := lo.Ternary(self.c.Contexts().LocalCommits.GetOverviewMode(), + self.c.Tr.CommitsOverviewTitle, self.c.Tr.CommitsTitle) + view.Title = title + if len(view.Tabs) > 0 { + view.Tabs[view.TabIndex] = title + } +} + +func (self *ModeHelper) EnterOverviewMode() error { + commitsContext := self.c.Contexts().LocalCommits + + // If the selected commit is about to be hidden, remember it and move the + // selection to the nearest commit above it that stays visible (or the top + // one); exiting the mode jumps back to it if the selection hasn't moved + // in the meantime. + hiddenSelectionHash := "" + autoSelectHash := "" + if selected := commitsContext.GetSelected(); selected != nil && !commitShownInOverview(selected) { + hiddenSelectionHash = selected.Hash() + commits := self.c.Model().Commits + for i := commitsContext.GetSelectedLineIdx() - 1; i >= 0; i-- { + if commitShownInOverview(commits[i]) { + autoSelectHash = commits[i].Hash() + break + } + } + } + + commitsContext.SetOverviewMode(true) + self.updateCommitsViewTitle() + // The condensed list can have far fewer rows than the threshold of the + // "load more commits when the selection approaches the end of the list" + // heuristic, which would then never kick in; load the whole history + // instead. + commitsContext.SetLimitCommits(false) + + return self.c.WithWaitingStatus(self.c.Tr.LoadingCommits, func(gocui.Task) error { + self.c.RefreshFromWorker(types.RefreshOptions{ + Mode: types.SYNC, + Scope: []types.RefreshableView{types.COMMITS}, + Then: func() error { + if hiddenSelectionHash == "" { + return nil + } + if autoSelectHash == "" || !commitsContext.SelectCommitByHash(autoSelectHash) { + commitsContext.SetSelection(0) + } + commitsContext.SetOverviewReturnHashes( + hiddenSelectionHash, commitsContext.GetSelectedCommitHash()) + self.c.PostRefreshUpdate(commitsContext) + commitsContext.FocusLine(true) + return nil + }, + }) + return nil + }) +} + +func (self *ModeHelper) ExitOverviewMode() error { + commitsContext := self.c.Contexts().LocalCommits + + returnHash, autoSelectedHash := commitsContext.GetOverviewReturnHashes() + jumpBack := returnHash != "" && commitsContext.GetSelectedCommitHash() == autoSelectedHash + + commitsContext.SetOverviewMode(false) + commitsContext.SetOverviewReturnHashes("", "") + self.updateCommitsViewTitle() + + self.c.Refresh(types.RefreshOptions{ + Scope: []types.RefreshableView{types.COMMITS}, + Then: func() error { + if jumpBack { + commitsContext.SelectCommitByHash(returnHash) + self.c.PostRefreshUpdate(commitsContext) + commitsContext.FocusLine(true) + } + return nil + }, + }) + return nil +} + func (self *ModeHelper) ClearFiltering() error { selectedCommitHash := self.c.Contexts().LocalCommits.GetSelectedCommitHash() self.c.Modes().Filtering.Reset() diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 3c8524ffe8f..5d07dd8fa3a 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -579,6 +579,7 @@ type capturedCommitState struct { selectionRange *localCommitSelectionRange limitCommits bool showWholeGitGraph bool + overviewMode bool filterPath string filterAuthor string mainBranches *git_commands.MainBranches @@ -601,6 +602,7 @@ func (self *RefreshHelper) captureCommitsState(commitSelection types.CommitSelec selectionRange: selectionRange, limitCommits: self.c.Contexts().LocalCommits.GetLimitCommits(), showWholeGitGraph: self.c.Contexts().LocalCommits.GetShowWholeGitGraph(), + overviewMode: self.c.Contexts().LocalCommits.GetOverviewMode(), filterPath: self.c.Modes().Filtering.GetPath(), filterAuthor: self.c.Modes().Filtering.GetAuthor(), mainBranches: self.c.Model().MainBranches, @@ -685,11 +687,21 @@ func (self *RefreshHelper) refreshCommitsWithLimit(captured capturedCommitState, if err != nil { return err } + + var overviewSource []*models.Commit + var overviewIndices []int + if captured.overviewMode { + overviewSource = commits + commits, overviewIndices = filterCommitsForOverview(commits) + } + workingTreeState := self.c.Git().Status.WorkingTreeState() self.onUIThreadUnlessRepoChanged(env, func() error { self.c.Model().BisectInfo = bisectInfo self.c.Model().Commits = commits + self.c.Model().OverviewCommitsSource = overviewSource + self.c.Model().OverviewCommitIndices = overviewIndices self.RefreshAuthors(commits) self.c.Model().WorkingTreeStateAtLastCommitRefresh = workingTreeState if checkedOutRef != nil { @@ -733,6 +745,30 @@ func (self *RefreshHelper) refreshCommitsWithLimit(captured capturedCommitState, return nil } +// commitShownInOverview says whether the commits panel's overview mode shows +// the given commit: merge commits, commits that a ref points to, and rebase +// todo entries. +func commitShownInOverview(commit *models.Commit) bool { + return commit.IsTODO() || commit.IsMerge() || commit.ExtraInfo != "" +} + +// filterCommitsForOverview returns only the commits that overview mode shows. +// The second return value holds, for each kept commit, its index in the full +// slice; the graph is rendered from the full slice so that hiding rows +// doesn't change its shape. +func filterCommitsForOverview(commits []*models.Commit) ([]*models.Commit, []int) { + kept := make([]*models.Commit, 0, len(commits)) + indices := make([]int, 0, len(commits)) + for i, commit := range commits { + if commitShownInOverview(commit) { + kept = append(kept, commit) + indices = append(indices, i) + } + } + + return kept, indices +} + type localCommitSelectionRange struct { selectedHash string selectedIsTODO bool diff --git a/pkg/gui/controllers/helpers/refresh_helper_test.go b/pkg/gui/controllers/helpers/refresh_helper_test.go index 3a5f6ea829c..8d2dc19f76c 100644 --- a/pkg/gui/controllers/helpers/refresh_helper_test.go +++ b/pkg/gui/controllers/helpers/refresh_helper_test.go @@ -160,6 +160,43 @@ func TestFindLocalCommitSelectionRange(t *testing.T) { } } +func TestFilterCommitsForOverview(t *testing.T) { + hashPool := &utils.StringPool{} + + commit := func(hash string, extraInfo string, parents ...string) *models.Commit { + return models.NewCommit(hashPool, models.NewCommitOpts{ + Hash: hash, + ExtraInfo: extraInfo, + Parents: parents, + }) + } + + commits := []*models.Commit{ + commit("master3", "(HEAD -> master)", "master2"), + commit("master2", "", "M"), + commit("M", "", "master1", "feature2"), + commit("feature2", "(feature)", "feature1"), + commit("feature1", "", "base"), + commit("master1", "", "base"), + commit("base", "(tag: v1)"), + } + todoCommit := models.NewCommit(hashPool, models.NewCommitOpts{Hash: "todo", Action: todo.Pick}) + commits = append([]*models.Commit{todoCommit}, commits...) + + kept, indices := filterCommitsForOverview(commits) + + // merge commits, ref tips, and rebase todo entries are kept; everything + // else is hidden + assert.Equal(t, []string{"todo", "master3", "M", "feature2", "base"}, + lo.Map(kept, func(commit *models.Commit, _ int) string { return commit.Hash() })) + + // each kept commit's index points back at it in the full slice + assert.Equal(t, []int{0, 1, 3, 4, 7}, indices) + for i, modelIdx := range indices { + assert.Same(t, commits[modelIdx], kept[i]) + } +} + func TestGetGithubBaseRemote(t *testing.T) { cases := []struct { name string diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 23e94adf7fd..0bd5290545f 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -56,7 +56,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ bindings := []*types.Binding{ { Keys: opts.GetKeys(opts.Config.Commits.SquashDown), - Handler: opts.Guards.OutsideFilterMode(self.withItemsRange(self.squashDown)), + Handler: opts.Guards.OutsideFilterMode(self.outsideOverviewMode(self.withItemsRange(self.squashDown))), GetDisabledReason: self.require( self.itemRangeSelected( self.midRebaseCommandEnabled, @@ -69,7 +69,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Keys: opts.GetKeys(opts.Config.Commits.MarkCommitAsFixup), - Handler: opts.Guards.OutsideFilterMode(self.withItemsRange(self.fixup)), + Handler: opts.Guards.OutsideFilterMode(self.outsideOverviewMode(self.withItemsRange(self.fixup))), GetDisabledReason: self.require( self.itemRangeSelected( self.midRebaseCommandEnabled, @@ -91,7 +91,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Keys: opts.GetKeys(opts.Config.Commits.RenameCommit), - Handler: self.withItem(self.reword), + Handler: self.outsideOverviewMode(self.withItem(self.reword)), GetDisabledReason: self.require( self.singleItemSelected(self.rewordEnabled), ), @@ -102,7 +102,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Keys: opts.GetKeys(opts.Config.Commits.RenameCommitWithEditor), - Handler: self.withItem(self.rewordEditor), + Handler: self.outsideOverviewMode(self.withItem(self.rewordEditor)), GetDisabledReason: self.require( self.singleItemSelected(self.rewordEnabled), ), @@ -110,7 +110,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Keys: opts.GetKeys(opts.Config.Universal.Remove), - Handler: self.withItemsRange(self.drop), + Handler: self.outsideOverviewMode(self.withItemsRange(self.drop)), GetDisabledReason: self.require( self.itemRangeSelected( self.canDropCommits, @@ -122,7 +122,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Keys: opts.GetKeys(editCommitKey), - Handler: opts.Guards.OutsideFilterMode(self.withItemsRange(self.edit)), + Handler: opts.Guards.OutsideFilterMode(self.outsideOverviewMode(self.withItemsRange(self.edit))), GetDisabledReason: self.require( self.itemRangeSelected(self.midRebaseCommandEnabled), ), @@ -136,7 +136,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ // we're calling it 'quick-start interactive rebase' to differentiate it from // when you manually select the base commit. Keys: opts.GetKeys(opts.Config.Commits.StartInteractiveRebase), - Handler: opts.Guards.OutsideFilterMode(self.quickStartInteractiveRebase), + Handler: opts.Guards.OutsideFilterMode(self.outsideOverviewMode(self.quickStartInteractiveRebase)), GetDisabledReason: self.require(self.notMidRebase(self.c.Tr.AlreadyRebasing), self.canFindCommitForQuickStart), Description: self.c.Tr.QuickStartInteractiveRebase, Tooltip: utils.ResolvePlaceholderString(self.c.Tr.QuickStartInteractiveRebaseTooltip, map[string]string{ @@ -145,7 +145,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Keys: opts.GetKeys(opts.Config.Commits.PickCommit), - Handler: opts.Guards.OutsideFilterMode(self.withItems(self.pick)), + Handler: opts.Guards.OutsideFilterMode(self.outsideOverviewMode(self.withItems(self.pick))), GetDisabledReason: self.require( self.itemRangeSelected(self.pickEnabled), ), @@ -154,7 +154,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Keys: opts.GetKeys(opts.Config.Commits.CreateFixupCommit), - Handler: opts.Guards.OutsideFilterMode(self.withItem(self.createFixupCommit)), + Handler: opts.Guards.OutsideFilterMode(self.outsideOverviewMode(self.withItem(self.createFixupCommit))), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.CreateFixupCommit, Tooltip: utils.ResolvePlaceholderString( @@ -166,7 +166,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Keys: opts.GetKeys(opts.Config.Commits.SquashAboveCommits), - Handler: opts.Guards.OutsideFilterMode(self.squashFixupCommits), + Handler: opts.Guards.OutsideFilterMode(self.outsideOverviewMode(self.squashFixupCommits)), GetDisabledReason: self.require( self.notMidRebase(self.c.Tr.AlreadyRebasing), ), @@ -176,7 +176,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Keys: opts.GetKeys(opts.Config.Commits.MoveDownCommit), - Handler: opts.Guards.OutsideFilterMode(self.withItemsRange(self.moveDown)), + Handler: opts.Guards.OutsideFilterMode(self.outsideOverviewMode(self.withItemsRange(self.moveDown))), GetDisabledReason: self.require(self.itemRangeSelected( self.midRebaseMoveCommandEnabled, self.canMoveDown, @@ -185,7 +185,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Keys: opts.GetKeys(opts.Config.Commits.MoveUpCommit), - Handler: opts.Guards.OutsideFilterMode(self.withItemsRange(self.moveUp)), + Handler: opts.Guards.OutsideFilterMode(self.outsideOverviewMode(self.withItemsRange(self.moveUp))), GetDisabledReason: self.require(self.itemRangeSelected( self.midRebaseMoveCommandEnabled, self.canMoveUp, @@ -194,14 +194,14 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Keys: opts.GetKeys(opts.Config.Commits.PasteCommits), - Handler: opts.Guards.OutsideFilterMode(self.paste), + Handler: opts.Guards.OutsideFilterMode(self.outsideOverviewMode(self.paste)), GetDisabledReason: self.require(self.canPaste), Description: self.c.Tr.PasteCommits, DisplayStyle: &style.FgCyan, }, { Keys: opts.GetKeys(opts.Config.Commits.MarkCommitAsBaseForRebase), - Handler: opts.Guards.OutsideFilterMode(self.withItem(self.markAsBaseCommit)), + Handler: opts.Guards.OutsideFilterMode(self.outsideOverviewMode(self.withItem(self.markAsBaseCommit))), GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.MarkAsBaseCommit, Tooltip: self.c.Tr.MarkAsBaseCommitTooltip, @@ -216,7 +216,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Keys: opts.GetKeys(opts.Config.Commits.AmendToCommit), - Handler: self.withItem(self.amendTo), + Handler: self.outsideOverviewMode(self.withItem(self.amendTo)), GetDisabledReason: self.require(self.singleItemSelected(self.canAmend)), Description: self.c.Tr.Amend, Tooltip: self.c.Tr.AmendCommitTooltip, @@ -224,7 +224,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Keys: opts.GetKeys(opts.Config.Commits.ResetCommitAuthor), - Handler: self.withItemsRange(self.amendAttribute), + Handler: self.outsideOverviewMode(self.withItemsRange(self.amendAttribute)), GetDisabledReason: self.require(self.itemRangeSelected(self.canAmendRange)), Description: self.c.Tr.AmendCommitAttribute, Tooltip: self.c.Tr.AmendCommitAttributeTooltip, @@ -251,6 +251,12 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ Tooltip: self.c.Tr.OpenLogMenuTooltip, OpensMenu: true, }, + { + Keys: opts.GetKeys(opts.Config.Commits.ToggleOverviewMode), + Handler: self.toggleOverviewMode, + Description: self.c.Tr.ToggleOverviewMode, + Tooltip: self.c.Tr.ToggleOverviewModeTooltip, + }, { Keys: opts.GetKeys(opts.Config.Commits.OpenPullRequestInBrowser), Handler: self.openPRInBrowser, @@ -1202,6 +1208,32 @@ func (self *LocalCommitsController) openSearch() error { return self.c.Helpers().Search.OpenSearchPrompt(self.context()) } +// Commands that pick the rebase base from the position of the selection in +// the commits list would operate relative to commits that overview mode +// hides, so require exiting the mode first (like filter mode does). +func (self *LocalCommitsController) outsideOverviewMode(f func() error) func() error { + return func() error { + if !self.context().GetOverviewMode() { + return f() + } + + self.c.Confirm(types.ConfirmOpts{ + Title: self.c.Tr.MustExitFilterModeTitle, + Prompt: self.c.Tr.MustExitOverviewModePrompt, + HandleConfirm: self.c.Helpers().Mode.ExitOverviewMode, + }) + + return nil + } +} + +func (self *LocalCommitsController) toggleOverviewMode() error { + if self.context().GetOverviewMode() { + return self.c.Helpers().Mode.ExitOverviewMode() + } + return self.c.Helpers().Mode.EnterOverviewMode() +} + func (self *LocalCommitsController) handleOpenLogMenu() error { return self.c.Menu(types.CreateMenuOptions{ Title: self.c.Tr.LogMenuTitle, @@ -1223,6 +1255,11 @@ func (self *LocalCommitsController) handleOpenLogMenu() error { }) }, }, + { + Label: self.c.Tr.ToggleOverviewMode, + Tooltip: self.c.Tr.ToggleOverviewModeTooltip, + OnPress: self.toggleOverviewMode, + }, { Label: self.c.Tr.ShowGitGraph, Tooltip: self.c.Tr.ShowGitGraphTooltip, diff --git a/pkg/gui/presentation/commits.go b/pkg/gui/presentation/commits.go index 67fa62ac82c..581613ae92b 100644 --- a/pkg/gui/presentation/commits.go +++ b/pkg/gui/presentation/commits.go @@ -54,6 +54,11 @@ func GetCommitListDisplayStrings( selectedCommitHashPtr *string, startIdx int, endIdx int, + // If non-nil, the indices into commits of the rows to render (ascending, + // already windowed), overriding startIdx/endIdx; used by the commits + // panel's overview mode to omit rows while the graph is still rendered + // from the full list. Must be nil for a divergence log. + displayIndices []int, showGraph bool, bisectInfo *git_commands.BisectInfo, ) [][]string { @@ -64,15 +69,22 @@ func GetCommitListDisplayStrings( return nil } - if startIdx >= len(commits) { - return nil + if displayIndices == nil { + if startIdx >= len(commits) { + return nil + } + displayIndices = lo.RangeFrom(startIdx, endIdx-startIdx) + } else { + if len(displayIndices) == 0 { + return nil + } + startIdx = displayIndices[0] + endIdx = displayIndices[len(displayIndices)-1] + 1 } // this is where my non-TODO commits begin rebaseOffset := min(indexOfFirstNonTODOCommit(commits), endIdx) - filteredCommits := commits[startIdx:endIdx] - bisectBounds := getbisectBounds(commits, bisectInfo) // function expects to be passed the index of the commit in terms of the `commits` slice @@ -129,24 +141,29 @@ func GetCommitListDisplayStrings( return allGraphLines[idx-startIdx] } } else { - // this is where the graph begins (may be beyond the TODO commits depending on startIdx, - // but we'll never include TODO commits as part of the graph because it'll be messy) - graphOffset := max(startIdx, rebaseOffset) - + // the graph never includes TODO commits because it'll be messy, + // so only render rows from the first non-TODO commit on pipeSets := loadPipesets(commits[rebaseOffset:]) - pipeSetOffset := max(startIdx-rebaseOffset, 0) - graphPipeSets := pipeSets[pipeSetOffset:max(endIdx-rebaseOffset, 0)] - graphCommits := commits[graphOffset:endIdx] + graphIndices := lo.Filter(displayIndices, func(idx int, _ int) bool { + return idx >= rebaseOffset + }) + graphPipeSets := lo.Map(graphIndices, func(idx int, _ int) []graph.Pipe { + return pipeSets[idx-rebaseOffset] + }) + graphCommits := lo.Map(graphIndices, func(idx int, _ int) *models.Commit { + return commits[idx] + }) graphLines := graph.RenderAux( graphPipeSets, graphCommits, selectedCommitHashPtr, ) + graphLineByIndex := make(map[int]string, len(graphIndices)) + for i, idx := range graphIndices { + graphLineByIndex[idx] = graphLines[i] + } getGraphLine = func(idx int) string { - if idx >= graphOffset { - return graphLines[idx-graphOffset] - } - return "" + return graphLineByIndex[idx] } } } else { @@ -177,11 +194,11 @@ func GetCommitListDisplayStrings( (hasRebaseUpdateRefsConfig || b.CommitHash != commits[0].Hash()) })) - lines := make([][]string, 0, len(filteredCommits)) + lines := make([][]string, 0, len(displayIndices)) var bisectStatus BisectStatus willBeRebased := markedBaseCommit == "" - for i, commit := range filteredCommits { - unfilteredIdx := i + startIdx + for _, unfilteredIdx := range displayIndices { + commit := commits[unfilteredIdx] bisectStatus = getBisectStatus(unfilteredIdx, commit.Hash(), bisectInfo, bisectBounds) isMarkedBaseCommit := commit.Hash() != "" && commit.Hash() == markedBaseCommit if isMarkedBaseCommit { diff --git a/pkg/gui/presentation/commits_test.go b/pkg/gui/presentation/commits_test.go index 12e0fc1d661..3de8f495b12 100644 --- a/pkg/gui/presentation/commits_test.go +++ b/pkg/gui/presentation/commits_test.go @@ -39,6 +39,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) { selectedCommitHashPtr *string startIdx int endIdx int + displayIndices []int showGraph bool bisectInfo *git_commands.BisectInfo expected string @@ -209,6 +210,51 @@ func TestGetCommitListDisplayStrings(t *testing.T) { hash5 ○ commit5 `), }, + { + testName: "showing graph with display indices, keeping the shape of the full graph", + commitOpts: []models.NewCommitOpts{ + {Name: "commit1", Hash: "hash1", Parents: []string{"hash2", "hash3"}}, + {Name: "commit2", Hash: "hash2", Parents: []string{"hash3"}}, + {Name: "commit3", Hash: "hash3", Parents: []string{"hash4"}}, + {Name: "commit4", Hash: "hash4", Parents: []string{"hash5"}}, + {Name: "commit5", Hash: "hash5", Parents: []string{"hash7"}}, + }, + startIdx: 0, + endIdx: 5, + displayIndices: []int{0, 2, 4}, + showGraph: true, + bisectInfo: git_commands.NewNullBisectInfo(), + cherryPickedCommitHashSet: set.New[string](), + now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + expected: formatExpected(` + hash1 ◎─╮ commit1 + hash3 ○─╯ commit3 + hash5 ○ commit5 + `), + }, + { + testName: "showing graph with display indices, including rebase commits", + commitOpts: []models.NewCommitOpts{ + {Name: "commit1", Hash: "hash1", Parents: []string{"hash2", "hash3"}, Action: todo.Pick}, + {Name: "commit2", Hash: "hash2", Parents: []string{"hash3"}, Action: todo.Pick}, + {Name: "commit3", Hash: "hash3", Parents: []string{"hash4"}}, + {Name: "commit4", Hash: "hash4", Parents: []string{"hash5"}}, + {Name: "commit5", Hash: "hash5", Parents: []string{"hash7"}}, + }, + startIdx: 0, + endIdx: 5, + displayIndices: []int{0, 1, 2, 4}, + showGraph: true, + bisectInfo: git_commands.NewNullBisectInfo(), + cherryPickedCommitHashSet: set.New[string](), + now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + expected: formatExpected(` + hash1 pick commit1 + hash2 pick commit2 + hash3 ○ commit3 + hash5 ○ commit5 + `), + }, { testName: "showing graph, including rebase commits", commitOpts: []models.NewCommitOpts{ @@ -564,6 +610,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) { s.selectedCommitHashPtr, s.startIdx, s.endIdx, + s.displayIndices, s.showGraph, s.bisectInfo, ) diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 08ff53bb0fd..0792dc03cab 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -316,17 +316,26 @@ func (self *MenuItem) ID() string { } type Model struct { - CommitFiles []*models.CommitFile - Files []*models.File - Submodules []*models.SubmoduleConfig - Branches []*models.Branch - Commits []*models.Commit - StashEntries []*models.StashEntry - SubCommits []*models.Commit - Remotes []*models.Remote - Worktrees []*models.Worktree - PullRequests []*models.GithubPullRequest - PullRequestsMap map[string]*models.GithubPullRequest + CommitFiles []*models.CommitFile + Files []*models.File + Submodules []*models.SubmoduleConfig + Branches []*models.Branch + Commits []*models.Commit + StashEntries []*models.StashEntry + + // When the commits panel's overview mode is active, Commits contains only + // the rows the overview shows; OverviewCommitsSource then holds the full + // commit list, and OverviewCommitIndices holds, for each entry of Commits, + // its index in OverviewCommitsSource. The graph is rendered from the full + // list so that hiding rows doesn't change its shape. Both are nil while + // overview mode is off. + OverviewCommitsSource []*models.Commit + OverviewCommitIndices []int + SubCommits []*models.Commit + Remotes []*models.Remote + Worktrees []*models.Worktree + PullRequests []*models.GithubPullRequest + PullRequestsMap map[string]*models.GithubPullRequest // FilteredReflogCommits are the ones that appear in the reflog panel. // When in filtering mode we only include the ones that match the given path diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 22b05e6c06d..59b82025c68 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -665,6 +665,10 @@ type TranslationSet struct { WillCancelExistingFilterTooltip string MustExitFilterModeTitle string MustExitFilterModePrompt string + MustExitOverviewModePrompt string + OverviewModeStatus string + ExitOverviewMode string + CommitsOverviewTitle string Diff string EnterRefToDiff string EnterRefName string @@ -808,6 +812,8 @@ type TranslationSet struct { OpenLogMenuTooltip string LogMenuTitle string ToggleShowGitGraphAll string + ToggleOverviewMode string + ToggleOverviewModeTooltip string ShowGitGraph string ShowGitGraphTooltip string SortOrder string @@ -1812,6 +1818,10 @@ func EnglishTranslationSet() *TranslationSet { WillCancelExistingFilterTooltip: "Note: this will cancel the existing filter", MustExitFilterModeTitle: "Command not available", MustExitFilterModePrompt: "Command not available in filter-by-path mode. Exit filter-by-path mode?", + MustExitOverviewModePrompt: "Command not available in overview mode. Exit overview mode?", + OverviewModeStatus: "Showing commits overview", + ExitOverviewMode: "Exit overview mode", + CommitsOverviewTitle: "Commits (overview)", Diff: "Diff", EnterRefToDiff: "Enter ref to diff", EnterRefName: "Enter ref:", @@ -1954,6 +1964,8 @@ func EnglishTranslationSet() *TranslationSet { OpenLogMenuTooltip: "View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph.", LogMenuTitle: "Commit Log Options", ToggleShowGitGraphAll: "Toggle show whole git graph (pass the `--all` flag to `git log`)", + ToggleOverviewMode: "Toggle overview mode", + ToggleOverviewModeTooltip: "Only show merge commits, and commits that a local or remote branch, a tag, or HEAD points to; hide all other commits. Useful for getting a high-level view of the history.", ShowGitGraph: "Show git graph", ShowGitGraphTooltip: "Show or hide the git graph in the commit log.\n\nThe default can be changed in the config file with the key 'git.log.showGraph'.", SortOrder: "Sort order", diff --git a/pkg/integration/tests/commit/toggle_overview_mode.go b/pkg/integration/tests/commit/toggle_overview_mode.go new file mode 100644 index 00000000000..009dd0af215 --- /dev/null +++ b/pkg/integration/tests/commit/toggle_overview_mode.go @@ -0,0 +1,146 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ToggleOverviewMode = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Toggle overview mode to only show merge commits and commits that a ref points to", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + // leave the graph on so that rendering it over the rewritten parents + // is exercised too + config.GetUserConfig().Git.Log.ShowGraph = "always" + }, + SetupRepo: func(shell *Shell) { + shell. + EmptyCommitWithDate("base", "2023-04-07 10:00:00"). + NewBranch("feature"). + EmptyCommitWithDate("feature-1", "2023-04-07 11:00:00"). + EmptyCommitWithDate("feature-2", "2023-04-07 12:00:00"). + Checkout("master"). + EmptyCommitWithDate("master-1", "2023-04-07 13:00:00"). + Merge("feature"). + EmptyCommitWithDate("master-2", "2023-04-07 14:00:00"). + CreateLightweightTag("some-tag", "HEAD"). + EmptyCommitWithDate("master-3", "2023-04-07 15:00:00") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("master-3"), + Contains("master-2"), + Contains("Merge branch 'feature'"), + Contains("feature-2"), + Contains("feature-1"), + Contains("master-1"), + Contains("base"), + ). + Press(keys.Commits.ToggleOverviewMode). + Lines( + // the tip of the checked-out branch, the merge commit, the + // tagged commit, and the tip of the feature branch remain; + // the graph keeps the shape it has in the full list, with the + // merge's join line and the feature branch's lane intact + Contains("master-3"), + Contains("master-2"), + Contains("Merge branch 'feature'").Contains("◎─╮"), + Contains("feature-2").Contains("│ ○"), + ) + + t.Views().Information().Content(Contains("Showing commits overview")) + + t.Views().Commits(). + Title(Equals("Commits (overview)")). + Press(keys.Commits.ToggleOverviewMode). + Title(Equals("Commits")). + Lines( + Contains("master-3"), + Contains("master-2"), + Contains("Merge branch 'feature'"), + Contains("feature-2"), + Contains("feature-1"), + Contains("master-1"), + Contains("base"), + ) + + t.Views().Information().Content(DoesNotContain("Showing commits overview")) + + // collapsing while on a commit that gets hidden moves the selection to + // the nearest visible commit above it, and expanding again jumps back + t.Views().Commits(). + NavigateToLine(Contains("feature-1")). + Press(keys.Commits.ToggleOverviewMode). + Lines( + Contains("master-3"), + Contains("master-2"), + Contains("Merge branch 'feature'"), + Contains("feature-2").IsSelected(), + ). + Press(keys.Commits.ToggleOverviewMode). + Lines( + Contains("master-3"), + Contains("master-2"), + Contains("Merge branch 'feature'"), + Contains("feature-2"), + Contains("feature-1").IsSelected(), + Contains("master-1"), + Contains("base"), + ) + + // ... but not if the selection was moved while collapsed + t.Views().Commits(). + NavigateToLine(Contains("master-1")). + Press(keys.Commits.ToggleOverviewMode). + Lines( + Contains("master-3"), + Contains("master-2"), + Contains("Merge branch 'feature'"), + Contains("feature-2").IsSelected(), + ). + NavigateToLine(Contains("master-3")). + Press(keys.Commits.ToggleOverviewMode). + Lines( + Contains("master-3").IsSelected(), + Contains("master-2"), + Contains("Merge branch 'feature'"), + Contains("feature-2"), + Contains("feature-1"), + Contains("master-1"), + Contains("base"), + ) + + // commands that rebase relative to the selection's position in the + // list require exiting overview mode first + t.Views().Commits(). + Press(keys.Commits.ToggleOverviewMode). + Lines( + Contains("master-3"), + Contains("master-2"), + Contains("Merge branch 'feature'"), + Contains("feature-2"), + ). + Press(keys.Commits.SquashDown) + + t.ExpectPopup().Confirmation(). + Title(Equals("Command not available")). + Content(Equals("Command not available in overview mode. Exit overview mode?")). + Confirm() + + t.Views().Commits(). + Lines( + Contains("master-3"), + Contains("master-2"), + Contains("Merge branch 'feature'"), + Contains("feature-2"), + Contains("feature-1"), + Contains("master-1"), + Contains("base"), + ) + + t.Views().Information().Content(DoesNotContain("Showing commits overview")) + }, +}) diff --git a/pkg/integration/tests/commit/toggle_overview_mode_scrolls_selection_into_view.go b/pkg/integration/tests/commit/toggle_overview_mode_scrolls_selection_into_view.go new file mode 100644 index 00000000000..a1447d4afdb --- /dev/null +++ b/pkg/integration/tests/commit/toggle_overview_mode_scrolls_selection_into_view.go @@ -0,0 +1,43 @@ +package commit + +import ( + "fmt" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ToggleOverviewModeScrollsSelectionIntoView = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Enabling overview mode scrolls the moved selection into view", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + // A long tail of tagged commits keeps the overview list taller than + // the viewport, so the view can't get away with not scrolling; a long + // run of plain commits on top lets us scroll the selection far away + // from where it ends up after collapsing. + shell.EmptyCommit("base") + for i := 1; i <= 80; i++ { + shell.EmptyCommit(fmt.Sprintf("tagged-%02d", i)) + shell.CreateLightweightTag(fmt.Sprintf("tag-%02d", i), "HEAD") + } + for i := 1; i <= 60; i++ { + shell.EmptyCommit(fmt.Sprintf("plain-%02d", i)) + } + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + // plain-01 is deep down the list, so navigating to it scrolls the + // view; every commit above it is plain except the HEAD commit, so + // collapsing moves the selection to the very top of the overview, + // which must scroll back into view. + t.Views().Commits(). + Focus(). + NavigateToLine(Contains("plain-01")). + Press(keys.Commits.ToggleOverviewMode). + SelectedLine(Contains("plain-60")). + TopLines( + Contains("plain-60"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index abf13073ed0..bad15d6d0d8 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -159,6 +159,8 @@ var tests = []*components.IntegrationTest{ commit.StageRangeOfLines, commit.Staged, commit.StagedWithoutHooks, + commit.ToggleOverviewMode, + commit.ToggleOverviewModeScrollsSelectionIntoView, commit.Unstaged, config.CustomCommandsInPerRepoConfig, config.NegativeRefspec, diff --git a/schema-master/config.json b/schema-master/config.json index 82dbebb0b7f..82e4aa831ae 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -1575,6 +1575,20 @@ ], "default": "\u003cctrl+l\u003e" }, + "toggleOverviewMode": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "default": "~" + }, "openInBrowser": { "oneOf": [ {