1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-12-01 22:52:01 +02:00

Add author filtering to commit view

This commit introduces a new feature to the commit view, allowing users
to filter commits based on the author's name or email address. Similar
to the existing path filtering functionality, accessible through <c-s>,
this feature allows users to filter the commit history by the currently
selected commit's author if the commit view is focused, or by typing in
the author's name or email address.

This feature adds an entry to the filtering menu, to provide users with
a familiar and intuitive experience
This commit is contained in:
Tristan Déplantes
2024-01-21 22:55:26 -05:00
committed by Stefan Haller
parent 329b434915
commit 503422a72e
22 changed files with 292 additions and 27 deletions

View File

@@ -64,6 +64,7 @@ func NewCommitLoader(
type GetCommitsOptions struct {
Limit bool
FilterPath string
FilterAuthor string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
RefForPushedStatus string // the ref to use for determining pushed/unpushed status
@@ -664,6 +665,7 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
Arg("--oneline").
Arg(prettyFormat).
Arg("--abbrev=40").
ArgIf(opts.FilterAuthor != "", "--author="+opts.FilterAuthor).
ArgIf(opts.Limit, "-300").
ArgIf(opts.FilterPath != "", "--follow").
Arg("--no-show-signature").

View File

@@ -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) ([]*models.Commit, bool, error) {
func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string, filterAuthor string) ([]*models.Commit, bool, error) {
commits := make([]*models.Commit, 0)
cmdArgs := NewGitCmd("log").
@@ -31,6 +31,7 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit
Arg("-g").
Arg("--abbrev=40").
Arg("--format=%h%x00%ct%x00%gs%x00%p").
ArgIf(filterAuthor != "", "--author="+filterAuthor).
ArgIf(filterPath != "", "--follow", "--", filterPath).
ToArgv()

View File

@@ -25,6 +25,7 @@ func TestGetReflogCommits(t *testing.T) {
runner *oscommands.FakeCmdObjRunner
lastReflogCommit *models.Commit
filterPath string
filterAuthor string
expectedCommits []*models.Commit
expectedOnlyObtainedNew bool
expectedError error
@@ -136,6 +137,31 @@ func TestGetReflogCommits(t *testing.T) {
expectedOnlyObtainedNew: true,
expectedError: nil,
},
{
testName: "when passing filterAuthor",
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p", "--author=John Doe <john@doe.com>"}, reflogOutput, nil),
lastReflogCommit: &models.Commit{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
filterAuthor: "John Doe <john@doe.com>",
expectedCommits: []*models.Commit{
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
},
expectedOnlyObtainedNew: true,
expectedError: nil,
},
{
testName: "when command returns error",
runner: oscommands.NewFakeRunner(t).
@@ -157,7 +183,7 @@ func TestGetReflogCommits(t *testing.T) {
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
}
commits, onlyObtainednew, err := builder.GetReflogCommits(scenario.lastReflogCommit, scenario.filterPath)
commits, onlyObtainednew, err := builder.GetReflogCommits(scenario.lastReflogCommit, scenario.filterPath, scenario.filterAuthor)
assert.Equal(t, scenario.expectedOnlyObtainedNew, onlyObtainednew)
assert.Equal(t, scenario.expectedError, err)
t.Logf("actual commits: \n%s", litter.Sdump(commits))

View File

@@ -13,6 +13,7 @@ type FilteringMenuAction struct {
func (self *FilteringMenuAction) Call() error {
fileName := ""
author := ""
switch self.c.CurrentSideContext() {
case self.c.Contexts().Files:
node := self.c.Contexts().Files.GetSelected()
@@ -24,16 +25,36 @@ func (self *FilteringMenuAction) Call() error {
if node != nil {
fileName = node.GetPath()
}
case self.c.Contexts().LocalCommits:
commit := self.c.Contexts().LocalCommits.GetSelected()
if commit != nil {
author = fmt.Sprintf("%s <%s>", commit.AuthorName, commit.AuthorEmail)
}
}
menuItems := []*types.MenuItem{}
tooltip := ""
if self.c.Modes().Filtering.Active() {
tooltip = self.c.Tr.WillCancelExistingFilterTooltip
}
if fileName != "" {
menuItems = append(menuItems, &types.MenuItem{
Label: fmt.Sprintf("%s '%s'", self.c.Tr.FilterBy, fileName),
OnPress: func() error {
return self.setFiltering(fileName)
return self.setFilteringPath(fileName)
},
Tooltip: tooltip,
})
}
if author != "" {
menuItems = append(menuItems, &types.MenuItem{
Label: fmt.Sprintf("%s '%s'", self.c.Tr.FilterBy, author),
OnPress: func() error {
return self.setFilteringAuthor(author)
},
Tooltip: tooltip,
})
}
@@ -44,10 +65,25 @@ func (self *FilteringMenuAction) Call() error {
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetFilePathSuggestionsFunc(),
Title: self.c.Tr.EnterFileName,
HandleConfirm: func(response string) error {
return self.setFiltering(strings.TrimSpace(response))
return self.setFilteringPath(strings.TrimSpace(response))
},
})
},
Tooltip: tooltip,
})
menuItems = append(menuItems, &types.MenuItem{
Label: self.c.Tr.FilterAuthorOption,
OnPress: func() error {
return self.c.Prompt(types.PromptOpts{
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetAuthorsSuggestionsFunc(),
Title: self.c.Tr.EnterAuthor,
HandleConfirm: func(response string) error {
return self.setFilteringAuthor(strings.TrimSpace(response))
},
})
},
Tooltip: tooltip,
})
if self.c.Modes().Filtering.Active() {
@@ -60,9 +96,19 @@ func (self *FilteringMenuAction) Call() error {
return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.FilteringMenuTitle, Items: menuItems})
}
func (self *FilteringMenuAction) setFiltering(path string) error {
func (self *FilteringMenuAction) setFilteringPath(path string) error {
self.c.Modes().Filtering.Reset()
self.c.Modes().Filtering.SetPath(path)
return self.setFiltering()
}
func (self *FilteringMenuAction) setFilteringAuthor(author string) error {
self.c.Modes().Filtering.Reset()
self.c.Modes().Filtering.SetAuthor(author)
return self.setFiltering()
}
func (self *FilteringMenuAction) setFiltering() error {
repoState := self.c.State().GetRepoState()
if repoState.GetScreenMode() == types.SCREEN_NORMAL {
repoState.SetScreenMode(types.SCREEN_HALF)

View File

@@ -72,11 +72,12 @@ 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,
self.c.Modes().Filtering.GetPath(),
filterContent,
),
style.FgRed,
)

View File

@@ -317,6 +317,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit() error {
git_commands.GetCommitsOptions{
Limit: self.c.Contexts().LocalCommits.GetLimitCommits(),
FilterPath: self.c.Modes().Filtering.GetPath(),
FilterAuthor: self.c.Modes().Filtering.GetAuthor(),
IncludeRebaseCommits: true,
RefName: self.refForLog(),
RefForPushedStatus: checkedOutBranchName,
@@ -342,6 +343,7 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit() error {
git_commands.GetCommitsOptions{
Limit: self.c.Contexts().SubCommits.GetLimitCommits(),
FilterPath: self.c.Modes().Filtering.GetPath(),
FilterAuthor: self.c.Modes().Filtering.GetAuthor(),
IncludeRebaseCommits: false,
RefName: self.c.Contexts().SubCommits.GetRef().FullRefName(),
RefToShowDivergenceFrom: self.c.Contexts().SubCommits.GetRefToShowDivergenceFrom(),
@@ -438,7 +440,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)
}
@@ -597,9 +599,9 @@ func (self *RefreshHelper) refreshReflogCommits() error {
lastReflogCommit = model.ReflogCommits[0]
}
refresh := func(stateCommits *[]*models.Commit, filterPath string) error {
refresh := func(stateCommits *[]*models.Commit, filterPath string, filterAuthor string) error {
commits, onlyObtainedNewReflogCommits, err := self.c.Git().Loaders.ReflogCommitLoader.
GetReflogCommits(lastReflogCommit, filterPath)
GetReflogCommits(lastReflogCommit, filterPath, filterAuthor)
if err != nil {
return self.c.Error(err)
}
@@ -612,12 +614,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()); err != nil {
if err := refresh(&model.FilteredReflogCommits, self.c.Modes().Filtering.GetPath(), self.c.Modes().Filtering.GetAuthor()); err != nil {
return err
}
} else {

View File

@@ -39,6 +39,7 @@ func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error {
git_commands.GetCommitsOptions{
Limit: true,
FilterPath: self.c.Modes().Filtering.GetPath(),
FilterAuthor: self.c.Modes().Filtering.GetAuthor(),
IncludeRebaseCommits: false,
RefName: opts.Ref.FullRefName(),
RefForPushedStatus: opts.Ref.FullRefName(),

View File

@@ -386,7 +386,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
Authors: map[string]*models.Author{},
},
Modes: &types.Modes{
Filtering: filtering.New(startArgs.FilterPath),
Filtering: filtering.New(startArgs.FilterPath, ""),
CherryPicking: cherrypicking.New(),
Diffing: diffing.New(),
MarkedBaseCommit: marked_base_commit.New(),

View File

@@ -1,19 +1,21 @@
package filtering
type Filtering struct {
path string // the filename that gets passed to git log
path string // the filename that gets passed to git log
author string // the author that gets passed to git log
}
func New(path string) Filtering {
return Filtering{path: path}
func New(path string, author string) Filtering {
return Filtering{path: path, author: author}
}
func (m *Filtering) Active() bool {
return m.path != ""
return m.path != "" || m.author != ""
}
func (m *Filtering) Reset() {
m.path = ""
m.author = ""
}
func (m *Filtering) SetPath(path string) {
@@ -23,3 +25,11 @@ func (m *Filtering) SetPath(path string) {
func (m *Filtering) GetPath() string {
return m.path
}
func (m *Filtering) SetAuthor(author string) {
m.author = author
}
func (m *Filtering) GetAuthor() string {
return m.author
}

View File

@@ -536,9 +536,13 @@ type TranslationSet struct {
OpenFilteringMenuTooltip string
FilterBy string
ExitFilterMode string
ExitFilterModeAuthor string
FilterPathOption string
FilterAuthorOption string
EnterFileName string
EnterAuthor string
FilteringMenuTitle string
WillCancelExistingFilterTooltip string
MustExitFilterModeTitle string
MustExitFilterModePrompt string
Diff string
@@ -1475,13 +1479,16 @@ func EnglishTranslationSet() TranslationSet {
GotoBottom: "Scroll to bottom",
FilteringBy: "Filtering by",
ResetInParentheses: "(Reset)",
OpenFilteringMenu: "View filter-by-path options",
OpenFilteringMenuTooltip: "View options for filtering the commit log by a file path, so that only commits relating to that path are shown.",
OpenFilteringMenu: "View filter options",
OpenFilteringMenuTooltip: "View options for filtering the commit log, so that only commits matching the filter are shown.",
FilterBy: "Filter by",
ExitFilterMode: "Stop filtering by path",
ExitFilterMode: "Stop filtering",
FilterPathOption: "Enter path to filter by",
FilterAuthorOption: "Enter author to filter by",
EnterFileName: "Enter path:",
EnterAuthor: "Enter author:",
FilteringMenuTitle: "Filtering",
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?",
Diff: "Diff",

View File

@@ -0,0 +1,70 @@
package filter_by_author
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var SelectAuthor = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Filter commits using the currently highlighted commit's author when the commit view is active",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.AppState.GitLogShowGraph = "never"
},
SetupRepo: func(shell *Shell) {
commonSetup(shell)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
SelectedLineIdx(0).
Press(keys.Universal.FilteringMenu)
t.ExpectPopup().Menu().
Title(Equals("Filtering")).
Select(Contains("Filter by 'Paul Oberstein <paul.oberstein@email.com>'")).
Confirm()
t.Views().Commits().
IsFocused().
Lines(
Contains("commit 7"),
Contains("commit 6"),
Contains("commit 5"),
Contains("commit 4"),
Contains("commit 3"),
Contains("commit 2"),
Contains("commit 1"),
Contains("commit 0"),
)
t.Views().Information().Content(Contains("Filtering by 'Paul Oberstein <paul.oberstein@email.com>'"))
t.Views().Commits().
Press(keys.Universal.FilteringMenu)
t.ExpectPopup().Menu().
Title(Equals("Filtering")).
Select(Contains("Stop filtering")).
Confirm()
t.Views().Commits().
IsFocused().
NavigateToLine(Contains("SK commit 0")).
Press(keys.Universal.FilteringMenu)
t.ExpectPopup().Menu().
Title(Equals("Filtering")).
Select(Contains("Filter by 'Siegfried Kircheis <siegfried.kircheis@email.com>'")).
Confirm()
t.Views().Commits().
IsFocused().
Lines(
Contains("commit 0"),
)
t.Views().Information().Content(Contains("Filtering by 'Siegfried Kircheis <siegfried.kircheis@email.com>'"))
},
})

View File

@@ -0,0 +1,30 @@
package filter_by_author
import (
"fmt"
"strings"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
type AuthorInfo struct {
name string
numberOfCommits int
}
func commonSetup(shell *Shell) {
authors := []AuthorInfo{{"Yang Wen-li", 3}, {"Siegfried Kircheis", 1}, {"Paul Oberstein", 8}}
totalCommits := 0
repoStartDaysAgo := 100
for _, authorInfo := range authors {
for i := 0; i < authorInfo.numberOfCommits; i++ {
authorEmail := strings.ToLower(strings.ReplaceAll(authorInfo.name, " ", ".")) + "@email.com"
commitMessage := fmt.Sprintf("commit %d", i)
shell.SetAuthor(authorInfo.name, authorEmail)
shell.EmptyCommitDaysAgo(commitMessage, repoStartDaysAgo-totalCommits)
totalCommits++
}
}
}

View File

@@ -0,0 +1,66 @@
package filter_by_author
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var TypeAuthor = 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) {
commonSetup(shell)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Status().
Focus().
Press(keys.Universal.FilteringMenu)
t.ExpectPopup().Menu().
Title(Equals("Filtering")).
Select(Contains("Enter author to filter by")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("Enter author:")).
Type("Yang").
SuggestionLines(Equals("Yang Wen-li <yang.wen-li@email.com>")).
ConfirmFirstSuggestion()
t.Views().Commits().
IsFocused().
Lines(
Contains("commit 2"),
Contains("commit 1"),
Contains("commit 0"),
)
t.Views().Information().Content(Contains("Filtering by 'Yang Wen-li <yang.wen-li@email.com>'"))
t.Views().Status().
Focus().
Press(keys.Universal.FilteringMenu)
t.ExpectPopup().Menu().
Title(Equals("Filtering")).
Select(Contains("Enter author to filter by")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("Enter author:")).
Type("Siegfried").
SuggestionLines(Equals("Siegfried Kircheis <siegfried.kircheis@email.com>")).
ConfirmFirstSuggestion()
t.Views().Commits().
IsFocused().
Lines(
Contains("commit 0"),
)
t.Views().Information().Content(Contains("Filtering by 'Siegfried Kircheis <siegfried.kircheis@email.com>'"))
},
})

View File

@@ -15,6 +15,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/integration/tests/diff"
"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_path"
"github.com/jesseduffield/lazygit/pkg/integration/tests/interactive_rebase"
"github.com/jesseduffield/lazygit/pkg/integration/tests/misc"
@@ -149,6 +150,8 @@ var tests = []*components.IntegrationTest{
filter_and_search.NestedFilter,
filter_and_search.NestedFilterTransient,
filter_and_search.NewSearch,
filter_by_author.SelectAuthor,
filter_by_author.TypeAuthor,
filter_by_path.CliArg,
filter_by_path.SelectFile,
filter_by_path.TypeFile,