From 624ae45ebb3f54499a25c4eba0844fa971277c34 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 28 Mar 2020 16:28:35 +1100 Subject: [PATCH] allow scoped mode where the commits/reflog/stash panels are scoped to a file WIP restrict certain actions in scoped mode WIP --- docs/Config.md | 1 + main.go | 5 ++- pkg/app/app.go | 4 +- pkg/commands/commit_list_builder.go | 22 +++++++--- pkg/commands/git.go | 61 +++++++++++++++++++++++--- pkg/commands/git_test.go | 2 +- pkg/config/app_config.go | 1 + pkg/gui/branches_panel.go | 12 +++++ pkg/gui/commits_panel.go | 68 ++++++++++++++++++++++++++++- pkg/gui/gui.go | 66 ++++++++++++++++++++++------ pkg/gui/keybindings.go | 9 +++- pkg/gui/patch_options_panel.go | 3 ++ pkg/gui/reflog_panel.go | 4 +- pkg/gui/scoping_menu_panel.go | 58 ++++++++++++++++++++++++ pkg/gui/stash_panel.go | 2 +- pkg/i18n/english.go | 24 ++++++++++ pkg/utils/utils.go | 4 +- 17 files changed, 308 insertions(+), 38 deletions(-) create mode 100644 pkg/gui/scoping_menu_panel.go diff --git a/docs/Config.md b/docs/Config.md index 9bf9e45af..e6ca06a71 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -89,6 +89,7 @@ Default path for the config file: prevScreenMode: '_' undo: 'z' redo: '' + scopingMenu: status: checkForUpdate: 'u' recentRepos: '' diff --git a/main.go b/main.go index 308d83a4e..348294b9e 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,9 @@ func main() { repoPath := "." flaggy.String(&repoPath, "p", "path", "Path of git repo") + logScope := "" + flaggy.String(&logScope, "s", "scope", "scope for `git log`, typically the path of a file") + dump := "" flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file") flaggy.DefaultParser.PositionalFlags[0].Hidden = true @@ -61,7 +64,7 @@ func main() { log.Fatal(err.Error()) } - app, err := app.NewApp(appConfig) + app, err := app.NewApp(appConfig, logScope) if err == nil { err = app.Run() diff --git a/pkg/app/app.go b/pkg/app/app.go index deec4b6a3..9e5463214 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -91,7 +91,7 @@ func newLogger(config config.AppConfigurer) *logrus.Entry { } // NewApp bootstrap a new application -func NewApp(config config.AppConfigurer) (*App, error) { +func NewApp(config config.AppConfigurer, logScope string) (*App, error) { app := &App{ closers: []io.Closer{}, Config: config, @@ -121,7 +121,7 @@ func NewApp(config config.AppConfigurer) (*App, error) { if err != nil { return app, err } - app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater) + app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, logScope) if err != nil { return app, err } diff --git a/pkg/commands/commit_list_builder.go b/pkg/commands/commit_list_builder.go index 965bcd4e0..c216cec7b 100644 --- a/pkg/commands/commit_list_builder.go +++ b/pkg/commands/commit_list_builder.go @@ -83,15 +83,20 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *Commit { } } +type GetCommitsOptions struct { + Limit bool + LogScope string +} + // GetCommits obtains the commits of the current branch -func (c *CommitListBuilder) GetCommits(limit bool) ([]*Commit, error) { +func (c *CommitListBuilder) GetCommits(options GetCommitsOptions) ([]*Commit, error) { commits := []*Commit{} var rebasingCommits []*Commit rebaseMode, err := c.GitCommand.RebaseMode() if err != nil { return nil, err } - if rebaseMode != "" { + if rebaseMode != "" && options.LogScope == "" { // here we want to also prepend the commits that we're in the process of rebasing rebasingCommits, err = c.getRebasingCommits(rebaseMode) if err != nil { @@ -103,7 +108,7 @@ func (c *CommitListBuilder) GetCommits(limit bool) ([]*Commit, error) { } unpushedCommits := c.getUnpushedCommits() - cmd := c.getLogCmd(limit) + cmd := c.getLogCmd(options) err = RunLineOutputCmd(cmd, func(line string) (bool, error) { commit := c.extractCommitFromLine(line) @@ -294,11 +299,16 @@ func (c *CommitListBuilder) getUnpushedCommits() map[string]bool { } // getLog gets the git log. -func (c *CommitListBuilder) getLogCmd(limit bool) *exec.Cmd { +func (c *CommitListBuilder) getLogCmd(options GetCommitsOptions) *exec.Cmd { limitFlag := "" - if limit { + if options.Limit { limitFlag = "-300" } - return c.OSCommand.ExecutableFromString(fmt.Sprintf("git log --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%s\" %s --abbrev=%d --date=unix ", SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, limitFlag, 20)) + scopeFlag := "" + if options.LogScope != "" { + scopeFlag = fmt.Sprintf(" -- %s", c.OSCommand.Quote(options.LogScope)) + } + + return c.OSCommand.ExecutableFromString(fmt.Sprintf("git log --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%s\" %s --abbrev=%d --date=unix %s", SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, limitFlag, 20, scopeFlag)) } diff --git a/pkg/commands/git.go b/pkg/commands/git.go index eb765f4fa..84e8d50a4 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -156,9 +156,7 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil } -// GetStashEntries stash entries -func (c *GitCommand) GetStashEntries() []*StashEntry { - // if we directly put this string in RunCommandWithOutput the compiler complains because it thinks it's a format string +func (c *GitCommand) getStashEntriesWithoutScope() []*StashEntry { unescaped := "git stash list --pretty='%gs'" rawString, _ := c.OSCommand.RunCommandWithOutput(unescaped) stashEntries := []*StashEntry{} @@ -168,6 +166,45 @@ func (c *GitCommand) GetStashEntries() []*StashEntry { return stashEntries } +// GetStashEntries stash entries +func (c *GitCommand) GetStashEntries(scope string) []*StashEntry { + if scope == "" { + return c.getStashEntriesWithoutScope() + } + + unescaped := fmt.Sprintf("git stash list --name-only") + rawString, err := c.OSCommand.RunCommandWithOutput(unescaped) + if err != nil { + return c.getStashEntriesWithoutScope() + } + stashEntries := []*StashEntry{} + var currentStashEntry *StashEntry + lines := utils.SplitLines(rawString) + isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") } + re := regexp.MustCompile(`stash@\{(\d+)\}`) + +outer: + for i := 0; i < len(lines); i++ { + if !isAStash(lines[i]) { + continue + } + match := re.FindStringSubmatch(lines[i]) + idx, err := strconv.Atoi(match[1]) + if err != nil { + return c.getStashEntriesWithoutScope() + } + currentStashEntry = stashEntryFromLine(lines[i], idx) + for i+1 < len(lines) && !isAStash(lines[i+1]) { + i++ + if lines[i] == scope { + stashEntries = append(stashEntries, currentStashEntry) + continue outer + } + } + } + return stashEntries +} + func stashEntryFromLine(line string, index int) *StashEntry { return &StashEntry{ Name: line, @@ -568,8 +605,12 @@ func (c *GitCommand) Ignore(filename string) error { return c.OSCommand.AppendLineToFile(".gitignore", filename) } -func (c *GitCommand) ShowCmdStr(sha string) string { - return fmt.Sprintf("git show --color=%s --no-renames --stat -p %s", c.colorArg(), sha) +func (c *GitCommand) ShowCmdStr(sha string, scope string) string { + scopeArg := "" + if scope != "" { + scopeArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(scope)) + } + return fmt.Sprintf("git show --color=%s --no-renames --stat -p %s %s", c.colorArg(), sha, scopeArg) } func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string { @@ -1121,11 +1162,17 @@ func (c *GitCommand) FetchRemote(remoteName string) error { // 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 (c *GitCommand) GetReflogCommits(lastReflogCommit *Commit) ([]*Commit, bool, error) { + +func (c *GitCommand) GetReflogCommits(lastReflogCommit *Commit, scope string) ([]*Commit, bool, error) { commits := make([]*Commit, 0) re := regexp.MustCompile(`(\w+).*HEAD@\{([^\}]+)\}: (.*)`) - cmd := c.OSCommand.ExecutableFromString("git reflog --abbrev=20 --date=unix") + scopeArg := "" + if scope != "" { + scopeArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(scope)) + } + + cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf("git reflog --abbrev=20 --date=unix %s", scopeArg)) onlyObtainedNewReflogCommits := false err := RunLineOutputCmd(cmd, func(line string) (bool, error) { match := re.FindStringSubmatch(line) diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go index 56e2923cd..c8b5bf271 100644 --- a/pkg/commands/git_test.go +++ b/pkg/commands/git_test.go @@ -312,7 +312,7 @@ func TestGitCommandGetStashEntries(t *testing.T) { gitCmd := NewDummyGitCommand() gitCmd.OSCommand.command = s.command - s.test(gitCmd.GetStashEntries()) + s.test(gitCmd.GetStashEntries("")) }) } } diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index b9b9f892b..bddc97d3e 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -320,6 +320,7 @@ keybinding: prevScreenMode: '_' undo: 'z' redo: '' + scopingMenu: status: checkForUpdate: 'u' recentRepos: '' diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index 9f3bd0f97..3d1692844 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -263,6 +263,10 @@ func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *c } func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + if gui.GitCommand.IsHeadDetached() { return gui.createErrorPanel("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on") } @@ -286,6 +290,10 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error { } func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + selectedBranchName := gui.getSelectedBranch().Name return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName) } @@ -296,6 +304,10 @@ func (gui *Gui) handleRebaseOntoLocalBranch(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + checkedOutBranch := gui.getCheckedOutBranch().Name if selectedBranchName == checkedOutBranch { return gui.createErrorPanel(gui.Tr.SLocalize("CantRebaseOntoSelf")) diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 3d96555f0..0bdc18fb0 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -62,7 +62,7 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { } cmd := gui.OSCommand.ExecutableFromString( - gui.GitCommand.ShowCmdStr(commit.Sha), + gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.LogScope), ) if err := gui.newPtyTask("main", cmd); err != nil { gui.Log.Error(err) @@ -122,7 +122,7 @@ func (gui *Gui) refreshCommitsWithLimit() error { return err } - commits, err := builder.GetCommits(gui.State.Panels.Commits.LimitCommits) + commits, err := builder.GetCommits(commands.GetCommitsOptions{Limit: gui.State.Panels.Commits.LimitCommits, LogScope: gui.State.LogScope}) if err != nil { return err } @@ -140,6 +140,10 @@ func (gui *Gui) refreshCommitsWithLimit() error { // specific functions func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + if len(gui.State.Commits) <= 1 { return gui.createErrorPanel(gui.Tr.SLocalize("YouNoCommitsToSquash")) } @@ -161,6 +165,10 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + if len(gui.State.Commits) <= 1 { return gui.createErrorPanel(gui.Tr.SLocalize("YouNoCommitsToSquash")) } @@ -182,6 +190,10 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + applied, err := gui.handleMidRebaseCommand("reword") if err != nil { return err @@ -203,6 +215,10 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + applied, err := gui.handleMidRebaseCommand("reword") if err != nil { return err @@ -249,6 +265,10 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) { } func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + applied, err := gui.handleMidRebaseCommand("drop") if err != nil { return err @@ -266,6 +286,10 @@ func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + index := gui.State.Panels.Commits.SelectedLine selectedCommit := gui.State.Commits[index] if selectedCommit.Status == "rebasing" { @@ -289,6 +313,10 @@ func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + index := gui.State.Panels.Commits.SelectedLine if index == 0 { return nil @@ -312,6 +340,10 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + applied, err := gui.handleMidRebaseCommand("edit") if err != nil { return err @@ -327,6 +359,10 @@ func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("AmendCommitTitle"), gui.Tr.SLocalize("AmendCommitPrompt"), func(*gocui.Gui, *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), func() error { err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha) @@ -336,6 +372,10 @@ func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + applied, err := gui.handleMidRebaseCommand("pick") if err != nil { return err @@ -350,6 +390,10 @@ func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha); err != nil { return gui.surfaceError(err) } @@ -358,6 +402,10 @@ func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + // get currently selected commit, add the sha to state. commit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine] @@ -397,6 +445,10 @@ func (gui *Gui) addCommitToCherryPickedCommits(index int) { } func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + commitShaMap := gui.cherryPickedCommitShaMap() // find the last commit that is copied that's above our position @@ -419,6 +471,10 @@ func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error { // HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("CherryPick"), gui.Tr.SLocalize("SureCherryPick"), func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.SLocalize("CherryPickingStatus"), func() error { err := gui.GitCommand.CherryPickCommits(gui.State.CherryPickedCommits) @@ -495,6 +551,10 @@ func (gui *Gui) unchooseCommit(commits []*commands.Commit, i int) []*commands.Co } func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + commit := gui.getSelectedCommit(g) if commit == nil { return nil @@ -515,6 +575,10 @@ func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error { + if ok, err := gui.validateNotInScopedMode(); err != nil || !ok { + return err + } + commit := gui.getSelectedCommit(g) if commit == nil { return nil diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 969c8c942..a3f110427 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -50,6 +50,7 @@ type SentinelErrors struct { ErrSubProcess error ErrNoFiles error ErrSwitchRepo error + ErrRestart error } // GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here @@ -67,6 +68,7 @@ func (gui *Gui) GenerateSentinelErrors() { ErrSubProcess: errors.New(gui.Tr.SLocalize("RunningSubprocess")), ErrNoFiles: errors.New(gui.Tr.SLocalize("NoChangedFiles")), ErrSwitchRepo: errors.New("switching repo"), + ErrRestart: errors.New("restarting"), } } @@ -214,13 +216,13 @@ type guiState struct { PrevMainWidth int PrevMainHeight int OldInformation string - StartupStage int // one of INITIAL and COMPLETE. Allows us to not load everything at once + StartupStage int // one of INITIAL and COMPLETE. Allows us to not load everything at once + LogScope string // the filename that gets passed to git log } // for now the split view will always be on - // NewGui builds a new gui handler -func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) { +func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater, logScope string) (*Gui, error) { initialState := &guiState{ Files: make([]*commands.File, 0), @@ -248,9 +250,9 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma EditHistory: stack.New(), }, }, - ScreenMode: SCREEN_NORMAL, - SideView: nil, - Ptmx: nil, + SideView: nil, + Ptmx: nil, + LogScope: logScope, } gui := &Gui{ @@ -509,7 +511,9 @@ func (gui *Gui) layout(g *gocui.Gui) error { donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.SLocalize("Donate")) information = donate + " " + information } - if len(gui.State.CherryPickedCommits) > 0 { + if gui.inScopedMode() { + information = utils.ColoredString(fmt.Sprintf("%s '%s' %s", gui.Tr.SLocalize("scopingTo"), gui.State.LogScope, utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)), color.FgRed, color.Bold) + } else if len(gui.State.CherryPickedCommits) > 0 { information = utils.ColoredString(fmt.Sprintf("%d commits copied", len(gui.State.CherryPickedCommits)), color.FgCyan) } @@ -799,11 +803,15 @@ func (gui *Gui) layout(g *gocui.Gui) error { } if gui.g.CurrentView() == nil { - if _, err := gui.g.SetCurrentView(gui.getFilesView().Name()); err != nil { + initialView := gui.getFilesView() + if gui.inScopedMode() { + initialView = gui.getCommitsView() + } + if _, err := gui.g.SetCurrentView(initialView.Name()); err != nil { return err } - if err := gui.switchFocus(gui.g, nil, gui.getFilesView()); err != nil { + if err := gui.switchFocus(gui.g, nil, initialView); err != nil { return err } } @@ -985,6 +993,12 @@ func (gui *Gui) Run() error { } defer g.Close() + if gui.inScopedMode() { + gui.State.ScreenMode = SCREEN_HALF + } else { + gui.State.ScreenMode = SCREEN_NORMAL + } + g.OnSearchEscape = gui.onSearchEscape g.SearchEscapeKey = gui.getKey("universal.return") g.NextSearchMatchKey = gui.getKey("universal.nextMatch") @@ -1061,6 +1075,8 @@ func (gui *Gui) RunWithSubprocesses() error { break } else if err == gui.Errors.ErrSwitchRepo { continue + } else if err == gui.Errors.ErrRestart { + continue } else if err == gui.Errors.ErrSubProcess { if err := gui.runCommand(); err != nil { return err @@ -1097,16 +1113,29 @@ func (gui *Gui) runCommand() error { return nil } -func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error { +func (gui *Gui) handleInfoClick(g *gocui.Gui, v *gocui.View) error { if !gui.g.Mouse { return nil } cx, _ := v.Cursor() - if cx > len(gui.Tr.SLocalize("Donate")) { - return nil + width, _ := v.Size() + + // if we're in the normal context there will be a donate button here + // if we have ('reset') at the end then + if gui.inScopedMode() { + if width-cx <= len(gui.Tr.SLocalize("(reset)")) { + gui.State.LogScope = "" + return gui.Errors.ErrRestart + } else { + return nil + } } - return gui.OSCommand.OpenLink("https://github.com/sponsors/jesseduffield") + + if cx <= len(gui.Tr.SLocalize("Donate")) { + return gui.OSCommand.OpenLink("https://github.com/sponsors/jesseduffield") + } + return nil } // setColorScheme sets the color scheme for the app based on the user config @@ -1147,3 +1176,14 @@ func (gui *Gui) handleMouseDownSecondary(g *gocui.Gui, v *gocui.View) error { return nil } + +func (gui *Gui) inScopedMode() bool { + return gui.State.LogScope != "" +} + +func (gui *Gui) validateNotInScopedMode() (bool, error) { + if gui.inScopedMode() { + return false, gui.createErrorPanel("command not available in scoped mode. Either exit scoped mode or restart lazygit") + } + return true, nil +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index accb30ede..6d5a54bae 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -938,7 +938,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { ViewName: "information", Key: gocui.MouseLeft, Modifier: gocui.ModNone, - Handler: gui.handleDonate, + Handler: gui.handleInfoClick, }, { ViewName: "commitFiles", @@ -982,6 +982,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleEnterCommitFile, Description: gui.Tr.SLocalize("enterFile"), }, + { + ViewName: "", + Key: gui.getKey("universal.scopingMenu"), + Modifier: gocui.ModNone, + Handler: gui.handleCreateScopingMenuPanel, + Description: gui.Tr.SLocalize("openScopingMenu"), + }, { ViewName: "secondary", Key: gocui.MouseWheelUp, diff --git a/pkg/gui/patch_options_panel.go b/pkg/gui/patch_options_panel.go index e51860b43..074b4f38e 100644 --- a/pkg/gui/patch_options_panel.go +++ b/pkg/gui/patch_options_panel.go @@ -59,6 +59,9 @@ func (gui *Gui) getPatchCommitIndex() int { } func (gui *Gui) validateNormalWorkingTreeState() (bool, error) { + if gui.GitCommand.WorkingTreeState() != "normal" { + return false, gui.createErrorPanel(gui.Tr.SLocalize("CantPatchWhileRebasingError")) + } if gui.GitCommand.WorkingTreeState() != "normal" { return false, gui.createErrorPanel(gui.Tr.SLocalize("CantPatchWhileRebasingError")) } diff --git a/pkg/gui/reflog_panel.go b/pkg/gui/reflog_panel.go index 34491bc29..fa7f994ee 100644 --- a/pkg/gui/reflog_panel.go +++ b/pkg/gui/reflog_panel.go @@ -37,7 +37,7 @@ func (gui *Gui) handleReflogCommitSelect(g *gocui.Gui, v *gocui.View) error { v.FocusPoint(0, gui.State.Panels.ReflogCommits.SelectedLine) cmd := gui.OSCommand.ExecutableFromString( - gui.GitCommand.ShowCmdStr(commit.Sha), + gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.LogScope), ) if err := gui.newPtyTask("main", cmd); err != nil { gui.Log.Error(err) @@ -52,7 +52,7 @@ func (gui *Gui) refreshReflogCommits() error { lastReflogCommit = gui.State.ReflogCommits[0] } - commits, onlyObtainedNewReflogCommits, err := gui.GitCommand.GetReflogCommits(lastReflogCommit) + commits, onlyObtainedNewReflogCommits, err := gui.GitCommand.GetReflogCommits(lastReflogCommit, gui.State.LogScope) if err != nil { return gui.surfaceError(err) } diff --git a/pkg/gui/scoping_menu_panel.go b/pkg/gui/scoping_menu_panel.go new file mode 100644 index 000000000..d1c2f85e3 --- /dev/null +++ b/pkg/gui/scoping_menu_panel.go @@ -0,0 +1,58 @@ +package gui + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/gocui" +) + +func (gui *Gui) handleCreateScopingMenuPanel(g *gocui.Gui, v *gocui.View) error { + fileName := "" + switch v.Name() { + case "files": + file, err := gui.getSelectedFile(gui.g) + if err == nil { + fileName = file.Name + } + case "commitFiles": + file := gui.getSelectedCommitFile(gui.g) + if file != nil { + fileName = file.Name + } + } + + menuItems := []*menuItem{} + + if fileName != "" { + menuItems = append(menuItems, &menuItem{ + displayString: fmt.Sprintf("%s '%s'", gui.Tr.SLocalize("scopeTo"), fileName), + onPress: func() error { + gui.State.LogScope = fileName + return gui.Errors.ErrRestart + }, + }) + } + + menuItems = append(menuItems, &menuItem{ + displayString: gui.Tr.SLocalize("fileToScopeToOption"), + onPress: func() error { + return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("enterFileName"), "", func(g *gocui.Gui, promptView *gocui.View) error { + gui.State.LogScope = strings.TrimSpace(promptView.Buffer()) + return gui.Errors.ErrRestart + }) + }, + }) + + if gui.inScopedMode() { + menuItems = append(menuItems, &menuItem{ + displayString: gui.Tr.SLocalize("exitOutOfScopedMode"), + onPress: func() error { + gui.State.LogScope = "" + return gui.Errors.ErrRestart + }, + }) + } + + return gui.createMenu(gui.Tr.SLocalize("scopingMenuTitle"), menuItems, createMenuOptions{showCancel: true}) +} diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go index 4fae6085c..797a9a624 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -47,7 +47,7 @@ func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) refreshStashEntries(g *gocui.Gui) error { - gui.State.StashEntries = gui.GitCommand.GetStashEntries() + gui.State.StashEntries = gui.GitCommand.GetStashEntries(gui.State.LogScope) gui.refreshSelectedLine(&gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries)) diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index e2db241f0..bcdbb3e0d 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -1083,6 +1083,30 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "gotoBottom", Other: "scroll to bottom", + }, &i18n.Message{ + ID: "scopingTo", + Other: "scoping to", + }, &i18n.Message{ + ID: "(reset)", + Other: "(reset)", + }, &i18n.Message{ + ID: "openScopingMenu", + Other: "view scoping options", + }, &i18n.Message{ + ID: "scopeTo", + Other: "scope to", + }, &i18n.Message{ + ID: "exitOutOfScopedMode", + Other: "stop scoping", + }, &i18n.Message{ + ID: "fileToScopeToOption", + Other: "enter path to scope to", + }, &i18n.Message{ + ID: "enterFileName", + Other: "enter path:", + }, &i18n.Message{ + ID: "scopingMenuTitle", + Other: "scoping", }, ) } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 4afee73b7..2e17527aa 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -39,8 +39,8 @@ func WithPadding(str string, padding int) string { // ColoredString takes a string and a colour attribute and returns a colored // string with that attribute -func ColoredString(str string, colorAttribute color.Attribute) string { - colour := color.New(colorAttribute) +func ColoredString(str string, colorAttributes ...color.Attribute) string { + colour := color.New(colorAttributes...) return ColoredStringDirect(str, colour) }