diff --git a/docs/Searching.md b/docs/Searching.md index 589831c55e3..646a8870124 100644 --- a/docs/Searching.md +++ b/docs/Searching.md @@ -17,5 +17,10 @@ You can filter the files view to only show staged/unstaged files by pressing `` and then enter the path of the file you want to filter by + +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 + +## Filtering by changes + +You can filter the commits view to only show commits that contain changes to a given string by pressing `` and selecting `Enter changes to filter by`. This feature uses Git's `-S` flag. diff --git a/pkg/commands/git_commands/commit_loader.go b/pkg/commands/git_commands/commit_loader.go index f9cfff1419e..97b60f5c259 100644 --- a/pkg/commands/git_commands/commit_loader.go +++ b/pkg/commands/git_commands/commit_loader.go @@ -59,6 +59,7 @@ type GetCommitsOptions struct { Limit bool FilterPath string FilterAuthor string + FilterChanges string IncludeRebaseCommits bool RefName string // e.g. "HEAD" or "my_branch" RefForPushedStatus string // the ref to use for determining pushed/unpushed status @@ -513,6 +514,7 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj { Arg(prettyFormat). Arg("--abbrev=40"). ArgIf(opts.FilterAuthor != "", "--author="+opts.FilterAuthor). + ArgIf(opts.FilterChanges != "", "-S"+opts.FilterChanges). ArgIf(opts.Limit, "-300"). ArgIf(opts.FilterPath != "", "--follow"). Arg("--no-show-signature"). diff --git a/pkg/commands/git_commands/reflog_commit_loader.go b/pkg/commands/git_commands/reflog_commit_loader.go index 721bb99e71f..93caea2067a 100644 --- a/pkg/commands/git_commands/reflog_commit_loader.go +++ b/pkg/commands/git_commands/reflog_commit_loader.go @@ -23,7 +23,7 @@ func NewReflogCommitLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) // GetReflogCommits only returns the new reflog commits since the given lastReflogCommit // if none is passed (i.e. it's value is nil) then we get all the reflog commits -func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string, filterAuthor string) ([]*models.Commit, bool, error) { +func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string, filterAuthor string, filterChanges string) ([]*models.Commit, bool, error) { commits := make([]*models.Commit, 0) cmdArgs := NewGitCmd("log"). @@ -33,6 +33,7 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit Arg("--format=%h%x00%ct%x00%gs%x00%p"). ArgIf(filterAuthor != "", "--author="+filterAuthor). ArgIf(filterPath != "", "--follow", "--", filterPath). + ArgIf(filterChanges != "", "-S", "filterChanges"). ToArgv() cmdObj := self.cmd.New(cmdArgs).DontLog() diff --git a/pkg/gui/controllers/filtering_menu_action.go b/pkg/gui/controllers/filtering_menu_action.go index 0f0f9ceecf5..79d978034db 100644 --- a/pkg/gui/controllers/filtering_menu_action.go +++ b/pkg/gui/controllers/filtering_menu_action.go @@ -90,6 +90,21 @@ func (self *FilteringMenuAction) Call() error { Tooltip: tooltip, }) + menuItems = append(menuItems, &types.MenuItem{ + Label: self.c.Tr.FilterChangesOption, + OnPress: func() error { + self.c.Prompt(types.PromptOpts{ + Title: self.c.Tr.EnterChanges, + HandleConfirm: func(response string) error { + return self.setFilteringChanges(strings.TrimSpace(response)) + }, + }) + + return nil + }, + Tooltip: tooltip, + }) + if self.c.Modes().Filtering.Active() { menuItems = append(menuItems, &types.MenuItem{ Label: self.c.Tr.ExitFilterMode, @@ -112,6 +127,12 @@ func (self *FilteringMenuAction) setFilteringAuthor(author string) error { return self.setFiltering() } +func (self *FilteringMenuAction) setFilteringChanges(changes string) error { + self.c.Modes().Filtering.Reset() + self.c.Modes().Filtering.SetChanges(changes) + return self.setFiltering() +} + func (self *FilteringMenuAction) setFiltering() error { self.c.Modes().Filtering.SetSelectedCommitHash(self.c.Contexts().LocalCommits.GetSelectedCommitHash()) diff --git a/pkg/gui/controllers/helpers/mode_helper.go b/pkg/gui/controllers/helpers/mode_helper.go index 5798621552f..ef9f9647eb9 100644 --- a/pkg/gui/controllers/helpers/mode_helper.go +++ b/pkg/gui/controllers/helpers/mode_helper.go @@ -46,6 +46,24 @@ type ModeStatus struct { Reset func() error } +func (self *ModeHelper) getFilter() string { + filtering := self.c.Modes().Filtering + + if path := filtering.GetPath(); path != "" { + return path + } + + if author := filtering.GetAuthor(); author != "" { + return author + } + + if changes := filtering.GetChanges(); changes != "" { + return changes + } + + return "" +} + func (self *ModeHelper) Statuses() []ModeStatus { return []ModeStatus{ { @@ -72,12 +90,11 @@ func (self *ModeHelper) Statuses() []ModeStatus { { IsActive: self.c.Modes().Filtering.Active, Description: func() string { - filterContent := lo.Ternary(self.c.Modes().Filtering.GetPath() != "", self.c.Modes().Filtering.GetPath(), self.c.Modes().Filtering.GetAuthor()) return self.withResetButton( fmt.Sprintf( "%s '%s'", self.c.Tr.FilteringBy, - filterContent, + self.getFilter(), ), style.FgRed, ) diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index cac9310d161..bdf14770d4e 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -328,6 +328,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit() error { Limit: self.c.Contexts().LocalCommits.GetLimitCommits(), FilterPath: self.c.Modes().Filtering.GetPath(), FilterAuthor: self.c.Modes().Filtering.GetAuthor(), + FilterChanges: self.c.Modes().Filtering.GetChanges(), IncludeRebaseCommits: true, RefName: self.refForLog(), RefForPushedStatus: checkedOutBranchName, @@ -356,6 +357,7 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit() error { Limit: self.c.Contexts().SubCommits.GetLimitCommits(), FilterPath: self.c.Modes().Filtering.GetPath(), FilterAuthor: self.c.Modes().Filtering.GetAuthor(), + FilterChanges: self.c.Modes().Filtering.GetChanges(), IncludeRebaseCommits: false, RefName: self.c.Contexts().SubCommits.GetRef().FullRefName(), RefToShowDivergenceFrom: self.c.Contexts().SubCommits.GetRefToShowDivergenceFrom(), @@ -456,7 +458,7 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele // which allows us to order them correctly. So if we're filtering we'll just // manually load all the reflog commits here var err error - reflogCommits, _, err = self.c.Git().Loaders.ReflogCommitLoader.GetReflogCommits(nil, "", "") + reflogCommits, _, err = self.c.Git().Loaders.ReflogCommitLoader.GetReflogCommits(nil, "", "", "") if err != nil { self.c.Log.Error(err) } @@ -623,9 +625,9 @@ func (self *RefreshHelper) refreshReflogCommits() error { lastReflogCommit = model.ReflogCommits[0] } - refresh := func(stateCommits *[]*models.Commit, filterPath string, filterAuthor string) error { + refresh := func(stateCommits *[]*models.Commit, filterPath string, filterAuthor string, filterChanges string) error { commits, onlyObtainedNewReflogCommits, err := self.c.Git().Loaders.ReflogCommitLoader. - GetReflogCommits(lastReflogCommit, filterPath, filterAuthor) + GetReflogCommits(lastReflogCommit, filterPath, filterAuthor, filterChanges) if err != nil { return err } @@ -638,12 +640,12 @@ func (self *RefreshHelper) refreshReflogCommits() error { return nil } - if err := refresh(&model.ReflogCommits, "", ""); err != nil { + if err := refresh(&model.ReflogCommits, "", "", ""); err != nil { return err } if self.c.Modes().Filtering.Active() { - if err := refresh(&model.FilteredReflogCommits, self.c.Modes().Filtering.GetPath(), self.c.Modes().Filtering.GetAuthor()); err != nil { + if err := refresh(&model.FilteredReflogCommits, self.c.Modes().Filtering.GetPath(), self.c.Modes().Filtering.GetAuthor(), self.c.Modes().Filtering.GetChanges()); err != nil { return err } } else { diff --git a/pkg/gui/controllers/helpers/sub_commits_helper.go b/pkg/gui/controllers/helpers/sub_commits_helper.go index a38acd2a77b..662fef98bce 100644 --- a/pkg/gui/controllers/helpers/sub_commits_helper.go +++ b/pkg/gui/controllers/helpers/sub_commits_helper.go @@ -40,6 +40,7 @@ func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error { Limit: true, FilterPath: self.c.Modes().Filtering.GetPath(), FilterAuthor: self.c.Modes().Filtering.GetAuthor(), + FilterChanges: self.c.Modes().Filtering.GetChanges(), IncludeRebaseCommits: false, RefName: opts.Ref.FullRefName(), RefForPushedStatus: opts.Ref.FullRefName(), diff --git a/pkg/gui/modes/filtering/filtering.go b/pkg/gui/modes/filtering/filtering.go index acdb94e5320..4f53aa0e7f7 100644 --- a/pkg/gui/modes/filtering/filtering.go +++ b/pkg/gui/modes/filtering/filtering.go @@ -4,6 +4,7 @@ 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 + changes string // changes that geds passed to git log after the -S flag } func New(path string, author string) Filtering { @@ -11,12 +12,13 @@ func New(path string, author string) Filtering { } func (m *Filtering) Active() bool { - return m.path != "" || m.author != "" + return m.path != "" || m.author != "" || m.changes != "" } func (m *Filtering) Reset() { m.path = "" m.author = "" + m.changes = "" } func (m *Filtering) SetPath(path string) { @@ -31,10 +33,18 @@ func (m *Filtering) SetAuthor(author string) { m.author = author } +func (m *Filtering) SetChanges(changes string) { + m.changes = changes +} + func (m *Filtering) GetAuthor() string { return m.author } +func (m *Filtering) GetChanges() string { + return m.changes +} + func (m *Filtering) SetSelectedCommitHash(hash string) { m.selectedCommitHash = hash } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 977f6863ce9..8f2699ca65d 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -596,8 +596,10 @@ type TranslationSet struct { ExitFilterMode string FilterPathOption string FilterAuthorOption string + FilterChangesOption string EnterFileName string EnterAuthor string + EnterChanges string FilteringMenuTitle string WillCancelExistingFilterTooltip string MustExitFilterModeTitle string @@ -1637,8 +1639,10 @@ func EnglishTranslationSet() *TranslationSet { ExitFilterMode: "Stop filtering", FilterPathOption: "Enter path to filter by", FilterAuthorOption: "Enter author to filter by", + FilterChangesOption: "Enter changes to filter by", EnterFileName: "Enter path:", EnterAuthor: "Enter author:", + EnterChanges: "Enter changes:", FilteringMenuTitle: "Filtering", WillCancelExistingFilterTooltip: "Note: this will cancel the existing filter", MustExitFilterModeTitle: "Command not available", diff --git a/pkg/integration/tests/filter_by_changes/type_changes.go b/pkg/integration/tests/filter_by_changes/type_changes.go new file mode 100644 index 00000000000..c4c73b1da7d --- /dev/null +++ b/pkg/integration/tests/filter_by_changes/type_changes.go @@ -0,0 +1,62 @@ +package filter_by_changes + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var TypeChanges = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Filter commits by author using the typed in author", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + }, + SetupRepo: func(shell *Shell) { + shell.CreateNCommits(4) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Status(). + Focus(). + Press(keys.Universal.FilteringMenu) + + t.ExpectPopup().Menu(). + Title(Equals("Filtering")). + Select(Contains("Enter changes to filter by")). + Confirm() + + t.ExpectPopup().Prompt(). + Title(Equals("Enter changes:")). + Type("file01"). + Confirm() + + t.Views().Commits(). + IsFocused(). + Lines( + Contains("commit 01"), + ) + + t.Views().Information().Content(Contains("Filtering by 'file01'")) + + t.Views().Status(). + Focus(). + Press(keys.Universal.FilteringMenu) + + t.ExpectPopup().Menu(). + Title(Equals("Filtering")). + Select(Contains("Enter changes to filter by")). + Confirm() + + t.ExpectPopup().Prompt(). + Title(Equals("Enter changes:")). + Type("file02"). + Confirm() + + t.Views().Commits(). + IsFocused(). + Lines( + Contains("commit 02"), + ) + + t.Views().Information().Content(Contains("Filtering by 'file02'")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 69a9ed5ec3a..c3e446f7f27 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -16,6 +16,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/integration/tests/file" "github.com/jesseduffield/lazygit/pkg/integration/tests/filter_and_search" "github.com/jesseduffield/lazygit/pkg/integration/tests/filter_by_author" + "github.com/jesseduffield/lazygit/pkg/integration/tests/filter_by_changes" "github.com/jesseduffield/lazygit/pkg/integration/tests/filter_by_path" "github.com/jesseduffield/lazygit/pkg/integration/tests/interactive_rebase" "github.com/jesseduffield/lazygit/pkg/integration/tests/misc" @@ -212,6 +213,7 @@ var tests = []*components.IntegrationTest{ filter_by_path.KeepSameCommitSelectedOnExit, filter_by_path.SelectFile, filter_by_path.TypeFile, + filter_by_changes.TypeChanges, interactive_rebase.AdvancedInteractiveRebase, interactive_rebase.AmendCommitWithConflict, interactive_rebase.AmendFirstCommit,