diff --git a/docs/Searching.md b/docs/Searching.md index 589831c55e3..f708532fb03 100644 --- a/docs/Searching.md +++ b/docs/Searching.md @@ -19,3 +19,12 @@ You can filter the commits view to only show commits which contain changes to a You can do this in a couple of ways: 1) Start lazygit with the -f flag e.g. `lazygit -f my/path` 2) From within lazygit, press `` and then enter the path of the file you want to filter by + +## Hiding merge commits + +You can hide merge commits from the commits view to get a cleaner, linear history. + +1) From within lazygit, press `` to open the filtering menu +2) Select "Hide merge commits" to toggle this option on/off + +**Note:** When hiding merge commits, the commit graph will be automatically disabled as merge commits are essential for proper graph visualization. diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index 72adf1b699c..554c84aea21 100644 --- a/pkg/commands/git_commands/commit_loader.go +++ b/pkg/commands/git_commands/commit_loader.go @@ -68,6 +68,8 @@ type GetCommitsOptions struct { RefToShowDivergenceFrom string MainBranches *MainBranches HashPool *utils.StringPool + // If true, exclude merge commits from the output + HideMerges bool } // GetCommits obtains the commits of the current branch @@ -589,6 +591,7 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) *oscommands.CmdObj { ArgIf(gitLogOrder != "default", "--"+gitLogOrder). ArgIf(opts.All, "--all"). Arg("--oneline"). + ArgIf(opts.HideMerges, "--no-merges"). Arg(prettyFormat). Arg("--abbrev=40"). ArgIf(opts.FilterAuthor != "", "--author="+opts.FilterAuthor). diff --git a/pkg/commands/git_commands/commit_loader_test.go b/pkg/commands/git_commands/commit_loader_test.go index 7f9873b0b0b..b49149db7f4 100644 --- a/pkg/commands/git_commands/commit_loader_test.go +++ b/pkg/commands/git_commands/commit_loader_test.go @@ -61,6 +61,39 @@ func TestGetCommits(t *testing.T) { expectedCommitOpts: []models.NewCommitOpts{}, expectedError: nil, }, + { + testName: "should hide merge commits when HideMerges is true", + logOrder: "topo-order", + opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: &models.Branch{Name: "mybranch"}, IncludeRebaseCommits: false, HideMerges: true}, + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs([]string{"rev-list", "refs/heads/mybranch", "^mybranch@{u}"}, "", nil). + ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--no-merges", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), + + expectedCommitOpts: []models.NewCommitOpts{}, + expectedError: nil, + }, + { + testName: "should not hide merge commits when HideMerges is false", + logOrder: "topo-order", + opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: &models.Branch{Name: "mybranch"}, IncludeRebaseCommits: false, HideMerges: false}, + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs([]string{"rev-list", "refs/heads/mybranch", "^mybranch@{u}"}, "", nil). + ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), + + expectedCommitOpts: []models.NewCommitOpts{}, + expectedError: nil, + }, + { + testName: "should hide merge commits when HideMerges is true with other options", + logOrder: "date-order", + opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: &models.Branch{Name: "mybranch"}, IncludeRebaseCommits: false, HideMerges: true, All: true}, + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs([]string{"rev-list", "refs/heads/mybranch", "^mybranch@{u}"}, "", nil). + ExpectGitArgs([]string{"log", "HEAD", "--date-order", "--oneline", "--all", "--no-merges", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil), + + expectedCommitOpts: []models.NewCommitOpts{}, + expectedError: nil, + }, { testName: "should return commits if they are present", logOrder: "topo-order", @@ -72,12 +105,12 @@ func TestGetCommits(t *testing.T) { // here it's actually getting all the commits in a formatted form, one per line ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:+%H%x00%at%x00%aN%x00%ae%x00%P%x00%m%x00%D%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil). // here it's testing which of the configured main branches have an upstream - ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil). // this one does - ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")). // this one doesn't, so it checks origin instead - ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/main"}, "", nil). // yep, origin/main exists - ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "develop@{u}"}, "", errors.New("error")). // this one doesn't, so it checks origin instead + ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil). // this one does + ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")). // this one doesn't, so it checks origin instead + ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/main"}, "", nil). // yep, origin/main exists + ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "develop@{u}"}, "", errors.New("error")). // this one doesn't, so it checks origin instead ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/develop"}, "", errors.New("error")). // doesn't exist there, either, so it checks for a local branch - ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/develop"}, "", errors.New("error")). // no local branch either + ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/develop"}, "", errors.New("error")). // no local branch either // here it's seeing which of our commits are not on any of the main branches yet ExpectGitArgs([]string{"rev-list", "HEAD", "^refs/remotes/origin/master", "^refs/remotes/origin/main"}, "0eea75e8c631fba6b58135697835d58ba4c18dbc\nb21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164\ne94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c\nd8084cd558925eb7c9c38afeed5725c21653ab90\n65f910ebd85283b5cce9bf67d03d3f1a9ea3813a\n", nil), diff --git a/pkg/gui/context/local_commits_context.go b/pkg/gui/context/local_commits_context.go index e873663c30d..95d721849f9 100644 --- a/pkg/gui/context/local_commits_context.go +++ b/pkg/gui/context/local_commits_context.go @@ -251,6 +251,10 @@ func shouldShowGraph(c *ContextCommon) bool { return false } + if c.Modes().Filtering.GetHideMerges() { + return false + } + value := c.UserConfig().Git.Log.ShowGraph switch value { diff --git a/pkg/gui/controllers/filtering_menu_action.go b/pkg/gui/controllers/filtering_menu_action.go index 2ed072676f5..d557a6d2291 100644 --- a/pkg/gui/controllers/filtering_menu_action.go +++ b/pkg/gui/controllers/filtering_menu_action.go @@ -91,6 +91,25 @@ func (self *FilteringMenuAction) Call() error { Tooltip: tooltip, }) + menuItems = append(menuItems, &types.MenuItem{ + Label: self.c.Tr.FilterHideMerges, + Tooltip: self.c.Tr.FilterHideMergesTooltip, + Widget: types.MakeMenuCheckBox(self.c.Modes().Filtering.GetHideMerges()), + OnPress: func() error { + currentValue := self.c.Modes().Filtering.GetHideMerges() + self.c.Modes().Filtering.SetHideMerges(!currentValue) + + self.c.Refresh(types.RefreshOptions{ + Scope: helpers.ScopesToRefreshWhenFilteringModeChanges(), + Then: func() { + self.c.Contexts().LocalCommits.SetSelection(0) + self.c.Contexts().LocalCommits.HandleFocus(types.OnFocusOpts{}) + }, + }) + return nil + }, + }) + if self.c.Modes().Filtering.Active() { menuItems = append(menuItems, &types.MenuItem{ Label: self.c.Tr.ExitFilterMode, diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 8ebc76d161d..461839edcdc 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -330,6 +330,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit() error { All: self.c.Contexts().LocalCommits.GetShowWholeGitGraph(), MainBranches: self.c.Model().MainBranches, HashPool: self.c.Model().HashPool, + HideMerges: self.c.Modes().Filtering.GetHideMerges(), }, ) if err != nil { @@ -367,6 +368,7 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit() error { RefForPushedStatus: self.c.Contexts().SubCommits.GetRef(), MainBranches: self.c.Model().MainBranches, HashPool: self.c.Model().HashPool, + HideMerges: self.c.Modes().Filtering.GetHideMerges(), }, ) if err != nil { diff --git a/pkg/gui/controllers/helpers/sub_commits_helper.go b/pkg/gui/controllers/helpers/sub_commits_helper.go index 080e1b45611..2c292ed26fb 100644 --- a/pkg/gui/controllers/helpers/sub_commits_helper.go +++ b/pkg/gui/controllers/helpers/sub_commits_helper.go @@ -43,6 +43,7 @@ func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error { RefToShowDivergenceFrom: opts.RefToShowDivergenceFrom, MainBranches: self.c.Model().MainBranches, HashPool: self.c.Model().HashPool, + HideMerges: self.c.Modes().Filtering.GetHideMerges(), }, ) if err != nil { diff --git a/pkg/gui/modes/filtering/filtering.go b/pkg/gui/modes/filtering/filtering.go index acdb94e5320..55951d4dfb0 100644 --- a/pkg/gui/modes/filtering/filtering.go +++ b/pkg/gui/modes/filtering/filtering.go @@ -4,10 +4,11 @@ type Filtering struct { path string // the filename that gets passed to git log author string // the author that gets passed to git log selectedCommitHash string // the commit that was selected before we entered filtering mode + hideMerges bool // whether to hide merge commits } func New(path string, author string) Filtering { - return Filtering{path: path, author: author} + return Filtering{path: path, author: author, hideMerges: false} } func (m *Filtering) Active() bool { @@ -17,6 +18,7 @@ func (m *Filtering) Active() bool { func (m *Filtering) Reset() { m.path = "" m.author = "" + m.hideMerges = false } func (m *Filtering) SetPath(path string) { @@ -42,3 +44,11 @@ func (m *Filtering) SetSelectedCommitHash(hash string) { func (m *Filtering) GetSelectedCommitHash() string { return m.selectedCommitHash } + +func (m *Filtering) SetHideMerges(hideMerges bool) { + m.hideMerges = hideMerges +} + +func (m *Filtering) GetHideMerges() bool { + return m.hideMerges +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index ea9f805a277..681d273abfe 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -623,6 +623,8 @@ type TranslationSet struct { ExitFilterMode string FilterPathOption string FilterAuthorOption string + FilterHideMerges string + FilterHideMergesTooltip string EnterFileName string EnterAuthor string FilteringMenuTitle string @@ -1704,6 +1706,8 @@ func EnglishTranslationSet() *TranslationSet { ExitFilterMode: "Stop filtering", FilterPathOption: "Enter path to filter by", FilterAuthorOption: "Enter author to filter by", + FilterHideMerges: "Hide merge commits", + FilterHideMergesTooltip: "Exclude merge commits from the list", EnterFileName: "Enter path:", EnterAuthor: "Enter author:", FilteringMenuTitle: "Filtering", diff --git a/pkg/i18n/translations/ja.json b/pkg/i18n/translations/ja.json index 3899845415e..f6ae226ed1d 100644 --- a/pkg/i18n/translations/ja.json +++ b/pkg/i18n/translations/ja.json @@ -564,6 +564,8 @@ "FilterBy": "フィルター条件", "ExitFilterMode": "フィルタリングを停止", "FilterPathOption": "フィルタリングするパスを入力", + "FilterHideMerges": "マージコミットを非表示", + "FilterHideMergesTooltip": "リストからマージコミットを除外", "FilterAuthorOption": "フィルタリングする作者を入力", "EnterFileName": "パスを入力:", "EnterAuthor": "作者を入力:", diff --git a/pkg/i18n/translations/ko.json b/pkg/i18n/translations/ko.json index b5db4b70456..9fd3cf6c7f1 100644 --- a/pkg/i18n/translations/ko.json +++ b/pkg/i18n/translations/ko.json @@ -234,6 +234,8 @@ "GotoBottom": "맨 아래로 스크롤 ", "ResetInParentheses": "(reset)", "OpenFilteringMenu": "View filter-by-path options", + "FilterHideMerges": "병합 커밋 숨기기", + "FilterHideMergesTooltip": "목록에서 병합 커밋 제외", "ExitFilterMode": "Stop filtering by path", "MustExitFilterModePrompt": "Command not available in filtered mode. Exit filtered mode?", "EnterRefName": "Ref 입력:", diff --git a/pkg/i18n/translations/nl.json b/pkg/i18n/translations/nl.json index 598a91c66ee..3ff39b1404b 100644 --- a/pkg/i18n/translations/nl.json +++ b/pkg/i18n/translations/nl.json @@ -232,6 +232,8 @@ "FilterBy": "Filter bij", "ExitFilterMode": "Stop met filteren bij pad", "FilterPathOption": "Vulin pad om op te filteren", + "FilterHideMerges": "Verberg merge commits", + "FilterHideMergesTooltip": "Sluit merge commits uit van de lijst", "EnterFileName": "Vulin path:", "FilteringMenuTitle": "Filteren", "MustExitFilterModeTitle": "Command niet beschikbaar", diff --git a/pkg/i18n/translations/pl.json b/pkg/i18n/translations/pl.json index 976eb5a7678..2e0723b44d0 100644 --- a/pkg/i18n/translations/pl.json +++ b/pkg/i18n/translations/pl.json @@ -461,6 +461,8 @@ "FilterBy": "Filtruj przez", "ExitFilterMode": "Zatrzymaj filtrowanie", "FilterPathOption": "Wprowadź ścieżkę do filtrowania", + "FilterHideMerges": "Ukryj commity merge", + "FilterHideMergesTooltip": "Wyklucz commity merge z listy", "FilterAuthorOption": "Wprowadź autora do filtrowania", "EnterFileName": "Wprowadź ścieżkę:", "EnterAuthor": "Wprowadź autora:", diff --git a/pkg/i18n/translations/pt.json b/pkg/i18n/translations/pt.json index cfd00322683..fd40516f9a7 100644 --- a/pkg/i18n/translations/pt.json +++ b/pkg/i18n/translations/pt.json @@ -479,6 +479,8 @@ "UpstreamGenericName": "upstream da branch selecionada", "CreatingTag": "Criando etiqueta", "ForceTag": "Forçar Etiqueta", + "FilterHideMerges": "Ocultar commits de merge", + "FilterHideMergesTooltip": "Excluir commits de merge da lista", "GitFlowOptions": "Exibir opções do git-flow", "Actions": {}, "Bisect": {}, diff --git a/pkg/i18n/translations/ru.json b/pkg/i18n/translations/ru.json index a1455f9c336..3113dc78a28 100644 --- a/pkg/i18n/translations/ru.json +++ b/pkg/i18n/translations/ru.json @@ -332,6 +332,8 @@ "FilterBy": "Фильтровать по", "ExitFilterMode": "Прекратить фильтрацию по пути", "FilterPathOption": "Введите путь для фильтрации", + "FilterHideMerges": "Скрыть merge коммиты", + "FilterHideMergesTooltip": "Исключить merge коммиты из списка", "EnterFileName": "Введите путь:", "FilteringMenuTitle": "Фильтрация", "MustExitFilterModeTitle": "Команда недоступна", diff --git a/pkg/i18n/translations/zh-CN.json b/pkg/i18n/translations/zh-CN.json index 62e442e42e2..ccaab1685af 100644 --- a/pkg/i18n/translations/zh-CN.json +++ b/pkg/i18n/translations/zh-CN.json @@ -563,6 +563,8 @@ "FilterBy": "过滤", "ExitFilterMode": "停止按路径过滤", "FilterPathOption": "输入要过滤的路径", + "FilterHideMerges": "隐藏合并提交", + "FilterHideMergesTooltip": "从列表中排除合并提交", "FilterAuthorOption": "输入作者进行过滤", "EnterFileName": "输入路径:", "EnterAuthor": "输入作者:", diff --git a/pkg/i18n/translations/zh-TW.json b/pkg/i18n/translations/zh-TW.json index 94c53444d26..e2c434af07c 100644 --- a/pkg/i18n/translations/zh-TW.json +++ b/pkg/i18n/translations/zh-TW.json @@ -391,6 +391,8 @@ "FilterBy": "篩選路徑", "ExitFilterMode": "停止按路徑篩選", "FilterPathOption": "輸入要依路徑篩選的路徑", + "FilterHideMerges": "隱藏合併提交", + "FilterHideMergesTooltip": "從列表中排除合併提交", "EnterFileName": "輸入路徑:", "FilteringMenuTitle": "篩選", "MustExitFilterModeTitle": "命令不可用", diff --git a/pkg/integration/tests/filter_and_search/hide_merge_commits.go b/pkg/integration/tests/filter_and_search/hide_merge_commits.go new file mode 100644 index 00000000000..24218f0b88f --- /dev/null +++ b/pkg/integration/tests/filter_and_search/hide_merge_commits.go @@ -0,0 +1,75 @@ +package filter_and_search + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var HideMergeCommits = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Test hiding merge commits functionality", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + // Create a repo with merge commits to test the functionality + shell.CreateFile("main.go", "package main\n\nfunc main() {\n\tprintln(\"Hello, World!\")\n}") + shell.EmptyCommit("Initial commit") + + // Create a feature branch + shell.NewBranch("feature-branch") + shell.CreateFile("feature.go", "package main\n\nfunc feature() {\n\tprintln(\"Feature!\")\n}") + shell.EmptyCommit("Add feature") + + // Switch back to master (default branch) and create another commit + shell.Checkout("master") + shell.CreateFile("utils.go", "package main\n\nfunc utils() {\n\tprintln(\"Utils!\")\n}") + shell.EmptyCommit("Add utils") + + // Merge feature branch into master (this creates a merge commit) + shell.Merge("feature-branch") + + // Create another commit after merge + shell.CreateFile("final.go", "package main\n\nfunc final() {\n\tprintln(\"Final!\")\n}") + shell.EmptyCommit("Final commit") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + // Verify that merge commit is initially visible + Content(Contains(`Merge branch 'feature-branch'`)). + Content(Contains(`Final commit`)). + Content(Contains(`Add utils`)). + Content(Contains(`Add feature`)). + Content(Contains(`Initial commit`)). + // Open filtering menu + Press(keys.Universal.FilteringMenu). + // Check that Hide merge commits option is available and toggle it + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Filtering")). + Select(Contains("Hide merge commits")). + Confirm() + }). + // Verify that merge commit is hidden + Content(DoesNotContain(`Merge branch 'feature-branch'`)). + Content(Contains(`Final commit`)). + Content(Contains(`Add utils`)). + Content(Contains(`Add feature`)). + Content(Contains(`Initial commit`)). + // Open filtering menu again to toggle off + Press(keys.Universal.FilteringMenu). + // Verify Hide merge commits is still selected and toggle it off + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Filtering")). + Select(Contains("Hide merge commits")). + Confirm() + }). + // Verify that merge commit is visible again + Content(Contains(`Merge branch 'feature-branch'`)). + Content(Contains(`Final commit`)). + Content(Contains(`Add utils`)). + Content(Contains(`Add feature`)). + Content(Contains(`Initial commit`)) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index a292227b388..417a953850d 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -227,6 +227,7 @@ var tests = []*components.IntegrationTest{ filter_and_search.FilterRemotes, filter_and_search.FilterSearchHistory, filter_and_search.FilterUpdatesWhenModelChanges, + filter_and_search.HideMergeCommits, filter_and_search.NestedFilter, filter_and_search.NestedFilterTransient, filter_and_search.NewSearch,