1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-02-01 13:17:53 +02:00

start moving commit panel handlers into controller

more

and more

move rebase commit refreshing into existing abstraction

and more

and more

WIP

and more

handling clicks

properly fix merge conflicts

update cheatsheet

lots more preparation to start moving things into controllers

WIP

better typing

expand on remotes controller

moving more code into controllers
This commit is contained in:
Jesse Duffield 2022-01-16 14:46:53 +11:00
parent a90b6efded
commit 1dd7307fde
104 changed files with 4980 additions and 4111 deletions

View File

@ -123,30 +123,30 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Commits Panel (Commits)
<pre>
<kbd>ctrl+l</kbd>: open log menu
<kbd>s</kbd>: squash down
<kbd>r</kbd>: reword commit
<kbd>R</kbd>: reword commit with editor
<kbd>g</kbd>: reset to this commit
<kbd>f</kbd>: fixup commit
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
<kbd>d</kbd>: delete commit
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>n</kbd>: create new branch off of commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>s</kbd>: squash down
<kbd>f</kbd>: fixup commit
<kbd>r</kbd>: reword commit
<kbd>R</kbd>: reword commit with editor
<kbd>d</kbd>: delete commit
<kbd>e</kbd>: edit commit
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>A</kbd>: amend commit with staged changes
<kbd>t</kbd>: revert commit
<kbd>ctrl+l</kbd>: open log menu
<kbd>g</kbd>: reset to this commit
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+y</kbd>: copy commit message to clipboard
<kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options
@ -183,7 +183,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: amend last commit
<kbd>C</kbd>: commit changes using git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: view 'discard changes' options
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
@ -200,6 +199,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>`</kbd>: toggle file tree view
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>space</kbd>: toggle staged
</pre>
## Files Panel (Submodules)

View File

@ -123,30 +123,30 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Commits Paneel (Commits)
<pre>
<kbd>ctrl+l</kbd>: open log menu
<kbd>s</kbd>: squash beneden
<kbd>r</kbd>: hernoem commit
<kbd>R</kbd>: hernoem commit met editor
<kbd>g</kbd>: reset naar deze commit
<kbd>f</kbd>: Fixup commit
<kbd>F</kbd>: creëer fixup commit voor deze commit
<kbd>S</kbd>: squash bovenstaande commits
<kbd>d</kbd>: verwijder commit
<kbd>ctrl+j</kbd>: verplaats commit 1 naar beneden
<kbd>ctrl+k</kbd>: verplaats commit 1 naar boven
<kbd>e</kbd>: wijzig commit
<kbd>A</kbd>: wijzig commit met staged veranderingen
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
<kbd>t</kbd>: commit ongedaan maken
<kbd>c</kbd>: kopieer commit (cherry-pick)
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>v</kbd>: plak commits (cherry-pick)
<kbd>n</kbd>: creëer nieuwe branch van commit
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>s</kbd>: squash beneden
<kbd>f</kbd>: Fixup commit
<kbd>r</kbd>: hernoem commit
<kbd>R</kbd>: hernoem commit met editor
<kbd>d</kbd>: verwijder commit
<kbd>e</kbd>: wijzig commit
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
<kbd>F</kbd>: creëer fixup commit voor deze commit
<kbd>S</kbd>: squash bovenstaande commits
<kbd>ctrl+j</kbd>: verplaats commit 1 naar beneden
<kbd>ctrl+k</kbd>: verplaats commit 1 naar boven
<kbd>A</kbd>: wijzig commit met staged veranderingen
<kbd>t</kbd>: commit ongedaan maken
<kbd>ctrl+l</kbd>: open log menu
<kbd>g</kbd>: reset naar deze commit
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: creëer nieuwe branch van commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>ctrl+y</kbd>: kopieer commit bericht naar klembord
<kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options
@ -183,7 +183,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>A</kbd>: wijzig laatste commit
<kbd>C</kbd>: commit veranderingen met de git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
@ -200,6 +199,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>`</kbd>: toggle bestandsboom weergave
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>space</kbd>: toggle staged
</pre>
## Bestanden Paneel (Submodules)

View File

@ -123,30 +123,30 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Commity Panel (Commity)
<pre>
<kbd>ctrl+l</kbd>: open log menu
<kbd>s</kbd>: ściśnij
<kbd>r</kbd>: zmień nazwę commita
<kbd>R</kbd>: zmień nazwę commita w edytorze
<kbd>g</kbd>: zresetuj do tego commita
<kbd>f</kbd>: napraw commit
<kbd>F</kbd>: utwórz commit naprawczy dla tego commita
<kbd>S</kbd>: spłaszcz wszystkie commity naprawcze powyżej zaznaczonych commitów (autosquash)
<kbd>d</kbd>: usuń commit
<kbd>ctrl+j</kbd>: przenieś commit 1 w dół
<kbd>ctrl+k</kbd>: przenieś commit 1 w górę
<kbd>e</kbd>: edytuj commit
<kbd>A</kbd>: popraw commit zmianami z poczekalni
<kbd>p</kbd>: wybierz commit (podczas zmiany bazy)
<kbd>t</kbd>: odwróć commit
<kbd>c</kbd>: kopiuj commit (przebieranie)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
<kbd>v</kbd>: wklej commity (przebieranie)
<kbd>n</kbd>: create new branch off of commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>s</kbd>: ściśnij
<kbd>f</kbd>: napraw commit
<kbd>r</kbd>: zmień nazwę commita
<kbd>R</kbd>: zmień nazwę commita w edytorze
<kbd>d</kbd>: usuń commit
<kbd>e</kbd>: edytuj commit
<kbd>p</kbd>: wybierz commit (podczas zmiany bazy)
<kbd>F</kbd>: utwórz commit naprawczy dla tego commita
<kbd>S</kbd>: spłaszcz wszystkie commity naprawcze powyżej zaznaczonych commitów (autosquash)
<kbd>ctrl+j</kbd>: przenieś commit 1 w dół
<kbd>ctrl+k</kbd>: przenieś commit 1 w górę
<kbd>A</kbd>: popraw commit zmianami z poczekalni
<kbd>t</kbd>: odwróć commit
<kbd>ctrl+l</kbd>: open log menu
<kbd>g</kbd>: zresetuj do tego commita
<kbd>enter</kbd>: przeglądaj pliki commita
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+y</kbd>: copy commit message to clipboard
<kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options
@ -183,7 +183,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
<kbd>A</kbd>: Zmień ostatni commit
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
<kbd>space</kbd>: przełącz stan poczekalni
<kbd>d</kbd>: pokaż opcje porzucania zmian
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
@ -200,6 +199,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>`</kbd>: toggle file tree view
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>space</kbd>: przełącz stan poczekalni
</pre>
## Pliki Panel (Submodules)

View File

@ -123,30 +123,30 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 提交 面板 (提交)
<pre>
<kbd>ctrl+l</kbd>: open log menu
<kbd>s</kbd>: 向下压缩
<kbd>r</kbd>: 改写提交
<kbd>R</kbd>: 使用编辑器重命名提交
<kbd>g</kbd>: 重置为此提交
<kbd>f</kbd>: 修正提交(fixup)
<kbd>F</kbd>: 为此提交创建修正
<kbd>S</kbd>: 压缩在所选提交之上的所有“fixup!”提交(自动压缩)
<kbd>d</kbd>: 删除提交
<kbd>ctrl+j</kbd>: 下移提交
<kbd>ctrl+k</kbd>: 上移提交
<kbd>e</kbd>: 编辑提交
<kbd>A</kbd>: 用已暂存的更改来修补提交
<kbd>p</kbd>: 选择提交(变基过程中)
<kbd>t</kbd>: 还原提交
<kbd>c</kbd>: 复制提交(拣选)
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
<kbd>C</kbd>: 复制提交范围(拣选)
<kbd>v</kbd>: 粘贴提交(拣选)
<kbd>n</kbd>: 从提交创建新分支
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
<kbd>s</kbd>: 向下压缩
<kbd>f</kbd>: 修正提交(fixup)
<kbd>r</kbd>: 改写提交
<kbd>R</kbd>: 使用编辑器重命名提交
<kbd>d</kbd>: 删除提交
<kbd>e</kbd>: 编辑提交
<kbd>p</kbd>: 选择提交(变基过程中)
<kbd>F</kbd>: 为此提交创建修正
<kbd>S</kbd>: 压缩在所选提交之上的所有“fixup!”提交(自动压缩)
<kbd>ctrl+j</kbd>: 下移提交
<kbd>ctrl+k</kbd>: 上移提交
<kbd>A</kbd>: 用已暂存的更改来修补提交
<kbd>t</kbd>: 还原提交
<kbd>ctrl+l</kbd>: open log menu
<kbd>g</kbd>: 重置为此提交
<kbd>enter</kbd>: 查看提交的文件
<kbd>space</kbd>: 检出提交
<kbd>n</kbd>: 从提交创建新分支
<kbd>T</kbd>: 标签提交
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
<kbd>ctrl+y</kbd>: 将提交消息复制到剪贴板
<kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options
@ -183,7 +183,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>w</kbd>: 提交更改而无需预先提交钩子
<kbd>A</kbd>: 修补最后一次提交
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
<kbd>space</kbd>: 切换暂存状态
<kbd>d</kbd>: 查看'放弃更改‘选项
<kbd>e</kbd>: 编辑文件
<kbd>o</kbd>: 打开文件
@ -200,6 +199,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>`</kbd>: 切换文件树视图
<kbd>M</kbd>: 打开合并工具
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白更改
<kbd>space</kbd>: 切换暂存状态
</pre>
## 文件 面板 (子模块)

View File

@ -17,13 +17,14 @@ import (
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/integration"
)
type bindingSection struct {
title string
bindings []*gui.Binding
bindings []*types.Binding
}
func CommandToRun() string {
@ -113,7 +114,7 @@ func formatTitle(title string) string {
return fmt.Sprintf("\n## %s\n\n", title)
}
func formatBinding(binding *gui.Binding) string {
func formatBinding(binding *types.Binding) string {
if binding.Alternative != "" {
return fmt.Sprintf(" <kbd>%s</kbd>: %s (%s)\n", gui.GetKeyDisplay(binding.Key), binding.Description, binding.Alternative)
}
@ -130,7 +131,7 @@ func getBindingSections(mApp *app.App) []*bindingSection {
title string
}
contextAndViewBindingMap := map[contextAndViewType][]*gui.Binding{}
contextAndViewBindingMap := map[contextAndViewType][]*types.Binding{}
outer:
for _, binding := range bindings {
@ -138,7 +139,7 @@ outer:
key := contextAndViewType{subtitle: "", title: "navigation"}
existing := contextAndViewBindingMap[key]
if existing == nil {
contextAndViewBindingMap[key] = []*gui.Binding{binding}
contextAndViewBindingMap[key] = []*types.Binding{binding}
} else {
for _, navBinding := range contextAndViewBindingMap[key] {
if navBinding.Description == binding.Description {
@ -162,7 +163,7 @@ outer:
key := contextAndViewType{subtitle: context, title: binding.ViewName}
existing := contextAndViewBindingMap[key]
if existing == nil {
contextAndViewBindingMap[key] = []*gui.Binding{binding}
contextAndViewBindingMap[key] = []*types.Binding{binding}
} else {
contextAndViewBindingMap[key] = append(contextAndViewBindingMap[key], binding)
}
@ -171,7 +172,7 @@ outer:
type groupedBindingsType struct {
contextAndView contextAndViewType
bindings []*gui.Binding
bindings []*types.Binding
}
groupedBindings := make([]groupedBindingsType, len(contextAndViewBindingMap))
@ -227,7 +228,7 @@ outer:
return bindingSections
}
func addBinding(title string, bindingSections []*bindingSection, binding *gui.Binding) []*bindingSection {
func addBinding(title string, bindingSections []*bindingSection, binding *types.Binding) []*bindingSection {
if binding.Description == "" && binding.Alternative == "" {
return bindingSections
}
@ -241,7 +242,7 @@ func addBinding(title string, bindingSections []*bindingSection, binding *gui.Bi
section := &bindingSection{
title: title,
bindings: []*gui.Binding{binding},
bindings: []*types.Binding{binding},
}
return append(bindingSections, section)

View File

@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"github.com/go-errors/errors"
@ -56,6 +57,7 @@ func NewGitCommand(
cmn *common.Common,
osCommand *oscommands.OSCommand,
gitConfig git_config.IGitConfig,
syncMutex *sync.Mutex,
) (*GitCommand, error) {
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
return nil, err
@ -77,6 +79,7 @@ func NewGitCommand(
gitConfig,
dotGitDir,
repo,
syncMutex,
), nil
}
@ -86,6 +89,7 @@ func NewGitCommandAux(
gitConfig git_config.IGitConfig,
dotGitDir string,
repo *gogit.Repository,
syncMutex *sync.Mutex,
) *GitCommand {
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
@ -95,7 +99,7 @@ func NewGitCommandAux(
// on the one struct.
// common ones are: cmn, osCommand, dotGitDir, configCommands
configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo)
gitCommon := git_commands.NewGitCommon(cmn, cmd, osCommand, dotGitDir, repo, configCommands)
gitCommon := git_commands.NewGitCommon(cmn, cmd, osCommand, dotGitDir, repo, configCommands, syncMutex)
statusCommands := git_commands.NewStatusCommands(gitCommon)
fileLoader := loaders.NewFileLoader(cmn, cmd, configCommands)

View File

@ -1,6 +1,8 @@
package git_commands
import (
"sync"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
@ -13,6 +15,8 @@ type GitCommon struct {
dotGitDir string
repo *gogit.Repository
config *ConfigCommands
// mutex for doing things like push/pull/fetch
syncMutex *sync.Mutex
}
func NewGitCommon(
@ -22,6 +26,7 @@ func NewGitCommon(
dotGitDir string,
repo *gogit.Repository,
config *ConfigCommands,
syncMutex *sync.Mutex,
) *GitCommon {
return &GitCommon{
Common: cmn,
@ -30,5 +35,6 @@ func NewGitCommon(
dotGitDir: dotGitDir,
repo: repo,
config: config,
syncMutex: syncMutex,
}
}

View File

@ -40,7 +40,7 @@ func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string
func (self *RemoteCommands) DeleteRemoteBranch(remoteName string, branchName string) error {
command := fmt.Sprintf("git push %s --delete %s", self.cmd.Quote(remoteName), self.cmd.Quote(branchName))
return self.cmd.New(command).PromptOnCredentialRequest().Run()
return self.cmd.New(command).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
}
// CheckRemoteBranchExists Returns remote branch

View File

@ -47,7 +47,7 @@ func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error)
cmdStr += " " + self.cmd.Quote(opts.UpstreamBranch)
}
cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest()
cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex)
return cmdObj, nil
}
@ -83,7 +83,7 @@ func (self *SyncCommands) Fetch(opts FetchOptions) error {
} else {
cmdObj.PromptOnCredentialRequest()
}
return cmdObj.Run()
return cmdObj.WithMutex(self.syncMutex).Run()
}
type PullOptions struct {
@ -108,15 +108,15 @@ func (self *SyncCommands) Pull(opts PullOptions) error {
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
// has 'pull.rebase = interactive' configured.
return self.cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest().Run()
return self.cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
}
func (self *SyncCommands) FastForward(branchName string, remoteName string, remoteBranchName string) error {
cmdStr := fmt.Sprintf("git fetch %s %s:%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName))
return self.cmd.New(cmdStr).PromptOnCredentialRequest().Run()
return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
}
func (self *SyncCommands) FetchRemote(remoteName string) error {
cmdStr := fmt.Sprintf("git fetch %s", self.cmd.Quote(remoteName))
return self.cmd.New(cmdStr).PromptOnCredentialRequest().Run()
return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
}

View File

@ -27,5 +27,5 @@ func (self *TagCommands) Delete(tagName string) error {
}
func (self *TagCommands) Push(remoteName string, tagName string) error {
return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().Run()
return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
}

View File

@ -3,6 +3,7 @@ package commands
import (
"fmt"
"os"
"sync"
"testing"
"time"
@ -211,7 +212,12 @@ func TestNewGitCommand(t *testing.T) {
s := s
t.Run(s.testName, func(t *testing.T) {
s.setup()
s.test(NewGitCommand(utils.NewDummyCommon(), oscommands.NewDummyOSCommand(), git_config.NewFakeGitConfig(nil)))
s.test(
NewGitCommand(utils.NewDummyCommon(),
oscommands.NewDummyOSCommand(),
git_config.NewFakeGitConfig(nil),
&sync.Mutex{},
))
})
}
}

View File

@ -2,6 +2,7 @@ package oscommands
import (
"os/exec"
"sync"
)
// A command object is a general way to represent a command to be run on the
@ -50,6 +51,9 @@ type ICmdObj interface {
PromptOnCredentialRequest() ICmdObj
FailOnCredentialRequest() ICmdObj
WithMutex(mutex *sync.Mutex) ICmdObj
Mutex() *sync.Mutex
GetCredentialStrategy() CredentialStrategy
}
@ -70,6 +74,9 @@ type CmdObj struct {
// if set to true, it means we might be asked to enter a username/password by this command.
credentialStrategy CredentialStrategy
// can be set so that we don't run certain commands simultaneously
mutex *sync.Mutex
}
type CredentialStrategy int
@ -132,6 +139,16 @@ func (self *CmdObj) IgnoreEmptyError() ICmdObj {
return self
}
func (self *CmdObj) Mutex() *sync.Mutex {
return self.mutex
}
func (self *CmdObj) WithMutex(mutex *sync.Mutex) ICmdObj {
self.mutex = mutex
return self
}
func (self *CmdObj) ShouldIgnoreEmptyError() bool {
return self.ignoreEmptyError
}

View File

@ -34,6 +34,11 @@ type cmdObjRunner struct {
var _ ICmdObjRunner = &cmdObjRunner{}
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
}
if cmdObj.GetCredentialStrategy() != NONE {
return self.runWithCredentialHandling(cmdObj)
}
@ -42,17 +47,14 @@ func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
return self.runAndStream(cmdObj)
}
_, err := self.RunWithOutput(cmdObj)
_, err := self.RunWithOutputAux(cmdObj)
return err
}
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
if cmdObj.ShouldStreamOutput() {
err := self.runAndStream(cmdObj)
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutput
// but ignore the output, hence why we've got this check here.
return "", err
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
}
if cmdObj.GetCredentialStrategy() != NONE {
@ -63,6 +65,18 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
return "", err
}
if cmdObj.ShouldStreamOutput() {
err := self.runAndStream(cmdObj)
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutput
// but ignore the output, hence why we've got this check here.
return "", err
}
return self.RunWithOutputAux(cmdObj)
}
func (self *cmdObjRunner) RunWithOutputAux(cmdObj ICmdObj) (string, error) {
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
if cmdObj.ShouldLog() {
@ -77,6 +91,11 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
}
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
}
if cmdObj.GetCredentialStrategy() != NONE {
return errors.New("cannot call RunAndProcessLines with credential strategy. If you're seeing this then a contributor to Lazygit has accidentally called this method! Please raise an issue")
}

View File

@ -83,7 +83,7 @@ func (m *statusManager) getStatusString() string {
return topStatus.message
}
func (gui *Gui) raiseToast(message string) {
func (gui *Gui) toast(message string) {
gui.statusManager.addToastStatus(message)
gui.renderAppStatus()
@ -119,7 +119,7 @@ func (gui *Gui) withWaitingStatus(message string, f func() error) error {
if err := f(); err != nil {
gui.OnUIThread(func() error {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
})
}
})

View File

@ -2,6 +2,7 @@ package gui
import (
"github.com/jesseduffield/lazygit/pkg/gui/boxlayout"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@ -44,7 +45,7 @@ func (gui *Gui) getMidSectionWeights() (int, int) {
currentWindow := gui.currentWindow()
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
sidePanelWidthRatio := gui.UserConfig.Gui.SidePanelWidth
sidePanelWidthRatio := gui.c.UserConfig.Gui.SidePanelWidth
// we could make this better by creating ratios like 2:3 rather than always 1:something
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
sideSectionWeight := 1
@ -115,7 +116,7 @@ func (gui *Gui) splitMainPanelSideBySide() bool {
return false
}
mainPanelSplitMode := gui.UserConfig.Gui.MainPanelSplitMode
mainPanelSplitMode := gui.c.UserConfig.Gui.MainPanelSplitMode
width, height := gui.g.Size()
switch mainPanelSplitMode {
@ -143,7 +144,7 @@ func (gui *Gui) getExtrasWindowSize(screenHeight int) int {
} else if screenHeight < 40 {
baseSize = 1
} else {
baseSize = gui.UserConfig.Gui.CommandLogSize
baseSize = gui.c.UserConfig.Gui.CommandLogSize
}
frameSize := 2
@ -259,7 +260,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
fullHeightBox("stash"),
}
} else if height >= 28 {
accordionMode := gui.UserConfig.Gui.ExpandFocusedSidePanel
accordionMode := gui.c.UserConfig.Gui.ExpandFocusedSidePanel
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
if accordionMode && defaultBox.Window == currentWindow {
return &boxlayout.Box{
@ -320,7 +321,7 @@ func (gui *Gui) currentSideWindowName() string {
reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx
context := gui.State.ContextManager.ContextStack[reversedIdx]
if context.GetKind() == SIDE_CONTEXT {
if context.GetKind() == types.SIDE_CONTEXT {
return context.GetWindowName()
}
}

View File

@ -1,22 +1,28 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type BasicContext struct {
OnFocus func(opts ...OnFocusOpts) error
OnFocus func(opts ...types.OnFocusOpts) error
OnFocusLost func() error
OnRender func() error
// this is for pushing some content to the main view
OnRenderToMain func(opts ...OnFocusOpts) error
Kind ContextKind
Key ContextKey
OnRenderToMain func(opts ...types.OnFocusOpts) error
Kind types.ContextKind
Key types.ContextKey
ViewName string
WindowName string
OnGetOptionsMap func() map[string]string
ParentContext Context
ParentContext types.Context
// we can't know on the calling end whether a Context is actually a nil value without reflection, so we're storing this flag here to tell us. There has got to be a better way around this
hasParent bool
}
var _ types.Context = &BasicContext{}
func (self *BasicContext) GetOptionsMap() map[string]string {
if self.OnGetOptionsMap != nil {
return self.OnGetOptionsMap()
@ -24,12 +30,12 @@ func (self *BasicContext) GetOptionsMap() map[string]string {
return nil
}
func (self *BasicContext) SetParentContext(context Context) {
func (self *BasicContext) SetParentContext(context types.Context) {
self.ParentContext = context
self.hasParent = true
}
func (self *BasicContext) GetParentContext() (Context, bool) {
func (self *BasicContext) GetParentContext() (types.Context, bool) {
return self.ParentContext, self.hasParent
}
@ -59,7 +65,7 @@ func (self *BasicContext) GetViewName() string {
return self.ViewName
}
func (self *BasicContext) HandleFocus(opts ...OnFocusOpts) error {
func (self *BasicContext) HandleFocus(opts ...types.OnFocusOpts) error {
if self.OnFocus != nil {
if err := self.OnFocus(opts...); err != nil {
return err
@ -90,10 +96,10 @@ func (self *BasicContext) HandleRenderToMain() error {
return nil
}
func (self *BasicContext) GetKind() ContextKind {
func (self *BasicContext) GetKind() types.ContextKind {
return self.Kind
}
func (self *BasicContext) GetKey() ContextKey {
func (self *BasicContext) GetKey() types.ContextKey {
return self.Key
}

View File

@ -1,219 +0,0 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
func (gui *Gui) handleOpenBisectMenu() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
// no shame in getting this directly rather than using the cached value
// given how cheap it is to obtain
info := gui.Git.Bisect.GetInfo()
commit := gui.getSelectedLocalCommit()
if info.Started() {
return gui.openMidBisectMenu(info, commit)
} else {
return gui.openStartBisectMenu(info, commit)
}
}
func (gui *Gui) openMidBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
// if there is not yet a 'current' bisect commit, or if we have
// selected the current commit, we need to jump to the next 'current' commit
// after we perform a bisect action. The reason we don't unconditionally jump
// is that sometimes the user will want to go and mark a few commits as skipped
// in a row and they wouldn't want to be jumped back to the current bisect
// commit each time.
// Originally we were allowing the user to, from the bisect menu, select whether
// they were talking about the selected commit or the current bisect commit,
// and that was a bit confusing (and required extra keypresses).
selectCurrentAfter := info.GetCurrentSha() == "" || info.GetCurrentSha() == commit.Sha
// we need to wait to reselect if our bisect commits aren't ancestors of our 'start'
// ref, because we'll be reloading our commits in that case.
waitToReselect := selectCurrentAfter && !gui.Git.Bisect.ReachableFromStart(info)
menuItems := []*menuItem{
{
displayString: fmt.Sprintf(gui.Tr.Bisect.Mark, commit.ShortSha(), info.NewTerm()),
onPress: func() error {
gui.logAction(gui.Tr.Actions.BisectMark)
if err := gui.Git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
return gui.surfaceError(err)
}
return gui.afterMark(selectCurrentAfter, waitToReselect)
},
},
{
displayString: fmt.Sprintf(gui.Tr.Bisect.Mark, commit.ShortSha(), info.OldTerm()),
onPress: func() error {
gui.logAction(gui.Tr.Actions.BisectMark)
if err := gui.Git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
return gui.surfaceError(err)
}
return gui.afterMark(selectCurrentAfter, waitToReselect)
},
},
{
displayString: fmt.Sprintf(gui.Tr.Bisect.Skip, commit.ShortSha()),
onPress: func() error {
gui.logAction(gui.Tr.Actions.BisectSkip)
if err := gui.Git.Bisect.Skip(commit.Sha); err != nil {
return gui.surfaceError(err)
}
return gui.afterMark(selectCurrentAfter, waitToReselect)
},
},
{
displayString: gui.Tr.Bisect.ResetOption,
onPress: func() error {
return gui.resetBisect()
},
},
}
return gui.createMenu(
gui.Tr.Bisect.BisectMenuTitle,
menuItems,
createMenuOptions{showCancel: true},
)
}
func (gui *Gui) openStartBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
return gui.createMenu(
gui.Tr.Bisect.BisectMenuTitle,
[]*menuItem{
{
displayString: fmt.Sprintf(gui.Tr.Bisect.MarkStart, commit.ShortSha(), info.NewTerm()),
onPress: func() error {
gui.logAction(gui.Tr.Actions.StartBisect)
if err := gui.Git.Bisect.Start(); err != nil {
return gui.surfaceError(err)
}
if err := gui.Git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
return gui.surfaceError(err)
}
return gui.postBisectCommandRefresh()
},
},
{
displayString: fmt.Sprintf(gui.Tr.Bisect.MarkStart, commit.ShortSha(), info.OldTerm()),
onPress: func() error {
gui.logAction(gui.Tr.Actions.StartBisect)
if err := gui.Git.Bisect.Start(); err != nil {
return gui.surfaceError(err)
}
if err := gui.Git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
return gui.surfaceError(err)
}
return gui.postBisectCommandRefresh()
},
},
},
createMenuOptions{showCancel: true},
)
}
func (gui *Gui) resetBisect() error {
return gui.ask(askOpts{
title: gui.Tr.Bisect.ResetTitle,
prompt: gui.Tr.Bisect.ResetPrompt,
handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.ResetBisect)
if err := gui.Git.Bisect.Reset(); err != nil {
return gui.surfaceError(err)
}
return gui.postBisectCommandRefresh()
},
})
}
func (gui *Gui) showBisectCompleteMessage(candidateShas []string) error {
prompt := gui.Tr.Bisect.CompletePrompt
if len(candidateShas) > 1 {
prompt = gui.Tr.Bisect.CompletePromptIndeterminate
}
formattedCommits, err := gui.Git.Commit.GetCommitsOneline(candidateShas)
if err != nil {
return gui.surfaceError(err)
}
return gui.ask(askOpts{
title: gui.Tr.Bisect.CompleteTitle,
prompt: fmt.Sprintf(prompt, strings.TrimSpace(formattedCommits)),
handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.ResetBisect)
if err := gui.Git.Bisect.Reset(); err != nil {
return gui.surfaceError(err)
}
return gui.postBisectCommandRefresh()
},
})
}
func (gui *Gui) afterMark(selectCurrent bool, waitToReselect bool) error {
done, candidateShas, err := gui.Git.Bisect.IsDone()
if err != nil {
return gui.surfaceError(err)
}
if err := gui.afterBisectMarkRefresh(selectCurrent, waitToReselect); err != nil {
return gui.surfaceError(err)
}
if done {
return gui.showBisectCompleteMessage(candidateShas)
}
return nil
}
func (gui *Gui) postBisectCommandRefresh() error {
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{}})
}
func (gui *Gui) afterBisectMarkRefresh(selectCurrent bool, waitToReselect bool) error {
selectFn := func() {
if selectCurrent {
gui.selectCurrentBisectCommit()
}
}
if waitToReselect {
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{}, then: selectFn})
} else {
selectFn()
return gui.postBisectCommandRefresh()
}
}
func (gui *Gui) selectCurrentBisectCommit() {
info := gui.Git.Bisect.GetInfo()
if info.GetCurrentSha() != "" {
// find index of commit with that sha, move cursor to that.
for i, commit := range gui.State.Commits {
if commit.Sha == info.GetCurrentSha() {
gui.State.Contexts.BranchCommits.GetPanelState().SetSelectedLineIdx(i)
_ = gui.State.Contexts.BranchCommits.HandleFocus()
break
}
}
}
}

View File

@ -7,6 +7,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
@ -31,9 +32,9 @@ func (gui *Gui) branchesRenderToMain() error {
var task updateTask
branch := gui.getSelectedBranch()
if branch == nil {
task = NewRenderStringTask(gui.Tr.NoBranchesThisRepo)
task = NewRenderStringTask(gui.c.Tr.NoBranchesThisRepo)
} else {
cmdObj := gui.Git.Branch.GetGraphCmdObj(branch.Name)
cmdObj := gui.git.Branch.GetGraphCmdObj(branch.Name)
task = NewRunPtyTask(cmdObj.GetCmd())
}
@ -56,21 +57,21 @@ func (gui *Gui) refreshBranches() {
// 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 = gui.Git.Loaders.ReflogCommits.GetReflogCommits(nil, "")
reflogCommits, _, err = gui.git.Loaders.ReflogCommits.GetReflogCommits(nil, "")
if err != nil {
gui.Log.Error(err)
gui.c.Log.Error(err)
}
}
branches, err := gui.Git.Loaders.Branches.Load(reflogCommits)
branches, err := gui.git.Loaders.Branches.Load(reflogCommits)
if err != nil {
_ = gui.PopupHandler.Error(err)
_ = gui.c.Error(err)
}
gui.State.Branches = branches
if err := gui.postRefreshUpdate(gui.State.Contexts.Branches); err != nil {
gui.Log.Error(err)
if err := gui.c.PostRefreshUpdate(gui.State.Contexts.Branches); err != nil {
gui.c.Log.Error(err)
}
gui.refreshStatus()
@ -83,11 +84,11 @@ func (gui *Gui) handleBranchPress() error {
return nil
}
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
return gui.PopupHandler.ErrorMsg(gui.Tr.AlreadyCheckedOutBranch)
return gui.c.ErrorMsg(gui.c.Tr.AlreadyCheckedOutBranch)
}
branch := gui.getSelectedBranch()
gui.logAction(gui.Tr.Actions.CheckoutBranch)
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{})
gui.c.LogAction(gui.c.Tr.Actions.CheckoutBranch)
return gui.refHelper.CheckoutRef(branch.Name, types.CheckoutRefOptions{})
}
func (gui *Gui) handleCreatePullRequestPress() error {
@ -110,129 +111,64 @@ func (gui *Gui) handleCopyPullRequestURLPress() error {
branch := gui.getSelectedBranch()
branchExistsOnRemote := gui.Git.Remote.CheckRemoteBranchExists(branch.Name)
branchExistsOnRemote := gui.git.Remote.CheckRemoteBranchExists(branch.Name)
if !branchExistsOnRemote {
return gui.PopupHandler.Error(errors.New(gui.Tr.NoBranchOnRemote))
return gui.c.Error(errors.New(gui.c.Tr.NoBranchOnRemote))
}
url, err := hostingServiceMgr.GetPullRequestURL(branch.Name, "")
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
gui.logAction(gui.Tr.Actions.CopyPullRequestURL)
gui.c.LogAction(gui.c.Tr.Actions.CopyPullRequestURL)
if err := gui.OSCommand.CopyToClipboard(url); err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
gui.raiseToast(gui.Tr.PullRequestURLCopiedToClipboard)
gui.c.Toast(gui.c.Tr.PullRequestURLCopiedToClipboard)
return nil
}
func (gui *Gui) handleGitFetch() error {
return gui.PopupHandler.WithLoaderPanel(gui.Tr.FetchWait, func() error {
return gui.c.WithLoaderPanel(gui.c.Tr.FetchWait, func() error {
if err := gui.fetch(); err != nil {
_ = gui.PopupHandler.Error(err)
_ = gui.c.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
})
}
func (gui *Gui) handleForceCheckout() error {
branch := gui.getSelectedBranch()
message := gui.Tr.SureForceCheckout
title := gui.Tr.ForceCheckoutBranch
message := gui.c.Tr.SureForceCheckout
title := gui.c.Tr.ForceCheckoutBranch
return gui.PopupHandler.Ask(popup.AskOpts{
return gui.c.Ask(popup.AskOpts{
Title: title,
Prompt: message,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.ForceCheckoutBranch)
if err := gui.Git.Branch.Checkout(branch.Name, git_commands.CheckoutOptions{Force: true}); err != nil {
_ = gui.PopupHandler.Error(err)
gui.c.LogAction(gui.c.Tr.Actions.ForceCheckoutBranch)
if err := gui.git.Branch.Checkout(branch.Name, git_commands.CheckoutOptions{Force: true}); err != nil {
_ = gui.c.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
},
})
}
type handleCheckoutRefOptions struct {
WaitingStatus string
EnvVars []string
onRefNotFound func(ref string) error
}
func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) error {
waitingStatus := options.WaitingStatus
if waitingStatus == "" {
waitingStatus = gui.Tr.CheckingOutStatus
}
cmdOptions := git_commands.CheckoutOptions{Force: false, EnvVars: options.EnvVars}
onSuccess := func() {
gui.State.Panels.Branches.SelectedLineIdx = 0
gui.State.Panels.Commits.SelectedLineIdx = 0
// loading a heap of commits is slow so we limit them whenever doing a reset
gui.State.Panels.Commits.LimitCommits = true
}
return gui.PopupHandler.WithWaitingStatus(waitingStatus, func() error {
if err := gui.Git.Branch.Checkout(ref, cmdOptions); err != nil {
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
if options.onRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") {
return options.onRefNotFound(ref)
}
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
// offer to autostash changes
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.AutoStashTitle,
Prompt: gui.Tr.AutoStashPrompt,
HandleConfirm: func() error {
if err := gui.Git.Stash.Save(gui.Tr.StashPrefix + ref); err != nil {
return gui.PopupHandler.Error(err)
}
if err := gui.Git.Branch.Checkout(ref, cmdOptions); err != nil {
return gui.PopupHandler.Error(err)
}
onSuccess()
if err := gui.Git.Stash.Pop(0); err != nil {
if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.BLOCK_UI}); err != nil {
return err
}
return gui.PopupHandler.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.BLOCK_UI})
},
})
}
if err := gui.PopupHandler.Error(err); err != nil {
return err
}
}
onSuccess()
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.BLOCK_UI})
})
}
func (gui *Gui) handleCheckoutByName() error {
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: gui.Tr.BranchName + ":",
FindSuggestionsFunc: gui.getRefsSuggestionsFunc(),
return gui.c.Prompt(popup.PromptOpts{
Title: gui.c.Tr.BranchName + ":",
FindSuggestionsFunc: gui.suggestionsHelper.GetRefsSuggestionsFunc(),
HandleConfirm: func(response string) error {
gui.logAction("Checkout branch")
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
onRefNotFound: func(ref string) error {
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.BranchNotFoundTitle,
Prompt: fmt.Sprintf("%s %s%s", gui.Tr.BranchNotFoundPrompt, ref, "?"),
gui.c.LogAction("Checkout branch")
return gui.refHelper.CheckoutRef(response, types.CheckoutRefOptions{
OnRefNotFound: func(ref string) error {
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.BranchNotFoundTitle,
Prompt: fmt.Sprintf("%s %s%s", gui.c.Tr.BranchNotFoundPrompt, ref, "?"),
HandleConfirm: func() error {
return gui.createNewBranchWithName(ref)
},
@ -257,12 +193,12 @@ func (gui *Gui) createNewBranchWithName(newBranchName string) error {
return nil
}
if err := gui.Git.Branch.New(newBranchName, branch.Name); err != nil {
return gui.PopupHandler.Error(err)
if err := gui.git.Branch.New(newBranchName, branch.Name); err != nil {
return gui.c.Error(err)
}
gui.State.Panels.Branches.SelectedLineIdx = 0
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
}
func (gui *Gui) handleDeleteBranch() error {
@ -276,18 +212,18 @@ func (gui *Gui) deleteBranch(force bool) error {
}
checkedOutBranch := gui.getCheckedOutBranch()
if checkedOutBranch.Name == selectedBranch.Name {
return gui.PopupHandler.ErrorMsg(gui.Tr.CantDeleteCheckOutBranch)
return gui.c.ErrorMsg(gui.c.Tr.CantDeleteCheckOutBranch)
}
return gui.deleteNamedBranch(selectedBranch, force)
}
func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) error {
title := gui.Tr.DeleteBranch
title := gui.c.Tr.DeleteBranch
var templateStr string
if force {
templateStr = gui.Tr.ForceDeleteBranchMessage
templateStr = gui.c.Tr.ForceDeleteBranchMessage
} else {
templateStr = gui.Tr.DeleteBranchMessage
templateStr = gui.c.Tr.DeleteBranchMessage
}
message := utils.ResolvePlaceholderString(
templateStr,
@ -296,59 +232,51 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err
},
)
return gui.PopupHandler.Ask(popup.AskOpts{
return gui.c.Ask(popup.AskOpts{
Title: title,
Prompt: message,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.DeleteBranch)
if err := gui.Git.Branch.Delete(selectedBranch.Name, force); err != nil {
gui.c.LogAction(gui.c.Tr.Actions.DeleteBranch)
if err := gui.git.Branch.Delete(selectedBranch.Name, force); err != nil {
errMessage := err.Error()
if !force && strings.Contains(errMessage, "git branch -D ") {
return gui.deleteNamedBranch(selectedBranch, true)
}
return gui.PopupHandler.ErrorMsg(errMessage)
return gui.c.ErrorMsg(errMessage)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
},
})
}
func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if gui.Git.Branch.IsHeadDetached() {
return gui.PopupHandler.ErrorMsg("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")
if gui.git.Branch.IsHeadDetached() {
return gui.c.ErrorMsg("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")
}
checkedOutBranchName := gui.getCheckedOutBranch().Name
if checkedOutBranchName == branchName {
return gui.PopupHandler.ErrorMsg(gui.Tr.CantMergeBranchIntoItself)
return gui.c.ErrorMsg(gui.c.Tr.CantMergeBranchIntoItself)
}
prompt := utils.ResolvePlaceholderString(
gui.Tr.ConfirmMerge,
gui.c.Tr.ConfirmMerge,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": branchName,
},
)
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.MergingTitle,
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.MergingTitle,
Prompt: prompt,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.Merge)
err := gui.Git.Branch.Merge(branchName, git_commands.MergeOpts{})
return gui.handleGenericMergeCommandResult(err)
gui.c.LogAction(gui.c.Tr.Actions.Merge)
err := gui.git.Branch.Merge(branchName, git_commands.MergeOpts{})
return gui.checkMergeOrRebase(err)
},
})
}
func (gui *Gui) handleMerge() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
selectedBranchName := gui.getSelectedBranch().Name
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
}
@ -359,29 +287,25 @@ func (gui *Gui) handleRebaseOntoLocalBranch() error {
}
func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
checkedOutBranch := gui.getCheckedOutBranch().Name
if selectedBranchName == checkedOutBranch {
return gui.PopupHandler.ErrorMsg(gui.Tr.CantRebaseOntoSelf)
return gui.c.ErrorMsg(gui.c.Tr.CantRebaseOntoSelf)
}
prompt := utils.ResolvePlaceholderString(
gui.Tr.ConfirmRebase,
gui.c.Tr.ConfirmRebase,
map[string]string{
"checkedOutBranch": checkedOutBranch,
"selectedBranch": selectedBranchName,
},
)
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.RebasingTitle,
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.RebasingTitle,
Prompt: prompt,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.RebaseBranch)
err := gui.Git.Rebase.RebaseBranch(selectedBranchName)
return gui.handleGenericMergeCommandResult(err)
gui.c.LogAction(gui.c.Tr.Actions.RebaseBranch)
err := gui.git.Rebase.RebaseBranch(selectedBranchName)
return gui.checkMergeOrRebase(err)
},
})
}
@ -393,35 +317,35 @@ func (gui *Gui) handleFastForward() error {
}
if !branch.IsTrackingRemote() {
return gui.PopupHandler.ErrorMsg(gui.Tr.FwdNoUpstream)
return gui.c.ErrorMsg(gui.c.Tr.FwdNoUpstream)
}
if !branch.RemoteBranchStoredLocally() {
return gui.PopupHandler.ErrorMsg(gui.Tr.FwdNoLocalUpstream)
return gui.c.ErrorMsg(gui.c.Tr.FwdNoLocalUpstream)
}
if branch.HasCommitsToPush() {
return gui.PopupHandler.ErrorMsg(gui.Tr.FwdCommitsToPush)
return gui.c.ErrorMsg(gui.c.Tr.FwdCommitsToPush)
}
action := gui.Tr.Actions.FastForwardBranch
action := gui.c.Tr.Actions.FastForwardBranch
message := utils.ResolvePlaceholderString(
gui.Tr.Fetching,
gui.c.Tr.Fetching,
map[string]string{
"from": fmt.Sprintf("%s/%s", branch.UpstreamRemote, branch.UpstreamBranch),
"to": branch.Name,
},
)
return gui.PopupHandler.WithLoaderPanel(message, func() error {
return gui.c.WithLoaderPanel(message, func() error {
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
_ = gui.pullWithLock(PullFilesOptions{action: action, FastForwardOnly: true})
_ = gui.Controllers.Sync.PullAux(controllers.PullFilesOptions{Action: action, FastForwardOnly: true})
} else {
gui.logAction(action)
err := gui.Git.Sync.FastForward(branch.Name, branch.UpstreamRemote, branch.UpstreamBranch)
gui.c.LogAction(action)
err := gui.git.Sync.FastForward(branch.Name, branch.UpstreamRemote, branch.UpstreamBranch)
if err != nil {
_ = gui.PopupHandler.Error(err)
_ = gui.c.Error(err)
}
_ = gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
_ = gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
}
return nil
@ -434,7 +358,7 @@ func (gui *Gui) handleCreateResetToBranchMenu() error {
return nil
}
return gui.createResetMenu(branch.Name)
return gui.refHelper.CreateGitResetMenu(branch.Name)
}
func (gui *Gui) handleRenameBranch() error {
@ -444,13 +368,13 @@ func (gui *Gui) handleRenameBranch() error {
}
promptForNewName := func() error {
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: gui.Tr.NewBranchNamePrompt + " " + branch.Name + ":",
return gui.c.Prompt(popup.PromptOpts{
Title: gui.c.Tr.NewBranchNamePrompt + " " + branch.Name + ":",
InitialContent: branch.Name,
HandleConfirm: func(newBranchName string) error {
gui.logAction(gui.Tr.Actions.RenameBranch)
if err := gui.Git.Branch.Rename(branch.Name, newBranchName); err != nil {
return gui.PopupHandler.Error(err)
gui.c.LogAction(gui.c.Tr.Actions.RenameBranch)
if err := gui.git.Branch.Rename(branch.Name, newBranchName); err != nil {
return gui.c.Error(err)
}
// need to find where the branch is now so that we can re-select it. That means we need to refetch the branches synchronously and then find our branch
@ -478,20 +402,13 @@ func (gui *Gui) handleRenameBranch() error {
return promptForNewName()
}
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.LcRenameBranch,
Prompt: gui.Tr.RenameBranchWarning,
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.LcRenameBranch,
Prompt: gui.c.Tr.RenameBranchWarning,
HandleConfirm: promptForNewName,
})
}
func (gui *Gui) currentBranch() *models.Branch {
if len(gui.State.Branches) == 0 {
return nil
}
return gui.State.Branches[0]
}
func (gui *Gui) handleNewBranchOffCurrentItem() error {
context := gui.currentSideListContext()
@ -501,7 +418,7 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
}
message := utils.ResolvePlaceholderString(
gui.Tr.NewBranchNameBranchOff,
gui.c.Tr.NewBranchNameBranchOff,
map[string]string{
"branchName": item.Description(),
},
@ -513,12 +430,12 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
prefilledName = strings.SplitAfterN(item.ID(), "/", 2)[1]
}
return gui.PopupHandler.Prompt(popup.PromptOpts{
return gui.c.Prompt(popup.PromptOpts{
Title: message,
InitialContent: prefilledName,
HandleConfirm: func(response string) error {
gui.logAction(gui.Tr.Actions.CreateBranch)
if err := gui.Git.Branch.New(sanitizedBranchName(response), item.ID()); err != nil {
gui.c.LogAction(gui.c.Tr.Actions.CreateBranch)
if err := gui.git.Branch.New(sanitizedBranchName(response), item.ID()); err != nil {
return err
}
@ -529,14 +446,14 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
}
if context.GetKey() != gui.State.Contexts.Branches.GetKey() {
if err := gui.pushContext(gui.State.Contexts.Branches); err != nil {
if err := gui.c.PushContext(gui.State.Contexts.Branches); err != nil {
return err
}
}
gui.State.Panels.Branches.SelectedLineIdx = 0
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
},
})
}

View File

@ -3,12 +3,13 @@ package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// you can only copy from one context at a time, because the order and position of commits matter
func (gui *Gui) resetCherryPickingIfNecessary(context Context) error {
oldContextKey := ContextKey(gui.State.Modes.CherryPicking.ContextKey)
func (gui *Gui) resetCherryPickingIfNecessary(context types.Context) error {
oldContextKey := types.ContextKey(gui.State.Modes.CherryPicking.ContextKey)
if oldContextKey != context.GetKey() {
// need to reset the cherry picking mode
@ -22,10 +23,6 @@ func (gui *Gui) resetCherryPickingIfNecessary(context Context) error {
}
func (gui *Gui) handleCopyCommit() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
// get currently selected commit, add the sha to state.
context := gui.currentSideListContext()
if context == nil {
@ -80,7 +77,7 @@ func (gui *Gui) commitsListForContext() []*models.Commit {
case SUB_COMMITS_CONTEXT_KEY:
return gui.State.SubCommits
default:
gui.Log.Errorf("no commit list for context %s", context.GetKey())
gui.c.Log.Errorf("no commit list for context %s", context.GetKey())
return nil
}
}
@ -102,10 +99,6 @@ func (gui *Gui) addCommitToCherryPickedCommits(index int) {
}
func (gui *Gui) handleCopyCommitRange() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
// get currently selected commit, add the sha to state.
context := gui.currentSideListContext()
if context == nil {
@ -142,38 +135,34 @@ func (gui *Gui) handleCopyCommitRange() error {
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
func (gui *Gui) HandlePasteCommits() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.CherryPick,
Prompt: gui.Tr.SureCherryPick,
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.CherryPick,
Prompt: gui.c.Tr.SureCherryPick,
HandleConfirm: func() error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.CherryPickingStatus, func() error {
gui.logAction(gui.Tr.Actions.CherryPick)
err := gui.Git.Rebase.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
return gui.handleGenericMergeCommandResult(err)
return gui.c.WithWaitingStatus(gui.c.Tr.CherryPickingStatus, func() error {
gui.c.LogAction(gui.c.Tr.Actions.CherryPick)
err := gui.git.Rebase.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
return gui.checkMergeOrRebase(err)
})
},
})
}
func (gui *Gui) exitCherryPickingMode() error {
contextKey := ContextKey(gui.State.Modes.CherryPicking.ContextKey)
contextKey := types.ContextKey(gui.State.Modes.CherryPicking.ContextKey)
gui.State.Modes.CherryPicking.ContextKey = ""
gui.State.Modes.CherryPicking.CherryPickedCommits = nil
if contextKey == "" {
gui.Log.Warn("context key blank when trying to exit cherry picking mode")
gui.c.Log.Warn("context key blank when trying to exit cherry picking mode")
return nil
}
return gui.rerenderContextViewIfPresent(contextKey)
}
func (gui *Gui) rerenderContextViewIfPresent(contextKey ContextKey) error {
func (gui *Gui) rerenderContextViewIfPresent(contextKey types.ContextKey) error {
if contextKey == "" {
return nil
}
@ -184,11 +173,11 @@ func (gui *Gui) rerenderContextViewIfPresent(contextKey ContextKey) error {
view, err := gui.g.View(viewName)
if err != nil {
gui.Log.Error(err)
gui.c.Log.Error(err)
return nil
}
if ContextKey(view.Context) == contextKey {
if types.ContextKey(view.Context) == contextKey {
if err := context.HandleRender(); err != nil {
return err
}

View File

@ -22,7 +22,7 @@ import (
// So we call logAction to log the 'Stage File' part and then we call logCommand to log the command itself.
// We pass logCommand to our OSCommand struct so that it can handle logging commands
// for us.
func (gui *Gui) logAction(action string) {
func (gui *Gui) LogAction(action string) {
if gui.Views.Extras == nil {
return
}
@ -32,7 +32,7 @@ func (gui *Gui) logAction(action string) {
fmt.Fprint(gui.Views.Extras, "\n"+style.FgYellow.Sprint(action))
}
func (gui *Gui) logCommand(cmdStr string, commandLine bool) {
func (gui *Gui) LogCommand(cmdStr string, commandLine bool) {
if gui.Views.Extras == nil {
return
}
@ -52,23 +52,23 @@ func (gui *Gui) logCommand(cmdStr string, commandLine bool) {
func (gui *Gui) printCommandLogHeader() {
introStr := fmt.Sprintf(
gui.Tr.CommandLogHeader,
gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.ExtrasMenu),
gui.c.Tr.CommandLogHeader,
gui.getKeyDisplay(gui.c.UserConfig.Keybinding.Universal.ExtrasMenu),
)
fmt.Fprintln(gui.Views.Extras, style.FgCyan.Sprint(introStr))
if gui.UserConfig.Gui.ShowRandomTip {
if gui.c.UserConfig.Gui.ShowRandomTip {
fmt.Fprintf(
gui.Views.Extras,
"%s: %s",
style.FgYellow.Sprint(gui.Tr.RandomTip),
style.FgYellow.Sprint(gui.c.Tr.RandomTip),
style.FgGreen.Sprint(gui.getRandomTip()),
)
}
}
func (gui *Gui) getRandomTip() string {
config := gui.UserConfig.Keybinding
config := gui.c.UserConfig.Keybinding
formattedKey := func(key string) string {
return gui.getKeyDisplay(key)

View File

@ -3,6 +3,7 @@ package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
@ -47,7 +48,7 @@ func (gui *Gui) commitFilesRenderToMain() error {
to := gui.State.CommitFileTreeViewModel.GetParent()
from, reverse := gui.getFromAndReverseArgsForDiff(to)
cmdObj := gui.Git.WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false)
cmdObj := gui.git.WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false)
task := NewRunPtyTask(cmdObj.GetCmd())
return gui.refreshMainViews(refreshMainOpts{
@ -65,12 +66,12 @@ func (gui *Gui) handleCheckoutCommitFile() error {
return nil
}
gui.logAction(gui.Tr.Actions.CheckoutFile)
if err := gui.Git.WorkingTree.CheckoutFile(gui.State.CommitFileTreeViewModel.GetParent(), node.GetPath()); err != nil {
return gui.PopupHandler.Error(err)
gui.c.LogAction(gui.c.Tr.Actions.CheckoutFile)
if err := gui.git.WorkingTree.CheckoutFile(gui.State.CommitFileTreeViewModel.GetParent(), node.GetPath()); err != nil {
return gui.c.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
}
func (gui *Gui) handleDiscardOldFileChange() error {
@ -80,19 +81,19 @@ func (gui *Gui) handleDiscardOldFileChange() error {
fileName := gui.getSelectedCommitFileName()
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.DiscardFileChangesTitle,
Prompt: gui.Tr.DiscardFileChangesPrompt,
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.DiscardFileChangesTitle,
Prompt: gui.c.Tr.DiscardFileChangesPrompt,
HandleConfirm: func() error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
gui.logAction(gui.Tr.Actions.DiscardOldFileChange)
if err := gui.Git.Rebase.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil {
if err := gui.handleGenericMergeCommandResult(err); err != nil {
return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error {
gui.c.LogAction(gui.c.Tr.Actions.DiscardOldFileChange)
if err := gui.git.Rebase.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil {
if err := gui.checkMergeOrRebase(err); err != nil {
return err
}
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.BLOCK_UI})
return gui.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI})
})
},
})
@ -109,14 +110,14 @@ func (gui *Gui) refreshCommitFilesView() error {
to := gui.State.Panels.CommitFiles.refName
from, reverse := gui.getFromAndReverseArgsForDiff(to)
files, err := gui.Git.Loaders.CommitFiles.GetFilesInDiff(from, to, reverse)
files, err := gui.git.Loaders.CommitFiles.GetFilesInDiff(from, to, reverse)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
gui.State.CommitFileTreeViewModel.SetParent(to)
gui.State.CommitFileTreeViewModel.SetFiles(files)
return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles)
return gui.c.PostRefreshUpdate(gui.State.Contexts.CommitFiles)
}
func (gui *Gui) handleOpenOldCommitFile() error {
@ -125,7 +126,7 @@ func (gui *Gui) handleOpenOldCommitFile() error {
return nil
}
return gui.openFile(node.GetPath())
return gui.fileHelper.OpenFile(node.GetPath())
}
func (gui *Gui) handleEditCommitFile() error {
@ -135,10 +136,10 @@ func (gui *Gui) handleEditCommitFile() error {
}
if node.File == nil {
return gui.PopupHandler.ErrorMsg(gui.Tr.ErrCannotEditDirectory)
return gui.c.ErrorMsg(gui.c.Tr.ErrCannotEditDirectory)
}
return gui.editFile(node.GetPath())
return gui.fileHelper.EditFile(node.GetPath())
}
func (gui *Gui) handleToggleFileForPatch() error {
@ -148,7 +149,7 @@ func (gui *Gui) handleToggleFileForPatch() error {
}
toggleTheFile := func() error {
if !gui.Git.Patch.PatchManager.Active() {
if !gui.git.Patch.PatchManager.Active() {
if err := gui.startPatchManager(); err != nil {
return err
}
@ -157,34 +158,34 @@ func (gui *Gui) handleToggleFileForPatch() error {
// if there is any file that hasn't been fully added we'll fully add everything,
// otherwise we'll remove everything
adding := node.AnyFile(func(file *models.CommitFile) bool {
return gui.Git.Patch.PatchManager.GetFileStatus(file.Name, gui.State.CommitFileTreeViewModel.GetParent()) != patch.WHOLE
return gui.git.Patch.PatchManager.GetFileStatus(file.Name, gui.State.CommitFileTreeViewModel.GetParent()) != patch.WHOLE
})
err := node.ForEachFile(func(file *models.CommitFile) error {
if adding {
return gui.Git.Patch.PatchManager.AddFileWhole(file.Name)
return gui.git.Patch.PatchManager.AddFileWhole(file.Name)
} else {
return gui.Git.Patch.PatchManager.RemoveFile(file.Name)
return gui.git.Patch.PatchManager.RemoveFile(file.Name)
}
})
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
if gui.Git.Patch.PatchManager.IsEmpty() {
gui.Git.Patch.PatchManager.Reset()
if gui.git.Patch.PatchManager.IsEmpty() {
gui.git.Patch.PatchManager.Reset()
}
return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles)
return gui.c.PostRefreshUpdate(gui.State.Contexts.CommitFiles)
}
if gui.Git.Patch.PatchManager.Active() && gui.Git.Patch.PatchManager.To != gui.State.CommitFileTreeViewModel.GetParent() {
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.DiscardPatch,
Prompt: gui.Tr.DiscardPatchConfirm,
if gui.git.Patch.PatchManager.Active() && gui.git.Patch.PatchManager.To != gui.State.CommitFileTreeViewModel.GetParent() {
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.DiscardPatch,
Prompt: gui.c.Tr.DiscardPatchConfirm,
HandleConfirm: func() error {
gui.Git.Patch.PatchManager.Reset()
gui.git.Patch.PatchManager.Reset()
return toggleTheFile()
},
})
@ -199,15 +200,15 @@ func (gui *Gui) startPatchManager() error {
to := gui.State.Panels.CommitFiles.refName
from, reverse := gui.getFromAndReverseArgsForDiff(to)
gui.Git.Patch.PatchManager.Start(from, to, reverse, canRebase)
gui.git.Patch.PatchManager.Start(from, to, reverse, canRebase)
return nil
}
func (gui *Gui) handleEnterCommitFile() error {
return gui.enterCommitFile(OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
return gui.enterCommitFile(types.OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
}
func (gui *Gui) enterCommitFile(opts OnFocusOpts) error {
func (gui *Gui) enterCommitFile(opts types.OnFocusOpts) error {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
@ -218,21 +219,21 @@ func (gui *Gui) enterCommitFile(opts OnFocusOpts) error {
}
enterTheFile := func() error {
if !gui.Git.Patch.PatchManager.Active() {
if !gui.git.Patch.PatchManager.Active() {
if err := gui.startPatchManager(); err != nil {
return err
}
}
return gui.pushContext(gui.State.Contexts.PatchBuilding, opts)
return gui.c.PushContext(gui.State.Contexts.PatchBuilding, opts)
}
if gui.Git.Patch.PatchManager.Active() && gui.Git.Patch.PatchManager.To != gui.State.CommitFileTreeViewModel.GetParent() {
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.DiscardPatch,
Prompt: gui.Tr.DiscardPatchConfirm,
if gui.git.Patch.PatchManager.Active() && gui.git.Patch.PatchManager.To != gui.State.CommitFileTreeViewModel.GetParent() {
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.DiscardPatch,
Prompt: gui.c.Tr.DiscardPatchConfirm,
HandleConfirm: func() error {
gui.Git.Patch.PatchManager.Reset()
gui.git.Patch.PatchManager.Reset()
return enterTheFile()
},
})
@ -249,29 +250,29 @@ func (gui *Gui) handleToggleCommitFileDirCollapsed() error {
gui.State.CommitFileTreeViewModel.ToggleCollapsed(node.GetPath())
if err := gui.postRefreshUpdate(gui.State.Contexts.CommitFiles); err != nil {
gui.Log.Error(err)
if err := gui.c.PostRefreshUpdate(gui.State.Contexts.CommitFiles); err != nil {
gui.c.Log.Error(err)
}
return nil
}
func (gui *Gui) switchToCommitFilesContext(refName string, canRebase bool, context Context, windowName string) error {
func (gui *Gui) SwitchToCommitFilesContext(opts controllers.SwitchToCommitFilesContextOpts) error {
// sometimes the commitFiles view is already shown in another window, so we need to ensure that window
// no longer considers the commitFiles view as its main view.
gui.resetWindowForView(gui.Views.CommitFiles)
gui.State.Panels.CommitFiles.SelectedLineIdx = 0
gui.State.Panels.CommitFiles.refName = refName
gui.State.Panels.CommitFiles.canRebase = canRebase
gui.State.Contexts.CommitFiles.SetParentContext(context)
gui.State.Contexts.CommitFiles.SetWindowName(windowName)
gui.State.Panels.CommitFiles.refName = opts.RefName
gui.State.Panels.CommitFiles.canRebase = opts.CanRebase
gui.State.Contexts.CommitFiles.SetParentContext(opts.Context)
gui.State.Contexts.CommitFiles.SetWindowName(opts.WindowName)
if err := gui.refreshCommitFilesView(); err != nil {
return err
}
return gui.pushContext(gui.State.Contexts.CommitFiles)
return gui.c.PushContext(gui.State.Contexts.CommitFiles)
}
// NOTE: this is very similar to handleToggleFileTreeView, could be DRY'd with generics
@ -289,12 +290,5 @@ func (gui *Gui) handleToggleCommitFileTreeView() error {
}
}
if err := gui.State.Contexts.CommitFiles.HandleRender(); err != nil {
return err
}
if err := gui.State.Contexts.CommitFiles.HandleFocus(); err != nil {
return err
}
return nil
return gui.c.PostRefreshUpdate(gui.State.Contexts.CommitFiles)
}

View File

@ -12,14 +12,14 @@ func (gui *Gui) handleCommitConfirm() error {
message := strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent())
gui.State.failedCommitMessage = message
if message == "" {
return gui.PopupHandler.ErrorMsg(gui.Tr.CommitWithoutMessageErr)
return gui.c.ErrorMsg(gui.c.Tr.CommitWithoutMessageErr)
}
cmdObj := gui.Git.Commit.CommitCmdObj(message)
gui.logAction(gui.Tr.Actions.Commit)
cmdObj := gui.git.Commit.CommitCmdObj(message)
gui.c.LogAction(gui.c.Tr.Actions.Commit)
_ = gui.returnFromContext()
return gui.withGpgHandling(cmdObj, gui.Tr.CommittingStatus, func() error {
return gui.withGpgHandling(cmdObj, gui.c.Tr.CommittingStatus, func() error {
gui.Views.CommitMessage.ClearTextArea()
gui.State.failedCommitMessage = ""
return nil
@ -32,14 +32,16 @@ func (gui *Gui) handleCommitClose() error {
func (gui *Gui) handleCommitMessageFocused() error {
message := utils.ResolvePlaceholderString(
gui.Tr.CommitMessageConfirm,
gui.c.Tr.CommitMessageConfirm,
map[string]string{
"keyBindClose": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.Return),
"keyBindConfirm": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.Confirm),
"keyBindNewLine": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.AppendNewline),
"keyBindClose": gui.getKeyDisplay(gui.c.UserConfig.Keybinding.Universal.Return),
"keyBindConfirm": gui.getKeyDisplay(gui.c.UserConfig.Keybinding.Universal.Confirm),
"keyBindNewLine": gui.getKeyDisplay(gui.c.UserConfig.Keybinding.Universal.AppendNewline),
},
)
gui.RenderCommitLength()
return gui.renderString(gui.Views.Options, message)
}
@ -49,7 +51,7 @@ func (gui *Gui) getBufferLength(view *gocui.View) string {
// RenderCommitLength is a function.
func (gui *Gui) RenderCommitLength() {
if !gui.UserConfig.Gui.CommitLength.Show {
if !gui.c.UserConfig.Gui.CommitLength.Show {
return
}

View File

@ -1,13 +1,10 @@
package gui
import (
"fmt"
"sync"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@ -31,7 +28,7 @@ func (gui *Gui) onCommitFocus() error {
state.LimitCommits = false
go utils.Safe(func() {
if err := gui.refreshCommitsWithLimit(); err != nil {
_ = gui.PopupHandler.Error(err)
_ = gui.c.Error(err)
}
})
}
@ -45,9 +42,9 @@ func (gui *Gui) branchCommitsRenderToMain() error {
var task updateTask
commit := gui.getSelectedLocalCommit()
if commit == nil {
task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch)
task = NewRenderStringTask(gui.c.Tr.NoCommitsThisBranch)
} else {
cmdObj := gui.Git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
task = NewRunPtyTask(cmdObj.GetCmd())
}
@ -118,7 +115,7 @@ func (gui *Gui) refreshCommitsWithLimit() error {
gui.Mutexes.BranchCommitsMutex.Lock()
defer gui.Mutexes.BranchCommitsMutex.Unlock()
commits, err := gui.Git.Loaders.Commits.GetCommits(
commits, err := gui.git.Loaders.Commits.GetCommits(
loaders.GetCommitsOptions{
Limit: gui.State.Panels.Commits.LimitCommits,
FilterPath: gui.State.Modes.Filtering.GetPath(),
@ -132,11 +129,11 @@ func (gui *Gui) refreshCommitsWithLimit() error {
}
gui.State.Commits = commits
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
return gui.c.PostRefreshUpdate(gui.State.Contexts.BranchCommits)
}
func (gui *Gui) refForLog() string {
bisectInfo := gui.Git.Bisect.GetInfo()
bisectInfo := gui.git.Bisect.GetInfo()
gui.State.BisectInfo = bisectInfo
if !bisectInfo.Started() {
@ -144,7 +141,7 @@ func (gui *Gui) refForLog() string {
}
// need to see if our bisect's current commit is reachable from our 'new' ref.
if bisectInfo.Bisecting() && !gui.Git.Bisect.ReachableFromStart(bisectInfo) {
if bisectInfo.Bisecting() && !gui.git.Bisect.ReachableFromStart(bisectInfo) {
return bisectInfo.GetNewSha()
}
@ -155,691 +152,11 @@ func (gui *Gui) refreshRebaseCommits() error {
gui.Mutexes.BranchCommitsMutex.Lock()
defer gui.Mutexes.BranchCommitsMutex.Unlock()
updatedCommits, err := gui.Git.Loaders.Commits.MergeRebasingCommits(gui.State.Commits)
updatedCommits, err := gui.git.Loaders.Commits.MergeRebasingCommits(gui.State.Commits)
if err != nil {
return err
}
gui.State.Commits = updatedCommits
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
}
// specific functions
func (gui *Gui) handleCommitSquashDown() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if len(gui.State.Commits) <= 1 {
return gui.PopupHandler.ErrorMsg(gui.Tr.YouNoCommitsToSquash)
}
applied, err := gui.handleMidRebaseCommand("squash")
if err != nil {
return err
}
if applied {
return nil
}
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.Squash,
Prompt: gui.Tr.SureSquashThisCommit,
HandleConfirm: func() error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
gui.logAction(gui.Tr.Actions.SquashCommitDown)
err := gui.Git.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "squash")
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleCommitFixup() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if len(gui.State.Commits) <= 1 {
return gui.PopupHandler.ErrorMsg(gui.Tr.YouNoCommitsToSquash)
}
applied, err := gui.handleMidRebaseCommand("fixup")
if err != nil {
return err
}
if applied {
return nil
}
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.Fixup,
Prompt: gui.Tr.SureFixupThisCommit,
HandleConfirm: func() error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.FixingStatus, func() error {
gui.logAction(gui.Tr.Actions.FixupCommit)
err := gui.Git.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "fixup")
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleRewordCommit() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
message, err := gui.Git.Commit.GetCommitMessage(commit.Sha)
if err != nil {
return gui.PopupHandler.Error(err)
}
// TODO: use the commit message panel here
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: gui.Tr.LcRewordCommit,
InitialContent: message,
HandleConfirm: func(response string) error {
gui.logAction(gui.Tr.Actions.RewordCommit)
if err := gui.Git.Rebase.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, response); err != nil {
return gui.PopupHandler.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
},
})
}
func (gui *Gui) handleRewordCommitEditor() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
gui.logAction(gui.Tr.Actions.RewordCommit)
subProcess, err := gui.Git.Rebase.RewordCommitInEditor(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx)
if err != nil {
return gui.PopupHandler.Error(err)
}
if subProcess != nil {
return gui.runSubprocessWithSuspenseAndRefresh(subProcess)
}
return nil
}
// handleMidRebaseCommand sees if the selected commit is in fact a rebasing
// commit meaning you are trying to edit the todo file rather than actually
// begin a rebase. It then updates the todo file with that action
func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
selectedCommit := gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx]
if selectedCommit.Status != "rebasing" {
return false, nil
}
// for now we do not support setting 'reword' because it requires an editor
// and that means we either unconditionally wait around for the subprocess to ask for
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == "reword" {
return true, gui.PopupHandler.ErrorMsg(gui.Tr.LcRewordNotSupported)
}
gui.logAction("Update rebase TODO")
gui.logCommand(
fmt.Sprintf("Updating rebase action of commit %s to '%s'", selectedCommit.ShortSha(), action),
false,
)
if err := gui.Git.Rebase.EditRebaseTodo(gui.State.Panels.Commits.SelectedLineIdx, action); err != nil {
return false, gui.PopupHandler.Error(err)
}
return true, gui.refreshRebaseCommits()
}
func (gui *Gui) handleCommitDelete() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("drop")
if err != nil {
return err
}
if applied {
return nil
}
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.DeleteCommitTitle,
Prompt: gui.Tr.DeleteCommitPrompt,
HandleConfirm: func() error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.DeletingStatus, func() error {
gui.logAction(gui.Tr.Actions.DropCommit)
err := gui.Git.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "drop")
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleCommitMoveDown() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
index := gui.State.Panels.Commits.SelectedLineIdx
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if gui.State.Commits[index+1].Status != "rebasing" {
return nil
}
// logging directly here because MoveTodoDown doesn't have enough information
// to provide a useful log
gui.logAction(gui.Tr.Actions.MoveCommitDown)
gui.logCommand(fmt.Sprintf("Moving commit %s down", selectedCommit.ShortSha()), false)
if err := gui.Git.Rebase.MoveTodoDown(index); err != nil {
return gui.PopupHandler.Error(err)
}
gui.State.Panels.Commits.SelectedLineIdx++
return gui.refreshRebaseCommits()
}
return gui.PopupHandler.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
gui.logAction(gui.Tr.Actions.MoveCommitDown)
err := gui.Git.Rebase.MoveCommitDown(gui.State.Commits, index)
if err == nil {
gui.State.Panels.Commits.SelectedLineIdx++
}
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitMoveUp() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
index := gui.State.Panels.Commits.SelectedLineIdx
if index == 0 {
return nil
}
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
// logging directly here because MoveTodoDown doesn't have enough information
// to provide a useful log
gui.logAction(gui.Tr.Actions.MoveCommitUp)
gui.logCommand(
fmt.Sprintf("Moving commit %s up", selectedCommit.ShortSha()),
false,
)
if err := gui.Git.Rebase.MoveTodoDown(index - 1); err != nil {
return gui.PopupHandler.Error(err)
}
gui.State.Panels.Commits.SelectedLineIdx--
return gui.refreshRebaseCommits()
}
return gui.PopupHandler.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
gui.logAction(gui.Tr.Actions.MoveCommitUp)
err := gui.Git.Rebase.MoveCommitDown(gui.State.Commits, index-1)
if err == nil {
gui.State.Panels.Commits.SelectedLineIdx--
}
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitEdit() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("edit")
if err != nil {
return err
}
if applied {
return nil
}
return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
gui.logAction(gui.Tr.Actions.EditCommit)
err = gui.Git.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "edit")
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitAmendTo() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.AmendCommitTitle,
Prompt: gui.Tr.AmendCommitPrompt,
HandleConfirm: func() error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.AmendingStatus, func() error {
gui.logAction(gui.Tr.Actions.AmendCommit)
err := gui.Git.Rebase.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha)
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleCommitPick() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("pick")
if err != nil {
return err
}
if applied {
return nil
}
// at this point we aren't actually rebasing so we will interpret this as an
// attempt to pull. We might revoke this later after enabling configurable keybindings
return gui.handlePullFiles()
}
func (gui *Gui) handleCommitRevert() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
commit := gui.getSelectedLocalCommit()
if commit.IsMerge() {
return gui.createRevertMergeCommitMenu(commit)
} else {
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.Actions.RevertCommit,
Prompt: utils.ResolvePlaceholderString(
gui.Tr.ConfirmRevertCommit,
map[string]string{
"selectedCommit": commit.ShortSha(),
}),
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.RevertCommit)
if err := gui.Git.Commit.Revert(commit.Sha); err != nil {
return gui.PopupHandler.Error(err)
}
return gui.afterRevertCommit()
},
})
}
}
func (gui *Gui) createRevertMergeCommitMenu(commit *models.Commit) error {
menuItems := make([]*popup.MenuItem, len(commit.Parents))
for i, parentSha := range commit.Parents {
i := i
message, err := gui.Git.Commit.GetCommitMessageFirstLine(parentSha)
if err != nil {
return gui.PopupHandler.Error(err)
}
menuItems[i] = &popup.MenuItem{
DisplayString: fmt.Sprintf("%s: %s", utils.SafeTruncate(parentSha, 8), message),
OnPress: func() error {
parentNumber := i + 1
gui.logAction(gui.Tr.Actions.RevertCommit)
if err := gui.Git.Commit.RevertMerge(commit.Sha, parentNumber); err != nil {
return gui.PopupHandler.Error(err)
}
return gui.afterRevertCommit()
},
}
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: gui.Tr.SelectParentCommitForMerge, Items: menuItems})
}
func (gui *Gui) afterRevertCommit() error {
gui.State.Panels.Commits.SelectedLineIdx++
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.BLOCK_UI, Scope: []types.RefreshableView{types.COMMITS, types.BRANCHES}})
}
func (gui *Gui) handleViewCommitFiles() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
return gui.switchToCommitFilesContext(commit.Sha, true, gui.State.Contexts.BranchCommits, "commits")
}
func (gui *Gui) handleCreateFixupCommit() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
prompt := utils.ResolvePlaceholderString(
gui.Tr.SureCreateFixupCommit,
map[string]string{
"commit": commit.Sha,
},
)
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.CreateFixupCommit,
Prompt: prompt,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.CreateFixupCommit)
if err := gui.Git.Commit.CreateFixupCommit(commit.Sha); err != nil {
return gui.PopupHandler.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
},
})
}
func (gui *Gui) handleSquashAllAboveFixupCommits() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
prompt := utils.ResolvePlaceholderString(
gui.Tr.SureSquashAboveCommits,
map[string]string{
"commit": commit.Sha,
},
)
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.SquashAboveCommits,
Prompt: prompt,
HandleConfirm: func() error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
gui.logAction(gui.Tr.Actions.SquashAllAboveFixupCommits)
err := gui.Git.Rebase.SquashAllAboveFixupCommits(commit.Sha)
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleTagCommit() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
return gui.createTagMenu(commit.Sha)
}
func (gui *Gui) createTagMenu(commitSha string) error {
return gui.PopupHandler.Menu(popup.CreateMenuOptions{
Title: gui.Tr.TagMenuTitle,
Items: []*popup.MenuItem{
{
DisplayString: gui.Tr.LcLightweightTag,
OnPress: func() error {
return gui.handleCreateLightweightTag(commitSha)
},
},
{
DisplayString: gui.Tr.LcAnnotatedTag,
OnPress: func() error {
return gui.handleCreateAnnotatedTag(commitSha)
},
},
},
})
}
func (gui *Gui) afterTagCreate() error {
gui.State.Panels.Tags.SelectedLineIdx = 0 // Set to the top
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}})
}
func (gui *Gui) handleCreateAnnotatedTag(commitSha string) error {
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: gui.Tr.TagNameTitle,
HandleConfirm: func(tagName string) error {
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: gui.Tr.TagMessageTitle,
HandleConfirm: func(msg string) error {
gui.logAction(gui.Tr.Actions.CreateAnnotatedTag)
if err := gui.Git.Tag.CreateAnnotated(tagName, commitSha, msg); err != nil {
return gui.PopupHandler.Error(err)
}
return gui.afterTagCreate()
},
})
},
})
}
func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: gui.Tr.TagNameTitle,
HandleConfirm: func(tagName string) error {
gui.logAction(gui.Tr.Actions.CreateLightweightTag)
if err := gui.Git.Tag.CreateLightweight(tagName, commitSha); err != nil {
return gui.PopupHandler.Error(err)
}
return gui.afterTagCreate()
},
})
}
func (gui *Gui) handleCheckoutCommit() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.LcCheckoutCommit,
Prompt: gui.Tr.SureCheckoutThisCommit,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.CheckoutCommit)
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
},
})
}
func (gui *Gui) handleCreateCommitResetMenu() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return gui.PopupHandler.ErrorMsg(gui.Tr.NoCommitsThisBranch)
}
return gui.createResetMenu(commit.Sha)
}
func (gui *Gui) handleOpenSearchForCommitsPanel(string) error {
// we usually lazyload these commits but now that we're searching we need to load them now
if gui.State.Panels.Commits.LimitCommits {
gui.State.Panels.Commits.LimitCommits = false
if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS}}); err != nil {
return err
}
}
return gui.handleOpenSearch("commits")
}
func (gui *Gui) handleGotoBottomForCommitsPanel() error {
// we usually lazyload these commits but now that we're searching we need to load them now
if gui.State.Panels.Commits.LimitCommits {
gui.State.Panels.Commits.LimitCommits = false
if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS}}); err != nil {
return err
}
}
for _, context := range gui.getListContexts() {
if context.GetViewName() == "commits" {
return context.handleGotoBottom()
}
}
return nil
}
func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
message, err := gui.Git.Commit.GetCommitMessage(commit.Sha)
if err != nil {
return gui.PopupHandler.Error(err)
}
gui.logAction(gui.Tr.Actions.CopyCommitMessageToClipboard)
if err := gui.OSCommand.CopyToClipboard(message); err != nil {
return gui.PopupHandler.Error(err)
}
gui.raiseToast(gui.Tr.CommitMessageCopiedToClipboard)
return nil
}
func (gui *Gui) handleOpenLogMenu() error {
return gui.PopupHandler.Menu(popup.CreateMenuOptions{
Title: gui.Tr.LogMenuTitle,
Items: []*popup.MenuItem{
{
DisplayString: gui.Tr.ToggleShowGitGraphAll,
OnPress: func() error {
gui.ShowWholeGitGraph = !gui.ShowWholeGitGraph
if gui.ShowWholeGitGraph {
gui.State.Panels.Commits.LimitCommits = false
}
return gui.PopupHandler.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS}})
})
},
},
{
DisplayString: gui.Tr.ShowGitGraph,
OpensMenu: true,
OnPress: func() error {
onPress := func(value string) func() error {
return func() error {
gui.UserConfig.Git.Log.ShowGraph = value
gui.render()
return nil
}
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{
Title: gui.Tr.LogMenuTitle,
Items: []*popup.MenuItem{
{
DisplayString: "always",
OnPress: onPress("always"),
},
{
DisplayString: "never",
OnPress: onPress("never"),
},
{
DisplayString: "when maximised",
OnPress: onPress("when-maximised"),
},
},
})
},
},
{
DisplayString: gui.Tr.SortCommits,
OpensMenu: true,
OnPress: func() error {
onPress := func(value string) func() error {
return func() error {
gui.UserConfig.Git.Log.Order = value
return gui.PopupHandler.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS}})
})
}
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{
Title: gui.Tr.LogMenuTitle,
Items: []*popup.MenuItem{
{
DisplayString: "topological (topo-order)",
OnPress: onPress("topo-order"),
},
{
DisplayString: "date-order",
OnPress: onPress("date-order"),
},
{
DisplayString: "author-date-order",
OnPress: onPress("author-date-order"),
},
},
})
},
},
},
})
}
func (gui *Gui) handleOpenCommitInBrowser() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
hostingServiceMgr := gui.getHostingServiceMgr()
url, err := hostingServiceMgr.GetCommitURL(commit.Sha)
if err != nil {
return gui.PopupHandler.Error(err)
}
gui.logAction(gui.Tr.Actions.OpenCommitInBrowser)
if err := gui.OSCommand.OpenLink(url); err != nil {
return gui.PopupHandler.Error(err)
}
return nil
return gui.c.PostRefreshUpdate(gui.State.Contexts.BranchCommits)
}

View File

@ -19,7 +19,7 @@ func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function f
if function != nil {
if err := function(); err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
}
@ -35,7 +35,7 @@ func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, func
if function != nil {
if err := function(getResponse()); err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
}
@ -132,7 +132,7 @@ func (gui *Gui) prepareConfirmationPanel(
suggestionsView.FgColor = theme.GocuiDefaultTextColor
gui.setSuggestions(findSuggestionsFunc(""))
suggestionsView.Visible = true
suggestionsView.Title = fmt.Sprintf(gui.Tr.SuggestionsTitle, gui.UserConfig.Keybinding.Universal.TogglePanel)
suggestionsView.Title = fmt.Sprintf(gui.c.Tr.SuggestionsTitle, gui.c.UserConfig.Keybinding.Universal.TogglePanel)
}
return nil
@ -171,12 +171,12 @@ func (gui *Gui) createPopupPanel(opts popup.CreatePopupPanelOpts) error {
return err
}
return gui.pushContext(gui.State.Contexts.Confirmation)
return gui.c.PushContext(gui.State.Contexts.Confirmation)
}
func (gui *Gui) setKeyBindings(opts popup.CreatePopupPanelOpts) error {
actions := utils.ResolvePlaceholderString(
gui.Tr.CloseConfirm,
gui.c.Tr.CloseConfirm,
map[string]string{
"keyBindClose": "esc",
"keyBindConfirm": "enter",
@ -197,7 +197,7 @@ func (gui *Gui) setKeyBindings(opts popup.CreatePopupPanelOpts) error {
handler func() error
}
keybindingConfig := gui.UserConfig.Keybinding
keybindingConfig := gui.c.UserConfig.Keybinding
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(
opts.HandlersManageFocus,
opts.HandleConfirmPrompt,
@ -262,7 +262,7 @@ func (gui *Gui) setKeyBindings(opts popup.CreatePopupPanelOpts) error {
}
func (gui *Gui) clearConfirmationViewKeyBindings() {
keybindingConfig := gui.UserConfig.Keybinding
keybindingConfig := gui.c.UserConfig.Keybinding
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
@ -276,3 +276,10 @@ func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View)
return f()
}
}
func (gui *Gui) refreshSuggestions() {
gui.suggestionsAsyncHandler.Do(func() func() {
suggestions := gui.findSuggestions(gui.c.GetPromptInput())
return func() { gui.setSuggestions(suggestions) }
})
}

View File

@ -5,44 +5,13 @@ import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type ContextKind int
const (
SIDE_CONTEXT ContextKind = iota
MAIN_CONTEXT
TEMPORARY_POPUP
PERSISTENT_POPUP
EXTRAS_CONTEXT
)
type OnFocusOpts struct {
ClickedViewName string
ClickedViewLineIdx int
}
type Context interface {
HandleFocus(opts ...OnFocusOpts) error
HandleFocusLost() error
HandleRender() error
HandleRenderToMain() error
GetKind() ContextKind
GetViewName() string
GetWindowName() string
SetWindowName(string)
GetKey() ContextKey
SetParentContext(Context)
// we return a bool here to tell us whether or not the returned value just wraps a nil
GetParentContext() (Context, bool)
GetOptionsMap() map[string]string
}
func (gui *Gui) popupViewNames() []string {
result := []string{}
for _, context := range gui.allContexts() {
if context.GetKind() == PERSISTENT_POPUP || context.GetKind() == TEMPORARY_POPUP {
if context.GetKind() == types.PERSISTENT_POPUP || context.GetKind() == types.TEMPORARY_POPUP {
result = append(result, context.GetViewName())
}
}
@ -50,7 +19,7 @@ func (gui *Gui) popupViewNames() []string {
return result
}
func (gui *Gui) currentContextKeyIgnoringPopups() ContextKey {
func (gui *Gui) currentContextKeyIgnoringPopups() types.ContextKey {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
@ -60,7 +29,7 @@ func (gui *Gui) currentContextKeyIgnoringPopups() ContextKey {
reversedIndex := len(stack) - 1 - i
context := stack[reversedIndex]
kind := stack[reversedIndex].GetKind()
if kind != TEMPORARY_POPUP && kind != PERSISTENT_POPUP {
if kind != types.TEMPORARY_POPUP && kind != types.PERSISTENT_POPUP {
return context.GetKey()
}
}
@ -70,12 +39,12 @@ func (gui *Gui) currentContextKeyIgnoringPopups() ContextKey {
// use replaceContext when you don't want to return to the original context upon
// hitting escape: you want to go that context's parent instead.
func (gui *Gui) replaceContext(c Context) error {
func (gui *Gui) replaceContext(c types.Context) error {
gui.State.ContextManager.Lock()
defer gui.State.ContextManager.Unlock()
if len(gui.State.ContextManager.ContextStack) == 0 {
gui.State.ContextManager.ContextStack = []Context{c}
gui.State.ContextManager.ContextStack = []types.Context{c}
} else {
// replace the last item with the given item
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack[0:len(gui.State.ContextManager.ContextStack)-1], c)
@ -84,7 +53,7 @@ func (gui *Gui) replaceContext(c Context) error {
return gui.activateContext(c)
}
func (gui *Gui) pushContext(c Context, opts ...OnFocusOpts) error {
func (gui *Gui) pushContext(c types.Context, opts ...types.OnFocusOpts) error {
// using triple dot but you should only ever pass one of these opt structs
if len(opts) > 1 {
return errors.New("cannot pass multiple opts to pushContext")
@ -94,7 +63,7 @@ func (gui *Gui) pushContext(c Context, opts ...OnFocusOpts) error {
// push onto stack
// if we are switching to a side context, remove all other contexts in the stack
if c.GetKind() == SIDE_CONTEXT {
if c.GetKind() == types.SIDE_CONTEXT {
for _, stackContext := range gui.State.ContextManager.ContextStack {
if stackContext.GetKey() != c.GetKey() {
if err := gui.deactivateContext(stackContext); err != nil {
@ -103,7 +72,7 @@ func (gui *Gui) pushContext(c Context, opts ...OnFocusOpts) error {
}
}
}
gui.State.ContextManager.ContextStack = []Context{c}
gui.State.ContextManager.ContextStack = []types.Context{c}
} else if len(gui.State.ContextManager.ContextStack) == 0 || gui.currentContextWithoutLock().GetKey() != c.GetKey() {
// Do not append if the one at the end is the same context (e.g. opening a menu from a menu)
// In that case we'll just close the menu entirely when the user hits escape.
@ -123,7 +92,7 @@ func (gui *Gui) pushContext(c Context, opts ...OnFocusOpts) error {
// want to switch to: you only know the view that you want to switch to. It will
// look up the context currently active for that view and switch to that context
func (gui *Gui) pushContextWithView(viewName string) error {
return gui.pushContext(gui.State.ViewContextMap[viewName])
return gui.c.PushContext(gui.State.ViewContextMap[viewName])
}
func (gui *Gui) returnFromContext() error {
@ -151,7 +120,7 @@ func (gui *Gui) returnFromContext() error {
return gui.activateContext(newContext)
}
func (gui *Gui) deactivateContext(c Context) error {
func (gui *Gui) deactivateContext(c types.Context) error {
view, _ := gui.g.View(c.GetViewName())
if view != nil && view.IsSearching() {
@ -161,7 +130,7 @@ func (gui *Gui) deactivateContext(c Context) error {
}
// if we are the kind of context that is sent to back upon deactivation, we should do that
if view != nil && (c.GetKind() == TEMPORARY_POPUP || c.GetKind() == PERSISTENT_POPUP || c.GetKey() == COMMIT_FILES_CONTEXT_KEY) {
if view != nil && (c.GetKind() == types.TEMPORARY_POPUP || c.GetKind() == types.PERSISTENT_POPUP || c.GetKey() == COMMIT_FILES_CONTEXT_KEY) {
view.Visible = false
}
@ -175,13 +144,13 @@ func (gui *Gui) deactivateContext(c Context) error {
// postRefreshUpdate is to be called on a context after the state that it depends on has been refreshed
// if the context's view is set to another context we do nothing.
// if the context's view is the current view we trigger a focus; re-selecting the current item.
func (gui *Gui) postRefreshUpdate(c Context) error {
func (gui *Gui) postRefreshUpdate(c types.Context) error {
v, err := gui.g.View(c.GetViewName())
if err != nil {
return nil
}
if ContextKey(v.Context) != c.GetKey() {
if types.ContextKey(v.Context) != c.GetKey() {
return nil
}
@ -198,13 +167,13 @@ func (gui *Gui) postRefreshUpdate(c Context) error {
return nil
}
func (gui *Gui) activateContext(c Context, opts ...OnFocusOpts) error {
func (gui *Gui) activateContext(c types.Context, opts ...types.OnFocusOpts) error {
viewName := c.GetViewName()
v, err := gui.g.View(viewName)
if err != nil {
return err
}
originalViewContextKey := ContextKey(v.Context)
originalViewContextKey := types.ContextKey(v.Context)
// ensure that any other window for which this view was active is now set to the default for that window.
gui.setViewAsActiveForWindow(v)
@ -260,14 +229,14 @@ func (gui *Gui) activateContext(c Context, opts ...OnFocusOpts) error {
// return result
// }
func (gui *Gui) currentContext() Context {
func (gui *Gui) currentContext() types.Context {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
return gui.currentContextWithoutLock()
}
func (gui *Gui) currentContextWithoutLock() Context {
func (gui *Gui) currentContextWithoutLock() types.Context {
if len(gui.State.ContextManager.ContextStack) == 0 {
return gui.defaultSideContext()
}
@ -277,16 +246,16 @@ func (gui *Gui) currentContextWithoutLock() Context {
// the status panel is not yet a list context (and may never be), so this method is not
// quite the same as currentSideContext()
func (gui *Gui) currentSideListContext() IListContext {
func (gui *Gui) currentSideListContext() types.IListContext {
context := gui.currentSideContext()
listContext, ok := context.(IListContext)
listContext, ok := context.(types.IListContext)
if !ok {
return nil
}
return listContext
}
func (gui *Gui) currentSideContext() Context {
func (gui *Gui) currentSideContext() types.Context {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
@ -297,11 +266,11 @@ func (gui *Gui) currentSideContext() Context {
return gui.defaultSideContext()
}
// find the first context in the stack with the type of SIDE_CONTEXT
// find the first context in the stack with the type of types.SIDE_CONTEXT
for i := range stack {
context := stack[len(stack)-1-i]
if context.GetKind() == SIDE_CONTEXT {
if context.GetKind() == types.SIDE_CONTEXT {
return context
}
}
@ -310,7 +279,7 @@ func (gui *Gui) currentSideContext() Context {
}
// static as opposed to popup
func (gui *Gui) currentStaticContext() Context {
func (gui *Gui) currentStaticContext() types.Context {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
@ -324,7 +293,7 @@ func (gui *Gui) currentStaticContext() Context {
for i := range stack {
context := stack[len(stack)-1-i]
if context.GetKind() != TEMPORARY_POPUP && context.GetKind() != PERSISTENT_POPUP {
if context.GetKind() != types.TEMPORARY_POPUP && context.GetKind() != types.PERSISTENT_POPUP {
return context
}
}
@ -332,7 +301,7 @@ func (gui *Gui) currentStaticContext() Context {
return gui.defaultSideContext()
}
func (gui *Gui) defaultSideContext() Context {
func (gui *Gui) defaultSideContext() types.Context {
if gui.State.Modes.Filtering.Active() {
return gui.State.Contexts.BranchCommits
} else {
@ -407,7 +376,7 @@ func (gui *Gui) onViewFocusLost(oldView *gocui.View, newView *gocui.View) error
// which currently just means a context that affects both the main and secondary views
// other views can have their context changed directly but this function helps
// keep the main and secondary views in sync
func (gui *Gui) changeMainViewsContext(contextKey ContextKey) {
func (gui *Gui) changeMainViewsContext(contextKey types.ContextKey) {
if gui.State.MainContext == contextKey {
return
}
@ -432,13 +401,13 @@ func (gui *Gui) viewTabNames(viewName string) []string {
result := make([]string, len(tabContexts))
for i, tabContext := range tabContexts {
result[i] = tabContext.tab
result[i] = tabContext.Tab
}
return result
}
func (gui *Gui) setViewTabForContext(c Context) {
func (gui *Gui) setViewTabForContext(c types.Context) {
// search for the context in our map and if we find it, set the tab for the corresponding view
tabContexts, ok := gui.State.ViewTabContextMap[c.GetViewName()]
if !ok {
@ -446,12 +415,12 @@ func (gui *Gui) setViewTabForContext(c Context) {
}
for tabIndex, tabContext := range tabContexts {
for _, context := range tabContext.contexts {
for _, context := range tabContext.Contexts {
if context.GetKey() == c.GetKey() {
// get the view, set the tab
v, err := gui.g.View(c.GetViewName())
if err != nil {
gui.Log.Error(err)
gui.c.Log.Error(err)
return
}
v.TabIndex = tabIndex
@ -461,12 +430,7 @@ func (gui *Gui) setViewTabForContext(c Context) {
}
}
type tabContext struct {
tab string
contexts []Context
}
func (gui *Gui) mustContextForContextKey(contextKey ContextKey) Context {
func (gui *Gui) mustContextForContextKey(contextKey types.ContextKey) types.Context {
context, ok := gui.contextForContextKey(contextKey)
if !ok {
@ -476,7 +440,7 @@ func (gui *Gui) mustContextForContextKey(contextKey ContextKey) Context {
return context
}
func (gui *Gui) contextForContextKey(contextKey ContextKey) (Context, bool) {
func (gui *Gui) contextForContextKey(contextKey types.ContextKey) (types.Context, bool) {
for _, context := range gui.allContexts() {
if context.GetKey() == contextKey {
return context, true
@ -487,7 +451,7 @@ func (gui *Gui) contextForContextKey(contextKey ContextKey) (Context, bool) {
}
func (gui *Gui) rerenderView(view *gocui.View) error {
contextKey := ContextKey(view.Context)
contextKey := types.ContextKey(view.Context)
context := gui.mustContextForContextKey(contextKey)
return context.HandleRender()

View File

@ -0,0 +1,98 @@
package context
import "github.com/jesseduffield/lazygit/pkg/gui/types"
type ContextTree struct {
Status types.Context
Files types.IListContext
Submodules types.IListContext
Menu types.IListContext
Branches types.IListContext
Remotes types.IListContext
RemoteBranches types.IListContext
Tags types.IListContext
BranchCommits types.IListContext
CommitFiles types.IListContext
ReflogCommits types.IListContext
SubCommits types.IListContext
Stash types.IListContext
Suggestions types.IListContext
Normal types.Context
Staging types.Context
PatchBuilding types.Context
Merging types.Context
Credentials types.Context
Confirmation types.Context
CommitMessage types.Context
Search types.Context
CommandLog types.Context
}
func (tree ContextTree) InitialViewContextMap() map[string]types.Context {
return map[string]types.Context{
"status": tree.Status,
"files": tree.Files,
"branches": tree.Branches,
"commits": tree.BranchCommits,
"commitFiles": tree.CommitFiles,
"stash": tree.Stash,
"menu": tree.Menu,
"confirmation": tree.Confirmation,
"credentials": tree.Credentials,
"commitMessage": tree.CommitMessage,
"main": tree.Normal,
"secondary": tree.Normal,
"extras": tree.CommandLog,
}
}
type TabContext struct {
Tab string
Contexts []types.Context
}
func (tree ContextTree) InitialViewTabContextMap() map[string][]TabContext {
return map[string][]TabContext{
"branches": {
{
Tab: "Local Branches",
Contexts: []types.Context{tree.Branches},
},
{
Tab: "Remotes",
Contexts: []types.Context{
tree.Remotes,
tree.RemoteBranches,
},
},
{
Tab: "Tags",
Contexts: []types.Context{tree.Tags},
},
},
"commits": {
{
Tab: "Commits",
Contexts: []types.Context{tree.BranchCommits},
},
{
Tab: "Reflog",
Contexts: []types.Context{
tree.ReflogCommits,
},
},
},
"files": {
{
Tab: "Files",
Contexts: []types.Context{tree.Files},
},
{
Tab: "Submodules",
Contexts: []types.Context{
tree.Submodules,
},
},
},
}
}

View File

@ -1,34 +1,37 @@
package gui
type ContextKey string
const (
STATUS_CONTEXT_KEY ContextKey = "status"
FILES_CONTEXT_KEY ContextKey = "files"
LOCAL_BRANCHES_CONTEXT_KEY ContextKey = "localBranches"
REMOTES_CONTEXT_KEY ContextKey = "remotes"
REMOTE_BRANCHES_CONTEXT_KEY ContextKey = "remoteBranches"
TAGS_CONTEXT_KEY ContextKey = "tags"
BRANCH_COMMITS_CONTEXT_KEY ContextKey = "commits"
REFLOG_COMMITS_CONTEXT_KEY ContextKey = "reflogCommits"
SUB_COMMITS_CONTEXT_KEY ContextKey = "subCommits"
COMMIT_FILES_CONTEXT_KEY ContextKey = "commitFiles"
STASH_CONTEXT_KEY ContextKey = "stash"
MAIN_NORMAL_CONTEXT_KEY ContextKey = "normal"
MAIN_MERGING_CONTEXT_KEY ContextKey = "merging"
MAIN_PATCH_BUILDING_CONTEXT_KEY ContextKey = "patchBuilding"
MAIN_STAGING_CONTEXT_KEY ContextKey = "staging"
MENU_CONTEXT_KEY ContextKey = "menu"
CREDENTIALS_CONTEXT_KEY ContextKey = "credentials"
CONFIRMATION_CONTEXT_KEY ContextKey = "confirmation"
SEARCH_CONTEXT_KEY ContextKey = "search"
COMMIT_MESSAGE_CONTEXT_KEY ContextKey = "commitMessage"
SUBMODULES_CONTEXT_KEY ContextKey = "submodules"
SUGGESTIONS_CONTEXT_KEY ContextKey = "suggestions"
COMMAND_LOG_CONTEXT_KEY ContextKey = "cmdLog"
import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
var allContextKeys = []ContextKey{
const (
STATUS_CONTEXT_KEY types.ContextKey = "status"
FILES_CONTEXT_KEY types.ContextKey = "files"
LOCAL_BRANCHES_CONTEXT_KEY types.ContextKey = "localBranches"
REMOTES_CONTEXT_KEY types.ContextKey = "remotes"
REMOTE_BRANCHES_CONTEXT_KEY types.ContextKey = "remoteBranches"
TAGS_CONTEXT_KEY types.ContextKey = "tags"
BRANCH_COMMITS_CONTEXT_KEY types.ContextKey = "commits"
REFLOG_COMMITS_CONTEXT_KEY types.ContextKey = "reflogCommits"
SUB_COMMITS_CONTEXT_KEY types.ContextKey = "subCommits"
COMMIT_FILES_CONTEXT_KEY types.ContextKey = "commitFiles"
STASH_CONTEXT_KEY types.ContextKey = "stash"
MAIN_NORMAL_CONTEXT_KEY types.ContextKey = "normal"
MAIN_MERGING_CONTEXT_KEY types.ContextKey = "merging"
MAIN_PATCH_BUILDING_CONTEXT_KEY types.ContextKey = "patchBuilding"
MAIN_STAGING_CONTEXT_KEY types.ContextKey = "staging"
MENU_CONTEXT_KEY types.ContextKey = "menu"
CREDENTIALS_CONTEXT_KEY types.ContextKey = "credentials"
CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation"
SEARCH_CONTEXT_KEY types.ContextKey = "search"
COMMIT_MESSAGE_CONTEXT_KEY types.ContextKey = "commitMessage"
SUBMODULES_CONTEXT_KEY types.ContextKey = "submodules"
SUGGESTIONS_CONTEXT_KEY types.ContextKey = "suggestions"
COMMAND_LOG_CONTEXT_KEY types.ContextKey = "cmdLog"
)
var AllContextKeys = []types.ContextKey{
STATUS_CONTEXT_KEY,
FILES_CONTEXT_KEY,
LOCAL_BRANCHES_CONTEXT_KEY,
@ -54,34 +57,8 @@ var allContextKeys = []ContextKey{
COMMAND_LOG_CONTEXT_KEY,
}
type ContextTree struct {
Status Context
Files IListContext
Submodules IListContext
Menu IListContext
Branches IListContext
Remotes IListContext
RemoteBranches IListContext
Tags IListContext
BranchCommits IListContext
CommitFiles IListContext
ReflogCommits IListContext
SubCommits IListContext
Stash IListContext
Suggestions IListContext
Normal Context
Staging Context
PatchBuilding Context
Merging Context
Credentials Context
Confirmation Context
CommitMessage Context
Search Context
CommandLog Context
}
func (gui *Gui) allContexts() []Context {
return []Context{
func (gui *Gui) allContexts() []types.Context {
return []types.Context{
gui.State.Contexts.Status,
gui.State.Contexts.Files,
gui.State.Contexts.Submodules,
@ -107,11 +84,11 @@ func (gui *Gui) allContexts() []Context {
}
}
func (gui *Gui) contextTree() ContextTree {
return ContextTree{
func (gui *Gui) contextTree() context.ContextTree {
return context.ContextTree{
Status: &BasicContext{
OnRenderToMain: OnFocusWrapper(gui.statusRenderToMain),
Kind: SIDE_CONTEXT,
Kind: types.SIDE_CONTEXT,
ViewName: "status",
Key: STATUS_CONTEXT_KEY,
},
@ -128,15 +105,15 @@ func (gui *Gui) contextTree() ContextTree {
Tags: gui.tagsListContext(),
Stash: gui.stashListContext(),
Normal: &BasicContext{
OnFocus: func(opts ...OnFocusOpts) error {
OnFocus: func(opts ...types.OnFocusOpts) error {
return nil // TODO: should we do something here? We should allow for scrolling the panel
},
Kind: MAIN_CONTEXT,
Kind: types.MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_NORMAL_CONTEXT_KEY,
},
Staging: &BasicContext{
OnFocus: func(opts ...OnFocusOpts) error {
OnFocus: func(opts ...types.OnFocusOpts) error {
forceSecondaryFocused := false
selectedLineIdx := -1
if len(opts) > 0 && opts[0].ClickedViewName != "" {
@ -149,12 +126,12 @@ func (gui *Gui) contextTree() ContextTree {
}
return gui.onStagingFocus(forceSecondaryFocused, selectedLineIdx)
},
Kind: MAIN_CONTEXT,
Kind: types.MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_STAGING_CONTEXT_KEY,
},
PatchBuilding: &BasicContext{
OnFocus: func(opts ...OnFocusOpts) error {
OnFocus: func(opts ...types.OnFocusOpts) error {
selectedLineIdx := -1
if len(opts) > 0 && (opts[0].ClickedViewName == "main" || opts[0].ClickedViewName == "secondary") {
selectedLineIdx = opts[0].ClickedViewLineIdx
@ -162,7 +139,7 @@ func (gui *Gui) contextTree() ContextTree {
return gui.onPatchBuildingFocus(selectedLineIdx)
},
Kind: MAIN_CONTEXT,
Kind: types.MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_PATCH_BUILDING_CONTEXT_KEY,
},
@ -175,30 +152,30 @@ func (gui *Gui) contextTree() ContextTree {
},
Credentials: &BasicContext{
OnFocus: OnFocusWrapper(gui.handleAskFocused),
Kind: PERSISTENT_POPUP,
Kind: types.PERSISTENT_POPUP,
ViewName: "credentials",
Key: CREDENTIALS_CONTEXT_KEY,
},
Confirmation: &BasicContext{
OnFocus: OnFocusWrapper(gui.handleAskFocused),
Kind: TEMPORARY_POPUP,
Kind: types.TEMPORARY_POPUP,
ViewName: "confirmation",
Key: CONFIRMATION_CONTEXT_KEY,
},
Suggestions: gui.suggestionsListContext(),
CommitMessage: &BasicContext{
OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused),
Kind: PERSISTENT_POPUP,
Kind: types.PERSISTENT_POPUP,
ViewName: "commitMessage",
Key: COMMIT_MESSAGE_CONTEXT_KEY,
},
Search: &BasicContext{
Kind: PERSISTENT_POPUP,
Kind: types.PERSISTENT_POPUP,
ViewName: "search",
Key: SEARCH_CONTEXT_KEY,
},
CommandLog: &BasicContext{
Kind: EXTRAS_CONTEXT,
Kind: types.EXTRAS_CONTEXT,
ViewName: "extras",
Key: COMMAND_LOG_CONTEXT_KEY,
OnGetOptionsMap: gui.getMergingOptions,
@ -212,72 +189,8 @@ func (gui *Gui) contextTree() ContextTree {
// using this wrapper for when an onFocus function doesn't care about any potential
// props that could be passed
func OnFocusWrapper(f func() error) func(opts ...OnFocusOpts) error {
return func(opts ...OnFocusOpts) error {
func OnFocusWrapper(f func() error) func(opts ...types.OnFocusOpts) error {
return func(opts ...types.OnFocusOpts) error {
return f()
}
}
func (tree ContextTree) initialViewContextMap() map[string]Context {
return map[string]Context{
"status": tree.Status,
"files": tree.Files,
"branches": tree.Branches,
"commits": tree.BranchCommits,
"commitFiles": tree.CommitFiles,
"stash": tree.Stash,
"menu": tree.Menu,
"confirmation": tree.Confirmation,
"credentials": tree.Credentials,
"commitMessage": tree.CommitMessage,
"main": tree.Normal,
"secondary": tree.Normal,
"extras": tree.CommandLog,
}
}
func (tree ContextTree) initialViewTabContextMap() map[string][]tabContext {
return map[string][]tabContext{
"branches": {
{
tab: "Local Branches",
contexts: []Context{tree.Branches},
},
{
tab: "Remotes",
contexts: []Context{
tree.Remotes,
tree.RemoteBranches,
},
},
{
tab: "Tags",
contexts: []Context{tree.Tags},
},
},
"commits": {
{
tab: "Commits",
contexts: []Context{tree.BranchCommits},
},
{
tab: "Reflog",
contexts: []Context{
tree.ReflogCommits,
},
},
},
"files": {
{
tab: "Files",
contexts: []Context{tree.Files},
},
{
tab: "Submodules",
contexts: []Context{
tree.Submodules,
},
},
},
}
}

View File

@ -0,0 +1,273 @@
package controllers
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type BisectController struct {
c *ControllerCommon
context types.IListContext
git *commands.GitCommand
getSelectedLocalCommit func() *models.Commit
getCommits func() []*models.Commit
}
var _ types.IController = &BisectController{}
func NewBisectController(
c *ControllerCommon,
context types.IListContext,
git *commands.GitCommand,
getSelectedLocalCommit func() *models.Commit,
getCommits func() []*models.Commit,
) *BisectController {
return &BisectController{
c: c,
context: context,
git: git,
getSelectedLocalCommit: getSelectedLocalCommit,
getCommits: getCommits,
}
}
func (self *BisectController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
bindings := []*types.Binding{
{
Key: getKey(config.Commits.ViewBisectOptions),
Handler: guards.OutsideFilterMode(self.checkSelected(self.openMenu)),
Description: self.c.Tr.LcViewBisectOptions,
OpensMenu: true,
},
}
return bindings
}
func (self *BisectController) openMenu(commit *models.Commit) error {
// no shame in getting this directly rather than using the cached value
// given how cheap it is to obtain
info := self.git.Bisect.GetInfo()
if info.Started() {
return self.openMidBisectMenu(info, commit)
} else {
return self.openStartBisectMenu(info, commit)
}
}
func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
// if there is not yet a 'current' bisect commit, or if we have
// selected the current commit, we need to jump to the next 'current' commit
// after we perform a bisect action. The reason we don't unconditionally jump
// is that sometimes the user will want to go and mark a few commits as skipped
// in a row and they wouldn't want to be jumped back to the current bisect
// commit each time.
// Originally we were allowing the user to, from the bisect menu, select whether
// they were talking about the selected commit or the current bisect commit,
// and that was a bit confusing (and required extra keypresses).
selectCurrentAfter := info.GetCurrentSha() == "" || info.GetCurrentSha() == commit.Sha
// we need to wait to reselect if our bisect commits aren't ancestors of our 'start'
// ref, because we'll be reloading our commits in that case.
waitToReselect := selectCurrentAfter && !self.git.Bisect.ReachableFromStart(info)
menuItems := []*popup.MenuItem{
{
DisplayString: fmt.Sprintf(self.c.Tr.Bisect.Mark, commit.ShortSha(), info.NewTerm()),
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.BisectMark)
if err := self.git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
return self.c.Error(err)
}
return self.afterMark(selectCurrentAfter, waitToReselect)
},
},
{
DisplayString: fmt.Sprintf(self.c.Tr.Bisect.Mark, commit.ShortSha(), info.OldTerm()),
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.BisectMark)
if err := self.git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
return self.c.Error(err)
}
return self.afterMark(selectCurrentAfter, waitToReselect)
},
},
{
DisplayString: fmt.Sprintf(self.c.Tr.Bisect.Skip, commit.ShortSha()),
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.BisectSkip)
if err := self.git.Bisect.Skip(commit.Sha); err != nil {
return self.c.Error(err)
}
return self.afterMark(selectCurrentAfter, waitToReselect)
},
},
{
DisplayString: self.c.Tr.Bisect.ResetOption,
OnPress: func() error {
return self.Reset()
},
},
}
return self.c.Menu(popup.CreateMenuOptions{
Title: self.c.Tr.Bisect.BisectMenuTitle,
Items: menuItems,
})
}
func (self *BisectController) openStartBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
return self.c.Menu(popup.CreateMenuOptions{
Title: self.c.Tr.Bisect.BisectMenuTitle,
Items: []*popup.MenuItem{
{
DisplayString: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortSha(), info.NewTerm()),
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.StartBisect)
if err := self.git.Bisect.Start(); err != nil {
return self.c.Error(err)
}
if err := self.git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
return self.c.Error(err)
}
return self.postBisectCommandRefresh()
},
},
{
DisplayString: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortSha(), info.OldTerm()),
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.StartBisect)
if err := self.git.Bisect.Start(); err != nil {
return self.c.Error(err)
}
if err := self.git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
return self.c.Error(err)
}
return self.postBisectCommandRefresh()
},
},
},
})
}
func (self *BisectController) Reset() error {
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.Bisect.ResetTitle,
Prompt: self.c.Tr.Bisect.ResetPrompt,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.ResetBisect)
if err := self.git.Bisect.Reset(); err != nil {
return self.c.Error(err)
}
return self.postBisectCommandRefresh()
},
})
}
func (self *BisectController) showBisectCompleteMessage(candidateShas []string) error {
prompt := self.c.Tr.Bisect.CompletePrompt
if len(candidateShas) > 1 {
prompt = self.c.Tr.Bisect.CompletePromptIndeterminate
}
formattedCommits, err := self.git.Commit.GetCommitsOneline(candidateShas)
if err != nil {
return self.c.Error(err)
}
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.Bisect.CompleteTitle,
Prompt: fmt.Sprintf(prompt, strings.TrimSpace(formattedCommits)),
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.ResetBisect)
if err := self.git.Bisect.Reset(); err != nil {
return self.c.Error(err)
}
return self.postBisectCommandRefresh()
},
})
}
func (self *BisectController) afterMark(selectCurrent bool, waitToReselect bool) error {
done, candidateShas, err := self.git.Bisect.IsDone()
if err != nil {
return self.c.Error(err)
}
if err := self.afterBisectMarkRefresh(selectCurrent, waitToReselect); err != nil {
return self.c.Error(err)
}
if done {
return self.showBisectCompleteMessage(candidateShas)
}
return nil
}
func (self *BisectController) postBisectCommandRefresh() error {
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{}})
}
func (self *BisectController) afterBisectMarkRefresh(selectCurrent bool, waitToReselect bool) error {
selectFn := func() {
if selectCurrent {
self.selectCurrentBisectCommit()
}
}
if waitToReselect {
return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{}, Then: selectFn})
} else {
selectFn()
return self.postBisectCommandRefresh()
}
}
func (self *BisectController) selectCurrentBisectCommit() {
info := self.git.Bisect.GetInfo()
if info.GetCurrentSha() != "" {
// find index of commit with that sha, move cursor to that.
for i, commit := range self.getCommits() {
if commit.Sha == info.GetCurrentSha() {
self.context.GetPanelState().SetSelectedLineIdx(i)
_ = self.context.HandleFocus()
break
}
}
}
}
func (self *BisectController) checkSelected(callback func(*models.Commit) error) func() error {
return func() error {
commit := self.getSelectedLocalCommit()
if commit == nil {
return nil
}
return callback(commit)
}
}
func (self *BisectController) Context() types.Context {
return self.context
}

View File

@ -0,0 +1,10 @@
package controllers
import "github.com/jesseduffield/lazygit/pkg/common"
// if Go let me do private struct embedding of structs with public fields (which it should)
// I would just do that. But alas.
type ControllerCommon struct {
*common.Common
IGuiCommon
}

View File

@ -0,0 +1,737 @@
package controllers
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type FilesController struct {
// I've said publicly that I'm against single-letter variable names but in this
// case I would actually prefer a _zero_ letter variable name in the form of
// struct embedding, but Go does not allow hiding public fields in an embedded struct
// to the client
c *ControllerCommon
context types.IListContext
git *commands.GitCommand
os *oscommands.OSCommand
getSelectedFileNode func() *filetree.FileNode
allContexts context.ContextTree
fileTreeViewModel *filetree.FileTreeViewModel
enterSubmodule func(submodule *models.SubmoduleConfig) error
getSubmodules func() []*models.SubmoduleConfig
setCommitMessage func(message string)
getCheckedOutBranch func() *models.Branch
withGpgHandling func(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error
getFailedCommitMessage func() string
getCommits func() []*models.Commit
getSelectedPath func() string
switchToMergeFn func(path string) error
suggestionsHelper ISuggestionsHelper
refHelper IRefHelper
fileHelper IFileHelper
workingTreeHelper IWorkingTreeHelper
}
var _ types.IController = &FilesController{}
func NewFilesController(
c *ControllerCommon,
context types.IListContext,
git *commands.GitCommand,
os *oscommands.OSCommand,
getSelectedFileNode func() *filetree.FileNode,
allContexts context.ContextTree,
fileTreeViewModel *filetree.FileTreeViewModel,
enterSubmodule func(submodule *models.SubmoduleConfig) error,
getSubmodules func() []*models.SubmoduleConfig,
setCommitMessage func(message string),
withGpgHandling func(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error,
getFailedCommitMessage func() string,
getCommits func() []*models.Commit,
getSelectedPath func() string,
switchToMergeFn func(path string) error,
suggestionsHelper ISuggestionsHelper,
refHelper IRefHelper,
fileHelper IFileHelper,
workingTreeHelper IWorkingTreeHelper,
) *FilesController {
return &FilesController{
c: c,
context: context,
git: git,
os: os,
getSelectedFileNode: getSelectedFileNode,
allContexts: allContexts,
fileTreeViewModel: fileTreeViewModel,
enterSubmodule: enterSubmodule,
getSubmodules: getSubmodules,
setCommitMessage: setCommitMessage,
withGpgHandling: withGpgHandling,
getFailedCommitMessage: getFailedCommitMessage,
getCommits: getCommits,
getSelectedPath: getSelectedPath,
switchToMergeFn: switchToMergeFn,
suggestionsHelper: suggestionsHelper,
refHelper: refHelper,
fileHelper: fileHelper,
workingTreeHelper: workingTreeHelper,
}
}
func (self *FilesController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
bindings := []*types.Binding{
{
Key: getKey(config.Universal.Select),
Handler: self.checkSelectedFileNode(self.press),
Description: self.c.Tr.LcToggleStaged,
},
{
Key: gocui.MouseLeft,
Handler: func() error { return self.context.HandleClick(self.checkSelectedFileNode(self.press)) },
},
{
Key: getKey("<c-b>"), // TODO: softcode
Handler: self.handleStatusFilterPressed,
Description: self.c.Tr.LcFileFilter,
},
{
Key: getKey(config.Files.CommitChanges),
Handler: self.HandleCommitPress,
Description: self.c.Tr.CommitChanges,
},
{
Key: getKey(config.Files.CommitChangesWithoutHook),
Handler: self.HandleWIPCommitPress,
Description: self.c.Tr.LcCommitChangesWithoutHook,
},
{
Key: getKey(config.Files.AmendLastCommit),
Handler: self.handleAmendCommitPress,
Description: self.c.Tr.AmendLastCommit,
},
{
Key: getKey(config.Files.CommitChangesWithEditor),
Handler: self.HandleCommitEditorPress,
Description: self.c.Tr.CommitChangesWithEditor,
},
{
Key: getKey(config.Universal.Edit),
Handler: self.edit,
Description: self.c.Tr.LcEditFile,
},
{
Key: getKey(config.Universal.OpenFile),
Handler: self.Open,
Description: self.c.Tr.LcOpenFile,
},
{
Key: getKey(config.Files.IgnoreFile),
Handler: self.ignore,
Description: self.c.Tr.LcIgnoreFile,
},
{
Key: getKey(config.Files.RefreshFiles),
Handler: self.refresh,
Description: self.c.Tr.LcRefreshFiles,
},
{
Key: getKey(config.Files.StashAllChanges),
Handler: self.stash,
Description: self.c.Tr.LcStashAllChanges,
},
{
Key: getKey(config.Files.ViewStashOptions),
Handler: self.createStashMenu,
Description: self.c.Tr.LcViewStashOptions,
OpensMenu: true,
},
{
Key: getKey(config.Files.ToggleStagedAll),
Handler: self.stageAll,
Description: self.c.Tr.LcToggleStagedAll,
},
{
Key: getKey(config.Universal.GoInto),
Handler: self.enter,
Description: self.c.Tr.FileEnter,
},
{
ViewName: "",
Key: getKey(config.Universal.ExecuteCustomCommand),
Handler: self.handleCustomCommand,
Description: self.c.Tr.LcExecuteCustomCommand,
},
{
Key: getKey(config.Commits.ViewResetOptions),
Handler: self.createResetMenu,
Description: self.c.Tr.LcViewResetToUpstreamOptions,
OpensMenu: true,
},
{
Key: getKey(config.Files.ToggleTreeView),
Handler: self.toggleTreeView,
Description: self.c.Tr.LcToggleTreeView,
},
{
Key: getKey(config.Files.OpenMergeTool),
Handler: self.OpenMergeTool,
Description: self.c.Tr.LcOpenMergeTool,
},
}
return append(bindings, self.context.Keybindings(getKey, config, guards)...)
}
func (self *FilesController) press(node *filetree.FileNode) error {
if node.IsLeaf() {
file := node.File
if file.HasInlineMergeConflicts {
return self.c.PushContext(self.allContexts.Merging)
}
if file.HasUnstagedChanges {
self.c.LogAction(self.c.Tr.Actions.StageFile)
if err := self.git.WorkingTree.StageFile(file.Name); err != nil {
return self.c.Error(err)
}
} else {
self.c.LogAction(self.c.Tr.Actions.UnstageFile)
if err := self.git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return self.c.Error(err)
}
}
} else {
// if any files within have inline merge conflicts we can't stage or unstage,
// or it'll end up with those >>>>>> lines actually staged
if node.GetHasInlineMergeConflicts() {
return self.c.ErrorMsg(self.c.Tr.ErrStageDirWithInlineMergeConflicts)
}
if node.GetHasUnstagedChanges() {
self.c.LogAction(self.c.Tr.Actions.StageFile)
if err := self.git.WorkingTree.StageFile(node.Path); err != nil {
return self.c.Error(err)
}
} else {
// pretty sure it doesn't matter that we're always passing true here
self.c.LogAction(self.c.Tr.Actions.UnstageFile)
if err := self.git.WorkingTree.UnStageFile([]string{node.Path}, true); err != nil {
return self.c.Error(err)
}
}
}
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil {
return err
}
return self.context.HandleFocus()
}
func (self *FilesController) checkSelectedFileNode(callback func(*filetree.FileNode) error) func() error {
return func() error {
node := self.getSelectedFileNode()
if node == nil {
return nil
}
return callback(node)
}
}
func (self *FilesController) checkSelectedFile(callback func(*models.File) error) func() error {
return func() error {
file := self.getSelectedFile()
if file == nil {
return nil
}
return callback(file)
}
}
func (self *FilesController) Context() types.Context {
return self.context
}
func (self *FilesController) getSelectedFile() *models.File {
node := self.getSelectedFileNode()
if node == nil {
return nil
}
return node.File
}
func (self *FilesController) enter() error {
return self.EnterFile(types.OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
}
func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
node := self.getSelectedFileNode()
if node == nil {
return nil
}
if node.File == nil {
return self.handleToggleDirCollapsed()
}
file := node.File
submoduleConfigs := self.getSubmodules()
if file.IsSubmodule(submoduleConfigs) {
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
return self.enterSubmodule(submoduleConfig)
}
if file.HasInlineMergeConflicts {
return self.switchToMerge()
}
if file.HasMergeConflicts {
return self.c.ErrorMsg(self.c.Tr.FileStagingRequirements)
}
return self.c.PushContext(self.allContexts.Staging, opts)
}
func (self *FilesController) allFilesStaged() bool {
for _, file := range self.fileTreeViewModel.GetAllFiles() {
if file.HasUnstagedChanges {
return false
}
}
return true
}
func (self *FilesController) stageAll() error {
var err error
if self.allFilesStaged() {
self.c.LogAction(self.c.Tr.Actions.UnstageAllFiles)
err = self.git.WorkingTree.UnstageAll()
} else {
self.c.LogAction(self.c.Tr.Actions.StageAllFiles)
err = self.git.WorkingTree.StageAll()
}
if err != nil {
_ = self.c.Error(err)
}
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil {
return err
}
return self.allContexts.Files.HandleFocus()
}
func (self *FilesController) ignore() error {
node := self.getSelectedFileNode()
if node == nil {
return nil
}
if node.GetPath() == ".gitignore" {
return self.c.ErrorMsg("Cannot ignore .gitignore")
}
unstageFiles := func() error {
return node.ForEachFile(func(file *models.File) error {
if file.HasStagedChanges {
if err := self.git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return err
}
}
return nil
})
}
if node.GetIsTracked() {
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.IgnoreTracked,
Prompt: self.c.Tr.IgnoreTrackedPrompt,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.IgnoreFile)
// not 100% sure if this is necessary but I'll assume it is
if err := unstageFiles(); err != nil {
return err
}
if err := self.git.WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil {
return err
}
if err := self.git.WorkingTree.Ignore(node.GetPath()); err != nil {
return err
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
},
})
}
self.c.LogAction(self.c.Tr.Actions.IgnoreFile)
if err := unstageFiles(); err != nil {
return err
}
if err := self.git.WorkingTree.Ignore(node.GetPath()); err != nil {
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
}
func (self *FilesController) HandleWIPCommitPress() error {
skipHookPrefix := self.c.UserConfig.Git.SkipHookPrefix
if skipHookPrefix == "" {
return self.c.ErrorMsg(self.c.Tr.SkipHookPrefixNotConfigured)
}
self.setCommitMessage(skipHookPrefix)
return self.HandleCommitPress()
}
func (self *FilesController) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
cfg, ok := self.c.UserConfig.Git.CommitPrefixes[utils.GetCurrentRepoName()]
if !ok {
return nil
}
return &cfg
}
func (self *FilesController) prepareFilesForCommit() error {
noStagedFiles := !self.workingTreeHelper.AnyStagedFiles()
if noStagedFiles && self.c.UserConfig.Gui.SkipNoStagedFilesWarning {
self.c.LogAction(self.c.Tr.Actions.StageAllFiles)
err := self.git.WorkingTree.StageAll()
if err != nil {
return err
}
return self.syncRefresh()
}
return nil
}
// for when you need to refetch files before continuing an action. Runs synchronously.
func (self *FilesController) syncRefresh() error {
return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.FILES}})
}
func (self *FilesController) refresh() error {
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
}
func (self *FilesController) HandleCommitPress() error {
if err := self.prepareFilesForCommit(); err != nil {
return self.c.Error(err)
}
if self.fileTreeViewModel.GetItemsLength() == 0 {
return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle)
}
if !self.workingTreeHelper.AnyStagedFiles() {
return self.promptToStageAllAndRetry(self.HandleCommitPress)
}
failedCommitMessage := self.getFailedCommitMessage()
if len(failedCommitMessage) > 0 {
self.setCommitMessage(failedCommitMessage)
} else {
commitPrefixConfig := self.commitPrefixConfigForRepo()
if commitPrefixConfig != nil {
prefixPattern := commitPrefixConfig.Pattern
prefixReplace := commitPrefixConfig.Replace
rgx, err := regexp.Compile(prefixPattern)
if err != nil {
return self.c.ErrorMsg(fmt.Sprintf("%s: %s", self.c.Tr.LcCommitPrefixPatternError, err.Error()))
}
prefix := rgx.ReplaceAllString(self.getCheckedOutBranch().Name, prefixReplace)
self.setCommitMessage(prefix)
}
}
if err := self.c.PushContext(self.allContexts.CommitMessage); err != nil {
return err
}
return nil
}
func (self *FilesController) promptToStageAllAndRetry(retry func() error) error {
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.NoFilesStagedTitle,
Prompt: self.c.Tr.NoFilesStagedPrompt,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.StageAllFiles)
if err := self.git.WorkingTree.StageAll(); err != nil {
return self.c.Error(err)
}
if err := self.syncRefresh(); err != nil {
return self.c.Error(err)
}
return retry()
},
})
}
func (self *FilesController) handleAmendCommitPress() error {
if self.fileTreeViewModel.GetItemsLength() == 0 {
return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle)
}
if !self.workingTreeHelper.AnyStagedFiles() {
return self.promptToStageAllAndRetry(self.handleAmendCommitPress)
}
if len(self.getCommits()) == 0 {
return self.c.ErrorMsg(self.c.Tr.NoCommitToAmend)
}
return self.c.Ask(popup.AskOpts{
Title: strings.Title(self.c.Tr.AmendLastCommit),
Prompt: self.c.Tr.SureToAmend,
HandleConfirm: func() error {
cmdObj := self.git.Commit.AmendHeadCmdObj()
self.c.LogAction(self.c.Tr.Actions.AmendCommit)
return self.withGpgHandling(cmdObj, self.c.Tr.AmendingStatus, nil)
},
})
}
// HandleCommitEditorPress - handle when the user wants to commit changes via
// their editor rather than via the popup panel
func (self *FilesController) HandleCommitEditorPress() error {
if self.fileTreeViewModel.GetItemsLength() == 0 {
return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle)
}
if !self.workingTreeHelper.AnyStagedFiles() {
return self.promptToStageAllAndRetry(self.HandleCommitEditorPress)
}
self.c.LogAction(self.c.Tr.Actions.Commit)
return self.c.RunSubprocessAndRefresh(
self.git.Commit.CommitEditorCmdObj(),
)
}
func (self *FilesController) handleStatusFilterPressed() error {
return self.c.Menu(popup.CreateMenuOptions{
Title: self.c.Tr.FilteringMenuTitle,
Items: []*popup.MenuItem{
{
DisplayString: self.c.Tr.FilterStagedFiles,
OnPress: func() error {
return self.setStatusFiltering(filetree.DisplayStaged)
},
},
{
DisplayString: self.c.Tr.FilterUnstagedFiles,
OnPress: func() error {
return self.setStatusFiltering(filetree.DisplayUnstaged)
},
},
{
DisplayString: self.c.Tr.ResetCommitFilterState,
OnPress: func() error {
return self.setStatusFiltering(filetree.DisplayAll)
},
},
},
})
}
func (self *FilesController) setStatusFiltering(filter filetree.FileTreeDisplayFilter) error {
self.fileTreeViewModel.SetFilter(filter)
return self.c.PostRefreshUpdate(self.context)
}
func (self *FilesController) edit() error {
node := self.getSelectedFileNode()
if node == nil {
return nil
}
if node.File == nil {
return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory)
}
return self.fileHelper.EditFile(node.GetPath())
}
func (self *FilesController) Open() error {
node := self.getSelectedFileNode()
if node == nil {
return nil
}
return self.fileHelper.OpenFile(node.GetPath())
}
func (self *FilesController) switchToMerge() error {
file := self.getSelectedFile()
if file == nil {
return nil
}
self.switchToMergeFn(path)
}
func (self *FilesController) handleCustomCommand() error {
return self.c.Prompt(popup.PromptOpts{
Title: self.c.Tr.CustomCommand,
FindSuggestionsFunc: self.suggestionsHelper.GetCustomCommandsHistorySuggestionsFunc(),
HandleConfirm: func(command string) error {
self.c.GetAppState().CustomCommandsHistory = utils.Limit(
utils.Uniq(
append(self.c.GetAppState().CustomCommandsHistory, command),
),
1000,
)
err := self.c.SaveAppState()
if err != nil {
self.c.Log.Error(err)
}
self.c.LogAction(self.c.Tr.Actions.CustomCommand)
return self.c.RunSubprocessAndRefresh(
self.os.Cmd.NewShell(command),
)
},
})
}
func (self *FilesController) createStashMenu() error {
return self.c.Menu(popup.CreateMenuOptions{
Title: self.c.Tr.LcStashOptions,
Items: []*popup.MenuItem{
{
DisplayString: self.c.Tr.LcStashAllChanges,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.StashAllChanges)
return self.handleStashSave(self.git.Stash.Save)
},
},
{
DisplayString: self.c.Tr.LcStashStagedChanges,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.StashStagedChanges)
return self.handleStashSave(self.git.Stash.SaveStagedChanges)
},
},
},
})
}
func (self *FilesController) stash() error {
return self.handleStashSave(self.git.Stash.Save)
}
func (self *FilesController) createResetMenu() error {
return self.refHelper.CreateGitResetMenu("@{upstream}")
}
func (self *FilesController) handleToggleDirCollapsed() error {
node := self.getSelectedFileNode()
if node == nil {
return nil
}
self.fileTreeViewModel.ToggleCollapsed(node.GetPath())
if err := self.c.PostRefreshUpdate(self.allContexts.Files); err != nil {
self.c.Log.Error(err)
}
return nil
}
func (self *FilesController) toggleTreeView() error {
// get path of currently selected file
path := self.getSelectedPath()
self.fileTreeViewModel.ToggleShowTree()
// find that same node in the new format and move the cursor to it
if path != "" {
self.fileTreeViewModel.ExpandToPath(path)
index, found := self.fileTreeViewModel.GetIndexForPath(path)
if found {
self.context.GetPanelState().SetSelectedLineIdx(index)
}
}
return self.c.PostRefreshUpdate(self.context)
}
func (self *FilesController) OpenMergeTool() error {
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.MergeToolTitle,
Prompt: self.c.Tr.MergeToolPrompt,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.OpenMergeTool)
return self.c.RunSubprocessAndRefresh(
self.git.WorkingTree.OpenMergeToolCmdObj(),
)
},
})
}
func (self *FilesController) ResetSubmodule(submodule *models.SubmoduleConfig) error {
return self.c.WithWaitingStatus(self.c.Tr.LcResettingSubmoduleStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.ResetSubmodule)
file := self.workingTreeHelper.FileForSubmodule(submodule)
if file != nil {
if err := self.git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return self.c.Error(err)
}
}
if err := self.git.Submodule.Stash(submodule); err != nil {
return self.c.Error(err)
}
if err := self.git.Submodule.Reset(submodule); err != nil {
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.SUBMODULES}})
})
}
func (self *FilesController) handleStashSave(stashFunc func(message string) error) error {
if !self.workingTreeHelper.IsWorkingTreeDirty() {
return self.c.ErrorMsg(self.c.Tr.NoTrackedStagedFilesStash)
}
return self.c.Prompt(popup.PromptOpts{
Title: self.c.Tr.StashChanges,
HandleConfirm: func(stashComment string) error {
if err := stashFunc(stashComment); err != nil {
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH, types.FILES}})
},
})
}

View File

@ -0,0 +1,783 @@
package controllers
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type (
CheckoutRefFn func(refName string, opts types.CheckoutRefOptions) error
CreateGitResetMenuFn func(refName string) error
SwitchToCommitFilesContextFn func(SwitchToCommitFilesContextOpts) error
CreateTagMenuFn func(commitSha string) error
GetHostingServiceMgrFn func() *hosting_service.HostingServiceMgr
PullFilesFn func() error
CheckMergeOrRebase func(error) error
OpenSearchFn func(viewName string) error
)
type LocalCommitsController struct {
c *ControllerCommon
context types.IListContext
os *oscommands.OSCommand
git *commands.GitCommand
refHelper IRefHelper
getSelectedLocalCommit func() *models.Commit
getCommits func() []*models.Commit
getSelectedLocalCommitIdx func() int
checkMergeOrRebase CheckMergeOrRebase
pullFiles PullFilesFn
createTagMenu CreateTagMenuFn
getHostingServiceMgr GetHostingServiceMgrFn
switchToCommitFilesContext SwitchToCommitFilesContextFn
openSearch OpenSearchFn
getLimitCommits func() bool
setLimitCommits func(bool)
getShowWholeGitGraph func() bool
setShowWholeGitGraph func(bool)
}
var _ types.IController = &LocalCommitsController{}
func NewLocalCommitsController(
c *ControllerCommon,
context types.IListContext,
os *oscommands.OSCommand,
git *commands.GitCommand,
refHelper IRefHelper,
getSelectedLocalCommit func() *models.Commit,
getCommits func() []*models.Commit,
getSelectedLocalCommitIdx func() int,
checkMergeOrRebase CheckMergeOrRebase,
pullFiles PullFilesFn,
createTagMenu CreateTagMenuFn,
getHostingServiceMgr GetHostingServiceMgrFn,
switchToCommitFilesContext SwitchToCommitFilesContextFn,
openSearch OpenSearchFn,
getLimitCommits func() bool,
setLimitCommits func(bool),
getShowWholeGitGraph func() bool,
setShowWholeGitGraph func(bool),
) *LocalCommitsController {
return &LocalCommitsController{
c: c,
context: context,
os: os,
git: git,
refHelper: refHelper,
getSelectedLocalCommit: getSelectedLocalCommit,
getCommits: getCommits,
getSelectedLocalCommitIdx: getSelectedLocalCommitIdx,
checkMergeOrRebase: checkMergeOrRebase,
pullFiles: pullFiles,
createTagMenu: createTagMenu,
getHostingServiceMgr: getHostingServiceMgr,
switchToCommitFilesContext: switchToCommitFilesContext,
openSearch: openSearch,
getLimitCommits: getLimitCommits,
setLimitCommits: setLimitCommits,
getShowWholeGitGraph: getShowWholeGitGraph,
setShowWholeGitGraph: setShowWholeGitGraph,
}
}
func (self *LocalCommitsController) Keybindings(
getKey func(key string) interface{},
config config.KeybindingConfig,
guards types.KeybindingGuards,
) []*types.Binding {
outsideFilterModeBindings := []*types.Binding{
{
Key: getKey(config.Commits.SquashDown),
Handler: self.squashDown,
Description: self.c.Tr.LcSquashDown,
},
{
Key: getKey(config.Commits.MarkCommitAsFixup),
Handler: self.fixup,
Description: self.c.Tr.LcFixupCommit,
},
{
Key: getKey(config.Commits.RenameCommit),
Handler: self.checkSelected(self.reword),
Description: self.c.Tr.LcRewordCommit,
},
{
Key: getKey(config.Commits.RenameCommitWithEditor),
Handler: self.rewordEditor,
Description: self.c.Tr.LcRenameCommitEditor,
},
{
Key: getKey(config.Universal.Remove),
Handler: self.drop,
Description: self.c.Tr.LcDeleteCommit,
},
{
Key: getKey(config.Universal.Edit),
Handler: self.edit,
Description: self.c.Tr.LcEditCommit,
},
{
Key: getKey(config.Commits.PickCommit),
Handler: self.pick,
Description: self.c.Tr.LcPickCommit,
},
{
Key: getKey(config.Commits.CreateFixupCommit),
Handler: self.checkSelected(self.handleCreateFixupCommit),
Description: self.c.Tr.LcCreateFixupCommit,
},
{
Key: getKey(config.Commits.SquashAboveCommits),
Handler: self.checkSelected(self.handleSquashAllAboveFixupCommits),
Description: self.c.Tr.LcSquashAboveCommits,
},
{
Key: getKey(config.Commits.MoveDownCommit),
Handler: self.handleCommitMoveDown,
Description: self.c.Tr.LcMoveDownCommit,
},
{
Key: getKey(config.Commits.MoveUpCommit),
Handler: self.handleCommitMoveUp,
Description: self.c.Tr.LcMoveUpCommit,
},
{
Key: getKey(config.Commits.AmendToCommit),
Handler: self.handleCommitAmendTo,
Description: self.c.Tr.LcAmendToCommit,
},
{
Key: getKey(config.Commits.RevertCommit),
Handler: self.checkSelected(self.handleCommitRevert),
Description: self.c.Tr.LcRevertCommit,
},
// overriding these navigation keybindings because we might need to load
// more commits on demand
{
Key: getKey(config.Universal.StartSearch),
Handler: func() error { return self.handleOpenSearch("commits") },
Description: self.c.Tr.LcStartSearch,
Tag: "navigation",
},
{
Key: getKey(config.Universal.GotoBottom),
Handler: self.gotoBottom,
Description: self.c.Tr.LcGotoBottom,
Tag: "navigation",
},
{
Key: gocui.MouseLeft,
Handler: func() error { return self.context.HandleClick(self.checkSelected(self.enter)) },
},
}
for _, binding := range outsideFilterModeBindings {
binding.Handler = guards.OutsideFilterMode(binding.Handler)
}
bindings := append(outsideFilterModeBindings, []*types.Binding{
{
Key: getKey(config.Commits.OpenLogMenu),
Handler: self.handleOpenLogMenu,
Description: self.c.Tr.LcOpenLogMenu,
OpensMenu: true,
},
{
Key: getKey(config.Commits.ViewResetOptions),
Handler: self.checkSelected(self.handleCreateCommitResetMenu),
Description: self.c.Tr.LcResetToThisCommit,
},
{
Key: getKey(config.Universal.GoInto),
Handler: self.checkSelected(self.enter),
Description: self.c.Tr.LcViewCommitFiles,
},
{
Key: getKey(config.Commits.CheckoutCommit),
Handler: self.checkSelected(self.handleCheckoutCommit),
Description: self.c.Tr.LcCheckoutCommit,
},
{
Key: getKey(config.Commits.TagCommit),
Handler: self.checkSelected(self.handleTagCommit),
Description: self.c.Tr.LcTagCommit,
},
{
Key: getKey(config.Commits.CopyCommitMessageToClipboard),
Handler: self.checkSelected(self.handleCopySelectedCommitMessageToClipboard),
Description: self.c.Tr.LcCopyCommitMessageToClipboard,
},
{
Key: getKey(config.Commits.OpenInBrowser),
Handler: self.checkSelected(self.handleOpenCommitInBrowser),
Description: self.c.Tr.LcOpenCommitInBrowser,
},
}...)
return append(bindings, self.context.Keybindings(getKey, config, guards)...)
}
func (self *LocalCommitsController) squashDown() error {
if len(self.getCommits()) <= 1 {
return self.c.ErrorMsg(self.c.Tr.YouNoCommitsToSquash)
}
applied, err := self.handleMidRebaseCommand("squash")
if err != nil {
return err
}
if applied {
return nil
}
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.Squash,
Prompt: self.c.Tr.SureSquashThisCommit,
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.SquashCommitDown)
return self.interactiveRebase("squash")
})
},
})
}
func (self *LocalCommitsController) fixup() error {
if len(self.getCommits()) <= 1 {
return self.c.ErrorMsg(self.c.Tr.YouNoCommitsToSquash)
}
applied, err := self.handleMidRebaseCommand("fixup")
if err != nil {
return err
}
if applied {
return nil
}
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.Fixup,
Prompt: self.c.Tr.SureFixupThisCommit,
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.FixupCommit)
return self.interactiveRebase("fixup")
})
},
})
}
func (self *LocalCommitsController) reword(commit *models.Commit) error {
applied, err := self.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
message, err := self.git.Commit.GetCommitMessage(commit.Sha)
if err != nil {
return self.c.Error(err)
}
// TODO: use the commit message panel here
return self.c.Prompt(popup.PromptOpts{
Title: self.c.Tr.LcRewordCommit,
InitialContent: message,
HandleConfirm: func(response string) error {
self.c.LogAction(self.c.Tr.Actions.RewordCommit)
if err := self.git.Rebase.RewordCommit(self.getCommits(), self.getSelectedLocalCommitIdx(), response); err != nil {
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
},
})
}
func (self *LocalCommitsController) rewordEditor() error {
applied, err := self.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
self.c.LogAction(self.c.Tr.Actions.RewordCommit)
subProcess, err := self.git.Rebase.RewordCommitInEditor(
self.getCommits(), self.getSelectedLocalCommitIdx(),
)
if err != nil {
return self.c.Error(err)
}
if subProcess != nil {
return self.c.RunSubprocessAndRefresh(subProcess)
}
return nil
}
func (self *LocalCommitsController) drop() error {
applied, err := self.handleMidRebaseCommand("drop")
if err != nil {
return err
}
if applied {
return nil
}
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.DeleteCommitTitle,
Prompt: self.c.Tr.DeleteCommitPrompt,
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.DropCommit)
return self.interactiveRebase("drop")
})
},
})
}
func (self *LocalCommitsController) edit() error {
applied, err := self.handleMidRebaseCommand("edit")
if err != nil {
return err
}
if applied {
return nil
}
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.EditCommit)
return self.interactiveRebase("edit")
})
}
func (self *LocalCommitsController) pick() error {
applied, err := self.handleMidRebaseCommand("pick")
if err != nil {
return err
}
if applied {
return nil
}
// at this point we aren't actually rebasing so we will interpret this as an
// attempt to pull. We might revoke this later after enabling configurable keybindings
return self.pullFiles()
}
func (self *LocalCommitsController) interactiveRebase(action string) error {
err := self.git.Rebase.InteractiveRebase(self.getCommits(), self.getSelectedLocalCommitIdx(), action)
return self.checkMergeOrRebase(err)
}
// handleMidRebaseCommand sees if the selected commit is in fact a rebasing
// commit meaning you are trying to edit the todo file rather than actually
// begin a rebase. It then updates the todo file with that action
func (self *LocalCommitsController) handleMidRebaseCommand(action string) (bool, error) {
selectedCommit := self.getSelectedLocalCommit()
if selectedCommit.Status != "rebasing" {
return false, nil
}
// for now we do not support setting 'reword' because it requires an editor
// and that means we either unconditionally wait around for the subprocess to ask for
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == "reword" {
return true, self.c.ErrorMsg(self.c.Tr.LcRewordNotSupported)
}
self.c.LogAction("Update rebase TODO")
self.c.LogCommand(
fmt.Sprintf("Updating rebase action of commit %s to '%s'", selectedCommit.ShortSha(), action),
false,
)
if err := self.git.Rebase.EditRebaseTodo(
self.getSelectedLocalCommitIdx(), action,
); err != nil {
return false, self.c.Error(err)
}
return true, self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS},
})
}
func (self *LocalCommitsController) handleCommitMoveDown() error {
index := self.context.GetPanelState().GetSelectedLineIdx()
commits := self.getCommits()
selectedCommit := self.getCommits()[index]
if selectedCommit.Status == "rebasing" {
if commits[index+1].Status != "rebasing" {
return nil
}
// logging directly here because MoveTodoDown doesn't have enough information
// to provide a useful log
self.c.LogAction(self.c.Tr.Actions.MoveCommitDown)
self.c.LogCommand(fmt.Sprintf("Moving commit %s down", selectedCommit.ShortSha()), false)
if err := self.git.Rebase.MoveTodoDown(index); err != nil {
return self.c.Error(err)
}
self.context.HandleNextLine()
return self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS},
})
}
return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.MoveCommitDown)
err := self.git.Rebase.MoveCommitDown(self.getCommits(), index)
if err == nil {
self.context.HandleNextLine()
}
return self.checkMergeOrRebase(err)
})
}
func (self *LocalCommitsController) handleCommitMoveUp() error {
index := self.context.GetPanelState().GetSelectedLineIdx()
if index == 0 {
return nil
}
selectedCommit := self.getCommits()[index]
if selectedCommit.Status == "rebasing" {
// logging directly here because MoveTodoDown doesn't have enough information
// to provide a useful log
self.c.LogAction(self.c.Tr.Actions.MoveCommitUp)
self.c.LogCommand(
fmt.Sprintf("Moving commit %s up", selectedCommit.ShortSha()),
false,
)
if err := self.git.Rebase.MoveTodoDown(index - 1); err != nil {
return self.c.Error(err)
}
self.context.HandlePrevLine()
return self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS},
})
}
return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.MoveCommitUp)
err := self.git.Rebase.MoveCommitDown(self.getCommits(), index-1)
if err == nil {
self.context.HandlePrevLine()
}
return self.checkMergeOrRebase(err)
})
}
func (self *LocalCommitsController) handleCommitAmendTo() error {
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.AmendCommitTitle,
Prompt: self.c.Tr.AmendCommitPrompt,
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.AmendCommit)
err := self.git.Rebase.AmendTo(self.getSelectedLocalCommit().Sha)
return self.checkMergeOrRebase(err)
})
},
})
}
func (self *LocalCommitsController) handleCommitRevert(commit *models.Commit) error {
if commit.IsMerge() {
return self.createRevertMergeCommitMenu(commit)
} else {
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.Actions.RevertCommit,
Prompt: utils.ResolvePlaceholderString(
self.c.Tr.ConfirmRevertCommit,
map[string]string{
"selectedCommit": commit.ShortSha(),
}),
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.RevertCommit)
if err := self.git.Commit.Revert(commit.Sha); err != nil {
return self.c.Error(err)
}
return self.afterRevertCommit()
},
})
}
}
func (self *LocalCommitsController) createRevertMergeCommitMenu(commit *models.Commit) error {
menuItems := make([]*popup.MenuItem, len(commit.Parents))
for i, parentSha := range commit.Parents {
i := i
message, err := self.git.Commit.GetCommitMessageFirstLine(parentSha)
if err != nil {
return self.c.Error(err)
}
menuItems[i] = &popup.MenuItem{
DisplayString: fmt.Sprintf("%s: %s", utils.SafeTruncate(parentSha, 8), message),
OnPress: func() error {
parentNumber := i + 1
self.c.LogAction(self.c.Tr.Actions.RevertCommit)
if err := self.git.Commit.RevertMerge(commit.Sha, parentNumber); err != nil {
return self.c.Error(err)
}
return self.afterRevertCommit()
},
}
}
return self.c.Menu(popup.CreateMenuOptions{Title: self.c.Tr.SelectParentCommitForMerge, Items: menuItems})
}
func (self *LocalCommitsController) afterRevertCommit() error {
self.context.HandleNextLine()
return self.c.Refresh(types.RefreshOptions{
Mode: types.BLOCK_UI, Scope: []types.RefreshableView{types.COMMITS, types.BRANCHES},
})
}
func (self *LocalCommitsController) enter(commit *models.Commit) error {
return self.switchToCommitFilesContext(SwitchToCommitFilesContextOpts{
RefName: commit.Sha,
CanRebase: true,
Context: self.context,
WindowName: "commits",
})
}
func (self *LocalCommitsController) handleCreateFixupCommit(commit *models.Commit) error {
prompt := utils.ResolvePlaceholderString(
self.c.Tr.SureCreateFixupCommit,
map[string]string{
"commit": commit.Sha,
},
)
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.CreateFixupCommit,
Prompt: prompt,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.CreateFixupCommit)
if err := self.git.Commit.CreateFixupCommit(commit.Sha); err != nil {
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
},
})
}
func (self *LocalCommitsController) handleSquashAllAboveFixupCommits(commit *models.Commit) error {
prompt := utils.ResolvePlaceholderString(
self.c.Tr.SureSquashAboveCommits,
map[string]string{
"commit": commit.Sha,
},
)
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.SquashAboveCommits,
Prompt: prompt,
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits)
err := self.git.Rebase.SquashAllAboveFixupCommits(commit.Sha)
return self.checkMergeOrRebase(err)
})
},
})
}
func (self *LocalCommitsController) handleTagCommit(commit *models.Commit) error {
return self.createTagMenu(commit.Sha)
}
func (self *LocalCommitsController) handleCheckoutCommit(commit *models.Commit) error {
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.LcCheckoutCommit,
Prompt: self.c.Tr.SureCheckoutThisCommit,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.CheckoutCommit)
return self.refHelper.CheckoutRef(commit.Sha, types.CheckoutRefOptions{})
},
})
}
func (self *LocalCommitsController) handleCreateCommitResetMenu(commit *models.Commit) error {
return self.refHelper.CreateGitResetMenu(commit.Sha)
}
func (self *LocalCommitsController) handleOpenSearch(string) error {
// we usually lazyload these commits but now that we're searching we need to load them now
if self.getLimitCommits() {
self.setLimitCommits(false)
if err := self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS}}); err != nil {
return err
}
}
return self.openSearch("commits")
}
func (self *LocalCommitsController) gotoBottom() error {
// we usually lazyload these commits but now that we're jumping to the bottom we need to load them now
if self.getLimitCommits() {
self.setLimitCommits(false)
if err := self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS}}); err != nil {
return err
}
}
self.context.HandleGotoBottom()
return nil
}
func (self *LocalCommitsController) handleCopySelectedCommitMessageToClipboard(commit *models.Commit) error {
message, err := self.git.Commit.GetCommitMessage(commit.Sha)
if err != nil {
return self.c.Error(err)
}
self.c.LogAction(self.c.Tr.Actions.CopyCommitMessageToClipboard)
if err := self.os.CopyToClipboard(message); err != nil {
return self.c.Error(err)
}
self.c.Toast(self.c.Tr.CommitMessageCopiedToClipboard)
return nil
}
func (self *LocalCommitsController) handleOpenLogMenu() error {
return self.c.Menu(popup.CreateMenuOptions{
Title: self.c.Tr.LogMenuTitle,
Items: []*popup.MenuItem{
{
DisplayString: self.c.Tr.ToggleShowGitGraphAll,
OnPress: func() error {
self.setShowWholeGitGraph(!self.getShowWholeGitGraph())
if self.getShowWholeGitGraph() {
self.setLimitCommits(false)
}
return self.c.WithWaitingStatus(self.c.Tr.LcLoadingCommits, func() error {
return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS}})
})
},
},
{
DisplayString: self.c.Tr.ShowGitGraph,
OpensMenu: true,
OnPress: func() error {
onPress := func(value string) func() error {
return func() error {
self.c.UserConfig.Git.Log.ShowGraph = value
return nil
}
}
return self.c.Menu(popup.CreateMenuOptions{
Title: self.c.Tr.LogMenuTitle,
Items: []*popup.MenuItem{
{
DisplayString: "always",
OnPress: onPress("always"),
},
{
DisplayString: "never",
OnPress: onPress("never"),
},
{
DisplayString: "when maximised",
OnPress: onPress("when-maximised"),
},
},
})
},
},
{
DisplayString: self.c.Tr.SortCommits,
OpensMenu: true,
OnPress: func() error {
onPress := func(value string) func() error {
return func() error {
self.c.UserConfig.Git.Log.Order = value
return self.c.WithWaitingStatus(self.c.Tr.LcLoadingCommits, func() error {
return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS}})
})
}
}
return self.c.Menu(popup.CreateMenuOptions{
Title: self.c.Tr.LogMenuTitle,
Items: []*popup.MenuItem{
{
DisplayString: "topological (topo-order)",
OnPress: onPress("topo-order"),
},
{
DisplayString: "date-order",
OnPress: onPress("date-order"),
},
{
DisplayString: "author-date-order",
OnPress: onPress("author-date-order"),
},
},
})
},
},
},
})
}
func (self *LocalCommitsController) handleOpenCommitInBrowser(commit *models.Commit) error {
hostingServiceMgr := self.getHostingServiceMgr()
url, err := hostingServiceMgr.GetCommitURL(commit.Sha)
if err != nil {
return self.c.Error(err)
}
self.c.LogAction(self.c.Tr.Actions.OpenCommitInBrowser)
if err := self.os.OpenLink(url); err != nil {
return self.c.Error(err)
}
return nil
}
func (self *LocalCommitsController) checkSelected(callback func(*models.Commit) error) func() error {
return func() error {
commit := self.getSelectedLocalCommit()
if commit == nil {
return nil
}
return callback(commit)
}
}
func (self *LocalCommitsController) Context() types.Context {
return self.context
}

View File

@ -0,0 +1,70 @@
package controllers
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type MenuController struct {
c *ControllerCommon
context types.IListContext
getSelectedMenuItem func() *popup.MenuItem
}
var _ types.IController = &MenuController{}
func NewMenuController(
c *ControllerCommon,
context types.IListContext,
getSelectedMenuItem func() *popup.MenuItem,
) *MenuController {
return &MenuController{
c: c,
context: context,
getSelectedMenuItem: getSelectedMenuItem,
}
}
func (self *MenuController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
bindings := []*types.Binding{
{
Key: getKey(config.Universal.Select),
Handler: self.press,
},
{
Key: getKey(config.Universal.Confirm),
Handler: self.press,
},
{
Key: getKey(config.Universal.ConfirmAlt1),
Handler: self.press,
},
{
Key: gocui.MouseLeft,
Handler: func() error { return self.context.HandleClick(self.press) },
},
}
return append(bindings, self.context.Keybindings(getKey, config, guards)...)
}
func (self *MenuController) press() error {
selectedItem := self.getSelectedMenuItem()
if err := self.c.PopContext(); err != nil {
return err
}
if err := selectedItem.OnPress(); err != nil {
return err
}
return nil
}
func (self *MenuController) Context() types.Context {
return self.context
}

View File

@ -0,0 +1,204 @@
package controllers
import (
"sync"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type RemotesController struct {
c *ControllerCommon
context types.IListContext
git *commands.GitCommand
getSelectedRemote func() *models.Remote
setRemoteBranches func([]*models.RemoteBranch)
allContexts context.ContextTree
fetchMutex *sync.Mutex
}
var _ types.IController = &RemotesController{}
func NewRemotesController(
c *ControllerCommon,
context types.IListContext,
git *commands.GitCommand,
allContexts context.ContextTree,
getSelectedRemote func() *models.Remote,
setRemoteBranches func([]*models.RemoteBranch),
fetchMutex *sync.Mutex,
) *RemotesController {
return &RemotesController{
c: c,
git: git,
allContexts: allContexts,
context: context,
getSelectedRemote: getSelectedRemote,
setRemoteBranches: setRemoteBranches,
fetchMutex: fetchMutex,
}
}
func (self *RemotesController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
bindings := []*types.Binding{
{
Key: getKey(config.Universal.GoInto),
Handler: self.checkSelected(self.enter),
},
{
Key: gocui.MouseLeft,
Handler: func() error { return self.context.HandleClick(self.checkSelected(self.enter)) },
},
{
Key: getKey(config.Branches.FetchRemote),
Handler: self.checkSelected(self.fetch),
Description: self.c.Tr.LcFetchRemote,
},
{
Key: getKey(config.Universal.New),
Handler: self.add,
Description: self.c.Tr.LcAddNewRemote,
},
{
Key: getKey(config.Universal.Remove),
Handler: self.checkSelected(self.remove),
Description: self.c.Tr.LcRemoveRemote,
},
{
Key: getKey(config.Universal.Edit),
Handler: self.checkSelected(self.edit),
Description: self.c.Tr.LcEditRemote,
},
}
return append(bindings, self.context.Keybindings(getKey, config, guards)...)
}
func (self *RemotesController) enter(remote *models.Remote) error {
// naive implementation: get the branches from the remote and render them to the list, change the context
self.setRemoteBranches(remote.Branches)
newSelectedLine := 0
if len(remote.Branches) == 0 {
newSelectedLine = -1
}
self.allContexts.RemoteBranches.GetPanelState().SetSelectedLineIdx(newSelectedLine)
return self.c.PushContext(self.allContexts.RemoteBranches)
}
func (self *RemotesController) add() error {
return self.c.Prompt(popup.PromptOpts{
Title: self.c.Tr.LcNewRemoteName,
HandleConfirm: func(remoteName string) error {
return self.c.Prompt(popup.PromptOpts{
Title: self.c.Tr.LcNewRemoteUrl,
HandleConfirm: func(remoteUrl string) error {
self.c.LogAction(self.c.Tr.Actions.AddRemote)
if err := self.git.Remote.AddRemote(remoteName, remoteUrl); err != nil {
return err
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.REMOTES}})
},
})
},
})
}
func (self *RemotesController) remove(remote *models.Remote) error {
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.LcRemoveRemote,
Prompt: self.c.Tr.LcRemoveRemotePrompt + " '" + remote.Name + "'?",
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.RemoveRemote)
if err := self.git.Remote.RemoveRemote(remote.Name); err != nil {
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
},
})
}
func (self *RemotesController) edit(remote *models.Remote) error {
editNameMessage := utils.ResolvePlaceholderString(
self.c.Tr.LcEditRemoteName,
map[string]string{
"remoteName": remote.Name,
},
)
return self.c.Prompt(popup.PromptOpts{
Title: editNameMessage,
InitialContent: remote.Name,
HandleConfirm: func(updatedRemoteName string) error {
if updatedRemoteName != remote.Name {
self.c.LogAction(self.c.Tr.Actions.UpdateRemote)
if err := self.git.Remote.RenameRemote(remote.Name, updatedRemoteName); err != nil {
return self.c.Error(err)
}
}
editUrlMessage := utils.ResolvePlaceholderString(
self.c.Tr.LcEditRemoteUrl,
map[string]string{
"remoteName": updatedRemoteName,
},
)
urls := remote.Urls
url := ""
if len(urls) > 0 {
url = urls[0]
}
return self.c.Prompt(popup.PromptOpts{
Title: editUrlMessage,
InitialContent: url,
HandleConfirm: func(updatedRemoteUrl string) error {
self.c.LogAction(self.c.Tr.Actions.UpdateRemote)
if err := self.git.Remote.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
},
})
},
})
}
func (self *RemotesController) fetch(remote *models.Remote) error {
return self.c.WithWaitingStatus(self.c.Tr.FetchingRemoteStatus, func() error {
self.fetchMutex.Lock()
defer self.fetchMutex.Unlock()
err := self.git.Sync.FetchRemote(remote.Name)
if err != nil {
_ = self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
})
}
func (self *RemotesController) checkSelected(callback func(*models.Remote) error) func() error {
return func() error {
file := self.getSelectedRemote()
if file == nil {
return nil
}
return callback(file)
}
}
func (self *RemotesController) Context() types.Context {
return self.context
}

View File

@ -5,65 +5,57 @@ import (
"path/filepath"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// if Go let me do private struct embedding of structs with public fields (which it should)
// I would just do that. But alas.
type ControllerCommon struct {
*common.Common
IGuiCommon
type SubmodulesController struct {
c *ControllerCommon
context types.IListContext
git *commands.GitCommand
enterSubmodule func(submodule *models.SubmoduleConfig) error
getSelectedSubmodule func() *models.SubmoduleConfig
}
type SubmodulesController struct {
// I've said publicly that I'm against single-letter variable names but in this
// case I would actually prefer a _zero_ letter variable name in the form of
// struct embedding, but Go does not allow hiding public fields in an embedded struct
// to the client
c *ControllerCommon
enterSubmoduleFn func(submodule *models.SubmoduleConfig) error
getSelectedSubmodule func() *models.SubmoduleConfig
git *commands.GitCommand
submodules []*models.SubmoduleConfig
}
var _ types.IController = &SubmodulesController{}
func NewSubmodulesController(
c *ControllerCommon,
enterSubmoduleFn func(submodule *models.SubmoduleConfig) error,
context types.IListContext,
git *commands.GitCommand,
submodules []*models.SubmoduleConfig,
enterSubmodule func(submodule *models.SubmoduleConfig) error,
getSelectedSubmodule func() *models.SubmoduleConfig,
) *SubmodulesController {
return &SubmodulesController{
c: c,
enterSubmoduleFn: enterSubmoduleFn,
context: context,
git: git,
submodules: submodules,
enterSubmodule: enterSubmodule,
getSelectedSubmodule: getSelectedSubmodule,
}
}
func (self *SubmodulesController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig) []*types.Binding {
return []*types.Binding{
func (self *SubmodulesController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
bindings := []*types.Binding{
{
Key: getKey(config.Universal.GoInto),
Handler: self.forSubmodule(self.enter),
Handler: self.checkSelected(self.enter),
Description: self.c.Tr.LcEnterSubmodule,
},
{
Key: getKey(config.Universal.Remove),
Handler: self.forSubmodule(self.remove),
Handler: self.checkSelected(self.remove),
Description: self.c.Tr.LcRemoveSubmodule,
},
{
Key: getKey(config.Submodules.Update),
Handler: self.forSubmodule(self.update),
Handler: self.checkSelected(self.update),
Description: self.c.Tr.LcSubmoduleUpdate,
},
{
@ -73,12 +65,12 @@ func (self *SubmodulesController) Keybindings(getKey func(key string) interface{
},
{
Key: getKey(config.Universal.Edit),
Handler: self.forSubmodule(self.editURL),
Handler: self.checkSelected(self.editURL),
Description: self.c.Tr.LcEditSubmoduleUrl,
},
{
Key: getKey(config.Submodules.Init),
Handler: self.forSubmodule(self.init),
Handler: self.checkSelected(self.init),
Description: self.c.Tr.LcInitSubmodule,
},
{
@ -87,11 +79,17 @@ func (self *SubmodulesController) Keybindings(getKey func(key string) interface{
Description: self.c.Tr.LcViewBulkSubmoduleOptions,
OpensMenu: true,
},
{
Key: gocui.MouseLeft,
Handler: func() error { return self.context.HandleClick(self.checkSelected(self.enter)) },
},
}
return append(bindings, self.context.Keybindings(getKey, config, guards)...)
}
func (self *SubmodulesController) enter(submodule *models.SubmoduleConfig) error {
return self.enterSubmoduleFn(submodule)
return self.enterSubmodule(submodule)
}
func (self *SubmodulesController) add() error {
@ -231,7 +229,7 @@ func (self *SubmodulesController) remove(submodule *models.SubmoduleConfig) erro
})
}
func (self *SubmodulesController) forSubmodule(callback func(*models.SubmoduleConfig) error) func() error {
func (self *SubmodulesController) checkSelected(callback func(*models.SubmoduleConfig) error) func() error {
return func() error {
submodule := self.getSelectedSubmodule()
if submodule == nil {
@ -241,3 +239,7 @@ func (self *SubmodulesController) forSubmodule(callback func(*models.SubmoduleCo
return callback(submodule)
}
}
func (self *SubmodulesController) Context() types.Context {
return self.context
}

View File

@ -0,0 +1,253 @@
package controllers
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type SyncController struct {
// I've said publicly that I'm against single-letter variable names but in this
// case I would actually prefer a _zero_ letter variable name in the form of
// struct embedding, but Go does not allow hiding public fields in an embedded struct
// to the client
c *ControllerCommon
git *commands.GitCommand
getCheckedOutBranch func() *models.Branch
suggestionsHelper ISuggestionsHelper
getSuggestedRemote func() string
checkMergeOrRebase func(error) error
}
var _ types.IController = &SyncController{}
func NewSyncController(
c *ControllerCommon,
git *commands.GitCommand,
getCheckedOutBranch func() *models.Branch,
suggestionsHelper ISuggestionsHelper,
getSuggestedRemote func() string,
checkMergeOrRebase func(error) error,
) *SyncController {
return &SyncController{
c: c,
git: git,
getCheckedOutBranch: getCheckedOutBranch,
suggestionsHelper: suggestionsHelper,
getSuggestedRemote: getSuggestedRemote,
checkMergeOrRebase: checkMergeOrRebase,
}
}
func (self *SyncController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
bindings := []*types.Binding{
{
Key: getKey(config.Universal.PushFiles),
Handler: guards.NoPopupPanel(self.HandlePush),
Description: self.c.Tr.LcPush,
},
{
Key: getKey(config.Universal.PullFiles),
Handler: guards.NoPopupPanel(self.HandlePull),
Description: self.c.Tr.LcPull,
},
}
return bindings
}
func (self *SyncController) Context() types.Context {
return nil
}
func (self *SyncController) HandlePush() error {
return self.branchCheckedOut(self.push)()
}
func (self *SyncController) HandlePull() error {
return self.branchCheckedOut(self.pull)()
}
func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func() error {
return func() error {
currentBranch := self.getCheckedOutBranch()
if currentBranch == nil {
// need to wait for branches to refresh
return nil
}
return f(currentBranch)
}
}
func (self *SyncController) push(currentBranch *models.Branch) error {
// if we have pullables we'll ask if the user wants to force push
if currentBranch.IsTrackingRemote() {
opts := pushOpts{
force: false,
upstreamRemote: currentBranch.UpstreamRemote,
upstreamBranch: currentBranch.UpstreamBranch,
}
if currentBranch.HasCommitsToPull() {
opts.force = true
return self.requestToForcePush(opts)
} else {
return self.pushAux(opts)
}
} else {
if self.git.Config.GetPushToCurrent() {
return self.pushAux(pushOpts{setUpstream: true})
} else {
return self.promptForUpstream(currentBranch, func(upstream string) error {
var upstreamBranch, upstreamRemote string
split := strings.Split(upstream, " ")
if len(split) == 2 {
upstreamRemote = split[0]
upstreamBranch = split[1]
} else {
upstreamRemote = upstream
upstreamBranch = ""
}
return self.pushAux(pushOpts{
force: false,
upstreamRemote: upstreamRemote,
upstreamBranch: upstreamBranch,
setUpstream: true,
})
})
}
}
}
func (self *SyncController) pull(currentBranch *models.Branch) error {
action := self.c.Tr.Actions.Pull
// if we have no upstream branch we need to set that first
if !currentBranch.IsTrackingRemote() {
return self.promptForUpstream(currentBranch, func(upstream string) error {
var upstreamBranch, upstreamRemote string
split := strings.Split(upstream, " ")
if len(split) != 2 {
return self.c.ErrorMsg(self.c.Tr.InvalidUpstream)
}
upstreamRemote = split[0]
upstreamBranch = split[1]
if err := self.git.Branch.SetCurrentBranchUpstream(upstreamRemote, upstreamBranch); err != nil {
errorMessage := err.Error()
if strings.Contains(errorMessage, "does not exist") {
errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
}
return self.c.ErrorMsg(errorMessage)
}
return self.PullAux(PullFilesOptions{UpstreamRemote: upstreamRemote, UpstreamBranch: upstreamBranch, Action: action})
})
}
return self.PullAux(PullFilesOptions{UpstreamRemote: currentBranch.UpstreamRemote, UpstreamBranch: currentBranch.UpstreamBranch, Action: action})
}
func (self *SyncController) promptForUpstream(currentBranch *models.Branch, onConfirm func(string) error) error {
suggestedRemote := self.getSuggestedRemote()
return self.c.Prompt(popup.PromptOpts{
Title: self.c.Tr.EnterUpstream,
InitialContent: suggestedRemote + " " + currentBranch.Name,
FindSuggestionsFunc: self.suggestionsHelper.GetRemoteBranchesSuggestionsFunc(" "),
HandleConfirm: onConfirm,
})
}
type PullFilesOptions struct {
UpstreamRemote string
UpstreamBranch string
FastForwardOnly bool
Action string
}
func (self *SyncController) PullAux(opts PullFilesOptions) error {
return self.c.WithLoaderPanel(self.c.Tr.PullWait, func() error {
return self.pullWithLock(opts)
})
}
func (self *SyncController) pullWithLock(opts PullFilesOptions) error {
self.c.LogAction(opts.Action)
err := self.git.Sync.Pull(
git_commands.PullOptions{
RemoteName: opts.UpstreamRemote,
BranchName: opts.UpstreamBranch,
FastForwardOnly: opts.FastForwardOnly,
},
)
return self.checkMergeOrRebase(err)
}
type pushOpts struct {
force bool
upstreamRemote string
upstreamBranch string
setUpstream bool
}
func (self *SyncController) pushAux(opts pushOpts) error {
return self.c.WithLoaderPanel(self.c.Tr.PushWait, func() error {
self.c.LogAction(self.c.Tr.Actions.Push)
err := self.git.Sync.Push(git_commands.PushOpts{
Force: opts.force,
UpstreamRemote: opts.upstreamRemote,
UpstreamBranch: opts.upstreamBranch,
SetUpstream: opts.setUpstream,
})
if err != nil {
if !opts.force && strings.Contains(err.Error(), "Updates were rejected") {
forcePushDisabled := self.c.UserConfig.Git.DisableForcePushing
if forcePushDisabled {
_ = self.c.ErrorMsg(self.c.Tr.UpdatesRejectedAndForcePushDisabled)
return nil
}
_ = self.c.Ask(popup.AskOpts{
Title: self.c.Tr.ForcePush,
Prompt: self.c.Tr.ForcePushPrompt,
HandleConfirm: func() error {
newOpts := opts
newOpts.force = true
return self.pushAux(newOpts)
},
})
return nil
}
_ = self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
})
}
func (self *SyncController) requestToForcePush(opts pushOpts) error {
forcePushDisabled := self.c.UserConfig.Git.DisableForcePushing
if forcePushDisabled {
return self.c.ErrorMsg(self.c.Tr.ForcePushDisabled)
}
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.ForcePush,
Prompt: self.c.Tr.ForcePushPrompt,
HandleConfirm: func() error {
return self.pushAux(opts)
},
})
}

View File

@ -0,0 +1,229 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type TagsController struct {
c *ControllerCommon
context types.IListContext
git *commands.GitCommand
allContexts context.ContextTree
refHelper IRefHelper
suggestionsHelper ISuggestionsHelper
getSelectedTag func() *models.Tag
switchToSubCommitsContext func(string) error
}
var _ types.IController = &TagsController{}
func NewTagsController(
c *ControllerCommon,
context types.IListContext,
git *commands.GitCommand,
allContexts context.ContextTree,
refHelper IRefHelper,
suggestionsHelper ISuggestionsHelper,
getSelectedTag func() *models.Tag,
switchToSubCommitsContext func(string) error,
) *TagsController {
return &TagsController{
c: c,
context: context,
git: git,
allContexts: allContexts,
refHelper: refHelper,
suggestionsHelper: suggestionsHelper,
getSelectedTag: getSelectedTag,
switchToSubCommitsContext: switchToSubCommitsContext,
}
}
func (self *TagsController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
bindings := []*types.Binding{
{
Key: getKey(config.Universal.Select),
Handler: self.withSelectedTag(self.checkout),
Description: self.c.Tr.LcCheckout,
},
{
Key: getKey(config.Universal.Remove),
Handler: self.withSelectedTag(self.delete),
Description: self.c.Tr.LcDeleteTag,
},
{
Key: getKey(config.Branches.PushTag),
Handler: self.withSelectedTag(self.push),
Description: self.c.Tr.LcPushTag,
},
{
Key: getKey(config.Universal.New),
Handler: self.create,
Description: self.c.Tr.LcCreateTag,
},
{
Key: getKey(config.Commits.ViewResetOptions),
Handler: self.withSelectedTag(self.createResetMenu),
Description: self.c.Tr.LcViewResetOptions,
OpensMenu: true,
},
{
Key: getKey(config.Universal.GoInto),
Handler: self.withSelectedTag(self.enter),
Description: self.c.Tr.LcViewCommits,
},
}
return append(bindings, self.context.Keybindings(getKey, config, guards)...)
}
func (self *TagsController) checkout(tag *models.Tag) error {
self.c.LogAction(self.c.Tr.Actions.CheckoutTag)
if err := self.refHelper.CheckoutRef(tag.Name, types.CheckoutRefOptions{}); err != nil {
return err
}
return self.c.PushContext(self.allContexts.Branches)
}
func (self *TagsController) enter(tag *models.Tag) error {
return self.switchToSubCommitsContext(tag.Name)
}
func (self *TagsController) delete(tag *models.Tag) error {
prompt := utils.ResolvePlaceholderString(
self.c.Tr.DeleteTagPrompt,
map[string]string{
"tagName": tag.Name,
},
)
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.DeleteTagTitle,
Prompt: prompt,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.DeleteTag)
if err := self.git.Tag.Delete(tag.Name); err != nil {
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}})
},
})
}
func (self *TagsController) push(tag *models.Tag) error {
title := utils.ResolvePlaceholderString(
self.c.Tr.PushTagTitle,
map[string]string{
"tagName": tag.Name,
},
)
return self.c.Prompt(popup.PromptOpts{
Title: title,
InitialContent: "origin",
FindSuggestionsFunc: self.suggestionsHelper.GetRemoteSuggestionsFunc(),
HandleConfirm: func(response string) error {
return self.c.WithWaitingStatus(self.c.Tr.PushingTagStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.PushTag)
err := self.git.Tag.Push(response, tag.Name)
if err != nil {
_ = self.c.Error(err)
}
return nil
})
},
})
}
func (self *TagsController) createResetMenu(tag *models.Tag) error {
return self.refHelper.CreateGitResetMenu(tag.Name)
}
func (self *TagsController) CreateTagMenu(commitSha string) error {
return self.c.Menu(popup.CreateMenuOptions{
Title: self.c.Tr.TagMenuTitle,
Items: []*popup.MenuItem{
{
DisplayString: self.c.Tr.LcLightweightTag,
OnPress: func() error {
return self.handleCreateLightweightTag(commitSha)
},
},
{
DisplayString: self.c.Tr.LcAnnotatedTag,
OnPress: func() error {
return self.handleCreateAnnotatedTag(commitSha)
},
},
},
})
}
func (self *TagsController) afterTagCreate() error {
self.context.GetPanelState().SetSelectedLineIdx(0)
return self.c.Refresh(types.RefreshOptions{
Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS},
})
}
func (self *TagsController) handleCreateAnnotatedTag(commitSha string) error {
return self.c.Prompt(popup.PromptOpts{
Title: self.c.Tr.TagNameTitle,
HandleConfirm: func(tagName string) error {
return self.c.Prompt(popup.PromptOpts{
Title: self.c.Tr.TagMessageTitle,
HandleConfirm: func(msg string) error {
self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag)
if err := self.git.Tag.CreateAnnotated(tagName, commitSha, msg); err != nil {
return self.c.Error(err)
}
return self.afterTagCreate()
},
})
},
})
}
func (self *TagsController) handleCreateLightweightTag(commitSha string) error {
return self.c.Prompt(popup.PromptOpts{
Title: self.c.Tr.TagNameTitle,
HandleConfirm: func(tagName string) error {
self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag)
if err := self.git.Tag.CreateLightweight(tagName, commitSha); err != nil {
return self.c.Error(err)
}
return self.afterTagCreate()
},
})
}
func (self *TagsController) create() error {
// leaving commit SHA blank so that we're just creating the tag for the current commit
return self.CreateTagMenu("")
}
func (self *TagsController) withSelectedTag(f func(tag *models.Tag) error) func() error {
return func() error {
tag := self.getSelectedTag()
if tag == nil {
return nil
}
return f(tag)
}
}
func (self *TagsController) Context() types.Context {
return self.context
}

View File

@ -1,6 +1,9 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@ -8,6 +11,54 @@ import (
type IGuiCommon interface {
popup.IPopupHandler
LogAction(string)
LogAction(action string)
LogCommand(cmdStr string, isCommandLine bool)
// we call this when we want to refetch some models and render the result. Internally calls PostRefreshUpdate
Refresh(types.RefreshOptions) error
// we call this when we've changed something in the view model but not the actual model,
// e.g. expanding or collapsing a folder in a file view. Calling 'Refresh' in this
// case would be overkill, although refresh will internally call 'PostRefreshUpdate'
PostRefreshUpdate(types.Context) error
RunSubprocessAndRefresh(oscommands.ICmdObj) error
PushContext(context types.Context, opts ...types.OnFocusOpts) error
PopContext() error
GetAppState() *config.AppState
SaveAppState() error
}
type IRefHelper interface {
CheckoutRef(ref string, options types.CheckoutRefOptions) error
CreateGitResetMenu(ref string) error
ResetToRef(ref string, strength string, envVars []string) error
}
type ISuggestionsHelper interface {
GetRemoteSuggestionsFunc() func(string) []*types.Suggestion
GetBranchNameSuggestionsFunc() func(string) []*types.Suggestion
GetFilePathSuggestionsFunc() func(string) []*types.Suggestion
GetRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion
GetRefsSuggestionsFunc() func(string) []*types.Suggestion
GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion
}
type IFileHelper interface {
EditFile(filename string) error
EditFileAtLine(filename string, lineNumber int) error
OpenFile(filename string) error
}
type IWorkingTreeHelper interface {
AnyStagedFiles() bool
AnyTrackedFiles() bool
IsWorkingTreeDirty() bool
FileForSubmodule(submodule *models.SubmoduleConfig) *models.File
}
// all fields mandatory (except `CanRebase` because it's boolean)
type SwitchToCommitFilesContextOpts struct {
RefName string
CanRebase bool
Context types.Context
WindowName string
}

View File

@ -1,7 +1,10 @@
package gui
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
@ -17,6 +20,36 @@ import (
// the reflog will read UUCBA, and when I read the first two undos, I know to skip the following
// two user actions, meaning we end up undoing reflog entry C. Redoing works in a similar way.
type UndoController struct {
c *ControllerCommon
git *commands.GitCommand
refHelper IRefHelper
workingTreeHelper IWorkingTreeHelper
getFilteredReflogCommits func() []*models.Commit
}
var _ types.IController = &UndoController{}
func NewUndoController(
c *ControllerCommon,
git *commands.GitCommand,
refHelper IRefHelper,
workingTreeHelper IWorkingTreeHelper,
getFilteredReflogCommits func() []*models.Commit,
) *UndoController {
return &UndoController{
c: c,
git: git,
refHelper: refHelper,
workingTreeHelper: workingTreeHelper,
getFilteredReflogCommits: getFilteredReflogCommits,
}
}
type ReflogActionKind int
const (
@ -32,15 +65,113 @@ type reflogAction struct {
to string
}
func (self *UndoController) Keybindings(
getKey func(key string) interface{},
config config.KeybindingConfig,
guards types.KeybindingGuards,
) []*types.Binding {
bindings := []*types.Binding{
{
Key: getKey(config.Universal.Undo),
Handler: self.reflogUndo,
Description: self.c.Tr.LcUndoReflog,
},
{
Key: getKey(config.Universal.Redo),
Handler: self.reflogRedo,
Description: self.c.Tr.LcRedoReflog,
},
}
return bindings
}
func (self *UndoController) Context() types.Context {
return nil
}
func (self *UndoController) reflogUndo() error {
undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"}
undoingStatus := self.c.Tr.UndoingStatus
if self.git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
return self.c.ErrorMsg(self.c.Tr.LcCantUndoWhileRebasing)
}
return self.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
if counter != 0 {
return false, nil
}
switch action.kind {
case COMMIT, REBASE:
self.c.LogAction(self.c.Tr.Actions.Undo)
return true, self.hardResetWithAutoStash(action.from, hardResetOptions{
EnvVars: undoEnvVars,
WaitingStatus: undoingStatus,
})
case CHECKOUT:
self.c.LogAction(self.c.Tr.Actions.Undo)
return true, self.refHelper.CheckoutRef(action.from, types.CheckoutRefOptions{
EnvVars: undoEnvVars,
WaitingStatus: undoingStatus,
})
case CURRENT_REBASE:
// do nothing
}
self.c.Log.Error("didn't match on the user action when trying to undo")
return true, nil
})
}
func (self *UndoController) reflogRedo() error {
redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"}
redoingStatus := self.c.Tr.RedoingStatus
if self.git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
return self.c.ErrorMsg(self.c.Tr.LcCantRedoWhileRebasing)
}
return self.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
// if we're redoing and the counter is zero, we just return
if counter == 0 {
return true, nil
} else if counter > 1 {
return false, nil
}
switch action.kind {
case COMMIT, REBASE:
self.c.LogAction(self.c.Tr.Actions.Redo)
return true, self.hardResetWithAutoStash(action.to, hardResetOptions{
EnvVars: redoEnvVars,
WaitingStatus: redoingStatus,
})
case CHECKOUT:
self.c.LogAction(self.c.Tr.Actions.Redo)
return true, self.refHelper.CheckoutRef(action.to, types.CheckoutRefOptions{
EnvVars: redoEnvVars,
WaitingStatus: redoingStatus,
})
case CURRENT_REBASE:
// do nothing
}
self.c.Log.Error("didn't match on the user action when trying to redo")
return true, nil
})
}
// Here we're going through the reflog and maintaining a counter that represents how many
// undos/redos/user actions we've seen. when we hit a user action we call the callback specifying
// what the counter is up to and the nature of the action.
// If we find ourselves mid-rebase, we just return because undo/redo mid rebase
// requires knowledge of previous TODO file states, which you can't just get from the reflog.
// Though we might support this later, hence the use of the CURRENT_REBASE action kind.
func (gui *Gui) parseReflogForActions(onUserAction func(counter int, action reflogAction) (bool, error)) error {
func (self *UndoController) parseReflogForActions(onUserAction func(counter int, action reflogAction) (bool, error)) error {
counter := 0
reflogCommits := gui.State.FilteredReflogCommits
reflogCommits := self.getFilteredReflogCommits()
rebaseFinishCommitSha := ""
var action *reflogAction
for reflogCommitIdx, reflogCommit := range reflogCommits {
@ -86,115 +217,42 @@ func (gui *Gui) parseReflogForActions(onUserAction func(counter int, action refl
return nil
}
func (gui *Gui) reflogUndo() error {
undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"}
undoingStatus := gui.Tr.UndoingStatus
if gui.Git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
return gui.PopupHandler.ErrorMsg(gui.Tr.LcCantUndoWhileRebasing)
}
return gui.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
if counter != 0 {
return false, nil
}
switch action.kind {
case COMMIT, REBASE:
gui.logAction(gui.Tr.Actions.Undo)
return true, gui.handleHardResetWithAutoStash(action.from, handleHardResetWithAutoStashOptions{
EnvVars: undoEnvVars,
WaitingStatus: undoingStatus,
})
case CHECKOUT:
gui.logAction(gui.Tr.Actions.Undo)
return true, gui.handleCheckoutRef(action.from, handleCheckoutRefOptions{
EnvVars: undoEnvVars,
WaitingStatus: undoingStatus,
})
case CURRENT_REBASE:
// do nothing
}
gui.Log.Error("didn't match on the user action when trying to undo")
return true, nil
})
}
func (gui *Gui) reflogRedo() error {
redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"}
redoingStatus := gui.Tr.RedoingStatus
if gui.Git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
return gui.PopupHandler.ErrorMsg(gui.Tr.LcCantRedoWhileRebasing)
}
return gui.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
// if we're redoing and the counter is zero, we just return
if counter == 0 {
return true, nil
} else if counter > 1 {
return false, nil
}
switch action.kind {
case COMMIT, REBASE:
gui.logAction(gui.Tr.Actions.Redo)
return true, gui.handleHardResetWithAutoStash(action.to, handleHardResetWithAutoStashOptions{
EnvVars: redoEnvVars,
WaitingStatus: redoingStatus,
})
case CHECKOUT:
gui.logAction(gui.Tr.Actions.Redo)
return true, gui.handleCheckoutRef(action.to, handleCheckoutRefOptions{
EnvVars: redoEnvVars,
WaitingStatus: redoingStatus,
})
case CURRENT_REBASE:
// do nothing
}
gui.Log.Error("didn't match on the user action when trying to redo")
return true, nil
})
}
type handleHardResetWithAutoStashOptions struct {
type hardResetOptions struct {
WaitingStatus string
EnvVars []string
}
// only to be used in the undo flow for now
func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHardResetWithAutoStashOptions) error {
// only to be used in the undo flow for now (does an autostash)
func (self *UndoController) hardResetWithAutoStash(commitSha string, options hardResetOptions) error {
reset := func() error {
if err := gui.resetToRef(commitSha, "hard", options.EnvVars); err != nil {
return gui.PopupHandler.Error(err)
if err := self.refHelper.ResetToRef(commitSha, "hard", options.EnvVars); err != nil {
return self.c.Error(err)
}
return nil
}
// if we have any modified tracked files we need to ask the user if they want us to stash for them
dirtyWorkingTree := len(gui.trackedFiles()) > 0 || len(gui.stagedFiles()) > 0
dirtyWorkingTree := self.workingTreeHelper.IsWorkingTreeDirty()
if dirtyWorkingTree {
// offer to autostash changes
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.AutoStashTitle,
Prompt: gui.Tr.AutoStashPrompt,
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.AutoStashTitle,
Prompt: self.c.Tr.AutoStashPrompt,
HandleConfirm: func() error {
return gui.PopupHandler.WithWaitingStatus(options.WaitingStatus, func() error {
if err := gui.Git.Stash.Save(gui.Tr.StashPrefix + commitSha); err != nil {
return gui.PopupHandler.Error(err)
return self.c.WithWaitingStatus(options.WaitingStatus, func() error {
if err := self.git.Stash.Save(self.c.Tr.StashPrefix + commitSha); err != nil {
return self.c.Error(err)
}
if err := reset(); err != nil {
return err
}
err := gui.Git.Stash.Pop(0)
if err := gui.refreshSidePanels(types.RefreshOptions{}); err != nil {
err := self.git.Stash.Pop(0)
if err := self.c.Refresh(types.RefreshOptions{}); err != nil {
return err
}
if err != nil {
return gui.PopupHandler.Error(err)
return self.c.Error(err)
}
return nil
})
@ -202,7 +260,7 @@ func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHar
})
}
return gui.PopupHandler.WithWaitingStatus(options.WaitingStatus, func() error {
return self.c.WithWaitingStatus(options.WaitingStatus, func() error {
return reset()
})
}

View File

@ -17,21 +17,20 @@ func (gui *Gui) promptUserForCredential(passOrUname oscommands.CredentialType) s
credentialsView := gui.Views.Credentials
switch passOrUname {
case oscommands.Username:
credentialsView.Title = gui.Tr.CredentialsUsername
credentialsView.Title = gui.c.Tr.CredentialsUsername
credentialsView.Mask = 0
case oscommands.Password:
credentialsView.Title = gui.Tr.CredentialsPassword
credentialsView.Title = gui.c.Tr.CredentialsPassword
credentialsView.Mask = '*'
case oscommands.Passphrase:
credentialsView.Title = gui.Tr.CredentialsPassphrase
credentialsView.Title = gui.c.Tr.CredentialsPassphrase
credentialsView.Mask = '*'
}
if err := gui.pushContext(gui.State.Contexts.Credentials); err != nil {
if err := gui.c.PushContext(gui.State.Contexts.Credentials); err != nil {
return err
}
gui.RenderCommitLength()
return nil
})
@ -49,7 +48,7 @@ func (gui *Gui) handleSubmitCredential() error {
return err
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
}
func (gui *Gui) handleCloseCredentialsView() error {
@ -59,10 +58,10 @@ func (gui *Gui) handleCloseCredentialsView() error {
}
func (gui *Gui) handleAskFocused() error {
keybindingConfig := gui.UserConfig.Keybinding
keybindingConfig := gui.c.UserConfig.Keybinding
message := utils.ResolvePlaceholderString(
gui.Tr.CloseConfirm,
gui.c.Tr.CloseConfirm,
map[string]string{
"keyBindClose": gui.getKeyDisplay(keybindingConfig.Universal.Return),
"keyBindConfirm": gui.getKeyDisplay(keybindingConfig.Universal.Confirm),

View File

@ -54,7 +54,7 @@ func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (s
SelectedCommitFile: gui.getSelectedCommitFile(),
SelectedCommitFilePath: gui.getSelectedCommitFilePath(),
SelectedSubCommit: gui.getSelectedSubCommit(),
CheckedOutBranch: gui.currentBranch(),
CheckedOutBranch: gui.getCheckedOutBranch(),
PromptResponses: promptResponses,
}
@ -64,15 +64,15 @@ func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (s
func (gui *Gui) inputPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
return gui.PopupHandler.Prompt(popup.PromptOpts{
return gui.c.Prompt(popup.PromptOpts{
Title: title,
InitialContent: initialValue,
HandleConfirm: func(str string) error {
@ -95,17 +95,17 @@ func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses []
}
name, err := gui.resolveTemplate(nameTemplate, promptResponses)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
description, err := gui.resolveTemplate(option.Description, promptResponses)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
value, err := gui.resolveTemplate(option.Value, promptResponses)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
menuItems[i] = &popup.MenuItem{
@ -119,30 +119,30 @@ func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses []
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: title, Items: menuItems})
return gui.c.Menu(popup.CreateMenuOptions{Title: title, Items: menuItems})
}
func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, labelFormat string) ([]commandMenuEntry, error) {
reg, err := regexp.Compile(filter)
if err != nil {
return nil, gui.PopupHandler.Error(errors.New("unable to parse filter regex, error: " + err.Error()))
return nil, gui.c.Error(errors.New("unable to parse filter regex, error: " + err.Error()))
}
buff := bytes.NewBuffer(nil)
valueTemp, err := template.New("format").Parse(valueFormat)
if err != nil {
return nil, gui.PopupHandler.Error(errors.New("unable to parse value format, error: " + err.Error()))
return nil, gui.c.Error(errors.New("unable to parse value format, error: " + err.Error()))
}
colorFuncMap := style.TemplateFuncMapAddColors(template.FuncMap{})
descTemp, err := template.New("format").Funcs(colorFuncMap).Parse(labelFormat)
if err != nil {
return nil, gui.PopupHandler.Error(errors.New("unable to parse label format, error: " + err.Error()))
return nil, gui.c.Error(errors.New("unable to parse label format, error: " + err.Error()))
}
candidates := []commandMenuEntry{}
@ -167,7 +167,7 @@ func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, label
err = valueTemp.Execute(buff, tmplData)
if err != nil {
return candidates, gui.PopupHandler.Error(err)
return candidates, gui.c.Error(err)
}
entry := commandMenuEntry{
value: strings.TrimSpace(buff.String()),
@ -177,7 +177,7 @@ func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, label
buff.Reset()
err = descTemp.Execute(buff, tmplData)
if err != nil {
return candidates, gui.PopupHandler.Error(err)
return candidates, gui.c.Error(err)
}
entry.label = strings.TrimSpace(buff.String())
} else {
@ -195,25 +195,25 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR
// Collect cmd to run from config
cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
// Collect Filter regexp
filter, err := gui.resolveTemplate(prompt.Filter, promptResponses)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
// Run and save output
message, err := gui.Git.Custom.RunWithOutput(cmdStr)
message, err := gui.git.Custom.RunWithOutput(cmdStr)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
// Need to make a menu out of what the cmd has displayed
candidates, err := gui.GenerateMenuCandidates(message, filter, prompt.ValueFormat, prompt.LabelFormat)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
menuItems := make([]*popup.MenuItem, len(candidates))
@ -230,10 +230,10 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: title, Items: menuItems})
return gui.c.Menu(popup.CreateMenuOptions{Title: title, Items: menuItems})
}
func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand) func() error {
@ -243,7 +243,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
f := func() error {
cmdStr, err := gui.resolveTemplate(customCommand.Command, promptResponses)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
if customCommand.Subprocess {
@ -252,19 +252,19 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
loadingText := customCommand.LoadingText
if loadingText == "" {
loadingText = gui.Tr.LcRunningCustomCommandStatus
loadingText = gui.c.Tr.LcRunningCustomCommandStatus
}
return gui.PopupHandler.WithWaitingStatus(loadingText, func() error {
gui.logAction(gui.Tr.Actions.CustomCommand)
return gui.c.WithWaitingStatus(loadingText, func() error {
gui.c.LogAction(gui.c.Tr.Actions.CustomCommand)
cmdObj := gui.OSCommand.Cmd.NewShell(cmdStr)
if customCommand.Stream {
cmdObj.StreamOutput()
}
err := cmdObj.Run()
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{})
return gui.c.Refresh(types.RefreshOptions{})
})
}
@ -293,7 +293,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
return gui.menuPromptFromCommand(prompt, promptResponses, idx, wrappedF)
}
default:
return gui.PopupHandler.ErrorMsg("custom command prompt must have a type of 'input', 'menu' or 'menuFromCommand'")
return gui.c.ErrorMsg("custom command prompt must have a type of 'input', 'menu' or 'menuFromCommand'")
}
}
@ -304,7 +304,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
func (gui *Gui) GetCustomCommandKeybindings() []*types.Binding {
bindings := []*types.Binding{}
customCommands := gui.UserConfig.CustomCommands
customCommands := gui.c.UserConfig.CustomCommands
for _, customCommand := range customCommands {
var viewName string
@ -315,11 +315,11 @@ func (gui *Gui) GetCustomCommandKeybindings() []*types.Binding {
case "":
log.Fatalf("Error parsing custom command keybindings: context not provided (use context: 'global' for the global context). Key: %s, Command: %s", customCommand.Key, customCommand.Command)
default:
context, ok := gui.contextForContextKey(ContextKey(customCommand.Context))
context, ok := gui.contextForContextKey(types.ContextKey(customCommand.Context))
// stupid golang making me build an array of strings for this.
allContextKeyStrings := make([]string, len(allContextKeys))
for i := range allContextKeys {
allContextKeyStrings[i] = string(allContextKeys[i])
allContextKeyStrings := make([]string, len(AllContextKeys))
for i := range AllContextKeys {
allContextKeyStrings[i] = string(AllContextKeys[i])
}
if !ok {
log.Fatalf("Error when setting custom command keybindings: unknown context: %s. Key: %s, Command: %s.\nPermitted contexts: %s", customCommand.Context, customCommand.Key, customCommand.Command, strings.Join(allContextKeyStrings, ", "))

View File

@ -2,9 +2,11 @@ package gui
import (
"errors"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
var CONTEXT_KEYS_SHOWING_DIFFS = []ContextKey{
var CONTEXT_KEYS_SHOWING_DIFFS = []types.ContextKey{
FILES_CONTEXT_KEY,
COMMIT_FILES_CONTEXT_KEY,
STASH_CONTEXT_KEY,
@ -28,10 +30,10 @@ func isShowingDiff(gui *Gui) bool {
func (gui *Gui) IncreaseContextInDiffView() error {
if isShowingDiff(gui) {
if err := gui.CheckCanChangeContext(); err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
gui.UserConfig.Git.DiffContextSize = gui.UserConfig.Git.DiffContextSize + 1
gui.c.UserConfig.Git.DiffContextSize = gui.c.UserConfig.Git.DiffContextSize + 1
return gui.handleDiffContextSizeChange()
}
@ -39,14 +41,14 @@ func (gui *Gui) IncreaseContextInDiffView() error {
}
func (gui *Gui) DecreaseContextInDiffView() error {
old_size := gui.UserConfig.Git.DiffContextSize
old_size := gui.c.UserConfig.Git.DiffContextSize
if isShowingDiff(gui) && old_size > 1 {
if err := gui.CheckCanChangeContext(); err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
gui.UserConfig.Git.DiffContextSize = old_size - 1
gui.c.UserConfig.Git.DiffContextSize = old_size - 1
return gui.handleDiffContextSizeChange()
}
@ -67,8 +69,8 @@ func (gui *Gui) handleDiffContextSizeChange() error {
}
func (gui *Gui) CheckCanChangeContext() error {
if gui.Git.Patch.PatchManager.Active() {
return errors.New(gui.Tr.CantChangeContextSizeError)
if gui.git.Patch.PatchManager.Active() {
return errors.New(gui.c.Tr.CantChangeContextSizeError)
}
return nil

View File

@ -29,7 +29,7 @@ func setupGuiForTest(gui *Gui) {
gui.Views.Main, _ = gui.prepareView("main")
gui.Views.Secondary, _ = gui.prepareView("secondary")
gui.Views.Options, _ = gui.prepareView("options")
gui.Git.Patch.PatchManager = &patch.PatchManager{}
gui.git.Patch.PatchManager = &patch.PatchManager{}
_, _ = gui.refreshLineByLinePanel(diffForTest, "", false, 11)
}
@ -48,12 +48,12 @@ func TestIncreasesContextInDiffViewByOneInContextWithDiff(t *testing.T) {
gui := NewDummyGui()
context := c(gui)
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 1
_ = gui.pushContext(context)
gui.c.UserConfig.Git.DiffContextSize = 1
_ = gui.c.PushContext(context)
_ = gui.IncreaseContextInDiffView()
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
assert.Equal(t, 2, gui.c.UserConfig.Git.DiffContextSize, string(context.GetKey()))
}
}
@ -76,12 +76,12 @@ func TestDoesntIncreaseContextInDiffViewInContextWithoutDiff(t *testing.T) {
gui := NewDummyGui()
context := c(gui)
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 1
_ = gui.pushContext(context)
gui.c.UserConfig.Git.DiffContextSize = 1
_ = gui.c.PushContext(context)
_ = gui.IncreaseContextInDiffView()
assert.Equal(t, 1, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
assert.Equal(t, 1, gui.c.UserConfig.Git.DiffContextSize, string(context.GetKey()))
}
}
@ -100,12 +100,12 @@ func TestDecreasesContextInDiffViewByOneInContextWithDiff(t *testing.T) {
gui := NewDummyGui()
context := c(gui)
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContext(context)
gui.c.UserConfig.Git.DiffContextSize = 2
_ = gui.c.PushContext(context)
_ = gui.DecreaseContextInDiffView()
assert.Equal(t, 1, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
assert.Equal(t, 1, gui.c.UserConfig.Git.DiffContextSize, string(context.GetKey()))
}
}
@ -128,26 +128,26 @@ func TestDoesntDecreaseContextInDiffViewInContextWithoutDiff(t *testing.T) {
gui := NewDummyGui()
context := c(gui)
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContext(context)
gui.c.UserConfig.Git.DiffContextSize = 2
_ = gui.c.PushContext(context)
_ = gui.DecreaseContextInDiffView()
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
assert.Equal(t, 2, gui.c.UserConfig.Git.DiffContextSize, string(context.GetKey()))
}
}
func TestDoesntIncreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) {
gui := NewDummyGui()
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContext(gui.State.Contexts.CommitFiles)
gui.Git.Patch.PatchManager.Start("from", "to", false, false)
gui.c.UserConfig.Git.DiffContextSize = 2
_ = gui.c.PushContext(gui.State.Contexts.CommitFiles)
gui.git.Patch.PatchManager.Start("from", "to", false, false)
errorCount := 0
gui.PopupHandler = &popup.TestPopupHandler{
OnErrorMsg: func(message string) error {
assert.Equal(t, gui.Tr.CantChangeContextSizeError, message)
assert.Equal(t, gui.c.Tr.CantChangeContextSizeError, message)
errorCount += 1
return nil
},
@ -156,20 +156,20 @@ func TestDoesntIncreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *test
_ = gui.IncreaseContextInDiffView()
assert.Equal(t, 1, errorCount)
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize)
assert.Equal(t, 2, gui.c.UserConfig.Git.DiffContextSize)
}
func TestDoesntDecreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) {
gui := NewDummyGui()
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContext(gui.State.Contexts.CommitFiles)
gui.Git.Patch.PatchManager.Start("from", "to", false, false)
gui.c.UserConfig.Git.DiffContextSize = 2
_ = gui.c.PushContext(gui.State.Contexts.CommitFiles)
gui.git.Patch.PatchManager.Start("from", "to", false, false)
errorCount := 0
gui.PopupHandler = &popup.TestPopupHandler{
OnErrorMsg: func(message string) error {
assert.Equal(t, gui.Tr.CantChangeContextSizeError, message)
assert.Equal(t, gui.c.Tr.CantChangeContextSizeError, message)
errorCount += 1
return nil
},
@ -177,15 +177,15 @@ func TestDoesntDecreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *test
_ = gui.DecreaseContextInDiffView()
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize)
assert.Equal(t, 2, gui.c.UserConfig.Git.DiffContextSize)
}
func TestDecreasesContextInDiffViewNoFurtherThanOne(t *testing.T) {
gui := NewDummyGui()
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 1
gui.c.UserConfig.Git.DiffContextSize = 1
_ = gui.DecreaseContextInDiffView()
assert.Equal(t, 1, gui.UserConfig.Git.DiffContextSize)
assert.Equal(t, 1, gui.c.UserConfig.Git.DiffContextSize)
}

View File

@ -11,7 +11,7 @@ import (
func (gui *Gui) exitDiffMode() error {
gui.State.Modes.Diffing = diffing.New()
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
}
func (gui *Gui) renderDiff() error {
@ -112,11 +112,11 @@ func (gui *Gui) handleCreateDiffingMenuPanel() error {
name := name
menuItems = append(menuItems, []*popup.MenuItem{
{
DisplayString: fmt.Sprintf("%s %s", gui.Tr.LcDiff, name),
DisplayString: fmt.Sprintf("%s %s", gui.c.Tr.LcDiff, name),
OnPress: func() error {
gui.State.Modes.Diffing.Ref = name
// can scope this down based on current view but too lazy right now
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
},
},
}...)
@ -124,14 +124,14 @@ func (gui *Gui) handleCreateDiffingMenuPanel() error {
menuItems = append(menuItems, []*popup.MenuItem{
{
DisplayString: gui.Tr.LcEnterRefToDiff,
DisplayString: gui.c.Tr.LcEnterRefToDiff,
OnPress: func() error {
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: gui.Tr.LcEnteRefName,
FindSuggestionsFunc: gui.getRefsSuggestionsFunc(),
return gui.c.Prompt(popup.PromptOpts{
Title: gui.c.Tr.LcEnteRefName,
FindSuggestionsFunc: gui.suggestionsHelper.GetRefsSuggestionsFunc(),
HandleConfirm: func(response string) error {
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response)
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
},
})
},
@ -141,21 +141,21 @@ func (gui *Gui) handleCreateDiffingMenuPanel() error {
if gui.State.Modes.Diffing.Active() {
menuItems = append(menuItems, []*popup.MenuItem{
{
DisplayString: gui.Tr.LcSwapDiff,
DisplayString: gui.c.Tr.LcSwapDiff,
OnPress: func() error {
gui.State.Modes.Diffing.Reverse = !gui.State.Modes.Diffing.Reverse
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
},
},
{
DisplayString: gui.Tr.LcExitDiffMode,
DisplayString: gui.c.Tr.LcExitDiffMode,
OnPress: func() error {
gui.State.Modes.Diffing = diffing.New()
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
},
},
}...)
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: gui.Tr.DiffingMenuTitle, Items: menuItems})
return gui.c.Menu(popup.CreateMenuOptions{Title: gui.c.Tr.DiffingMenuTitle, Items: menuItems})
}

View File

@ -15,27 +15,27 @@ func (gui *Gui) handleCreateDiscardMenu() error {
if node.File == nil {
menuItems = []*popup.MenuItem{
{
DisplayString: gui.Tr.LcDiscardAllChanges,
DisplayString: gui.c.Tr.LcDiscardAllChanges,
OnPress: func() error {
gui.logAction(gui.Tr.Actions.DiscardAllChangesInDirectory)
if err := gui.Git.WorkingTree.DiscardAllDirChanges(node); err != nil {
return gui.PopupHandler.Error(err)
gui.c.LogAction(gui.c.Tr.Actions.DiscardAllChangesInDirectory)
if err := gui.git.WorkingTree.DiscardAllDirChanges(node); err != nil {
return gui.c.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
},
},
}
if node.GetHasStagedChanges() && node.GetHasUnstagedChanges() {
menuItems = append(menuItems, &popup.MenuItem{
DisplayString: gui.Tr.LcDiscardUnstagedChanges,
DisplayString: gui.c.Tr.LcDiscardUnstagedChanges,
OnPress: func() error {
gui.logAction(gui.Tr.Actions.DiscardUnstagedChangesInDirectory)
if err := gui.Git.WorkingTree.DiscardUnstagedDirChanges(node); err != nil {
return gui.PopupHandler.Error(err)
gui.c.LogAction(gui.c.Tr.Actions.DiscardUnstagedChangesInDirectory)
if err := gui.git.WorkingTree.DiscardUnstagedDirChanges(node); err != nil {
return gui.c.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
},
})
}
@ -48,41 +48,41 @@ func (gui *Gui) handleCreateDiscardMenu() error {
menuItems = []*popup.MenuItem{
{
DisplayString: gui.Tr.LcSubmoduleStashAndReset,
DisplayString: gui.c.Tr.LcSubmoduleStashAndReset,
OnPress: func() error {
return gui.resetSubmodule(submodule)
return gui.Controllers.Files.ResetSubmodule(submodule)
},
},
}
} else {
menuItems = []*popup.MenuItem{
{
DisplayString: gui.Tr.LcDiscardAllChanges,
DisplayString: gui.c.Tr.LcDiscardAllChanges,
OnPress: func() error {
gui.logAction(gui.Tr.Actions.DiscardAllChangesInFile)
if err := gui.Git.WorkingTree.DiscardAllFileChanges(file); err != nil {
return gui.PopupHandler.Error(err)
gui.c.LogAction(gui.c.Tr.Actions.DiscardAllChangesInFile)
if err := gui.git.WorkingTree.DiscardAllFileChanges(file); err != nil {
return gui.c.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
},
},
}
if file.HasStagedChanges && file.HasUnstagedChanges {
menuItems = append(menuItems, &popup.MenuItem{
DisplayString: gui.Tr.LcDiscardUnstagedChanges,
DisplayString: gui.c.Tr.LcDiscardUnstagedChanges,
OnPress: func() error {
gui.logAction(gui.Tr.Actions.DiscardAllUnstagedChangesInFile)
if err := gui.Git.WorkingTree.DiscardUnstagedFileChanges(file); err != nil {
return gui.PopupHandler.Error(err)
gui.c.LogAction(gui.c.Tr.Actions.DiscardAllUnstagedChangesInFile)
if err := gui.git.WorkingTree.DiscardUnstagedFileChanges(file); err != nil {
return gui.c.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
},
})
}
}
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: node.GetPath(), Items: menuItems})
return gui.c.Menu(popup.CreateMenuOptions{Title: node.GetPath(), Items: menuItems})
}

View File

@ -7,7 +7,7 @@ import (
)
func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch rune, mod gocui.Modifier, allowMultiline bool) bool {
newlineKey, ok := gui.getKey(gui.UserConfig.Keybinding.Universal.AppendNewline).(gocui.Key)
newlineKey, ok := gui.getKey(gui.c.UserConfig.Keybinding.Universal.AppendNewline).(gocui.Key)
if !ok {
newlineKey = gocui.KeyAltEnter
}
@ -62,7 +62,7 @@ func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod g
// considered out of bounds to add a newline, meaning we can avoid unnecessary scrolling.
err := gui.resizePopupPanel(v, v.TextArea.GetContent())
if err != nil {
gui.Log.Error(err)
gui.c.Log.Error(err)
}
v.RenderTextArea()
gui.RenderCommitLength()

View File

@ -8,11 +8,11 @@ import (
)
func (gui *Gui) handleCreateExtrasMenuPanel() error {
return gui.PopupHandler.Menu(popup.CreateMenuOptions{
Title: gui.Tr.CommandLog,
return gui.c.Menu(popup.CreateMenuOptions{
Title: gui.c.Tr.CommandLog,
Items: []*popup.MenuItem{
{
DisplayString: gui.Tr.ToggleShowCommandLog,
DisplayString: gui.c.Tr.ToggleShowCommandLog,
OnPress: func() error {
currentContext := gui.currentStaticContext()
if gui.ShowExtrasWindow && currentContext.GetKey() == COMMAND_LOG_CONTEXT_KEY {
@ -22,13 +22,13 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error {
}
show := !gui.ShowExtrasWindow
gui.ShowExtrasWindow = show
gui.Config.GetAppState().HideCommandLog = !show
_ = gui.Config.SaveAppState()
gui.c.GetAppState().HideCommandLog = !show
_ = gui.c.SaveAppState()
return nil
},
},
{
DisplayString: gui.Tr.FocusCommandLog,
DisplayString: gui.c.Tr.FocusCommandLog,
OnPress: gui.handleFocusCommandLog,
},
},
@ -37,8 +37,9 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error {
func (gui *Gui) handleFocusCommandLog() error {
gui.ShowExtrasWindow = true
// TODO: is this necessary? Can't I just call 'return from context'?
gui.State.Contexts.CommandLog.SetParentContext(gui.currentSideContext())
return gui.pushContext(gui.State.Contexts.CommandLog)
return gui.c.PushContext(gui.State.Contexts.CommandLog)
}
func (gui *Gui) scrollUpExtra() error {
@ -58,7 +59,7 @@ func (gui *Gui) scrollDownExtra() error {
}
func (gui *Gui) getCmdWriter() io.Writer {
return &prefixWriter{writer: gui.Views.Extras, prefix: style.FgMagenta.Sprintf("\n\n%s\n", gui.Tr.GitOutput)}
return &prefixWriter{writer: gui.Views.Extras, prefix: style.FgMagenta.Sprintf("\n\n%s\n", gui.c.Tr.GitOutput)}
}
// Ensures that the first write is preceded by writing a prefix.

51
pkg/gui/file_helper.go Normal file
View File

@ -0,0 +1,51 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
)
type FileHelper struct {
c *controllers.ControllerCommon
git *commands.GitCommand
os *oscommands.OSCommand
}
func NewFileHelper(
c *controllers.ControllerCommon,
git *commands.GitCommand,
os *oscommands.OSCommand,
) *FileHelper {
return &FileHelper{
c: c,
git: git,
os: os,
}
}
var _ controllers.IFileHelper = &FileHelper{}
func (self *FileHelper) EditFile(filename string) error {
return self.EditFileAtLine(filename, 1)
}
func (self *FileHelper) EditFileAtLine(filename string, lineNumber int) error {
cmdStr, err := self.git.File.GetEditCmdStr(filename, lineNumber)
if err != nil {
return self.c.Error(err)
}
self.c.LogAction(self.c.Tr.Actions.EditFile)
return self.c.RunSubprocessAndRefresh(
self.os.Cmd.NewShell(cmdStr),
)
}
func (self *FileHelper) OpenFile(filename string) error {
self.c.LogAction(self.c.Tr.Actions.OpenFile)
if err := self.os.OpenFile(filename); err != nil {
return self.c.Error(err)
}
return nil
}

View File

@ -118,13 +118,13 @@ func (gui *Gui) watchFilesForChanges() {
}
// only refresh if we're not already
if !gui.State.IsRefreshingFiles {
_ = gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
_ = gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
}
// watch for errors
case err := <-gui.fileWatcher.Watcher.Errors:
if err != nil {
gui.Log.Error(err)
gui.c.Log.Error(err)
}
}
}

View File

@ -1,15 +1,10 @@
package gui
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
@ -52,7 +47,7 @@ func (gui *Gui) filesRenderToMain() error {
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "",
task: NewRenderStringTask(gui.Tr.NoChangedFiles),
task: NewRenderStringTask(gui.c.Tr.NoChangedFiles),
},
})
}
@ -69,24 +64,24 @@ func (gui *Gui) filesRenderToMain() error {
gui.resetMergeStateWithLock()
cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.IgnoreWhitespaceInDiffView)
cmdObj := gui.git.WorkingTree.WorktreeFileDiffCmdObj(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.IgnoreWhitespaceInDiffView)
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
title: gui.Tr.UnstagedChanges,
title: gui.c.Tr.UnstagedChanges,
task: NewRunPtyTask(cmdObj.GetCmd()),
}}
if node.GetHasUnstagedChanges() {
if node.GetHasStagedChanges() {
cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(node, false, true, gui.IgnoreWhitespaceInDiffView)
cmdObj := gui.git.WorkingTree.WorktreeFileDiffCmdObj(node, false, true, gui.IgnoreWhitespaceInDiffView)
refreshOpts.secondary = &viewUpdateOpts{
title: gui.Tr.StagedChanges,
title: gui.c.Tr.StagedChanges,
task: NewRunPtyTask(cmdObj.GetCmd()),
}
}
} else {
refreshOpts.main.title = gui.Tr.StagedChanges
refreshOpts.main.title = gui.c.Tr.StagedChanges
}
return gui.refreshMainViews(refreshOpts)
@ -115,12 +110,12 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
}
gui.OnUIThread(func() error {
if err := gui.postRefreshUpdate(gui.State.Contexts.Submodules); err != nil {
gui.Log.Error(err)
if err := gui.c.PostRefreshUpdate(gui.State.Contexts.Submodules); err != nil {
gui.c.Log.Error(err)
}
if ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY {
// doing this a little custom (as opposed to using gui.postRefreshUpdate) because we handle selecting the file explicitly below
if types.ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY {
// doing this a little custom (as opposed to using gui.c.PostRefreshUpdate) because we handle selecting the file explicitly below
if err := gui.State.Contexts.Files.HandleRender(); err != nil {
return err
}
@ -143,418 +138,6 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
return nil
}
// specific functions
func (gui *Gui) stagedFiles() []*models.File {
files := gui.State.FileTreeViewModel.GetAllFiles()
result := make([]*models.File, 0)
for _, file := range files {
if file.HasStagedChanges {
result = append(result, file)
}
}
return result
}
func (gui *Gui) trackedFiles() []*models.File {
files := gui.State.FileTreeViewModel.GetAllFiles()
result := make([]*models.File, 0, len(files))
for _, file := range files {
if file.Tracked {
result = append(result, file)
}
}
return result
}
func (gui *Gui) handleEnterFile() error {
return gui.enterFile(OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
}
func (gui *Gui) enterFile(opts OnFocusOpts) error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if node.File == nil {
return gui.handleToggleDirCollapsed()
}
file := node.File
submoduleConfigs := gui.State.Submodules
if file.IsSubmodule(submoduleConfigs) {
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
return gui.enterSubmodule(submoduleConfig)
}
if file.HasInlineMergeConflicts {
return gui.switchToMerge()
}
if file.HasMergeConflicts {
return gui.PopupHandler.ErrorMsg(gui.Tr.FileStagingRequirements)
}
return gui.pushContext(gui.State.Contexts.Staging, opts)
}
func (gui *Gui) handleFilePress() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if node.IsLeaf() {
file := node.File
if file.HasInlineMergeConflicts {
return gui.switchToMerge()
}
if file.HasUnstagedChanges {
gui.logAction(gui.Tr.Actions.StageFile)
if err := gui.Git.WorkingTree.StageFile(file.Name); err != nil {
return gui.PopupHandler.Error(err)
}
} else {
gui.logAction(gui.Tr.Actions.UnstageFile)
if err := gui.Git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return gui.PopupHandler.Error(err)
}
}
} else {
// if any files within have inline merge conflicts we can't stage or unstage,
// or it'll end up with those >>>>>> lines actually staged
if node.GetHasInlineMergeConflicts() {
return gui.PopupHandler.ErrorMsg(gui.Tr.ErrStageDirWithInlineMergeConflicts)
}
if node.GetHasUnstagedChanges() {
gui.logAction(gui.Tr.Actions.StageFile)
if err := gui.Git.WorkingTree.StageFile(node.Path); err != nil {
return gui.PopupHandler.Error(err)
}
} else {
// pretty sure it doesn't matter that we're always passing true here
gui.logAction(gui.Tr.Actions.UnstageFile)
if err := gui.Git.WorkingTree.UnStageFile([]string{node.Path}, true); err != nil {
return gui.PopupHandler.Error(err)
}
}
}
if err := gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil {
return err
}
return gui.State.Contexts.Files.HandleFocus()
}
func (gui *Gui) allFilesStaged() bool {
for _, file := range gui.State.FileTreeViewModel.GetAllFiles() {
if file.HasUnstagedChanges {
return false
}
}
return true
}
func (gui *Gui) onFocusFile() error {
gui.takeOverMergeConflictScrolling()
return nil
}
func (gui *Gui) handleStageAll() error {
var err error
if gui.allFilesStaged() {
gui.logAction(gui.Tr.Actions.UnstageAllFiles)
err = gui.Git.WorkingTree.UnstageAll()
} else {
gui.logAction(gui.Tr.Actions.StageAllFiles)
err = gui.Git.WorkingTree.StageAll()
}
if err != nil {
_ = gui.PopupHandler.Error(err)
}
if err := gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil {
return err
}
return gui.State.Contexts.Files.HandleFocus()
}
func (gui *Gui) handleIgnoreFile() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if node.GetPath() == ".gitignore" {
return gui.PopupHandler.ErrorMsg("Cannot ignore .gitignore")
}
unstageFiles := func() error {
return node.ForEachFile(func(file *models.File) error {
if file.HasStagedChanges {
if err := gui.Git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return err
}
}
return nil
})
}
if node.GetIsTracked() {
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.IgnoreTracked,
Prompt: gui.Tr.IgnoreTrackedPrompt,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.IgnoreFile)
// not 100% sure if this is necessary but I'll assume it is
if err := unstageFiles(); err != nil {
return err
}
if err := gui.Git.WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil {
return err
}
if err := gui.Git.WorkingTree.Ignore(node.GetPath()); err != nil {
return err
}
return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
},
})
}
gui.logAction(gui.Tr.Actions.IgnoreFile)
if err := unstageFiles(); err != nil {
return err
}
if err := gui.Git.WorkingTree.Ignore(node.GetPath()); err != nil {
return gui.PopupHandler.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
}
func (gui *Gui) handleWIPCommitPress() error {
skipHookPrefix := gui.UserConfig.Git.SkipHookPrefix
if skipHookPrefix == "" {
return gui.PopupHandler.ErrorMsg(gui.Tr.SkipHookPrefixNotConfigured)
}
textArea := gui.Views.CommitMessage.TextArea
textArea.Clear()
textArea.TypeString(skipHookPrefix)
gui.Views.CommitMessage.RenderTextArea()
return gui.handleCommitPress()
}
func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
cfg, ok := gui.UserConfig.Git.CommitPrefixes[utils.GetCurrentRepoName()]
if !ok {
return nil
}
return &cfg
}
func (gui *Gui) prepareFilesForCommit() error {
noStagedFiles := len(gui.stagedFiles()) == 0
if noStagedFiles && gui.UserConfig.Gui.SkipNoStagedFilesWarning {
gui.logAction(gui.Tr.Actions.StageAllFiles)
err := gui.Git.WorkingTree.StageAll()
if err != nil {
return err
}
return gui.refreshFilesAndSubmodules()
}
return nil
}
func (gui *Gui) handleCommitPress() error {
if err := gui.prepareFilesForCommit(); err != nil {
return gui.PopupHandler.Error(err)
}
if gui.State.FileTreeViewModel.GetItemsLength() == 0 {
return gui.PopupHandler.ErrorMsg(gui.Tr.NoFilesStagedTitle)
}
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(gui.handleCommitPress)
}
if len(gui.State.failedCommitMessage) > 0 {
gui.Views.CommitMessage.ClearTextArea()
gui.Views.CommitMessage.TextArea.TypeString(gui.State.failedCommitMessage)
gui.Views.CommitMessage.RenderTextArea()
} else {
commitPrefixConfig := gui.commitPrefixConfigForRepo()
if commitPrefixConfig != nil {
prefixPattern := commitPrefixConfig.Pattern
prefixReplace := commitPrefixConfig.Replace
rgx, err := regexp.Compile(prefixPattern)
if err != nil {
return gui.PopupHandler.ErrorMsg(fmt.Sprintf("%s: %s", gui.Tr.LcCommitPrefixPatternError, err.Error()))
}
prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace)
gui.Views.CommitMessage.ClearTextArea()
gui.Views.CommitMessage.TextArea.TypeString(prefix)
gui.Views.CommitMessage.RenderTextArea()
}
}
if err := gui.pushContext(gui.State.Contexts.CommitMessage); err != nil {
return err
}
gui.RenderCommitLength()
return nil
}
func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.NoFilesStagedTitle,
Prompt: gui.Tr.NoFilesStagedPrompt,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.StageAllFiles)
if err := gui.Git.WorkingTree.StageAll(); err != nil {
return gui.PopupHandler.Error(err)
}
if err := gui.refreshFilesAndSubmodules(); err != nil {
return gui.PopupHandler.Error(err)
}
return retry()
},
})
}
func (gui *Gui) handleAmendCommitPress() error {
if gui.State.FileTreeViewModel.GetItemsLength() == 0 {
return gui.PopupHandler.ErrorMsg(gui.Tr.NoFilesStagedTitle)
}
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(gui.handleAmendCommitPress)
}
if len(gui.State.Commits) == 0 {
return gui.PopupHandler.ErrorMsg(gui.Tr.NoCommitToAmend)
}
return gui.PopupHandler.Ask(popup.AskOpts{
Title: strings.Title(gui.Tr.AmendLastCommit),
Prompt: gui.Tr.SureToAmend,
HandleConfirm: func() error {
cmdObj := gui.Git.Commit.AmendHeadCmdObj()
gui.logAction(gui.Tr.Actions.AmendCommit)
return gui.withGpgHandling(cmdObj, gui.Tr.AmendingStatus, nil)
},
})
}
// handleCommitEditorPress - handle when the user wants to commit changes via
// their editor rather than via the popup panel
func (gui *Gui) handleCommitEditorPress() error {
if gui.State.FileTreeViewModel.GetItemsLength() == 0 {
return gui.PopupHandler.ErrorMsg(gui.Tr.NoFilesStagedTitle)
}
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(gui.handleCommitEditorPress)
}
gui.logAction(gui.Tr.Actions.Commit)
return gui.runSubprocessWithSuspenseAndRefresh(
gui.Git.Commit.CommitEditorCmdObj(),
)
}
func (gui *Gui) handleStatusFilterPressed() error {
return gui.PopupHandler.Menu(popup.CreateMenuOptions{
Title: gui.Tr.FilteringMenuTitle,
Items: []*popup.MenuItem{
{
DisplayString: gui.Tr.FilterStagedFiles,
OnPress: func() error {
return gui.setStatusFiltering(filetree.DisplayStaged)
},
},
{
DisplayString: gui.Tr.FilterUnstagedFiles,
OnPress: func() error {
return gui.setStatusFiltering(filetree.DisplayUnstaged)
},
},
{
DisplayString: gui.Tr.ResetCommitFilterState,
OnPress: func() error {
return gui.setStatusFiltering(filetree.DisplayAll)
},
},
},
})
}
func (gui *Gui) setStatusFiltering(filter filetree.FileTreeDisplayFilter) error {
state := gui.State
state.FileTreeViewModel.SetFilter(filter)
return gui.handleRefreshFiles()
}
func (gui *Gui) editFile(filename string) error {
return gui.editFileAtLine(filename, 1)
}
func (gui *Gui) editFileAtLine(filename string, lineNumber int) error {
cmdStr, err := gui.Git.File.GetEditCmdStr(filename, lineNumber)
if err != nil {
return gui.PopupHandler.Error(err)
}
gui.logAction(gui.Tr.Actions.EditFile)
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.Cmd.NewShell(cmdStr),
)
}
func (gui *Gui) handleFileEdit() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if node.File == nil {
return gui.PopupHandler.ErrorMsg(gui.Tr.ErrCannotEditDirectory)
}
return gui.editFile(node.GetPath())
}
func (gui *Gui) handleFileOpen() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
return gui.openFile(node.GetPath())
}
func (gui *Gui) handleRefreshFiles() error {
return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
}
func (gui *Gui) refreshStateFiles() error {
state := gui.State
@ -591,13 +174,13 @@ func (gui *Gui) refreshStateFiles() error {
}
if len(pathsToStage) > 0 {
gui.logAction(gui.Tr.Actions.StageResolvedFiles)
if err := gui.Git.WorkingTree.StageFiles(pathsToStage); err != nil {
return gui.surfaceError(err)
gui.c.LogAction(gui.Tr.Actions.StageResolvedFiles)
if err := gui.git.WorkingTree.StageFiles(pathsToStage); err != nil {
return gui.c.Error(err)
}
}
files := gui.Git.Loaders.Files.
files := gui.git.Loaders.Files.
GetStatusFiles(loaders.GetStatusFileOptions{})
conflictFileCount := 0
@ -607,7 +190,7 @@ func (gui *Gui) refreshStateFiles() error {
}
}
if gui.Git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && conflictFileCount == 0 && prevConflictFileCount > 0 {
if gui.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && conflictFileCount == 0 && prevConflictFileCount > 0 {
gui.OnUIThread(func() error { return gui.promptToContinueRebase() })
}
@ -716,218 +299,7 @@ func (gui *Gui) findNewSelectedIdx(prevNodes []*filetree.FileNode, currNodes []*
return -1
}
func (gui *Gui) handlePullFiles() error {
if gui.popupPanelFocused() {
return nil
}
action := gui.Tr.Actions.Pull
currentBranch := gui.currentBranch()
if currentBranch == nil {
// need to wait for branches to refresh
return nil
}
// if we have no upstream branch we need to set that first
if !currentBranch.IsTrackingRemote() {
suggestedRemote := getSuggestedRemote(gui.State.Remotes)
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: gui.Tr.EnterUpstream,
InitialContent: suggestedRemote + " " + currentBranch.Name,
FindSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "),
HandleConfirm: func(upstream string) error {
var upstreamBranch, upstreamRemote string
split := strings.Split(upstream, " ")
if len(split) != 2 {
return gui.PopupHandler.ErrorMsg(gui.Tr.InvalidUpstream)
}
upstreamRemote = split[0]
upstreamBranch = split[1]
if err := gui.Git.Branch.SetCurrentBranchUpstream(upstreamRemote, upstreamBranch); err != nil {
errorMessage := err.Error()
if strings.Contains(errorMessage, "does not exist") {
errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
}
return gui.PopupHandler.ErrorMsg(errorMessage)
}
return gui.pullFiles(PullFilesOptions{UpstreamRemote: upstreamRemote, UpstreamBranch: upstreamBranch, action: action})
},
})
}
return gui.pullFiles(PullFilesOptions{UpstreamRemote: currentBranch.UpstreamRemote, UpstreamBranch: currentBranch.UpstreamBranch, action: action})
}
type PullFilesOptions struct {
UpstreamRemote string
UpstreamBranch string
FastForwardOnly bool
action string
}
func (gui *Gui) pullFiles(opts PullFilesOptions) error {
return gui.PopupHandler.WithLoaderPanel(gui.Tr.PullWait, func() error {
return gui.pullWithLock(opts)
})
}
func (gui *Gui) pullWithLock(opts PullFilesOptions) error {
gui.Mutexes.FetchMutex.Lock()
defer gui.Mutexes.FetchMutex.Unlock()
gui.logAction(opts.action)
err := gui.Git.Sync.Pull(
git_commands.PullOptions{
RemoteName: opts.UpstreamRemote,
BranchName: opts.UpstreamBranch,
FastForwardOnly: opts.FastForwardOnly,
},
)
if err == nil {
_ = gui.closeConfirmationPrompt(false)
}
return gui.handleGenericMergeCommandResult(err)
}
type pushOpts struct {
force bool
upstreamRemote string
upstreamBranch string
setUpstream bool
}
func (gui *Gui) push(opts pushOpts) error {
return gui.PopupHandler.WithLoaderPanel(gui.Tr.PushWait, func() error {
gui.logAction(gui.Tr.Actions.Push)
err := gui.Git.Sync.Push(git_commands.PushOpts{
Force: opts.force,
UpstreamRemote: opts.upstreamRemote,
UpstreamBranch: opts.upstreamBranch,
SetUpstream: opts.setUpstream,
})
if err != nil {
if !opts.force && strings.Contains(err.Error(), "Updates were rejected") {
forcePushDisabled := gui.UserConfig.Git.DisableForcePushing
if forcePushDisabled {
_ = gui.PopupHandler.ErrorMsg(gui.Tr.UpdatesRejectedAndForcePushDisabled)
return nil
}
_ = gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.ForcePush,
Prompt: gui.Tr.ForcePushPrompt,
HandleConfirm: func() error {
newOpts := opts
newOpts.force = true
return gui.push(newOpts)
},
})
return nil
}
_ = gui.PopupHandler.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
})
}
func (gui *Gui) pushFiles() error {
if gui.popupPanelFocused() {
return nil
}
// if we have pullables we'll ask if the user wants to force push
currentBranch := gui.currentBranch()
if currentBranch == nil {
// need to wait for branches to refresh
return nil
}
if currentBranch.IsTrackingRemote() {
opts := pushOpts{
force: false,
upstreamRemote: currentBranch.UpstreamRemote,
upstreamBranch: currentBranch.UpstreamBranch,
}
if currentBranch.HasCommitsToPull() {
opts.force = true
return gui.requestToForcePush(opts)
} else {
return gui.push(opts)
}
} else {
suggestedRemote := getSuggestedRemote(gui.State.Remotes)
if gui.Git.Config.GetPushToCurrent() {
return gui.push(pushOpts{setUpstream: true})
} else {
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: gui.Tr.EnterUpstream,
InitialContent: suggestedRemote + " " + currentBranch.Name,
FindSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "),
HandleConfirm: func(upstream string) error {
var upstreamBranch, upstreamRemote string
split := strings.Split(upstream, " ")
if len(split) == 2 {
upstreamRemote = split[0]
upstreamBranch = split[1]
} else {
upstreamRemote = upstream
upstreamBranch = ""
}
return gui.push(pushOpts{
force: false,
upstreamRemote: upstreamRemote,
upstreamBranch: upstreamBranch,
setUpstream: true,
})
},
})
}
}
}
func getSuggestedRemote(remotes []*models.Remote) string {
if len(remotes) == 0 {
return "origin"
}
for _, remote := range remotes {
if remote.Name == "origin" {
return remote.Name
}
}
return remotes[0].Name
}
func (gui *Gui) requestToForcePush(opts pushOpts) error {
forcePushDisabled := gui.UserConfig.Git.DisableForcePushing
if forcePushDisabled {
return gui.PopupHandler.ErrorMsg(gui.Tr.ForcePushDisabled)
}
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.ForcePush,
Prompt: gui.Tr.ForcePushPrompt,
HandleConfirm: func() error {
return gui.push(opts)
},
})
}
func (gui *Gui) switchToMerge() error {
file := gui.getSelectedFile()
if file == nil {
return nil
}
func (gui *Gui) onFocusFile() error {
gui.takeOverMergeConflictScrolling()
if gui.State.Panels.Merging.GetPath() != file.Name {
@ -940,155 +312,14 @@ func (gui *Gui) switchToMerge() error {
}
}
// TODO: this can't be right.
return gui.pushContext(gui.State.Contexts.Merging)
}
func (gui *Gui) openFile(filename string) error {
gui.logAction(gui.Tr.Actions.OpenFile)
if err := gui.OSCommand.OpenFile(filename); err != nil {
return gui.PopupHandler.Error(err)
func (gui *Gui) getSetTextareaTextFn(view *gocui.View) func(string) {
return func(text string) {
view.ClearTextArea()
view.TextArea.TypeString(text)
view.RenderTextArea()
}
return nil
}
func (gui *Gui) handleCustomCommand() error {
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: gui.Tr.CustomCommand,
FindSuggestionsFunc: gui.getCustomCommandsHistorySuggestionsFunc(),
HandleConfirm: func(command string) error {
gui.Config.GetAppState().CustomCommandsHistory = utils.Limit(
utils.Uniq(
append(gui.Config.GetAppState().CustomCommandsHistory, command),
),
1000,
)
err := gui.Config.SaveAppState()
if err != nil {
gui.Log.Error(err)
}
gui.logAction(gui.Tr.Actions.CustomCommand)
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.Cmd.NewShell(command),
)
},
})
}
func (gui *Gui) handleCreateStashMenu() error {
return gui.PopupHandler.Menu(popup.CreateMenuOptions{
Title: gui.Tr.LcStashOptions,
Items: []*popup.MenuItem{
{
DisplayString: gui.Tr.LcStashAllChanges,
OnPress: func() error {
gui.logAction(gui.Tr.Actions.StashAllChanges)
return gui.handleStashSave(gui.Git.Stash.Save)
},
},
{
DisplayString: gui.Tr.LcStashStagedChanges,
OnPress: func() error {
gui.logAction(gui.Tr.Actions.StashStagedChanges)
return gui.handleStashSave(gui.Git.Stash.SaveStagedChanges)
},
},
},
})
}
func (gui *Gui) handleStashChanges() error {
return gui.handleStashSave(gui.Git.Stash.Save)
}
func (gui *Gui) handleCreateResetToUpstreamMenu() error {
return gui.createResetMenu("@{upstream}")
}
func (gui *Gui) handleToggleDirCollapsed() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
gui.State.FileTreeViewModel.ToggleCollapsed(node.GetPath())
if err := gui.postRefreshUpdate(gui.State.Contexts.Files); err != nil {
gui.Log.Error(err)
}
return nil
}
func (gui *Gui) handleToggleFileTreeView() error {
// get path of currently selected file
path := gui.getSelectedPath()
gui.State.FileTreeViewModel.ToggleShowTree()
// find that same node in the new format and move the cursor to it
if path != "" {
gui.State.FileTreeViewModel.ExpandToPath(path)
index, found := gui.State.FileTreeViewModel.GetIndexForPath(path)
if found {
gui.filesListContext().GetPanelState().SetSelectedLineIdx(index)
}
}
if ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY {
if err := gui.State.Contexts.Files.HandleRender(); err != nil {
return err
}
if err := gui.State.Contexts.Files.HandleFocus(); err != nil {
return err
}
}
return nil
}
func (gui *Gui) handleOpenMergeTool() error {
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.MergeToolTitle,
Prompt: gui.Tr.MergeToolPrompt,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.OpenMergeTool)
return gui.runSubprocessWithSuspenseAndRefresh(
gui.Git.WorkingTree.OpenMergeToolCmdObj(),
)
},
})
}
func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.LcResettingSubmoduleStatus, func() error {
gui.logAction(gui.Tr.Actions.ResetSubmodule)
file := gui.fileForSubmodule(submodule)
if file != nil {
if err := gui.Git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return gui.PopupHandler.Error(err)
}
}
if err := gui.Git.Submodule.Stash(submodule); err != nil {
return gui.PopupHandler.Error(err)
}
if err := gui.Git.Submodule.Reset(submodule); err != nil {
return gui.PopupHandler.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.SUBMODULES}})
})
}
func (gui *Gui) fileForSubmodule(submodule *models.SubmoduleConfig) *models.File {
for _, file := range gui.State.FileManager.GetAllFiles() {
if file.IsSubmodule([]*models.SubmoduleConfig{submodule}) {
return file
}
}
return nil
}

View File

@ -2,6 +2,7 @@ package filetree
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type CommitFileNode struct {
@ -12,8 +13,7 @@ type CommitFileNode struct {
}
var _ INode = &CommitFileNode{}
// methods satisfying ListItem interface
var _ types.ListItem = &CommitFileNode{}
func (s *CommitFileNode) ID() string {
return s.GetPath()

View File

@ -2,6 +2,7 @@ package filetree
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type FileNode struct {
@ -12,8 +13,7 @@ type FileNode struct {
}
var _ INode = &FileNode{}
// methods satisfying ListItem interface
var _ types.ListItem = &FileNode{}
func (s *FileNode) ID() string {
return s.GetPath()

View File

@ -5,17 +5,27 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
func (gui *Gui) validateNotInFilterMode() (bool, error) {
func (gui *Gui) validateNotInFilterMode() bool {
if gui.State.Modes.Filtering.Active() {
err := gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.MustExitFilterModeTitle,
Prompt: gui.Tr.MustExitFilterModePrompt,
_ = gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.MustExitFilterModeTitle,
Prompt: gui.c.Tr.MustExitFilterModePrompt,
HandleConfirm: gui.exitFilterMode,
})
return false, err
return false
}
return true
}
func (gui *Gui) outsideFilterMode(f func() error) func() error {
return func() error {
if !gui.validateNotInFilterMode() {
return nil
}
return f()
}
return true, nil
}
func (gui *Gui) exitFilterMode() error {
@ -28,7 +38,7 @@ func (gui *Gui) clearFiltering() error {
gui.State.ScreenMode = SCREEN_NORMAL
}
return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}})
return gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}})
}
func (gui *Gui) setFiltering(path string) error {
@ -37,11 +47,11 @@ func (gui *Gui) setFiltering(path string) error {
gui.State.ScreenMode = SCREEN_HALF
}
if err := gui.pushContext(gui.State.Contexts.BranchCommits); err != nil {
if err := gui.c.PushContext(gui.State.Contexts.BranchCommits); err != nil {
return err
}
return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}, Then: func() {
return gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}, Then: func() {
gui.State.Contexts.BranchCommits.GetPanelState().SetSelectedLineIdx(0)
}})
}

View File

@ -26,7 +26,7 @@ func (gui *Gui) handleCreateFilteringMenuPanel() error {
if fileName != "" {
menuItems = append(menuItems, &popup.MenuItem{
DisplayString: fmt.Sprintf("%s '%s'", gui.Tr.LcFilterBy, fileName),
DisplayString: fmt.Sprintf("%s '%s'", gui.c.Tr.LcFilterBy, fileName),
OnPress: func() error {
return gui.setFiltering(fileName)
},
@ -34,11 +34,11 @@ func (gui *Gui) handleCreateFilteringMenuPanel() error {
}
menuItems = append(menuItems, &popup.MenuItem{
DisplayString: gui.Tr.LcFilterPathOption,
DisplayString: gui.c.Tr.LcFilterPathOption,
OnPress: func() error {
return gui.PopupHandler.Prompt(popup.PromptOpts{
FindSuggestionsFunc: gui.getFilePathSuggestionsFunc(),
Title: gui.Tr.EnterFileName,
return gui.c.Prompt(popup.PromptOpts{
FindSuggestionsFunc: gui.suggestionsHelper.GetFilePathSuggestionsFunc(),
Title: gui.c.Tr.EnterFileName,
HandleConfirm: func(response string) error {
return gui.setFiltering(strings.TrimSpace(response))
},
@ -48,10 +48,10 @@ func (gui *Gui) handleCreateFilteringMenuPanel() error {
if gui.State.Modes.Filtering.Active() {
menuItems = append(menuItems, &popup.MenuItem{
DisplayString: gui.Tr.LcExitFilterMode,
DisplayString: gui.c.Tr.LcExitFilterMode,
OnPress: gui.clearFiltering,
})
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: gui.Tr.FilteringMenuTitle, Items: menuItems})
return gui.c.Menu(popup.CreateMenuOptions{Title: gui.c.Tr.FilteringMenuTitle, Items: menuItems})
}

View File

@ -13,27 +13,27 @@ func (gui *Gui) handleCreateGitFlowMenu() error {
return nil
}
if !gui.Git.Flow.GitFlowEnabled() {
return gui.PopupHandler.ErrorMsg("You need to install git-flow and enable it in this repo to use git-flow features")
if !gui.git.Flow.GitFlowEnabled() {
return gui.c.ErrorMsg("You need to install git-flow and enable it in this repo to use git-flow features")
}
startHandler := func(branchType string) func() error {
return func() error {
title := utils.ResolvePlaceholderString(gui.Tr.NewGitFlowBranchPrompt, map[string]string{"branchType": branchType})
title := utils.ResolvePlaceholderString(gui.c.Tr.NewGitFlowBranchPrompt, map[string]string{"branchType": branchType})
return gui.PopupHandler.Prompt(popup.PromptOpts{
return gui.c.Prompt(popup.PromptOpts{
Title: title,
HandleConfirm: func(name string) error {
gui.logAction(gui.Tr.Actions.GitFlowStart)
gui.c.LogAction(gui.c.Tr.Actions.GitFlowStart)
return gui.runSubprocessWithSuspenseAndRefresh(
gui.Git.Flow.StartCmdObj(branchType, name),
gui.git.Flow.StartCmdObj(branchType, name),
)
},
})
}
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{
return gui.c.Menu(popup.CreateMenuOptions{
Title: "git flow",
Items: []*popup.MenuItem{
{
@ -64,11 +64,11 @@ func (gui *Gui) handleCreateGitFlowMenu() error {
}
func (gui *Gui) gitFlowFinishBranch(branchName string) error {
cmdObj, err := gui.Git.Flow.FinishCmdObj(branchName)
cmdObj, err := gui.git.Flow.FinishCmdObj(branchName)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
gui.logAction(gui.Tr.Actions.GitFlowFinish)
gui.c.LogAction(gui.c.Tr.Actions.GitFlowFinish)
return gui.runSubprocessWithSuspenseAndRefresh(cmdObj)
}

View File

@ -65,7 +65,7 @@ func (gui *Gui) prevScreenMode() error {
func (gui *Gui) scrollUpView(view *gocui.View) error {
ox, oy := view.Origin()
newOy := int(math.Max(0, float64(oy-gui.UserConfig.Gui.ScrollHeight)))
newOy := int(math.Max(0, float64(oy-gui.c.UserConfig.Gui.ScrollHeight)))
return view.SetOrigin(ox, newOy)
}
@ -87,12 +87,12 @@ func (gui *Gui) scrollDownView(view *gocui.View) error {
func (gui *Gui) linesToScrollDown(view *gocui.View) int {
_, oy := view.Origin()
y := oy
canScrollPastBottom := gui.UserConfig.Gui.ScrollPastBottom
canScrollPastBottom := gui.c.UserConfig.Gui.ScrollPastBottom
if !canScrollPastBottom {
_, sy := view.Size()
y += sy
}
scrollHeight := gui.UserConfig.Gui.ScrollHeight
scrollHeight := gui.c.UserConfig.Gui.ScrollHeight
scrollableLines := view.ViewLinesHeight() - y
if scrollableLines < 0 {
return 0
@ -177,7 +177,7 @@ func (gui *Gui) scrollDownConfirmationPanel() error {
}
func (gui *Gui) handleRefresh() error {
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
}
func (gui *Gui) handleMouseDownMain() error {
@ -190,9 +190,9 @@ func (gui *Gui) handleMouseDownMain() error {
// set filename, set primary/secondary selected, set line number, then switch context
// I'll need to know it was changed though.
// Could I pass something along to the context change?
return gui.enterFile(OnFocusOpts{ClickedViewName: "main", ClickedViewLineIdx: gui.Views.Main.SelectedLineIdx()})
return gui.Controllers.Files.EnterFile(types.OnFocusOpts{ClickedViewName: "main", ClickedViewLineIdx: gui.Views.Main.SelectedLineIdx()})
case gui.State.Contexts.CommitFiles:
return gui.enterCommitFile(OnFocusOpts{ClickedViewName: "main", ClickedViewLineIdx: gui.Views.Main.SelectedLineIdx()})
return gui.enterCommitFile(types.OnFocusOpts{ClickedViewName: "main", ClickedViewLineIdx: gui.Views.Main.SelectedLineIdx()})
}
return nil
@ -205,35 +205,29 @@ func (gui *Gui) handleMouseDownSecondary() error {
switch gui.g.CurrentView() {
case gui.Views.Files:
return gui.enterFile(OnFocusOpts{ClickedViewName: "secondary", ClickedViewLineIdx: gui.Views.Secondary.SelectedLineIdx()})
return gui.Controllers.Files.EnterFile(types.OnFocusOpts{ClickedViewName: "secondary", ClickedViewLineIdx: gui.Views.Secondary.SelectedLineIdx()})
}
return nil
}
func (gui *Gui) fetch() (err error) {
gui.Mutexes.FetchMutex.Lock()
defer gui.Mutexes.FetchMutex.Unlock()
gui.logAction("Fetch")
err = gui.Git.Sync.Fetch(git_commands.FetchOptions{})
gui.c.LogAction("Fetch")
err = gui.git.Sync.Fetch(git_commands.FetchOptions{})
if err != nil && strings.Contains(err.Error(), "exit status 128") {
_ = gui.PopupHandler.ErrorMsg(gui.Tr.PassUnameWrong)
_ = gui.c.ErrorMsg(gui.c.Tr.PassUnameWrong)
}
_ = gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
_ = gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
return err
}
func (gui *Gui) backgroundFetch() (err error) {
gui.Mutexes.FetchMutex.Lock()
defer gui.Mutexes.FetchMutex.Unlock()
err = gui.git.Sync.Fetch(git_commands.FetchOptions{Background: true})
err = gui.Git.Sync.Fetch(git_commands.FetchOptions{Background: true})
_ = gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
_ = gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
return err
}
@ -246,14 +240,14 @@ func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error {
return nil
}
gui.logAction(gui.Tr.Actions.CopyToClipboard)
gui.c.LogAction(gui.c.Tr.Actions.CopyToClipboard)
if err := gui.OSCommand.CopyToClipboard(itemId); err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
truncatedItemId := utils.TruncateWithEllipsis(strings.Replace(itemId, "\n", " ", -1), 50)
gui.raiseToast(fmt.Sprintf("'%s' %s", truncatedItemId, gui.Tr.LcCopiedToClipboard))
gui.c.Toast(fmt.Sprintf("'%s' %s", truncatedItemId, gui.c.Tr.LcCopiedToClipboard))
return nil
}

View File

@ -14,9 +14,9 @@ import (
// we don't need to see a loading status if we're in a subprocess.
// TODO: work out if we actually need to use a shell command here
func (gui *Gui) withGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error {
gui.logCommand(cmdObj.ToString(), true)
gui.LogCommand(cmdObj.ToString(), true)
useSubprocess := gui.Git.Config.UsingGpg()
useSubprocess := gui.git.Config.UsingGpg()
if useSubprocess {
success, err := gui.runSubprocessWithSuspense(gui.OSCommand.Cmd.NewShell(cmdObj.ToString()))
if success && onSuccess != nil {
@ -24,7 +24,7 @@ func (gui *Gui) withGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string,
return err
}
}
if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}); err != nil {
if err := gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}); err != nil {
return err
}
@ -35,7 +35,7 @@ func (gui *Gui) withGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string,
}
func (gui *Gui) RunAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error {
return gui.PopupHandler.WithWaitingStatus(waitingStatus, func() error {
return gui.c.WithWaitingStatus(waitingStatus, func() error {
cmdObj := gui.OSCommand.Cmd.NewShell(cmdObj.ToString())
cmdObj.AddEnvVars("TERM=dumb")
cmdWriter := gui.getCmdWriter()
@ -45,12 +45,12 @@ func (gui *Gui) RunAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, on
if err := cmd.Run(); err != nil {
if _, err := cmd.Stdout.Write([]byte(fmt.Sprintf("%s\n", style.FgRed.Sprint(err.Error())))); err != nil {
gui.Log.Error(err)
gui.c.Log.Error(err)
}
_ = gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.PopupHandler.Error(
_ = gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Error(
fmt.Errorf(
gui.Tr.GitCommandFailed, gui.UserConfig.Keybinding.Universal.ExtrasMenu,
gui.c.Tr.GitCommandFailed, gui.c.UserConfig.Keybinding.Universal.ExtrasMenu,
),
)
}
@ -61,6 +61,6 @@ func (gui *Gui) RunAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, on
}
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
})
}

View File

@ -18,6 +18,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/lbl"
@ -25,6 +26,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
"github.com/jesseduffield/lazygit/pkg/gui/modes/filtering"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
@ -55,13 +57,13 @@ const StartupPopupVersion = 5
var OverlappingEdges = false
type ContextManager struct {
ContextStack []Context
ContextStack []types.Context
sync.RWMutex
}
func NewContextManager(initialContext Context) ContextManager {
func NewContextManager(initialContext types.Context) ContextManager {
return ContextManager{
ContextStack: []Context{initialContext},
ContextStack: []types.Context{initialContext},
RWMutex: sync.RWMutex{},
}
}
@ -72,7 +74,7 @@ type Repo string
type Gui struct {
*common.Common
g *gocui.Gui
Git *commands.GitCommand
git *commands.GitCommand
OSCommand *oscommands.OSCommand
// this is the state of the GUI for the current repo
@ -126,6 +128,7 @@ type Gui struct {
IsNewRepo bool
// controllers define keybindings for a given context
Controllers Controllers
// flag as to whether or not the diff view should ignore whitespace
@ -133,10 +136,19 @@ type Gui struct {
// if this is true, we'll load our commits using `git log --all`
ShowWholeGitGraph bool
// we use this to decide whether we'll return to the original directory that
// lazygit was opened in, or if we'll retain the one we're currently in.
RetainOriginalDir bool
PrevLayout PrevLayout
c *controllers.ControllerCommon
refHelper *RefHelper
suggestionsHelper *SuggestionsHelper
fileHelper *FileHelper
workingTreeHelper *WorkingTreeHelper
// this is the initial dir we are in upon opening lazygit. We hold onto this
// in case we want to restore it before quitting for users who have set up
// the feature for changing directory upon quit.
@ -182,7 +194,7 @@ type GuiRepoState struct {
Updating bool
Panels *panelStates
SplitMainPanel bool
MainContext ContextKey // used to keep the main and secondary views' contexts in sync
MainContext types.ContextKey // used to keep the main and secondary views' contexts in sync
IsRefreshingFiles bool
Searching searchingState
@ -192,9 +204,9 @@ type GuiRepoState struct {
Modes Modes
ContextManager ContextManager
Contexts ContextTree
ViewContextMap map[string]Context
ViewTabContextMap map[string][]tabContext
Contexts context.ContextTree
ViewContextMap map[string]types.Context
ViewTabContextMap map[string][]context.TabContext
// WindowViewNameMap is a mapping of windows to the current view of that window.
// Some views move between windows for example the commitFiles view and when cycling through
@ -212,12 +224,19 @@ type GuiRepoState struct {
// this is the message of the last failed commit attempt
failedCommitMessage string
// TODO: move these into the gui struct
ScreenMode WindowMaximisation
}
type Controllers struct {
Submodules *controllers.SubmodulesController
Submodules *controllers.SubmodulesController
Tags *controllers.TagsController
LocalCommits *controllers.LocalCommitsController
Files *controllers.FilesController
Remotes *controllers.RemotesController
Menu *controllers.MenuController
Bisect *controllers.BisectController
Undo *controllers.UndoController
Sync *controllers.SyncController
}
type listPanelState struct {
@ -373,13 +392,15 @@ type Modes struct {
Diffing diffing.Diffing
}
// if you add a new mutex here be sure to instantiate it. We're using pointers to
// mutexes so that we can pass the mutexes to controllers.
type guiMutexes struct {
RefreshingFilesMutex sync.Mutex
RefreshingStatusMutex sync.Mutex
FetchMutex sync.Mutex
BranchCommitsMutex sync.Mutex
LineByLinePanelMutex sync.Mutex
SubprocessMutex sync.Mutex
RefreshingFilesMutex *sync.Mutex
RefreshingStatusMutex *sync.Mutex
FetchMutex *sync.Mutex
BranchCommitsMutex *sync.Mutex
LineByLinePanelMutex *sync.Mutex
SubprocessMutex *sync.Mutex
}
// reuseState determines if we pull the repo state from our repo state map or
@ -402,7 +423,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
return
}
} else {
gui.Log.Error(err)
gui.c.Log.Error(err)
}
}
@ -424,6 +445,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
FilteredReflogCommits: make([]*models.Commit, 0),
ReflogCommits: make([]*models.Commit, 0),
StashEntries: make([]*models.StashEntry, 0),
BisectInfo: git_commands.NewNullBisectInfo(),
Panels: &panelStates{
// TODO: work out why some of these are -1 and some are 0. Last time I checked there was a good reason but I'm less certain now
Files: &filePanelState{listPanelState{SelectedLineIdx: -1}},
@ -450,8 +472,8 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
CherryPicking: cherrypicking.New(),
Diffing: diffing.New(),
},
ViewContextMap: contexts.initialViewContextMap(),
ViewTabContextMap: contexts.initialViewTabContextMap(),
ViewContextMap: contexts.InitialViewContextMap(),
ViewTabContextMap: contexts.InitialViewTabContextMap(),
ScreenMode: screenMode,
// TODO: put contexts in the context manager
ContextManager: NewContextManager(initialContext),
@ -462,21 +484,6 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
gui.RepoStateMap[Repo(currentDir)] = gui.State
}
type guiCommon struct {
gui *Gui
popup.IPopupHandler
}
var _ controllers.IGuiCommon = &guiCommon{}
func (self *guiCommon) LogAction(msg string) {
self.gui.logAction(msg)
}
func (self *guiCommon) Refresh(opts types.RefreshOptions) error {
return self.gui.refreshSidePanels(opts)
}
// for now the split view will always be on
// NewGui builds a new gui handler
func NewGui(
@ -504,13 +511,20 @@ func NewGui(
// but now we do it via state. So we need to still support the config for the
// sake of backwards compatibility. We're making use of short circuiting here
ShowExtrasWindow: cmn.UserConfig.Gui.ShowCommandLog && !config.GetAppState().HideCommandLog,
Mutexes: guiMutexes{
RefreshingFilesMutex: &sync.Mutex{},
RefreshingStatusMutex: &sync.Mutex{},
FetchMutex: &sync.Mutex{},
BranchCommitsMutex: &sync.Mutex{},
LineByLinePanelMutex: &sync.Mutex{},
SubprocessMutex: &sync.Mutex{},
},
InitialDir: initialDir,
}
guiIO := oscommands.NewGuiIO(
cmn.Log,
gui.logCommand,
gui.LogCommand,
gui.getCmdWriter,
gui.promptUserForCredential,
)
@ -519,42 +533,151 @@ func NewGui(
gui.OSCommand = osCommand
var err error
gui.Git, err = commands.NewGitCommand(
gui.git, err = commands.NewGitCommand(
cmn,
osCommand,
gitConfig,
gui.Mutexes.FetchMutex,
)
if err != nil {
return nil, err
}
gui.resetState(filterPath, false)
gui.watchFilesForChanges()
gui.PopupHandler = popup.NewPopupHandler(
cmn,
gui.createPopupPanel,
func() error { return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}) },
func() error { return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) },
func() error { return gui.closeConfirmationPrompt(false) },
gui.createMenu,
gui.withWaitingStatus,
gui.toast,
func() string { return gui.Views.Confirmation.TextArea.GetContent() },
)
authors.SetCustomAuthors(gui.UserConfig.Gui.AuthorColors)
presentation.SetCustomBranches(gui.UserConfig.Gui.BranchColors)
guiCommon := &guiCommon{gui: gui, IPopupHandler: gui.PopupHandler}
controllerCommon := &controllers.ControllerCommon{IGuiCommon: guiCommon, Common: cmn}
// storing this stuff on the gui for now to ease refactoring
// TODO: reset these controllers upon changing repos due to state changing
gui.c = controllerCommon
gui.resetState(filterPath, false)
authors.SetCustomAuthors(gui.UserConfig.Gui.AuthorColors)
presentation.SetCustomBranches(gui.UserConfig.Gui.BranchColors)
refHelper := NewRefHelper(
controllerCommon,
gui.git,
gui.State,
)
gui.refHelper = refHelper
gui.suggestionsHelper = NewSuggestionsHelper(controllerCommon, gui.State, gui.refreshSuggestions)
gui.fileHelper = NewFileHelper(controllerCommon, gui.git, osCommand)
gui.workingTreeHelper = NewWorkingTreeHelper(gui.State.FileTreeViewModel)
tagsController := controllers.NewTagsController(
controllerCommon,
gui.State.Contexts.Tags,
gui.git,
gui.State.Contexts,
refHelper,
gui.suggestionsHelper,
gui.getSelectedTag,
gui.switchToSubCommitsContext,
)
syncController := controllers.NewSyncController(
controllerCommon,
gui.git,
gui.getCheckedOutBranch,
gui.suggestionsHelper,
gui.getSuggestedRemote,
gui.checkMergeOrRebase,
)
gui.Controllers = Controllers{
Submodules: controllers.NewSubmodulesController(
controllerCommon,
gui.State.Contexts.Submodules,
gui.git,
gui.enterSubmodule,
gui.Git,
gui.State.Submodules,
gui.getSelectedSubmodule,
),
Files: controllers.NewFilesController(
controllerCommon,
gui.State.Contexts.Files,
gui.git,
osCommand,
gui.getSelectedFileNode,
gui.State.Contexts,
gui.State.FileTreeViewModel,
gui.enterSubmodule,
func() []*models.SubmoduleConfig { return gui.State.Submodules },
gui.getSetTextareaTextFn(gui.Views.CommitMessage),
gui.withGpgHandling,
func() string { return gui.State.failedCommitMessage },
func() []*models.Commit { return gui.State.Commits },
gui.getSelectedPath,
gui.switchToMerge,
gui.suggestionsHelper,
gui.refHelper,
gui.fileHelper,
gui.workingTreeHelper,
),
Tags: tagsController,
LocalCommits: controllers.NewLocalCommitsController(
controllerCommon,
gui.State.Contexts.BranchCommits,
osCommand,
gui.git,
refHelper,
gui.getSelectedLocalCommit,
func() []*models.Commit { return gui.State.Commits },
func() int { return gui.State.Panels.Commits.SelectedLineIdx },
gui.checkMergeOrRebase,
syncController.HandlePull,
tagsController.CreateTagMenu,
gui.getHostingServiceMgr,
gui.SwitchToCommitFilesContext,
gui.handleOpenSearch,
func() bool { return gui.State.Panels.Commits.LimitCommits },
func(value bool) { gui.State.Panels.Commits.LimitCommits = value },
func() bool { return gui.ShowWholeGitGraph },
func(value bool) { gui.ShowWholeGitGraph = value },
),
Remotes: controllers.NewRemotesController(
controllerCommon,
gui.State.Contexts.Remotes,
gui.git,
gui.State.Contexts,
gui.getSelectedRemote,
func(branches []*models.RemoteBranch) { gui.State.RemoteBranches = branches },
gui.Mutexes.FetchMutex,
),
Menu: controllers.NewMenuController(
controllerCommon,
gui.State.Contexts.Menu,
gui.getSelectedMenuItem,
),
Bisect: controllers.NewBisectController(
controllerCommon,
gui.State.Contexts.BranchCommits,
gui.git,
gui.getSelectedLocalCommit,
func() []*models.Commit { return gui.State.Commits },
),
Undo: controllers.NewUndoController(
controllerCommon,
gui.git,
refHelper,
gui.workingTreeHelper,
func() []*models.Commit { return gui.State.FilteredReflogCommits },
),
Sync: syncController,
}
return gui, nil
@ -621,7 +744,7 @@ func (gui *Gui) Run() error {
}
gui.waitForIntro.Add(1)
if gui.UserConfig.Git.AutoFetch {
if gui.c.UserConfig.Git.AutoFetch {
go utils.Safe(gui.startBackgroundFetch)
}
@ -629,7 +752,7 @@ func (gui *Gui) Run() error {
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
gui.Log.Info("starting main loop")
gui.c.Log.Info("starting main loop")
err = g.MainLoop()
return err
@ -684,7 +807,7 @@ func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess oscommands.ICmdOb
return err
}
if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}); err != nil {
if err := gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}); err != nil {
return err
}
@ -705,7 +828,7 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool,
}
if err := gui.g.Suspend(); err != nil {
return false, gui.PopupHandler.Error(err)
return false, gui.c.Error(err)
}
gui.PauseBackgroundThreads = true
@ -719,14 +842,14 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool,
gui.PauseBackgroundThreads = false
if cmdErr != nil {
return false, gui.PopupHandler.Error(cmdErr)
return false, gui.c.Error(cmdErr)
}
return true, nil
}
func (gui *Gui) runSubprocess(cmdObj oscommands.ICmdObj) error { //nolint:unparam
gui.logCommand(cmdObj.ToString(), true)
gui.LogCommand(cmdObj.ToString(), true)
subprocess := cmdObj.GetCmd()
subprocess.Stdout = os.Stdout
@ -754,7 +877,7 @@ func (gui *Gui) loadNewRepo() error {
return err
}
if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}); err != nil {
if err := gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}); err != nil {
return err
}
@ -774,7 +897,7 @@ func (gui *Gui) showInitialPopups(tasks []func(chan struct{}) error) {
task := task
go utils.Safe(func() {
if err := task(done); err != nil {
_ = gui.PopupHandler.Error(err)
_ = gui.c.Error(err)
}
})
@ -787,13 +910,13 @@ func (gui *Gui) showInitialPopups(tasks []func(chan struct{}) error) {
func (gui *Gui) showIntroPopupMessage(done chan struct{}) error {
onConfirm := func() error {
done <- struct{}{}
gui.Config.GetAppState().StartupPopupVersion = StartupPopupVersion
return gui.Config.SaveAppState()
gui.c.GetAppState().StartupPopupVersion = StartupPopupVersion
return gui.c.SaveAppState()
}
return gui.PopupHandler.Ask(popup.AskOpts{
return gui.c.Ask(popup.AskOpts{
Title: "",
Prompt: gui.Tr.IntroPopupMessage,
Prompt: gui.c.Tr.IntroPopupMessage,
HandleConfirm: onConfirm,
HandleClose: onConfirm,
})
@ -826,9 +949,9 @@ func (gui *Gui) startBackgroundFetch() {
}
err := gui.backgroundFetch()
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
_ = gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.NoAutomaticGitFetchTitle,
Prompt: gui.Tr.NoAutomaticGitFetchBody,
_ = gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.NoAutomaticGitFetchTitle,
Prompt: gui.c.Tr.NoAutomaticGitFetchBody,
})
} else {
gui.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), gui.stopChan, func() error {

53
pkg/gui/gui_common.go Normal file
View File

@ -0,0 +1,53 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// hacking this by including the gui struct for now until we split more things out
type guiCommon struct {
gui *Gui
popup.IPopupHandler
}
var _ controllers.IGuiCommon = &guiCommon{}
func (self *guiCommon) LogAction(msg string) {
self.gui.LogAction(msg)
}
func (self *guiCommon) LogCommand(cmdStr string, isCommandLine bool) {
self.gui.LogCommand(cmdStr, isCommandLine)
}
func (self *guiCommon) Refresh(opts types.RefreshOptions) error {
return self.gui.Refresh(opts)
}
func (self *guiCommon) PostRefreshUpdate(context types.Context) error {
return self.gui.postRefreshUpdate(context)
}
func (self *guiCommon) RunSubprocessAndRefresh(cmdObj oscommands.ICmdObj) error {
return self.gui.runSubprocessWithSuspenseAndRefresh(cmdObj)
}
func (self *guiCommon) PushContext(context types.Context, opts ...types.OnFocusOpts) error {
return self.gui.pushContext(context, opts...)
}
func (self *guiCommon) PopContext() error {
return self.gui.returnFromContext()
}
func (self *guiCommon) GetAppState() *config.AppState {
return self.gui.Config.GetAppState()
}
func (self *guiCommon) SaveAppState() error {
return self.gui.Config.SaveAppState()
}

View File

@ -15,8 +15,8 @@ func (gui *Gui) informationStr() string {
}
if gui.g.Mouse {
donate := style.FgMagenta.SetUnderline().Sprint(gui.Tr.Donate)
askQuestion := style.FgYellow.SetUnderline().Sprint(gui.Tr.AskQuestion)
donate := style.FgMagenta.SetUnderline().Sprint(gui.c.Tr.Donate)
askQuestion := style.FgYellow.SetUnderline().Sprint(gui.c.Tr.AskQuestion)
return fmt.Sprintf("%s %s %s", donate, askQuestion, gui.Config.GetVersion())
} else {
return gui.Config.GetVersion()
@ -35,7 +35,7 @@ func (gui *Gui) handleInfoClick() error {
for _, mode := range gui.modeStatuses() {
if mode.isActive() {
if width-cx > len(gui.Tr.ResetInParentheses) {
if width-cx > len(gui.c.Tr.ResetInParentheses) {
return nil
}
return mode.reset()
@ -43,9 +43,9 @@ func (gui *Gui) handleInfoClick() error {
}
// if we're not in an active mode we show the donate button
if cx <= len(gui.Tr.Donate) {
if cx <= len(gui.c.Tr.Donate) {
return gui.OSCommand.OpenLink(constants.Links.Donate)
} else if cx <= len(gui.Tr.Donate)+1+len(gui.Tr.AskQuestion) {
} else if cx <= len(gui.c.Tr.Donate)+1+len(gui.c.Tr.AskQuestion) {
return gui.OSCommand.OpenLink(constants.Links.Discussions)
}
return nil

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
)
@ -50,36 +51,36 @@ func (gui *Gui) createAllViews() error {
gui.Views.SearchPrefix.Frame = false
gui.setViewContent(gui.Views.SearchPrefix, SEARCH_PREFIX)
gui.Views.Stash.Title = gui.Tr.StashTitle
gui.Views.Stash.Title = gui.c.Tr.StashTitle
gui.Views.Stash.FgColor = theme.GocuiDefaultTextColor
gui.Views.Commits.Title = gui.Tr.CommitsTitle
gui.Views.Commits.Title = gui.c.Tr.CommitsTitle
gui.Views.Commits.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitFiles.Title = gui.Tr.CommitFiles
gui.Views.CommitFiles.Title = gui.c.Tr.CommitFiles
gui.Views.CommitFiles.FgColor = theme.GocuiDefaultTextColor
gui.Views.Branches.Title = gui.Tr.BranchesTitle
gui.Views.Branches.Title = gui.c.Tr.BranchesTitle
gui.Views.Branches.FgColor = theme.GocuiDefaultTextColor
gui.Views.Files.Highlight = true
gui.Views.Files.Title = gui.Tr.FilesTitle
gui.Views.Files.Title = gui.c.Tr.FilesTitle
gui.Views.Files.FgColor = theme.GocuiDefaultTextColor
gui.Views.Secondary.Title = gui.Tr.DiffTitle
gui.Views.Secondary.Title = gui.c.Tr.DiffTitle
gui.Views.Secondary.Wrap = true
gui.Views.Secondary.FgColor = theme.GocuiDefaultTextColor
gui.Views.Secondary.IgnoreCarriageReturns = true
gui.Views.Main.Title = gui.Tr.DiffTitle
gui.Views.Main.Title = gui.c.Tr.DiffTitle
gui.Views.Main.Wrap = true
gui.Views.Main.FgColor = theme.GocuiDefaultTextColor
gui.Views.Main.IgnoreCarriageReturns = true
gui.Views.Limit.Title = gui.Tr.NotEnoughSpace
gui.Views.Limit.Title = gui.c.Tr.NotEnoughSpace
gui.Views.Limit.Wrap = true
gui.Views.Status.Title = gui.Tr.StatusTitle
gui.Views.Status.Title = gui.c.Tr.StatusTitle
gui.Views.Status.FgColor = theme.GocuiDefaultTextColor
gui.Views.Search.BgColor = gocui.ColorDefault
@ -93,7 +94,7 @@ func (gui *Gui) createAllViews() error {
gui.Views.AppStatus.Visible = false
gui.Views.CommitMessage.Visible = false
gui.Views.CommitMessage.Title = gui.Tr.CommitMessage
gui.Views.CommitMessage.Title = gui.c.Tr.CommitMessage
gui.Views.CommitMessage.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitMessage.Editable = true
gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor)
@ -101,7 +102,7 @@ func (gui *Gui) createAllViews() error {
gui.Views.Confirmation.Visible = false
gui.Views.Credentials.Visible = false
gui.Views.Credentials.Title = gui.Tr.CredentialsUsername
gui.Views.Credentials.Title = gui.c.Tr.CredentialsUsername
gui.Views.Credentials.FgColor = theme.GocuiDefaultTextColor
gui.Views.Credentials.Editable = true
@ -113,7 +114,7 @@ func (gui *Gui) createAllViews() error {
gui.Views.Information.FgColor = gocui.ColorGreen
gui.Views.Information.Frame = false
gui.Views.Extras.Title = gui.Tr.CommandLog
gui.Views.Extras.Title = gui.c.Tr.CommandLog
gui.Views.Extras.FgColor = theme.GocuiDefaultTextColor
gui.Views.Extras.Autoscroll = true
gui.Views.Extras.Wrap = true
@ -262,7 +263,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
// ignore contexts whose view is owned by another context right now
if ContextKey(view.Context) != listContext.GetKey() {
if types.ContextKey(view.Context) != listContext.GetKey() {
continue
}
@ -271,7 +272,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
view.SelBgColor = theme.GocuiSelectedLineBgColor
// I doubt this is expensive though it's admittedly redundant after the first render
view.SetOnSelectItem(gui.onSelectItemWrapper(listContext.onSearchSelect))
view.SetOnSelectItem(gui.onSelectItemWrapper(listContext.OnSearchSelect))
}
gui.Views.Main.SetOnSelectItem(gui.onSelectItemWrapper(gui.handlelineByLineNavigateTo))
@ -288,7 +289,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
// here is a good place log some stuff
// if you run `lazygit --logs`
// this will let you see these branches as prettified json
// gui.Log.Info(utils.AsJson(gui.State.Branches[0:4]))
// gui.c.Log.Info(utils.AsJson(gui.State.Branches[0:4]))
return gui.resizeCurrentPopupPanel()
}
@ -310,7 +311,7 @@ func (gui *Gui) onInitialViewsCreationForRepo() error {
}
initialContext := gui.currentSideContext()
if err := gui.pushContext(initialContext); err != nil {
if err := gui.c.PushContext(initialContext); err != nil {
return err
}
@ -372,9 +373,9 @@ func (gui *Gui) onInitialViewsCreation() error {
return err
}
if !gui.UserConfig.DisableStartupPopups {
if !gui.c.UserConfig.DisableStartupPopups {
popupTasks := []func(chan struct{}) error{}
storedPopupVersion := gui.Config.GetAppState().StartupPopupVersion
storedPopupVersion := gui.c.GetAppState().StartupPopupVersion
if storedPopupVersion < StartupPopupVersion {
popupTasks = append(popupTasks, gui.showIntroPopupMessage)
}

View File

@ -87,9 +87,9 @@ func (gui *Gui) copySelectedToClipboard() error {
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
selected := state.PlainRenderSelected()
gui.logAction(gui.Tr.Actions.CopySelectedTextToClipboard)
gui.c.LogAction(gui.c.Tr.Actions.CopySelectedTextToClipboard)
if err := gui.OSCommand.CopyToClipboard(selected); err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
return nil
@ -141,7 +141,7 @@ func (gui *Gui) refreshMainViewForLineByLine(state *LblPanelState) error {
if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() {
filename := gui.getSelectedCommitFileName()
var err error
includedLineIndices, err = gui.Git.Patch.PatchManager.GetFileIncLineIndices(filename)
includedLineIndices, err = gui.git.Patch.PatchManager.GetFileIncLineIndices(filename)
if err != nil {
return err
}
@ -285,5 +285,5 @@ func (gui *Gui) handleLineByLineEdit() error {
}
lineNumber := gui.State.Panels.LineByLine.CurrentLineNumber()
return gui.editFileAtLine(file.Name, lineNumber)
return gui.fileHelper.EditFileAtLine(file.Name, lineNumber)
}

View File

@ -4,19 +4,20 @@ import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type ListContext struct {
GetItemsLength func() int
GetDisplayStrings func(startIdx int, length int) [][]string
OnFocus func(...OnFocusOpts) error
OnRenderToMain func(...OnFocusOpts) error
OnFocusLost func() error
OnClickSelectedItem func() error
GetItemsLength func() int
GetDisplayStrings func(startIdx int, length int) [][]string
OnFocus func(...types.OnFocusOpts) error
OnRenderToMain func(...types.OnFocusOpts) error
OnFocusLost func() error
// the boolean here tells us whether the item is nil. This is needed because you can't work it out on the calling end once the pointer is wrapped in an interface (unless you want to use reflection)
SelectedItem func() (ListItem, bool)
OnGetPanelState func() IListPanelState
SelectedItem func() (types.ListItem, bool)
OnGetPanelState func() types.IListPanelState
// if this is true, we'll call GetDisplayStrings for just the visible part of the
// view and re-render that. This is useful when you need to render different
// content based on the selection (e.g. for showing the selected commit)
@ -27,45 +28,12 @@ type ListContext struct {
*BasicContext
}
type IListContext interface {
GetSelectedItem() (ListItem, bool)
GetSelectedItemId() string
handlePrevLine() error
handleNextLine() error
handleScrollLeft() error
handleScrollRight() error
handleLineChange(change int) error
handleNextPage() error
handleGotoTop() error
handleGotoBottom() error
handlePrevPage() error
handleClick() error
onSearchSelect(selectedLineIdx int) error
FocusLine()
HandleRenderToMain() error
var _ types.IListContext = &ListContext{}
GetPanelState() IListPanelState
Context
}
func (self *ListContext) GetPanelState() IListPanelState {
func (self *ListContext) GetPanelState() types.IListPanelState {
return self.OnGetPanelState()
}
type IListPanelState interface {
SetSelectedLineIdx(int)
GetSelectedLineIdx() int
}
type ListItem interface {
// ID is a SHA when the item is a commit, a filename when the item is a file, 'stash@{4}' when it's a stash entry, 'my_branch' when it's a branch
ID() string
// Description is something we would show in a message e.g. '123as14: push blah' for a commit
Description() string
}
func (self *ListContext) FocusLine() {
view, err := self.Gui.g.View(self.ViewName)
if err != nil {
@ -87,7 +55,7 @@ func formatListFooter(selectedLineIdx int, length int) string {
return fmt.Sprintf("%d of %d", selectedLineIdx+1, length)
}
func (self *ListContext) GetSelectedItem() (ListItem, bool) {
func (self *ListContext) GetSelectedItem() (types.ListItem, bool) {
return self.SelectedItem()
}
@ -132,7 +100,7 @@ func (self *ListContext) HandleFocusLost() error {
return nil
}
func (self *ListContext) HandleFocus(opts ...OnFocusOpts) error {
func (self *ListContext) HandleFocus(opts ...types.OnFocusOpts) error {
if self.Gui.popupPanelFocused() {
return nil
}
@ -158,19 +126,19 @@ func (self *ListContext) HandleFocus(opts ...OnFocusOpts) error {
return nil
}
func (self *ListContext) handlePrevLine() error {
func (self *ListContext) HandlePrevLine() error {
return self.handleLineChange(-1)
}
func (self *ListContext) handleNextLine() error {
func (self *ListContext) HandleNextLine() error {
return self.handleLineChange(1)
}
func (self *ListContext) handleScrollLeft() error {
func (self *ListContext) HandleScrollLeft() error {
return self.scroll(self.Gui.scrollLeft)
}
func (self *ListContext) handleScrollRight() error {
func (self *ListContext) HandleScrollRight() error {
return self.scroll(self.Gui.scrollRight)
}
@ -209,7 +177,7 @@ func (self *ListContext) handleLineChange(change int) error {
return self.HandleFocus()
}
func (self *ListContext) handleNextPage() error {
func (self *ListContext) HandleNextPage() error {
view, err := self.Gui.g.View(self.ViewName)
if err != nil {
return nil
@ -219,15 +187,15 @@ func (self *ListContext) handleNextPage() error {
return self.handleLineChange(delta)
}
func (self *ListContext) handleGotoTop() error {
func (self *ListContext) HandleGotoTop() error {
return self.handleLineChange(-self.GetItemsLength())
}
func (self *ListContext) handleGotoBottom() error {
func (self *ListContext) HandleGotoBottom() error {
return self.handleLineChange(self.GetItemsLength())
}
func (self *ListContext) handlePrevPage() error {
func (self *ListContext) HandlePrevPage() error {
view, err := self.Gui.g.View(self.ViewName)
if err != nil {
return nil
@ -238,7 +206,7 @@ func (self *ListContext) handlePrevPage() error {
return self.handleLineChange(-delta)
}
func (self *ListContext) handleClick() error {
func (self *ListContext) HandleClick(onClick func() error) error {
if self.ignoreKeybinding() {
return nil
}
@ -252,7 +220,7 @@ func (self *ListContext) handleClick() error {
newSelectedLineIdx := view.SelectedLineIdx()
// we need to focus the view
if err := self.Gui.pushContext(self); err != nil {
if err := self.Gui.c.PushContext(self); err != nil {
return err
}
@ -263,13 +231,13 @@ func (self *ListContext) handleClick() error {
self.GetPanelState().SetSelectedLineIdx(newSelectedLineIdx)
prevViewName := self.Gui.currentViewName()
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == self.ViewName && self.OnClickSelectedItem != nil {
return self.OnClickSelectedItem()
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == self.ViewName && onClick != nil {
return onClick()
}
return self.HandleFocus()
}
func (self *ListContext) onSearchSelect(selectedLineIdx int) error {
func (self *ListContext) OnSearchSelect(selectedLineIdx int) error {
self.GetPanelState().SetSelectedLineIdx(selectedLineIdx)
return self.HandleFocus()
}
@ -281,3 +249,35 @@ func (self *ListContext) HandleRenderToMain() error {
return nil
}
func (self *ListContext) Keybindings(
getKey func(key string) interface{},
config config.KeybindingConfig,
guards types.KeybindingGuards,
) []*types.Binding {
return []*types.Binding{
{Tag: "navigation", Key: getKey(config.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: self.HandlePrevLine},
{Tag: "navigation", Key: getKey(config.Universal.PrevItem), Modifier: gocui.ModNone, Handler: self.HandlePrevLine},
{Tag: "navigation", Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: self.HandlePrevLine},
{Tag: "navigation", Key: getKey(config.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: self.HandleNextLine},
{Tag: "navigation", Key: getKey(config.Universal.NextItem), Modifier: gocui.ModNone, Handler: self.HandleNextLine},
{Tag: "navigation", Key: getKey(config.Universal.PrevPage), Modifier: gocui.ModNone, Handler: self.HandlePrevPage, Description: self.Gui.c.Tr.LcPrevPage},
{Tag: "navigation", Key: getKey(config.Universal.NextPage), Modifier: gocui.ModNone, Handler: self.HandleNextPage, Description: self.Gui.c.Tr.LcNextPage},
{Tag: "navigation", Key: getKey(config.Universal.GotoTop), Modifier: gocui.ModNone, Handler: self.HandleGotoTop, Description: self.Gui.c.Tr.LcGotoTop},
{Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: func() error { return self.HandleClick(nil) }},
{Tag: "navigation", Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: self.HandleNextLine},
{Tag: "navigation", Key: getKey(config.Universal.ScrollLeft), Modifier: gocui.ModNone, Handler: self.HandleScrollLeft},
{Tag: "navigation", Key: getKey(config.Universal.ScrollRight), Modifier: gocui.ModNone, Handler: self.HandleScrollRight},
{
Key: getKey(config.Universal.StartSearch),
Handler: func() error { return self.Gui.handleOpenSearch(self.GetViewName()) },
Description: self.Gui.c.Tr.LcStartSearch,
Tag: "navigation",
},
{
Key: getKey(config.Universal.GotoBottom),
Description: self.Gui.c.Tr.LcGotoBottom,
Tag: "navigation",
},
}
}

View File

@ -3,44 +3,41 @@ package gui
import (
"log"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
func (gui *Gui) menuListContext() IListContext {
func (gui *Gui) menuListContext() types.IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "menu",
Key: "menu",
Kind: PERSISTENT_POPUP,
Kind: types.PERSISTENT_POPUP,
OnGetOptionsMap: gui.getMenuOptions,
},
GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
OnClickSelectedItem: gui.onMenuPress,
Gui: gui,
GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() },
OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.Menu },
Gui: gui,
// no GetDisplayStrings field because we do a custom render on menu creation
}
}
func (gui *Gui) filesListContext() IListContext {
func (gui *Gui) filesListContext() types.IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "files",
WindowName: "files",
Key: FILES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
Kind: types.SIDE_CONTEXT,
},
GetItemsLength: func() int { return gui.State.FileTreeViewModel.GetItemsLength() },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Files },
OnFocus: OnFocusWrapper(gui.onFocusFile),
OnRenderToMain: OnFocusWrapper(gui.filesRenderToMain),
OnClickSelectedItem: gui.handleFilePress,
Gui: gui,
GetItemsLength: func() int { return gui.State.FileTreeViewModel.GetItemsLength() },
OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.Files },
OnFocus: OnFocusWrapper(gui.onFocusFile),
OnRenderToMain: OnFocusWrapper(gui.filesRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
lines := presentation.RenderFileTree(gui.State.FileTreeViewModel, gui.State.Modes.Diffing.Ref, gui.State.Submodules)
mappedLines := make([][]string, len(lines))
@ -50,117 +47,115 @@ func (gui *Gui) filesListContext() IListContext {
return mappedLines
},
SelectedItem: func() (ListItem, bool) {
SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedFileNode()
return item, item != nil
},
}
}
func (gui *Gui) branchesListContext() IListContext {
func (gui *Gui) branchesListContext() types.IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
WindowName: "branches",
Key: LOCAL_BRANCHES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
Kind: types.SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Branches) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Branches },
OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.Branches },
OnRenderToMain: OnFocusWrapper(gui.branchesRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedBranch()
return item, item != nil
},
}
}
func (gui *Gui) remotesListContext() IListContext {
func (gui *Gui) remotesListContext() types.IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
WindowName: "branches",
Key: REMOTES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
Kind: types.SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Remotes) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Remotes },
OnRenderToMain: OnFocusWrapper(gui.remotesRenderToMain),
OnClickSelectedItem: gui.handleRemoteEnter,
Gui: gui,
GetItemsLength: func() int { return len(gui.State.Remotes) },
OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.Remotes },
OnRenderToMain: OnFocusWrapper(gui.remotesRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedRemote()
return item, item != nil
},
}
}
func (gui *Gui) remoteBranchesListContext() IListContext {
func (gui *Gui) remoteBranchesListContext() types.IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
WindowName: "branches",
Key: REMOTE_BRANCHES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
Kind: types.SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.RemoteBranches) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.RemoteBranches },
OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.RemoteBranches },
OnRenderToMain: OnFocusWrapper(gui.remoteBranchesRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedRemoteBranch()
return item, item != nil
},
}
}
func (gui *Gui) tagsListContext() IListContext {
func (gui *Gui) tagsListContext() types.IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
WindowName: "branches",
Key: TAGS_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
Kind: types.SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Tags) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Tags },
OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.Tags },
OnRenderToMain: OnFocusWrapper(gui.tagsRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedTag()
return item, item != nil
},
}
}
func (gui *Gui) branchCommitsListContext() IListContext {
parseEmoji := gui.UserConfig.Git.ParseEmoji
func (gui *Gui) branchCommitsListContext() types.IListContext {
parseEmoji := gui.c.UserConfig.Git.ParseEmoji
return &ListContext{
BasicContext: &BasicContext{
ViewName: "commits",
WindowName: "commits",
Key: BRANCH_COMMITS_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
Kind: types.SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Commits) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Commits },
OnFocus: OnFocusWrapper(gui.onCommitFocus),
OnRenderToMain: OnFocusWrapper(gui.branchCommitsRenderToMain),
OnClickSelectedItem: gui.handleViewCommitFiles,
Gui: gui,
GetItemsLength: func() int { return len(gui.State.Commits) },
OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.Commits },
OnFocus: OnFocusWrapper(gui.onCommitFocus),
OnRenderToMain: OnFocusWrapper(gui.branchCommitsRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
selectedCommitSha := ""
if gui.currentContext().GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
@ -182,7 +177,7 @@ func (gui *Gui) branchCommitsListContext() IListContext {
gui.State.BisectInfo,
)
},
SelectedItem: func() (ListItem, bool) {
SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedLocalCommit()
return item, item != nil
},
@ -190,17 +185,17 @@ func (gui *Gui) branchCommitsListContext() IListContext {
}
}
func (gui *Gui) subCommitsListContext() IListContext {
parseEmoji := gui.UserConfig.Git.ParseEmoji
func (gui *Gui) subCommitsListContext() types.IListContext {
parseEmoji := gui.c.UserConfig.Git.ParseEmoji
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
WindowName: "branches",
Key: SUB_COMMITS_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
Kind: types.SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.SubCommits) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.SubCommits },
OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.SubCommits },
OnRenderToMain: OnFocusWrapper(gui.subCommitsRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
@ -224,7 +219,7 @@ func (gui *Gui) subCommitsListContext() IListContext {
git_commands.NewNullBisectInfo(),
)
},
SelectedItem: func() (ListItem, bool) {
SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedSubCommit()
return item, item != nil
},
@ -237,7 +232,7 @@ func (gui *Gui) shouldShowGraph() bool {
return false
}
value := gui.UserConfig.Git.Log.ShowGraph
value := gui.c.UserConfig.Git.Log.ShowGraph
switch value {
case "always":
return true
@ -251,17 +246,17 @@ func (gui *Gui) shouldShowGraph() bool {
return false
}
func (gui *Gui) reflogCommitsListContext() IListContext {
parseEmoji := gui.UserConfig.Git.ParseEmoji
func (gui *Gui) reflogCommitsListContext() types.IListContext {
parseEmoji := gui.c.UserConfig.Git.ParseEmoji
return &ListContext{
BasicContext: &BasicContext{
ViewName: "commits",
WindowName: "commits",
Key: REFLOG_COMMITS_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
Kind: types.SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.ReflogCommits },
OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.ReflogCommits },
OnRenderToMain: OnFocusWrapper(gui.reflogCommitsRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
@ -273,45 +268,45 @@ func (gui *Gui) reflogCommitsListContext() IListContext {
parseEmoji,
)
},
SelectedItem: func() (ListItem, bool) {
SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedReflogCommit()
return item, item != nil
},
}
}
func (gui *Gui) stashListContext() IListContext {
func (gui *Gui) stashListContext() types.IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "stash",
WindowName: "stash",
Key: STASH_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
Kind: types.SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.StashEntries) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Stash },
OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.Stash },
OnRenderToMain: OnFocusWrapper(gui.stashRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedStashEntry()
return item, item != nil
},
}
}
func (gui *Gui) commitFilesListContext() IListContext {
func (gui *Gui) commitFilesListContext() types.IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "commitFiles",
WindowName: "commits",
Key: COMMIT_FILES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
Kind: types.SIDE_CONTEXT,
},
GetItemsLength: func() int { return gui.State.CommitFileTreeViewModel.GetItemsLength() },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.CommitFiles },
OnFocus: OnFocusWrapper(gui.onCommitFileFocus),
OnRenderToMain: OnFocusWrapper(gui.commitFilesRenderToMain),
Gui: gui,
@ -320,7 +315,7 @@ func (gui *Gui) commitFilesListContext() IListContext {
return [][]string{{style.FgRed.Sprint("(none)")}}
}
lines := presentation.RenderCommitFileTree(gui.State.CommitFileTreeViewModel, gui.State.Modes.Diffing.Ref, gui.Git.Patch.PatchManager)
lines := presentation.RenderCommitFileTree(gui.State.CommitFileTreeViewModel, gui.State.Modes.Diffing.Ref, gui.git.Patch.PatchManager)
mappedLines := make([][]string, len(lines))
for i, line := range lines {
mappedLines[i] = []string{line}
@ -328,45 +323,45 @@ func (gui *Gui) commitFilesListContext() IListContext {
return mappedLines
},
SelectedItem: func() (ListItem, bool) {
SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedCommitFileNode()
return item, item != nil
},
}
}
func (gui *Gui) submodulesListContext() IListContext {
func (gui *Gui) submodulesListContext() types.IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "files",
WindowName: "files",
Key: SUBMODULES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
Kind: types.SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Submodules) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Submodules },
OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.Submodules },
OnRenderToMain: OnFocusWrapper(gui.submodulesRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetSubmoduleListDisplayStrings(gui.State.Submodules)
},
SelectedItem: func() (ListItem, bool) {
SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedSubmodule()
return item, item != nil
},
}
}
func (gui *Gui) suggestionsListContext() IListContext {
func (gui *Gui) suggestionsListContext() types.IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "suggestions",
WindowName: "suggestions",
Key: SUGGESTIONS_CONTEXT_KEY,
Kind: PERSISTENT_POPUP,
Kind: types.PERSISTENT_POPUP,
},
GetItemsLength: func() int { return len(gui.State.Suggestions) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Suggestions },
OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.Suggestions },
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions)
@ -374,8 +369,8 @@ func (gui *Gui) suggestionsListContext() IListContext {
}
}
func (gui *Gui) getListContexts() []IListContext {
return []IListContext{
func (gui *Gui) getListContexts() []types.IListContext {
return []types.IListContext{
gui.State.Contexts.Menu,
gui.State.Contexts.Files,
gui.State.Contexts.Branches,
@ -391,58 +386,3 @@ func (gui *Gui) getListContexts() []IListContext {
gui.State.Contexts.Suggestions,
}
}
func (gui *Gui) getListContextKeyBindings() []*types.Binding {
bindings := make([]*types.Binding, 0)
keybindingConfig := gui.UserConfig.Keybinding
for _, listContext := range gui.getListContexts() {
listContext := listContext
bindings = append(bindings, []*types.Binding{
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.NextItem), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.PrevPage), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.LcPrevPage},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.NextPage), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.LcNextPage},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.GotoTop), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.LcGotoTop},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.GetViewName(), Contexts: []string{string(listContext.GetKey())}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listContext.handleClick},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.ScrollLeft), Modifier: gocui.ModNone, Handler: listContext.handleScrollLeft},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.ScrollRight), Modifier: gocui.ModNone, Handler: listContext.handleScrollRight},
}...)
openSearchHandler := gui.handleOpenSearch
gotoBottomHandler := listContext.handleGotoBottom
// the branch commits context needs to lazyload things so it has a couple of its own handlers
if listContext.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
openSearchHandler = gui.handleOpenSearchForCommitsPanel
gotoBottomHandler = gui.handleGotoBottomForCommitsPanel
}
bindings = append(bindings, []*types.Binding{
{
ViewName: listContext.GetViewName(),
Contexts: []string{string(listContext.GetKey())},
Key: gui.getKey(keybindingConfig.Universal.StartSearch),
Handler: func() error { return openSearchHandler(listContext.GetViewName()) },
Description: gui.Tr.LcStartSearch,
Tag: "navigation",
},
{
ViewName: listContext.GetViewName(),
Contexts: []string{string(listContext.GetKey())},
Key: gui.getKey(keybindingConfig.Universal.GotoBottom),
Handler: gotoBottomHandler,
Description: gui.Tr.LcGotoBottom,
Tag: "navigation",
},
}...)
}
return bindings
}

View File

@ -124,7 +124,7 @@ func (gui *Gui) refreshMainView(opts *viewUpdateOpts, view *gocui.View) error {
view.Highlight = opts.highlight
if err := gui.runTaskForView(view, opts.task); err != nil {
gui.Log.Error(err)
gui.c.Log.Error(err)
return nil
}

View File

@ -10,12 +10,12 @@ import (
)
func (gui *Gui) getMenuOptions() map[string]string {
keybindingConfig := gui.UserConfig.Keybinding
keybindingConfig := gui.c.UserConfig.Keybinding
return map[string]string{
gui.getKeyDisplay(keybindingConfig.Universal.Return): gui.Tr.LcClose,
fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevItem), gui.getKeyDisplay(keybindingConfig.Universal.NextItem)): gui.Tr.LcNavigate,
gui.getKeyDisplay(keybindingConfig.Universal.Select): gui.Tr.LcExecute,
gui.getKeyDisplay(keybindingConfig.Universal.Return): gui.c.Tr.LcClose,
fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevItem), gui.getKeyDisplay(keybindingConfig.Universal.NextItem)): gui.c.Tr.LcNavigate,
gui.getKeyDisplay(keybindingConfig.Universal.Select): gui.c.Tr.LcExecute,
}
}
@ -28,7 +28,7 @@ func (gui *Gui) createMenu(opts popup.CreateMenuOptions) error {
if !opts.HideCancel {
// this is mutative but I'm okay with that for now
opts.Items = append(opts.Items, &popup.MenuItem{
DisplayStrings: []string{gui.Tr.LcCancel},
DisplayStrings: []string{gui.c.Tr.LcCancel},
OnPress: func() error {
return nil
},
@ -66,18 +66,13 @@ func (gui *Gui) createMenu(opts popup.CreateMenuOptions) error {
menuView.SetContent(list)
gui.State.Panels.Menu.SelectedLineIdx = 0
return gui.pushContext(gui.State.Contexts.Menu)
return gui.c.PushContext(gui.State.Contexts.Menu)
}
func (gui *Gui) onMenuPress() error {
selectedLine := gui.State.Panels.Menu.SelectedLineIdx
if err := gui.returnFromContext(); err != nil {
return err
func (gui *Gui) getSelectedMenuItem() *popup.MenuItem {
if len(gui.State.MenuItems) == 0 {
return nil
}
if err := gui.State.MenuItems[selectedLine].OnPress(); err != nil {
return err
}
return nil
return gui.State.MenuItems[gui.State.Panels.Menu.SelectedLineIdx]
}

View File

@ -52,8 +52,8 @@ func (gui *Gui) handleMergeConflictUndo() error {
return nil
}
gui.logAction("Restoring file to previous state")
gui.logCommand("Undoing last conflict resolution", false)
gui.c.LogAction("Restoring file to previous state")
gui.LogCommand("Undoing last conflict resolution", false)
if err := ioutil.WriteFile(state.GetPath(), []byte(state.GetContent()), 0644); err != nil {
return err
}
@ -124,8 +124,8 @@ func (gui *Gui) resolveConflict(selection mergeconflicts.Selection) (bool, error
case mergeconflicts.ALL:
logStr = "Picking all hunks"
}
gui.logAction("Resolve merge conflict")
gui.logCommand(logStr, false)
gui.c.LogAction("Resolve merge conflict")
gui.LogCommand(logStr, false)
state.PushContent(content)
return true, ioutil.WriteFile(state.GetPath(), []byte(content), 0644)
}
@ -153,7 +153,7 @@ func (gui *Gui) renderConflicts(hasFocus bool) error {
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: gui.Tr.MergeConflictsTitle,
title: gui.c.Tr.MergeConflictsTitle,
task: NewRenderStringWithoutScrollTask(content),
noWrap: true,
},
@ -178,19 +178,19 @@ func (gui *Gui) centerYPos(view *gocui.View, y int) {
}
func (gui *Gui) getMergingOptions() map[string]string {
keybindingConfig := gui.UserConfig.Keybinding
keybindingConfig := gui.c.UserConfig.Keybinding
return map[string]string{
fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevItem), gui.getKeyDisplay(keybindingConfig.Universal.NextItem)): gui.Tr.LcSelectHunk,
fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevBlock), gui.getKeyDisplay(keybindingConfig.Universal.NextBlock)): gui.Tr.LcNavigateConflicts,
gui.getKeyDisplay(keybindingConfig.Universal.Select): gui.Tr.LcPickHunk,
gui.getKeyDisplay(keybindingConfig.Main.PickBothHunks): gui.Tr.LcPickAllHunks,
gui.getKeyDisplay(keybindingConfig.Universal.Undo): gui.Tr.LcUndo,
fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevItem), gui.getKeyDisplay(keybindingConfig.Universal.NextItem)): gui.c.Tr.LcSelectHunk,
fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevBlock), gui.getKeyDisplay(keybindingConfig.Universal.NextBlock)): gui.c.Tr.LcNavigateConflicts,
gui.getKeyDisplay(keybindingConfig.Universal.Select): gui.c.Tr.LcPickHunk,
gui.getKeyDisplay(keybindingConfig.Main.PickBothHunks): gui.c.Tr.LcPickAllHunks,
gui.getKeyDisplay(keybindingConfig.Universal.Undo): gui.c.Tr.LcUndo,
}
}
func (gui *Gui) handleEscapeMerge() error {
if err := gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil {
if err := gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil {
return err
}
@ -200,7 +200,7 @@ func (gui *Gui) handleEscapeMerge() error {
func (gui *Gui) onLastConflictResolved() error {
// as part of refreshing files, we handle the situation where a file has had
// its merge conflicts resolved.
return gui.refreshSidePanels(types.RefreshOptions{mode: types.ASYNC, scope: []types.RefreshableView{types.FILES}})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
}
func (gui *Gui) resetMergeState() {
@ -209,7 +209,7 @@ func (gui *Gui) resetMergeState() {
}
func (gui *Gui) setMergeState(path string) (bool, error) {
content, err := gui.Git.File.Cat(path)
content, err := gui.git.File.Cat(path)
if err != nil {
return false, err
}
@ -269,7 +269,6 @@ func (gui *Gui) setConflictsAndRender(path string, hasFocus bool) (bool, error)
return false, err
}
// if we don't have conflicts we'll fall through and show the diff
if hasConflicts {
return true, gui.renderConflicts(hasFocus)
}
@ -294,7 +293,7 @@ func (gui *Gui) refreshMergeState() error {
hasConflicts, err := gui.setConflictsAndRender(gui.State.Panels.Merging.GetPath(), true)
if err != nil {
return gui.surfaceError(err)
return gui.c.Error(err)
}
if !hasConflicts {
@ -303,3 +302,19 @@ func (gui *Gui) refreshMergeState() error {
return nil
}
func (gui *Gui) switchToMerge(path string) error {
gui.takeOverMergeConflictScrolling()
if gui.State.Panels.Merging.GetPath() != path {
hasConflicts, err := gui.setMergeStateWithLock(path)
if err != nil {
return err
}
if !hasConflicts {
return nil
}
}
return gui.c.PushContext(gui.State.Contexts.Merging)
}

19
pkg/gui/misc.go Normal file
View File

@ -0,0 +1,19 @@
package gui
// this file is to put things where it's not obvious where they belong while this refactor takes place
func (gui *Gui) getSuggestedRemote() string {
remotes := gui.State.Remotes
if len(remotes) == 0 {
return "origin"
}
for _, remote := range remotes {
if remote.Name == "origin" {
return remote.Name
}
}
return remotes[0].Name
}

View File

@ -21,7 +21,7 @@ func (gui *Gui) modeStatuses() []modeStatus {
return gui.withResetButton(
fmt.Sprintf(
"%s %s",
gui.Tr.LcShowingGitDiff,
gui.c.Tr.LcShowingGitDiff,
"git diff "+gui.diffStr(),
),
style.FgMagenta,
@ -30,9 +30,9 @@ func (gui *Gui) modeStatuses() []modeStatus {
reset: gui.exitDiffMode,
},
{
isActive: gui.Git.Patch.PatchManager.Active,
isActive: gui.git.Patch.PatchManager.Active,
description: func() string {
return gui.withResetButton(gui.Tr.LcBuildingPatch, style.FgYellow.SetBold())
return gui.withResetButton(gui.c.Tr.LcBuildingPatch, style.FgYellow.SetBold())
},
reset: gui.handleResetPatch,
},
@ -42,7 +42,7 @@ func (gui *Gui) modeStatuses() []modeStatus {
return gui.withResetButton(
fmt.Sprintf(
"%s '%s'",
gui.Tr.LcFilteringBy,
gui.c.Tr.LcFilteringBy,
gui.State.Modes.Filtering.GetPath(),
),
style.FgRed,
@ -65,10 +65,10 @@ func (gui *Gui) modeStatuses() []modeStatus {
},
{
isActive: func() bool {
return gui.Git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE
return gui.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE
},
description: func() string {
workingTreeState := gui.Git.Status.WorkingTreeState()
workingTreeState := gui.git.Status.WorkingTreeState()
return gui.withResetButton(
formatWorkingTreeState(workingTreeState), style.FgYellow,
)
@ -82,7 +82,7 @@ func (gui *Gui) modeStatuses() []modeStatus {
description: func() string {
return gui.withResetButton("bisecting", style.FgGreen)
},
reset: gui.resetBisect,
reset: gui.Controllers.Bisect.Reset,
},
}
}
@ -91,6 +91,6 @@ func (gui *Gui) withResetButton(content string, textStyle style.TextStyle) strin
return textStyle.Sprintf(
"%s %s",
content,
style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses),
style.AttrUnderline.Sprint(gui.c.Tr.ResetInParentheses),
)
}

View File

@ -74,8 +74,8 @@ func (gui *Gui) handleCreateOptionsMenu() error {
}
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{
Title: strings.Title(gui.Tr.LcMenu),
return gui.c.Menu(popup.CreateMenuOptions{
Title: strings.Title(gui.c.Tr.LcMenu),
Items: menuItems,
HideCancel: true,
})

View File

@ -18,7 +18,7 @@ func (gui *Gui) getFromAndReverseArgsForDiff(to string) (string, bool) {
}
func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
if !gui.Git.Patch.PatchManager.Active() {
if !gui.git.Patch.PatchManager.Active() {
return gui.handleEscapePatchBuildingPanel()
}
@ -33,12 +33,12 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
to := gui.State.CommitFileTreeViewModel.GetParent()
from, reverse := gui.getFromAndReverseArgsForDiff(to)
diff, err := gui.Git.WorkingTree.ShowFileDiff(from, to, reverse, node.GetPath(), true)
diff, err := gui.git.WorkingTree.ShowFileDiff(from, to, reverse, node.GetPath(), true)
if err != nil {
return err
}
secondaryDiff := gui.Git.Patch.PatchManager.RenderPatchForFile(node.GetPath(), true, false, true)
secondaryDiff := gui.git.Patch.PatchManager.RenderPatchForFile(node.GetPath(), true, false, true)
if err != nil {
return err
}
@ -75,15 +75,15 @@ func (gui *Gui) onPatchBuildingFocus(selectedLineIdx int) error {
func (gui *Gui) handleToggleSelectionForPatch() error {
err := gui.withLBLActiveCheck(func(state *LblPanelState) error {
toggleFunc := gui.Git.Patch.PatchManager.AddFileLineRange
toggleFunc := gui.git.Patch.PatchManager.AddFileLineRange
filename := gui.getSelectedCommitFileName()
includedLineIndices, err := gui.Git.Patch.PatchManager.GetFileIncLineIndices(filename)
includedLineIndices, err := gui.git.Patch.PatchManager.GetFileIncLineIndices(filename)
if err != nil {
return err
}
currentLineIsStaged := utils.IncludesInt(includedLineIndices, state.GetSelectedLineIdx())
if currentLineIsStaged {
toggleFunc = gui.Git.Patch.PatchManager.RemoveFileLineRange
toggleFunc = gui.git.Patch.PatchManager.RemoveFileLineRange
}
// add range of lines to those set for the file
@ -96,7 +96,7 @@ func (gui *Gui) handleToggleSelectionForPatch() error {
if err := toggleFunc(node.GetPath(), firstLineIdx, lastLineIdx); err != nil {
// might actually want to return an error here
gui.Log.Error(err)
gui.c.Log.Error(err)
}
return nil
@ -116,12 +116,12 @@ func (gui *Gui) handleToggleSelectionForPatch() error {
func (gui *Gui) handleEscapePatchBuildingPanel() error {
gui.escapeLineByLinePanel()
if gui.Git.Patch.PatchManager.IsEmpty() {
gui.Git.Patch.PatchManager.Reset()
if gui.git.Patch.PatchManager.IsEmpty() {
gui.git.Patch.PatchManager.Reset()
}
if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() {
return gui.pushContext(gui.State.Contexts.CommitFiles)
return gui.c.PushContext(gui.State.Contexts.CommitFiles)
} else {
// need to re-focus in case the secondary view should now be hidden
return gui.currentContext().HandleFocus()
@ -129,8 +129,8 @@ func (gui *Gui) handleEscapePatchBuildingPanel() error {
}
func (gui *Gui) secondaryPatchPanelUpdateOpts() *viewUpdateOpts {
if gui.Git.Patch.PatchManager.Active() {
patch := gui.Git.Patch.PatchManager.RenderAggregatedPatchColored(false)
if gui.git.Patch.PatchManager.Active() {
patch := gui.git.Patch.PatchManager.RenderAggregatedPatchColored(false)
return &viewUpdateOpts{
title: "Custom Patch",

View File

@ -9,8 +9,8 @@ import (
)
func (gui *Gui) handleCreatePatchOptionsMenu() error {
if !gui.Git.Patch.PatchManager.Active() {
return gui.PopupHandler.ErrorMsg(gui.Tr.NoPatchError)
if !gui.git.Patch.PatchManager.Active() {
return gui.c.ErrorMsg(gui.c.Tr.NoPatchError)
}
menuItems := []*popup.MenuItem{
@ -28,10 +28,10 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error {
},
}
if gui.Git.Patch.PatchManager.CanRebase && gui.Git.Status.WorkingTreeState() == enums.REBASE_MODE_NONE {
if gui.git.Patch.PatchManager.CanRebase && gui.git.Status.WorkingTreeState() == enums.REBASE_MODE_NONE {
menuItems = append(menuItems, []*popup.MenuItem{
{
DisplayString: fmt.Sprintf("remove patch from original commit (%s)", gui.Git.Patch.PatchManager.To),
DisplayString: fmt.Sprintf("remove patch from original commit (%s)", gui.git.Patch.PatchManager.To),
OnPress: gui.handleDeletePatchFromCommit,
},
{
@ -46,7 +46,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error {
if gui.currentContext().GetKey() == gui.State.Contexts.BranchCommits.GetKey() {
selectedCommit := gui.getSelectedLocalCommit()
if selectedCommit != nil && gui.Git.Patch.PatchManager.To != selectedCommit.Sha {
if selectedCommit != nil && gui.git.Patch.PatchManager.To != selectedCommit.Sha {
// adding this option to index 1
menuItems = append(
menuItems[:1],
@ -63,12 +63,12 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error {
}
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: gui.Tr.PatchOptionsTitle, Items: menuItems})
return gui.c.Menu(popup.CreateMenuOptions{Title: gui.c.Tr.PatchOptionsTitle, Items: menuItems})
}
func (gui *Gui) getPatchCommitIndex() int {
for index, commit := range gui.State.Commits {
if commit.Sha == gui.Git.Patch.PatchManager.To {
if commit.Sha == gui.git.Patch.PatchManager.To {
return index
}
}
@ -76,8 +76,8 @@ func (gui *Gui) getPatchCommitIndex() int {
}
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
if gui.Git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return false, gui.PopupHandler.ErrorMsg(gui.Tr.CantPatchWhileRebasingError)
if gui.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return false, gui.c.ErrorMsg(gui.c.Tr.CantPatchWhileRebasingError)
}
return true, nil
}
@ -98,11 +98,11 @@ func (gui *Gui) handleDeletePatchFromCommit() error {
return err
}
return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error {
commitIndex := gui.getPatchCommitIndex()
gui.logAction(gui.Tr.Actions.RemovePatchFromCommit)
err := gui.Git.Patch.DeletePatchesFromCommit(gui.State.Commits, commitIndex)
return gui.handleGenericMergeCommandResult(err)
gui.c.LogAction(gui.c.Tr.Actions.RemovePatchFromCommit)
err := gui.git.Patch.DeletePatchesFromCommit(gui.State.Commits, commitIndex)
return gui.checkMergeOrRebase(err)
})
}
@ -115,11 +115,11 @@ func (gui *Gui) handleMovePatchToSelectedCommit() error {
return err
}
return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error {
commitIndex := gui.getPatchCommitIndex()
gui.logAction(gui.Tr.Actions.MovePatchToSelectedCommit)
err := gui.Git.Patch.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx)
return gui.handleGenericMergeCommandResult(err)
gui.c.LogAction(gui.c.Tr.Actions.MovePatchToSelectedCommit)
err := gui.git.Patch.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx)
return gui.checkMergeOrRebase(err)
})
}
@ -133,18 +133,18 @@ func (gui *Gui) handleMovePatchIntoWorkingTree() error {
}
pull := func(stash bool) error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error {
commitIndex := gui.getPatchCommitIndex()
gui.logAction(gui.Tr.Actions.MovePatchIntoIndex)
err := gui.Git.Patch.MovePatchIntoIndex(gui.State.Commits, commitIndex, stash)
return gui.handleGenericMergeCommandResult(err)
gui.c.LogAction(gui.c.Tr.Actions.MovePatchIntoIndex)
err := gui.git.Patch.MovePatchIntoIndex(gui.State.Commits, commitIndex, stash)
return gui.checkMergeOrRebase(err)
})
}
if len(gui.trackedFiles()) > 0 {
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.MustStashTitle,
Prompt: gui.Tr.MustStashWarning,
if gui.workingTreeHelper.IsWorkingTreeDirty() {
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.MustStashTitle,
Prompt: gui.c.Tr.MustStashWarning,
HandleConfirm: func() error {
return pull(true)
},
@ -163,11 +163,11 @@ func (gui *Gui) handlePullPatchIntoNewCommit() error {
return err
}
return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error {
commitIndex := gui.getPatchCommitIndex()
gui.logAction(gui.Tr.Actions.MovePatchIntoNewCommit)
err := gui.Git.Patch.PullPatchIntoNewCommit(gui.State.Commits, commitIndex)
return gui.handleGenericMergeCommandResult(err)
gui.c.LogAction(gui.c.Tr.Actions.MovePatchIntoNewCommit)
err := gui.git.Patch.PullPatchIntoNewCommit(gui.State.Commits, commitIndex)
return gui.checkMergeOrRebase(err)
})
}
@ -176,21 +176,21 @@ func (gui *Gui) handleApplyPatch(reverse bool) error {
return err
}
action := gui.Tr.Actions.ApplyPatch
action := gui.c.Tr.Actions.ApplyPatch
if reverse {
action = "Apply patch in reverse"
}
gui.logAction(action)
if err := gui.Git.Patch.PatchManager.ApplyPatches(reverse); err != nil {
return gui.PopupHandler.Error(err)
gui.c.LogAction(action)
if err := gui.git.Patch.PatchManager.ApplyPatches(reverse); err != nil {
return gui.c.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC})
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
}
func (gui *Gui) handleResetPatch() error {
gui.Git.Patch.PatchManager.Reset()
gui.git.Patch.PatchManager.Reset()
if gui.currentContextKeyIgnoringPopups() == MAIN_PATCH_BUILDING_CONTEXT_KEY {
if err := gui.pushContext(gui.State.Contexts.CommitFiles); err != nil {
if err := gui.c.PushContext(gui.State.Contexts.CommitFiles); err != nil {
return err
}
}

View File

@ -19,6 +19,8 @@ type IPopupHandler interface {
WithLoaderPanel(message string, f func() error) error
WithWaitingStatus(message string, f func() error) error
Menu(opts CreateMenuOptions) error
Toast(message string)
GetPromptInput() string
}
type CreateMenuOptions struct {
@ -74,6 +76,8 @@ type RealPopupHandler struct {
closePopupFn func() error
createMenuFn func(CreateMenuOptions) error
withWaitingStatusFn func(message string, f func() error) error
toastFn func(message string)
getPromptInputFn func() string
}
var _ IPopupHandler = &RealPopupHandler{}
@ -85,6 +89,8 @@ func NewPopupHandler(
closePopupFn func() error,
createMenuFn func(CreateMenuOptions) error,
withWaitingStatusFn func(message string, f func() error) error,
toastFn func(message string),
getPromptInputFn func() string,
) *RealPopupHandler {
return &RealPopupHandler{
Common: common,
@ -94,6 +100,8 @@ func NewPopupHandler(
closePopupFn: closePopupFn,
createMenuFn: createMenuFn,
withWaitingStatusFn: withWaitingStatusFn,
toastFn: toastFn,
getPromptInputFn: getPromptInputFn,
}
}
@ -101,6 +109,10 @@ func (self *RealPopupHandler) Menu(opts CreateMenuOptions) error {
return self.createMenuFn(opts)
}
func (self *RealPopupHandler) Toast(message string) {
self.toastFn(message)
}
func (self *RealPopupHandler) WithWaitingStatus(message string, f func() error) error {
return self.withWaitingStatusFn(message, f)
}
@ -188,6 +200,12 @@ func (self *RealPopupHandler) WithLoaderPanel(message string, f func() error) er
return nil
}
// returns the content that has currently been typed into the prompt. Useful for
// asyncronously updating the suggestions list under the prompt.
func (self *RealPopupHandler) GetPromptInput() string {
return self.getPromptInputFn()
}
type TestPopupHandler struct {
OnErrorMsg func(message string) error
OnAsk func(opts AskOpts) error
@ -221,3 +239,11 @@ func (self *TestPopupHandler) WithWaitingStatus(message string, f func() error)
func (self *TestPopupHandler) Menu(opts CreateMenuOptions) error {
panic("not yet implemented")
}
func (self *TestPopupHandler) Toast(message string) {
panic("not yet implemented")
}
func (self *TestPopupHandler) CurrentInput() string {
panic("not yet implemented")
}

View File

@ -41,7 +41,7 @@ func (gui *Gui) onResize() error {
// command.
func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error {
width, _ := gui.Views.Main.Size()
pager := gui.Git.Config.GetPager(width)
pager := gui.git.Config.GetPager(width)
if pager == "" {
// if we're not using a custom pager we don't need to use a pty
@ -60,7 +60,7 @@ func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
start := func() (*exec.Cmd, io.Reader) {
ptmx, err := pty.StartWithSize(cmd, gui.desiredPtySize())
if err != nil {
gui.Log.Error(err)
gui.c.Log.Error(err)
}
gui.State.Ptmx = ptmx

View File

@ -18,17 +18,17 @@ func (gui *Gui) createPullRequestMenu(selectedBranch *models.Branch, checkedOutB
menuItemsForBranch := func(branch *models.Branch) []*popup.MenuItem {
return []*popup.MenuItem{
{
DisplayStrings: fromToDisplayStrings(branch.Name, gui.Tr.LcDefaultBranch),
DisplayStrings: fromToDisplayStrings(branch.Name, gui.c.Tr.LcDefaultBranch),
OnPress: func() error {
return gui.createPullRequest(branch.Name, "")
},
},
{
DisplayStrings: fromToDisplayStrings(branch.Name, gui.Tr.LcSelectBranch),
DisplayStrings: fromToDisplayStrings(branch.Name, gui.c.Tr.LcSelectBranch),
OnPress: func() error {
return gui.PopupHandler.Prompt(popup.PromptOpts{
return gui.c.Prompt(popup.PromptOpts{
Title: branch.Name + " →",
FindSuggestionsFunc: gui.getBranchNameSuggestionsFunc(),
FindSuggestionsFunc: gui.suggestionsHelper.GetBranchNameSuggestionsFunc(),
HandleConfirm: func(targetBranchName string) error {
return gui.createPullRequest(branch.Name, targetBranchName)
}},
@ -52,27 +52,27 @@ func (gui *Gui) createPullRequestMenu(selectedBranch *models.Branch, checkedOutB
menuItems = append(menuItems, menuItemsForBranch(selectedBranch)...)
return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: fmt.Sprintf(gui.Tr.CreatePullRequestOptions), Items: menuItems})
return gui.c.Menu(popup.CreateMenuOptions{Title: fmt.Sprintf(gui.c.Tr.CreatePullRequestOptions), Items: menuItems})
}
func (gui *Gui) createPullRequest(from string, to string) error {
hostingServiceMgr := gui.getHostingServiceMgr()
url, err := hostingServiceMgr.GetPullRequestURL(from, to)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
gui.logAction(gui.Tr.Actions.OpenPullRequest)
gui.c.LogAction(gui.c.Tr.Actions.OpenPullRequest)
if err := gui.OSCommand.OpenLink(url); err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
return nil
}
func (gui *Gui) getHostingServiceMgr() *hosting_service.HostingServiceMgr {
remoteUrl := gui.Git.Config.GetRemoteURL()
configServices := gui.UserConfig.Services
remoteUrl := gui.git.Config.GetRemoteURL()
configServices := gui.c.UserConfig.Services
return hosting_service.NewHostingServiceMgr(gui.Log, gui.Tr, remoteUrl, configServices)
}

View File

@ -44,7 +44,7 @@ func (gui *Gui) handleTopLevelReturn() error {
parentContext, hasParent := currentContext.GetParentContext()
if hasParent && currentContext != nil && parentContext != nil {
// TODO: think about whether this should be marked as a return rather than adding to the stack
return gui.pushContext(parentContext)
return gui.c.PushContext(parentContext)
}
for _, mode := range gui.modeStatuses() {
@ -60,7 +60,7 @@ func (gui *Gui) handleTopLevelReturn() error {
return gui.dispatchSwitchToRepo(path, true)
}
if gui.UserConfig.QuitOnTopLevelReturn {
if gui.c.UserConfig.QuitOnTopLevelReturn {
return gui.handleQuit()
}
@ -72,10 +72,10 @@ func (gui *Gui) quit() error {
return gui.createUpdateQuitConfirmation()
}
if gui.UserConfig.ConfirmOnQuit {
return gui.PopupHandler.Ask(popup.AskOpts{
if gui.c.UserConfig.ConfirmOnQuit {
return gui.c.Ask(popup.AskOpts{
Title: "",
Prompt: gui.Tr.ConfirmQuit,
Prompt: gui.c.Tr.ConfirmQuit,
HandleConfirm: func() error {
return gocui.ErrQuit
},

View File

@ -20,7 +20,7 @@ const (
func (gui *Gui) handleCreateRebaseOptionsMenu() error {
options := []string{REBASE_OPTION_CONTINUE, REBASE_OPTION_ABORT}
if gui.Git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if gui.git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
options = append(options, REBASE_OPTION_SKIP)
}
@ -37,23 +37,23 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error {
}
var title string
if gui.Git.Status.WorkingTreeState() == enums.REBASE_MODE_MERGING {
title = gui.Tr.MergeOptionsTitle
if gui.git.Status.WorkingTreeState() == enums.REBASE_MODE_MERGING {
title = gui.c.Tr.MergeOptionsTitle
} else {
title = gui.Tr.RebaseOptionsTitle
title = gui.c.Tr.RebaseOptionsTitle
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: title, Items: menuItems})
return gui.c.Menu(popup.CreateMenuOptions{Title: title, Items: menuItems})
}
func (gui *Gui) genericMergeCommand(command string) error {
status := gui.Git.Status.WorkingTreeState()
status := gui.git.Status.WorkingTreeState()
if status != enums.REBASE_MODE_MERGING && status != enums.REBASE_MODE_REBASING {
return gui.PopupHandler.ErrorMsg(gui.Tr.NotMergingOrRebasing)
return gui.c.ErrorMsg(gui.c.Tr.NotMergingOrRebasing)
}
gui.logAction(fmt.Sprintf("Merge/Rebase: %s", command))
gui.c.LogAction(fmt.Sprintf("Merge/Rebase: %s", command))
commandType := ""
switch status {
@ -68,14 +68,14 @@ func (gui *Gui) genericMergeCommand(command string) error {
// we should end up with a command like 'git merge --continue'
// it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge
if status == enums.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && gui.UserConfig.Git.Merging.ManualCommit {
if status == enums.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && gui.c.UserConfig.Git.Merging.ManualCommit {
// TODO: see if we should be calling more of the code from gui.Git.Rebase.GenericMergeOrRebaseAction
return gui.runSubprocessWithSuspenseAndRefresh(
gui.Git.Rebase.GenericMergeOrRebaseActionCmdObj(commandType, command),
gui.git.Rebase.GenericMergeOrRebaseActionCmdObj(commandType, command),
)
}
result := gui.Git.Rebase.GenericMergeOrRebaseAction(commandType, command)
if err := gui.handleGenericMergeCommandResult(result); err != nil {
result := gui.git.Rebase.GenericMergeOrRebaseAction(commandType, command)
if err := gui.checkMergeOrRebase(result); err != nil {
return err
}
return nil
@ -98,8 +98,8 @@ func isMergeConflictErr(errStr string) bool {
return false
}
func (gui *Gui) handleGenericMergeCommandResult(result error) error {
if err := gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC}); err != nil {
func (gui *Gui) checkMergeOrRebase(result error) error {
if err := gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}); err != nil {
return err
}
if result == nil {
@ -112,12 +112,12 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
// assume in this case that we're already done
return nil
} else if isMergeConflictErr(result.Error()) {
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.FoundConflictsTitle,
Prompt: gui.Tr.FoundConflicts,
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.FoundConflictsTitle,
Prompt: gui.c.Tr.FoundConflicts,
HandlersManageFocus: true,
HandleConfirm: func() error {
return gui.pushContext(gui.State.Contexts.Files)
return gui.c.PushContext(gui.State.Contexts.Files)
},
HandleClose: func() error {
if err := gui.returnFromContext(); err != nil {
@ -128,16 +128,16 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
},
})
} else {
return gui.PopupHandler.ErrorMsg(result.Error())
return gui.c.ErrorMsg(result.Error())
}
}
func (gui *Gui) abortMergeOrRebaseWithConfirm() error {
// prompt user to confirm that they want to abort, then do it
mode := gui.workingTreeStateNoun()
return gui.PopupHandler.Ask(popup.AskOpts{
Title: fmt.Sprintf(gui.Tr.AbortTitle, mode),
Prompt: fmt.Sprintf(gui.Tr.AbortPrompt, mode),
return gui.c.Ask(popup.AskOpts{
Title: fmt.Sprintf(gui.c.Tr.AbortTitle, mode),
Prompt: fmt.Sprintf(gui.c.Tr.AbortPrompt, mode),
HandleConfirm: func() error {
return gui.genericMergeCommand(REBASE_OPTION_ABORT)
},
@ -145,7 +145,7 @@ func (gui *Gui) abortMergeOrRebaseWithConfirm() error {
}
func (gui *Gui) workingTreeStateNoun() string {
workingTreeState := gui.Git.Status.WorkingTreeState()
workingTreeState := gui.git.Status.WorkingTreeState()
switch workingTreeState {
case enums.REBASE_MODE_NONE:
return ""

View File

@ -13,7 +13,7 @@ import (
)
func (gui *Gui) handleCreateRecentReposMenu() error {
recentRepoPaths := gui.Config.GetAppState().RecentRepos
recentRepoPaths := gui.c.GetAppState().RecentRepos
reposCount := utils.Min(len(recentRepoPaths), 20)
// we won't show the current repo hence the -1
@ -34,11 +34,11 @@ func (gui *Gui) handleCreateRecentReposMenu() error {
}
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{Title: gui.Tr.RecentRepos, Items: menuItems})
return gui.c.Menu(popup.CreateMenuOptions{Title: gui.c.Tr.RecentRepos, Items: menuItems})
}
func (gui *Gui) handleShowAllBranchLogs() error {
cmdObj := gui.Git.Branch.AllBranchesLogCmdObj()
cmdObj := gui.git.Branch.AllBranchesLogCmdObj()
task := NewRunPtyTask(cmdObj.GetCmd())
return gui.refreshMainViews(refreshMainOpts{
@ -58,7 +58,7 @@ func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error {
if err := os.Chdir(path); err != nil {
if os.IsNotExist(err) {
return gui.PopupHandler.ErrorMsg(gui.Tr.ErrRepositoryMovedOrDeleted)
return gui.c.ErrorMsg(gui.c.Tr.ErrRepositoryMovedOrDeleted)
}
return err
}
@ -71,11 +71,16 @@ func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error {
return err
}
newGitCommand, err := commands.NewGitCommand(gui.Common, gui.OSCommand, git_config.NewStdCachedGitConfig(gui.Log))
newGitCommand, err := commands.NewGitCommand(
gui.Common,
gui.OSCommand,
git_config.NewStdCachedGitConfig(gui.Log),
gui.Mutexes.FetchMutex,
)
if err != nil {
return err
}
gui.Git = newGitCommand
gui.git = newGitCommand
// these two mutexes are used by our background goroutines (triggered via `gui.goEvery`. We don't want to
// switch to a repo while one of these goroutines is in the process of updating something
@ -97,23 +102,23 @@ func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error {
// updateRecentRepoList registers the fact that we opened lazygit in this repo,
// so that we can open the same repo via the 'recent repos' menu
func (gui *Gui) updateRecentRepoList() error {
if gui.Git.Status.IsBareRepo() {
if gui.git.Status.IsBareRepo() {
// we could totally do this but it would require storing both the git-dir and the
// worktree in our recent repos list, which is a change that would need to be
// backwards compatible
gui.Log.Info("Not appending bare repo to recent repo list")
gui.c.Log.Info("Not appending bare repo to recent repo list")
return nil
}
recentRepos := gui.Config.GetAppState().RecentRepos
recentRepos := gui.c.GetAppState().RecentRepos
currentRepo, err := os.Getwd()
if err != nil {
return err
}
known, recentRepos := newRecentReposList(recentRepos, currentRepo)
gui.IsNewRepo = known
gui.Config.GetAppState().RecentRepos = recentRepos
return gui.Config.SaveAppState()
gui.c.GetAppState().RecentRepos = recentRepos
return gui.c.SaveAppState()
}
// newRecentReposList returns a new repo list with a new entry but only when it doesn't exist yet

137
pkg/gui/ref_helper.go Normal file
View File

@ -0,0 +1,137 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type RefHelper struct {
c *controllers.ControllerCommon
git *commands.GitCommand
State *GuiRepoState
}
func NewRefHelper(
c *controllers.ControllerCommon,
git *commands.GitCommand,
state *GuiRepoState,
) *RefHelper {
return &RefHelper{
c: c,
git: git,
State: state,
}
}
var _ controllers.IRefHelper = &RefHelper{}
func (self *RefHelper) CheckoutRef(ref string, options types.CheckoutRefOptions) error {
waitingStatus := options.WaitingStatus
if waitingStatus == "" {
waitingStatus = self.c.Tr.CheckingOutStatus
}
cmdOptions := git_commands.CheckoutOptions{Force: false, EnvVars: options.EnvVars}
onSuccess := func() {
self.State.Panels.Branches.SelectedLineIdx = 0
self.State.Panels.Commits.SelectedLineIdx = 0
// loading a heap of commits is slow so we limit them whenever doing a reset
self.State.Panels.Commits.LimitCommits = true
}
return self.c.WithWaitingStatus(waitingStatus, func() error {
if err := self.git.Branch.Checkout(ref, cmdOptions); err != nil {
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
if options.OnRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") {
return options.OnRefNotFound(ref)
}
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
// offer to autostash changes
return self.c.Ask(popup.AskOpts{
Title: self.c.Tr.AutoStashTitle,
Prompt: self.c.Tr.AutoStashPrompt,
HandleConfirm: func() error {
if err := self.git.Stash.Save(self.c.Tr.StashPrefix + ref); err != nil {
return self.c.Error(err)
}
if err := self.git.Branch.Checkout(ref, cmdOptions); err != nil {
return self.c.Error(err)
}
onSuccess()
if err := self.git.Stash.Pop(0); err != nil {
if err := self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI}); err != nil {
return err
}
return self.c.Error(err)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI})
},
})
}
if err := self.c.Error(err); err != nil {
return err
}
}
onSuccess()
return self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI})
})
}
func (self *RefHelper) ResetToRef(ref string, strength string, envVars []string) error {
if err := self.git.Commit.ResetToCommit(ref, strength, envVars); err != nil {
return self.c.Error(err)
}
self.State.Panels.Commits.SelectedLineIdx = 0
self.State.Panels.ReflogCommits.SelectedLineIdx = 0
// loading a heap of commits is slow so we limit them whenever doing a reset
self.State.Panels.Commits.LimitCommits = true
if err := self.c.PushContext(self.State.Contexts.BranchCommits); err != nil {
return err
}
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.BRANCHES, types.REFLOG, types.COMMITS}}); err != nil {
return err
}
return nil
}
func (self *RefHelper) CreateGitResetMenu(ref string) error {
strengths := []string{"soft", "mixed", "hard"}
menuItems := make([]*popup.MenuItem, len(strengths))
for i, strength := range strengths {
strength := strength
menuItems[i] = &popup.MenuItem{
DisplayStrings: []string{
fmt.Sprintf("%s reset", strength),
style.FgRed.Sprintf("reset --%s %s", strength, ref),
},
OnPress: func() error {
self.c.LogAction("Reset")
return self.ResetToRef(ref, strength, []string{})
},
}
}
return self.c.Menu(popup.CreateMenuOptions{
Title: fmt.Sprintf("%s %s", self.c.Tr.LcResetTo, ref),
Items: menuItems,
})
}

View File

@ -2,7 +2,9 @@ package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// list panel functions
@ -23,7 +25,7 @@ func (gui *Gui) reflogCommitsRenderToMain() error {
if commit == nil {
task = NewRenderStringTask("No reflog history")
} else {
cmdObj := gui.Git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
task = NewRunPtyTask(cmdObj.GetCmd())
}
@ -53,10 +55,10 @@ func (gui *Gui) refreshReflogCommits() error {
}
refresh := func(stateCommits *[]*models.Commit, filterPath string) error {
commits, onlyObtainedNewReflogCommits, err := gui.Git.Loaders.ReflogCommits.
commits, onlyObtainedNewReflogCommits, err := gui.git.Loaders.ReflogCommits.
GetReflogCommits(lastReflogCommit, filterPath)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
if onlyObtainedNewReflogCommits {
@ -79,21 +81,21 @@ func (gui *Gui) refreshReflogCommits() error {
state.FilteredReflogCommits = state.ReflogCommits
}
return gui.postRefreshUpdate(gui.State.Contexts.ReflogCommits)
return gui.c.PostRefreshUpdate(gui.State.Contexts.ReflogCommits)
}
func (gui *Gui) handleCheckoutReflogCommit() error {
func (gui *Gui) CheckoutReflogCommit() error {
commit := gui.getSelectedReflogCommit()
if commit == nil {
return nil
}
err := gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.LcCheckoutCommit,
Prompt: gui.Tr.SureCheckoutThisCommit,
err := gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.LcCheckoutCommit,
Prompt: gui.c.Tr.SureCheckoutThisCommit,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.CheckoutReflogCommit)
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
gui.c.LogAction(gui.c.Tr.Actions.CheckoutReflogCommit)
return gui.refHelper.CheckoutRef(commit.Sha, types.CheckoutRefOptions{})
},
})
if err != nil {
@ -108,7 +110,7 @@ func (gui *Gui) handleCheckoutReflogCommit() error {
func (gui *Gui) handleCreateReflogResetMenu() error {
commit := gui.getSelectedReflogCommit()
return gui.createResetMenu(commit.Sha)
return gui.refHelper.CreateGitResetMenu(commit.Sha)
}
func (gui *Gui) handleViewReflogCommitFiles() error {
@ -117,5 +119,10 @@ func (gui *Gui) handleViewReflogCommitFiles() error {
return nil
}
return gui.switchToCommitFilesContext(commit.Sha, false, gui.State.Contexts.ReflogCommits, "commits")
return gui.SwitchToCommitFilesContext(controllers.SwitchToCommitFilesContextOpts{
RefName: commit.Sha,
CanRebase: false,
Context: gui.State.Contexts.ReflogCommits,
WindowName: "commits",
})
}

View File

@ -26,7 +26,7 @@ func (gui *Gui) remoteBranchesRenderToMain() error {
if remoteBranch == nil {
task = NewRenderStringTask("No branches for this remote")
} else {
cmdObj := gui.Git.Branch.GetGraphCmdObj(remoteBranch.FullName())
cmdObj := gui.git.Branch.GetGraphCmdObj(remoteBranch.FullName())
task = NewRunCommandTask(cmdObj.GetCmd())
}
@ -39,7 +39,7 @@ func (gui *Gui) remoteBranchesRenderToMain() error {
}
func (gui *Gui) handleRemoteBranchesEscape() error {
return gui.pushContext(gui.State.Contexts.Remotes)
return gui.c.PushContext(gui.State.Contexts.Remotes)
}
func (gui *Gui) handleMergeRemoteBranch() error {
@ -52,20 +52,20 @@ func (gui *Gui) handleDeleteRemoteBranch() error {
if remoteBranch == nil {
return nil
}
message := fmt.Sprintf("%s '%s'?", gui.Tr.DeleteRemoteBranchMessage, remoteBranch.FullName())
message := fmt.Sprintf("%s '%s'?", gui.c.Tr.DeleteRemoteBranchMessage, remoteBranch.FullName())
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.DeleteRemoteBranch,
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.DeleteRemoteBranch,
Prompt: message,
HandleConfirm: func() error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.DeletingStatus, func() error {
gui.logAction(gui.Tr.Actions.DeleteRemoteBranch)
err := gui.Git.Remote.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name)
return gui.c.WithWaitingStatus(gui.c.Tr.DeletingStatus, func() error {
gui.c.LogAction(gui.c.Tr.Actions.DeleteRemoteBranch)
err := gui.git.Remote.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name)
if err != nil {
_ = gui.PopupHandler.Error(err)
_ = gui.c.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
return gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
})
},
})
@ -81,23 +81,23 @@ func (gui *Gui) handleSetBranchUpstream() error {
checkedOutBranch := gui.getCheckedOutBranch()
message := utils.ResolvePlaceholderString(
gui.Tr.SetUpstreamMessage,
gui.c.Tr.SetUpstreamMessage,
map[string]string{
"checkedOut": checkedOutBranch.Name,
"selected": selectedBranch.FullName(),
},
)
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.SetUpstreamTitle,
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.SetUpstreamTitle,
Prompt: message,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.SetBranchUpstream)
if err := gui.Git.Branch.SetUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil {
return gui.PopupHandler.Error(err)
gui.c.LogAction(gui.c.Tr.Actions.SetBranchUpstream)
if err := gui.git.Branch.SetUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil {
return gui.c.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
return gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
},
})
}
@ -108,5 +108,5 @@ func (gui *Gui) handleCreateResetToRemoteBranchMenu() error {
return nil
}
return gui.createResetMenu(selectedBranch.FullName())
return gui.refHelper.CreateGitResetMenu(selectedBranch.FullName())
}

View File

@ -5,10 +5,8 @@ import (
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
@ -42,9 +40,9 @@ func (gui *Gui) remotesRenderToMain() error {
func (gui *Gui) refreshRemotes() error {
prevSelectedRemote := gui.getSelectedRemote()
remotes, err := gui.Git.Loaders.Remotes.GetRemotes()
remotes, err := gui.git.Loaders.Remotes.GetRemotes()
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
gui.State.Remotes = remotes
@ -59,133 +57,5 @@ func (gui *Gui) refreshRemotes() error {
}
}
return gui.postRefreshUpdate(gui.mustContextForContextKey(ContextKey(gui.Views.Branches.Context)))
}
func (gui *Gui) handleRemoteEnter() error {
// naive implementation: get the branches and render them to the list, change the context
remote := gui.getSelectedRemote()
if remote == nil {
return nil
}
gui.State.RemoteBranches = remote.Branches
newSelectedLine := 0
if len(remote.Branches) == 0 {
newSelectedLine = -1
}
gui.State.Panels.RemoteBranches.SelectedLineIdx = newSelectedLine
return gui.pushContext(gui.State.Contexts.RemoteBranches)
}
func (gui *Gui) handleAddRemote() error {
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: gui.Tr.LcNewRemoteName,
HandleConfirm: func(remoteName string) error {
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: gui.Tr.LcNewRemoteUrl,
HandleConfirm: func(remoteUrl string) error {
gui.logAction(gui.Tr.Actions.AddRemote)
if err := gui.Git.Remote.AddRemote(remoteName, remoteUrl); err != nil {
return err
}
return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.REMOTES}})
},
})
},
})
}
func (gui *Gui) handleRemoveRemote() error {
remote := gui.getSelectedRemote()
if remote == nil {
return nil
}
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.LcRemoveRemote,
Prompt: gui.Tr.LcRemoveRemotePrompt + " '" + remote.Name + "'?",
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.RemoveRemote)
if err := gui.Git.Remote.RemoveRemote(remote.Name); err != nil {
return gui.PopupHandler.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
},
})
}
func (gui *Gui) handleEditRemote() error {
remote := gui.getSelectedRemote()
if remote == nil {
return nil
}
editNameMessage := utils.ResolvePlaceholderString(
gui.Tr.LcEditRemoteName,
map[string]string{
"remoteName": remote.Name,
},
)
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: editNameMessage,
InitialContent: remote.Name,
HandleConfirm: func(updatedRemoteName string) error {
if updatedRemoteName != remote.Name {
gui.logAction(gui.Tr.Actions.UpdateRemote)
if err := gui.Git.Remote.RenameRemote(remote.Name, updatedRemoteName); err != nil {
return gui.PopupHandler.Error(err)
}
}
editUrlMessage := utils.ResolvePlaceholderString(
gui.Tr.LcEditRemoteUrl,
map[string]string{
"remoteName": updatedRemoteName,
},
)
urls := remote.Urls
url := ""
if len(urls) > 0 {
url = urls[0]
}
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: editUrlMessage,
InitialContent: url,
HandleConfirm: func(updatedRemoteUrl string) error {
gui.logAction(gui.Tr.Actions.UpdateRemote)
if err := gui.Git.Remote.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
return gui.PopupHandler.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
},
})
},
})
}
func (gui *Gui) handleFetchRemote() error {
remote := gui.getSelectedRemote()
if remote == nil {
return nil
}
return gui.PopupHandler.WithWaitingStatus(gui.Tr.FetchingRemoteStatus, func() error {
gui.Mutexes.FetchMutex.Lock()
defer gui.Mutexes.FetchMutex.Unlock()
err := gui.Git.Sync.FetchRemote(remote.Name)
if err != nil {
_ = gui.PopupHandler.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
})
return gui.c.PostRefreshUpdate(gui.mustContextForContextKey(types.ContextKey(gui.Views.Branches.Context)))
}

View File

@ -1,53 +0,0 @@
package gui
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
func (gui *Gui) resetToRef(ref string, strength string, envVars []string) error {
if err := gui.Git.Commit.ResetToCommit(ref, strength, envVars); err != nil {
return gui.PopupHandler.Error(err)
}
gui.State.Panels.Commits.SelectedLineIdx = 0
gui.State.Panels.ReflogCommits.SelectedLineIdx = 0
// loading a heap of commits is slow so we limit them whenever doing a reset
gui.State.Panels.Commits.LimitCommits = true
if err := gui.pushContext(gui.State.Contexts.BranchCommits); err != nil {
return err
}
if err := gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.BRANCHES, types.REFLOG, types.COMMITS}}); err != nil {
return err
}
return nil
}
func (gui *Gui) createResetMenu(ref string) error {
strengths := []string{"soft", "mixed", "hard"}
menuItems := make([]*popup.MenuItem, len(strengths))
for i, strength := range strengths {
strength := strength
menuItems[i] = &popup.MenuItem{
DisplayStrings: []string{
fmt.Sprintf("%s reset", strength),
style.FgRed.Sprintf("reset --%s %s", strength, ref),
},
OnPress: func() error {
gui.logAction("Reset")
return gui.resetToRef(ref, strength, []string{})
},
}
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{
Title: fmt.Sprintf("%s %s", gui.Tr.LcResetTo, ref),
Items: menuItems,
})
}

View File

@ -17,7 +17,7 @@ func (gui *Gui) handleOpenSearch(viewName string) error {
gui.Views.Search.ClearTextArea()
if err := gui.pushContext(gui.State.Contexts.Search); err != nil {
if err := gui.c.PushContext(gui.State.Contexts.Search); err != nil {
return err
}
@ -43,7 +43,7 @@ func (gui *Gui) handleSearch() error {
}
func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error {
keybindingConfig := gui.UserConfig.Keybinding
keybindingConfig := gui.c.UserConfig.Keybinding
return func(y int, index int, total int) error {
if total == 0 {

View File

@ -28,16 +28,16 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
}
if secondaryFocused {
gui.Views.Main.Title = gui.Tr.StagedChanges
gui.Views.Secondary.Title = gui.Tr.UnstagedChanges
gui.Views.Main.Title = gui.c.Tr.StagedChanges
gui.Views.Secondary.Title = gui.c.Tr.UnstagedChanges
} else {
gui.Views.Main.Title = gui.Tr.UnstagedChanges
gui.Views.Secondary.Title = gui.Tr.StagedChanges
gui.Views.Main.Title = gui.c.Tr.UnstagedChanges
gui.Views.Secondary.Title = gui.c.Tr.StagedChanges
}
// note for custom diffs, we'll need to send a flag here saying not to use the custom diff
diff := gui.Git.WorkingTree.WorktreeFileDiff(file, true, secondaryFocused, false)
secondaryDiff := gui.Git.WorkingTree.WorktreeFileDiff(file, true, !secondaryFocused, false)
diff := gui.git.WorkingTree.WorktreeFileDiff(file, true, secondaryFocused, false)
secondaryDiff := gui.git.WorkingTree.WorktreeFileDiff(file, true, !secondaryFocused, false)
// if we have e.g. a deleted file with nothing else to the diff will have only
// 4-5 lines in which case we'll swap panels
@ -97,7 +97,7 @@ func (gui *Gui) handleTogglePanel() error {
func (gui *Gui) handleStagingEscape() error {
gui.escapeLineByLinePanel()
return gui.pushContext(gui.State.Contexts.Files)
return gui.c.PushContext(gui.State.Contexts.Files)
}
func (gui *Gui) handleToggleStagedSelection() error {
@ -113,10 +113,10 @@ func (gui *Gui) handleResetSelection() error {
return gui.applySelection(true, state)
}
if !gui.UserConfig.Gui.SkipUnstageLineWarning {
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.UnstageLinesTitle,
Prompt: gui.Tr.UnstageLinesPrompt,
if !gui.c.UserConfig.Gui.SkipUnstageLineWarning {
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.UnstageLinesTitle,
Prompt: gui.c.Tr.UnstageLinesPrompt,
HandleConfirm: func() error {
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
return gui.applySelection(true, state)
@ -148,17 +148,17 @@ func (gui *Gui) applySelection(reverse bool, state *LblPanelState) error {
if !reverse || state.SecondaryFocused {
applyFlags = append(applyFlags, "cached")
}
gui.logAction(gui.Tr.Actions.ApplyPatch)
err := gui.Git.WorkingTree.ApplyPatch(patch, applyFlags...)
gui.c.LogAction(gui.c.Tr.Actions.ApplyPatch)
err := gui.git.WorkingTree.ApplyPatch(patch, applyFlags...)
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
if state.SelectingRange() {
state.SetLineSelectMode()
}
if err := gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil {
if err := gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil {
return err
}
if err := gui.refreshStagingPanel(false, -1); err != nil {

View File

@ -2,6 +2,7 @@ package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@ -21,9 +22,9 @@ func (gui *Gui) stashRenderToMain() error {
var task updateTask
stashEntry := gui.getSelectedStashEntry()
if stashEntry == nil {
task = NewRenderStringTask(gui.Tr.NoStashEntries)
task = NewRenderStringTask(gui.c.Tr.NoStashEntries)
} else {
task = NewRunPtyTask(gui.Git.Stash.ShowStashEntryCmdObj(stashEntry.Index).GetCmd())
task = NewRunPtyTask(gui.git.Stash.ShowStashEntryCmdObj(stashEntry.Index).GetCmd())
}
return gui.refreshMainViews(refreshMainOpts{
@ -35,7 +36,7 @@ func (gui *Gui) stashRenderToMain() error {
}
func (gui *Gui) refreshStashEntries() error {
gui.State.StashEntries = gui.Git.Loaders.Stash.
gui.State.StashEntries = gui.git.Loaders.Stash.
GetStashEntries(gui.State.Modes.Filtering.GetPath())
return gui.postRefreshUpdate(gui.State.Contexts.Stash)
@ -49,14 +50,14 @@ func (gui *Gui) handleStashApply() error {
return nil
}
skipStashWarning := gui.UserConfig.Gui.SkipStashWarning
skipStashWarning := gui.c.UserConfig.Gui.SkipStashWarning
apply := func() error {
gui.logAction(gui.Tr.Actions.Stash)
err := gui.Git.Stash.Apply(stashEntry.Index)
gui.c.LogAction(gui.c.Tr.Actions.Stash)
err := gui.git.Stash.Apply(stashEntry.Index)
_ = gui.postStashRefresh()
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
return nil
}
@ -65,9 +66,9 @@ func (gui *Gui) handleStashApply() error {
return apply()
}
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.StashApply,
Prompt: gui.Tr.SureApplyStashEntry,
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.StashApply,
Prompt: gui.c.Tr.SureApplyStashEntry,
HandleConfirm: func() error {
return apply()
},
@ -80,14 +81,14 @@ func (gui *Gui) handleStashPop() error {
return nil
}
skipStashWarning := gui.UserConfig.Gui.SkipStashWarning
skipStashWarning := gui.c.UserConfig.Gui.SkipStashWarning
pop := func() error {
gui.logAction(gui.Tr.Actions.Stash)
err := gui.Git.Stash.Pop(stashEntry.Index)
gui.c.LogAction(gui.c.Tr.Actions.Stash)
err := gui.git.Stash.Pop(stashEntry.Index)
_ = gui.postStashRefresh()
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
return nil
}
@ -96,9 +97,9 @@ func (gui *Gui) handleStashPop() error {
return pop()
}
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.StashPop,
Prompt: gui.Tr.SurePopStashEntry,
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.StashPop,
Prompt: gui.c.Tr.SurePopStashEntry,
HandleConfirm: func() error {
return pop()
},
@ -111,15 +112,15 @@ func (gui *Gui) handleStashDrop() error {
return nil
}
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.StashDrop,
Prompt: gui.Tr.SureDropStashEntry,
return gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.StashDrop,
Prompt: gui.c.Tr.SureDropStashEntry,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.Stash)
err := gui.Git.Stash.Drop(stashEntry.Index)
_ = gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{STASH}})
gui.c.LogAction(gui.c.Tr.Actions.Stash)
err := gui.git.Stash.Drop(stashEntry.Index)
_ = gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH}})
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
return nil
},
@ -127,25 +128,7 @@ func (gui *Gui) handleStashDrop() error {
}
func (gui *Gui) postStashRefresh() error {
return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH, types.FILES}})
}
func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
return gui.PopupHandler.ErrorMsg(gui.Tr.NoTrackedStagedFilesStash)
}
return gui.prompt(promptOpts{
title: gui.Tr.StashChanges,
handleConfirm: func(stashComment string) error {
err := stashFunc(stashComment)
_ = gui.postStashRefresh()
if err != nil {
return gui.PopupHandler.Error(err)
}
return nil
},
})
return gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH, types.FILES}})
}
func (gui *Gui) handleViewStashFiles() error {
@ -154,5 +137,10 @@ func (gui *Gui) handleViewStashFiles() error {
return nil
}
return gui.switchToCommitFilesContext(stashEntry.RefName(), false, gui.State.Contexts.Stash, "stash")
return gui.SwitchToCommitFilesContext(controllers.SwitchToCommitFilesContextOpts{
RefName: stashEntry.RefName(),
CanRebase: false,
Context: gui.State.Contexts.Stash,
WindowName: "stash",
})
}

View File

@ -18,7 +18,7 @@ func (gui *Gui) refreshStatus() {
gui.Mutexes.RefreshingStatusMutex.Lock()
defer gui.Mutexes.RefreshingStatusMutex.Unlock()
currentBranch := gui.currentBranch()
currentBranch := gui.getCheckedOutBranch()
if currentBranch == nil {
// need to wait for branches to refresh
return
@ -29,7 +29,7 @@ func (gui *Gui) refreshStatus() {
status += presentation.ColoredBranchStatus(currentBranch) + " "
}
workingTreeState := gui.Git.Status.WorkingTreeState()
workingTreeState := gui.git.Status.WorkingTreeState()
if workingTreeState != enums.REBASE_MODE_NONE {
status += style.FgYellow.Sprintf("(%s) ", formatWorkingTreeState(workingTreeState))
}
@ -50,7 +50,7 @@ func cursorInSubstring(cx int, prefix string, substring string) bool {
}
func (gui *Gui) handleCheckForUpdate() error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.CheckingForUpdates, func() error {
return gui.c.WithWaitingStatus(gui.c.Tr.CheckingForUpdates, func() error {
gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
return nil
})
@ -62,20 +62,20 @@ func (gui *Gui) handleStatusClick() error {
return nil
}
currentBranch := gui.currentBranch()
currentBranch := gui.getCheckedOutBranch()
if currentBranch == nil {
// need to wait for branches to refresh
return nil
}
if err := gui.pushContext(gui.State.Contexts.Status); err != nil {
if err := gui.c.PushContext(gui.State.Contexts.Status); err != nil {
return err
}
cx, _ := gui.Views.Status.Cursor()
upstreamStatus := presentation.BranchStatus(currentBranch)
repoName := utils.GetCurrentRepoName()
workingTreeState := gui.Git.Status.WorkingTreeState()
workingTreeState := gui.git.Status.WorkingTreeState()
switch workingTreeState {
case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING:
workingTreeStatus := fmt.Sprintf("(%s)", formatWorkingTreeState(workingTreeState))
@ -135,7 +135,7 @@ func (gui *Gui) askForConfigFile(action func(file string) error) error {
confPaths := gui.Config.GetUserConfigPaths()
switch len(confPaths) {
case 0:
return errors.New(gui.Tr.NoConfigFileFoundErr)
return errors.New(gui.c.Tr.NoConfigFileFoundErr)
case 1:
return action(confPaths[0])
default:
@ -149,8 +149,8 @@ func (gui *Gui) askForConfigFile(action func(file string) error) error {
},
}
}
return gui.PopupHandler.Menu(popup.CreateMenuOptions{
Title: gui.Tr.SelectConfigFile,
return gui.c.Menu(popup.CreateMenuOptions{
Title: gui.c.Tr.SelectConfigFile,
Items: menuItems,
HideCancel: true,
})
@ -158,11 +158,11 @@ func (gui *Gui) askForConfigFile(action func(file string) error) error {
}
func (gui *Gui) handleOpenConfig() error {
return gui.askForConfigFile(gui.openFile)
return gui.askForConfigFile(gui.fileHelper.OpenFile)
}
func (gui *Gui) handleEditConfig() error {
return gui.askForConfigFile(gui.editFile)
return gui.askForConfigFile(gui.fileHelper.EditFile)
}
func lazygitTitle() string {

View File

@ -135,7 +135,7 @@ func TestMerge(t *testing.T) {
"\x1b[38;2;255;0;255;48;2;255;255;0;1;4mfoo\x1b[0m",
},
{
"mix color-16 with rgb colors",
"mix color-16 (background) with rgb (foreground)",
[]TextStyle{New().SetFg(rgbYellow), BgRed},
TextStyle{
fg: &rgbYellow,
@ -147,6 +147,19 @@ func TestMerge(t *testing.T) {
},
"\x1b[38;2;255;255;0;48;2;197;30;20mfoo\x1b[0m",
},
{
"mix color-16 (foreground) with rgb (background)",
[]TextStyle{FgRed, New().SetBg(rgbYellow)},
TextStyle{
fg: &Color{basic: &fgRed},
bg: &rgbYellow,
Style: color.NewRGBStyle(
fgRed.RGB(),
rgbYellowLib,
).SetOpts(color.Opts{}),
},
"\x1b[38;2;197;30;20;48;2;255;255;0mfoo\x1b[0m",
},
}
for _, s := range scenarios {

View File

@ -3,7 +3,9 @@ package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// list panel functions
@ -24,7 +26,7 @@ func (gui *Gui) subCommitsRenderToMain() error {
if commit == nil {
task = NewRenderStringTask("No commits")
} else {
cmdObj := gui.Git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
task = NewRunPtyTask(cmdObj.GetCmd())
}
@ -43,19 +45,19 @@ func (gui *Gui) handleCheckoutSubCommit() error {
return nil
}
err := gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.LcCheckoutCommit,
Prompt: gui.Tr.SureCheckoutThisCommit,
err := gui.c.Ask(popup.AskOpts{
Title: gui.c.Tr.LcCheckoutCommit,
Prompt: gui.c.Tr.SureCheckoutThisCommit,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.CheckoutCommit)
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
gui.c.LogAction(gui.c.Tr.Actions.CheckoutCommit)
return gui.refHelper.CheckoutRef(commit.Sha, types.CheckoutRefOptions{})
},
})
if err != nil {
return err
}
gui.State.Panels.SubCommits.SelectedLineIdx = 0
gui.State.Contexts.SubCommits.GetPanelState().SetSelectedLineIdx(0)
return nil
}
@ -63,7 +65,7 @@ func (gui *Gui) handleCheckoutSubCommit() error {
func (gui *Gui) handleCreateSubCommitResetMenu() error {
commit := gui.getSelectedSubCommit()
return gui.createResetMenu(commit.Sha)
return gui.refHelper.CreateGitResetMenu(commit.Sha)
}
func (gui *Gui) handleViewSubCommitFiles() error {
@ -72,12 +74,17 @@ func (gui *Gui) handleViewSubCommitFiles() error {
return nil
}
return gui.switchToCommitFilesContext(commit.Sha, false, gui.State.Contexts.SubCommits, "branches")
return gui.SwitchToCommitFilesContext(controllers.SwitchToCommitFilesContextOpts{
RefName: commit.Sha,
CanRebase: false,
Context: gui.State.Contexts.SubCommits,
WindowName: "branches",
})
}
func (gui *Gui) switchToSubCommitsContext(refName string) error {
// need to populate my sub commits
commits, err := gui.Git.Loaders.Commits.GetCommits(
commits, err := gui.git.Loaders.Commits.GetCommits(
loaders.GetCommitsOptions{
Limit: gui.State.Panels.Commits.LimitCommits,
FilterPath: gui.State.Modes.Filtering.GetPath(),
@ -91,10 +98,10 @@ func (gui *Gui) switchToSubCommitsContext(refName string) error {
gui.State.SubCommits = commits
gui.State.Panels.SubCommits.refName = refName
gui.State.Panels.SubCommits.SelectedLineIdx = 0
gui.State.Contexts.SubCommits.GetPanelState().SetSelectedLineIdx(0)
gui.State.Contexts.SubCommits.SetParentContext(gui.currentSideListContext())
return gui.pushContext(gui.State.Contexts.SubCommits)
return gui.c.PushContext(gui.State.Contexts.SubCommits)
}
func (gui *Gui) handleSwitchToSubCommits() error {

View File

@ -30,11 +30,11 @@ func (gui *Gui) submodulesRenderToMain() error {
style.FgCyan.Sprint(submodule.Url),
)
file := gui.fileForSubmodule(submodule)
file := gui.workingTreeHelper.FileForSubmodule(submodule)
if file == nil {
task = NewRenderStringTask(prefix)
} else {
cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, gui.IgnoreWhitespaceInDiffView)
cmdObj := gui.git.WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, gui.IgnoreWhitespaceInDiffView)
task = NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix)
}
}
@ -48,7 +48,7 @@ func (gui *Gui) submodulesRenderToMain() error {
}
func (gui *Gui) refreshStateSubmoduleConfigs() error {
configs, err := gui.Git.Submodule.GetConfigs()
configs, err := gui.git.Submodule.GetConfigs()
if err != nil {
return err
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"os"
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
@ -21,9 +22,30 @@ import (
// finding suggestions in this file, so that it's easy to see if a function already
// exists for fetching a particular model.
func (gui *Gui) getRemoteNames() []string {
result := make([]string, len(gui.State.Remotes))
for i, remote := range gui.State.Remotes {
type SuggestionsHelper struct {
c *controllers.ControllerCommon
State *GuiRepoState
refreshSuggestionsFn func()
}
var _ controllers.ISuggestionsHelper = &SuggestionsHelper{}
func NewSuggestionsHelper(
c *controllers.ControllerCommon,
state *GuiRepoState,
refreshSuggestionsFn func(),
) *SuggestionsHelper {
return &SuggestionsHelper{
c: c,
State: state,
refreshSuggestionsFn: refreshSuggestionsFn,
}
}
func (self *SuggestionsHelper) getRemoteNames() []string {
result := make([]string, len(self.State.Remotes))
for i, remote := range self.State.Remotes {
result[i] = remote.Name
}
return result
@ -40,22 +62,22 @@ func matchesToSuggestions(matches []string) []*types.Suggestion {
return suggestions
}
func (gui *Gui) getRemoteSuggestionsFunc() func(string) []*types.Suggestion {
remoteNames := gui.getRemoteNames()
func (self *SuggestionsHelper) GetRemoteSuggestionsFunc() func(string) []*types.Suggestion {
remoteNames := self.getRemoteNames()
return fuzzySearchFunc(remoteNames)
}
func (gui *Gui) getBranchNames() []string {
result := make([]string, len(gui.State.Branches))
for i, branch := range gui.State.Branches {
func (self *SuggestionsHelper) getBranchNames() []string {
result := make([]string, len(self.State.Branches))
for i, branch := range self.State.Branches {
result[i] = branch.Name
}
return result
}
func (gui *Gui) getBranchNameSuggestionsFunc() func(string) []*types.Suggestion {
branchNames := gui.getBranchNames()
func (self *SuggestionsHelper) GetBranchNameSuggestionsFunc() func(string) []*types.Suggestion {
branchNames := self.getBranchNames()
return func(input string) []*types.Suggestion {
var matchingBranchNames []string
@ -78,13 +100,13 @@ func (gui *Gui) getBranchNameSuggestionsFunc() func(string) []*types.Suggestion
}
// here we asynchronously fetch the latest set of paths in the repo and store in
// gui.State.FilesTrie. On the main thread we'll be doing a fuzzy search via
// gui.State.FilesTrie. So if we've looked for a file previously, we'll start with
// self.State.FilesTrie. On the main thread we'll be doing a fuzzy search via
// self.State.FilesTrie. So if we've looked for a file previously, we'll start with
// the old trie and eventually it'll be swapped out for the new one.
// Notably, unlike other suggestion functions we're not showing all the options
// if nothing has been typed because there'll be too much to display efficiently
func (gui *Gui) getFilePathSuggestionsFunc() func(string) []*types.Suggestion {
_ = gui.PopupHandler.WithWaitingStatus(gui.Tr.LcLoadingFileSuggestions, func() error {
func (self *SuggestionsHelper) GetFilePathSuggestionsFunc() func(string) []*types.Suggestion {
_ = self.c.WithWaitingStatus(self.c.Tr.LcLoadingFileSuggestions, func() error {
trie := patricia.NewTrie()
// load every non-gitignored file in the repo
ignore, err := gitignore.FromGit()
@ -101,22 +123,16 @@ func (gui *Gui) getFilePathSuggestionsFunc() func(string) []*types.Suggestion {
return nil
})
// cache the trie for future use
gui.State.FilesTrie = trie
self.State.FilesTrie = trie
// refresh the selections view
gui.suggestionsAsyncHandler.Do(func() func() {
// assuming here that the confirmation view is what we're typing into.
// This assumption may prove false over time
suggestions := gui.findSuggestions(gui.Views.Confirmation.TextArea.GetContent())
return func() { gui.setSuggestions(suggestions) }
})
self.refreshSuggestionsFn()
return err
})
return func(input string) []*types.Suggestion {
matchingNames := []string{}
_ = gui.State.FilesTrie.VisitFuzzy(patricia.Prefix(input), true, func(prefix patricia.Prefix, item patricia.Item, skipped int) error {
_ = self.State.FilesTrie.VisitFuzzy(patricia.Prefix(input), true, func(prefix patricia.Prefix, item patricia.Item, skipped int) error {
matchingNames = append(matchingNames, item.(string))
return nil
})
@ -136,9 +152,9 @@ func (gui *Gui) getFilePathSuggestionsFunc() func(string) []*types.Suggestion {
}
}
func (gui *Gui) getRemoteBranchNames(separator string) []string {
func (self *SuggestionsHelper) getRemoteBranchNames(separator string) []string {
result := []string{}
for _, remote := range gui.State.Remotes {
for _, remote := range self.State.Remotes {
for _, branch := range remote.Branches {
result = append(result, fmt.Sprintf("%s%s%s", remote.Name, separator, branch.Name))
}
@ -146,22 +162,22 @@ func (gui *Gui) getRemoteBranchNames(separator string) []string {
return result
}
func (gui *Gui) getRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion {
return fuzzySearchFunc(gui.getRemoteBranchNames(separator))
func (self *SuggestionsHelper) GetRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion {
return fuzzySearchFunc(self.getRemoteBranchNames(separator))
}
func (gui *Gui) getTagNames() []string {
result := make([]string, len(gui.State.Tags))
for i, tag := range gui.State.Tags {
func (self *SuggestionsHelper) getTagNames() []string {
result := make([]string, len(self.State.Tags))
for i, tag := range self.State.Tags {
result[i] = tag.Name
}
return result
}
func (gui *Gui) getRefsSuggestionsFunc() func(string) []*types.Suggestion {
remoteBranchNames := gui.getRemoteBranchNames("/")
localBranchNames := gui.getBranchNames()
tagNames := gui.getTagNames()
func (self *SuggestionsHelper) GetRefsSuggestionsFunc() func(string) []*types.Suggestion {
remoteBranchNames := self.getRemoteBranchNames("/")
localBranchNames := self.getBranchNames()
tagNames := self.getTagNames()
additionalRefNames := []string{"HEAD", "FETCH_HEAD", "MERGE_HEAD", "ORIG_HEAD"}
refNames := append(append(append(remoteBranchNames, localBranchNames...), tagNames...), additionalRefNames...)
@ -169,9 +185,9 @@ func (gui *Gui) getRefsSuggestionsFunc() func(string) []*types.Suggestion {
return fuzzySearchFunc(refNames)
}
func (gui *Gui) getCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion {
func (self *SuggestionsHelper) GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion {
// reversing so that we display the latest command first
history := utils.Reverse(gui.Config.GetAppState().CustomCommandsHistory)
history := utils.Reverse(self.c.GetAppState().CustomCommandsHistory)
return fuzzySearchFunc(history)
}

View File

@ -2,36 +2,28 @@ package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) getSelectedTag() *models.Tag {
selectedLine := gui.State.Panels.Tags.SelectedLineIdx
if selectedLine == -1 || len(gui.State.Tags) == 0 {
func (self *Gui) getSelectedTag() *models.Tag {
selectedLine := self.State.Panels.Tags.SelectedLineIdx
if selectedLine == -1 || len(self.State.Tags) == 0 {
return nil
}
return gui.State.Tags[selectedLine]
return self.State.Tags[selectedLine]
}
func (gui *Gui) handleCreateTag() error {
// leaving commit SHA blank so that we're just creating the tag for the current commit
return gui.createTagMenu("")
}
func (gui *Gui) tagsRenderToMain() error {
func (self *Gui) tagsRenderToMain() error {
var task updateTask
tag := gui.getSelectedTag()
tag := self.getSelectedTag()
if tag == nil {
task = NewRenderStringTask("No tags")
} else {
cmdObj := gui.Git.Branch.GetGraphCmdObj(tag.Name)
cmdObj := self.git.Branch.GetGraphCmdObj(tag.Name)
task = NewRunCommandTask(cmdObj.GetCmd())
}
return gui.refreshMainViews(refreshMainOpts{
return self.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Tag",
task: task,
@ -40,85 +32,13 @@ func (gui *Gui) tagsRenderToMain() error {
}
// this is a controller: it can't access tags directly. Or can it? It should be able to get but not set. But that's exactly what I'm doing here, setting it. but through a mutator which encapsulates the event.
func (gui *Gui) refreshTags() error {
tags, err := gui.Git.Loaders.Tags.GetTags()
func (self *Gui) refreshTags() error {
tags, err := self.git.Loaders.Tags.GetTags()
if err != nil {
return gui.PopupHandler.Error(err)
return self.c.Error(err)
}
gui.State.Tags = tags
self.State.Tags = tags
return gui.postRefreshUpdate(gui.State.Contexts.Tags)
}
func (gui *Gui) withSelectedTag(f func(tag *models.Tag) error) func() error {
return func() error {
tag := gui.getSelectedTag()
if tag == nil {
return nil
}
return f(tag)
}
}
// tag-specific handlers
func (gui *Gui) handleCheckoutTag(tag *models.Tag) error {
gui.logAction(gui.Tr.Actions.CheckoutTag)
if err := gui.handleCheckoutRef(tag.Name, handleCheckoutRefOptions{}); err != nil {
return err
}
return gui.pushContext(gui.State.Contexts.Branches)
}
func (gui *Gui) handleDeleteTag(tag *models.Tag) error {
prompt := utils.ResolvePlaceholderString(
gui.Tr.DeleteTagPrompt,
map[string]string{
"tagName": tag.Name,
},
)
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.DeleteTagTitle,
Prompt: prompt,
HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.DeleteTag)
if err := gui.Git.Tag.Delete(tag.Name); err != nil {
return gui.PopupHandler.Error(err)
}
return gui.refreshSidePanels(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}})
},
})
}
func (gui *Gui) handlePushTag(tag *models.Tag) error {
title := utils.ResolvePlaceholderString(
gui.Tr.PushTagTitle,
map[string]string{
"tagName": tag.Name,
},
)
return gui.PopupHandler.Prompt(popup.PromptOpts{
Title: title,
InitialContent: "origin",
FindSuggestionsFunc: gui.getRemoteSuggestionsFunc(),
HandleConfirm: func(response string) error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.PushingTagStatus, func() error {
gui.logAction(gui.Tr.Actions.PushTag)
err := gui.Git.Tag.Push(response, tag.Name)
if err != nil {
_ = gui.PopupHandler.Error(err)
}
return nil
})
},
})
}
func (gui *Gui) handleCreateResetToTagMenu(tag *models.Tag) error {
return gui.createResetMenu(tag.Name)
return self.postRefreshUpdate(self.State.Contexts.Tags)
}

View File

@ -11,7 +11,7 @@ import (
func (gui *Gui) newCmdTask(view *gocui.View, cmd *exec.Cmd, prefix string) error {
cmdStr := strings.Join(cmd.Args, " ")
gui.Log.WithField(
gui.c.Log.WithField(
"command",
cmdStr,
).Debug("RunCommand")
@ -24,19 +24,19 @@ func (gui *Gui) newCmdTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
start := func() (*exec.Cmd, io.Reader) {
r, err := cmd.StdoutPipe()
if err != nil {
gui.Log.Warn(err)
gui.c.Log.Warn(err)
}
cmd.Stderr = cmd.Stdout
if err := cmd.Start(); err != nil {
gui.Log.Warn(err)
gui.c.Log.Warn(err)
}
return cmd, r
}
if err := manager.NewTask(manager.NewCmdTask(start, prefix, height+oy+10, nil), cmdStr); err != nil {
gui.Log.Warn(err)
gui.c.Log.Warn(err)
}
return nil

View File

@ -0,0 +1,7 @@
package types
type CheckoutRefOptions struct {
WaitingStatus string
EnvVars []string
OnRefNotFound func(ref string) error
}

87
pkg/gui/types/context.go Normal file
View File

@ -0,0 +1,87 @@
package types
import "github.com/jesseduffield/lazygit/pkg/config"
type ContextKind int
const (
SIDE_CONTEXT ContextKind = iota
MAIN_CONTEXT
TEMPORARY_POPUP
PERSISTENT_POPUP
EXTRAS_CONTEXT
)
type Context interface {
HandleFocus(opts ...OnFocusOpts) error
HandleFocusLost() error
HandleRender() error
HandleRenderToMain() error
GetKind() ContextKind
GetViewName() string
GetWindowName() string
SetWindowName(string)
GetKey() ContextKey
SetParentContext(Context)
// we return a bool here to tell us whether or not the returned value just wraps a nil
GetParentContext() (Context, bool)
GetOptionsMap() map[string]string
}
type OnFocusOpts struct {
ClickedViewName string
ClickedViewLineIdx int
}
type ContextKey string
type HasKeybindings interface {
Keybindings(
getKey func(key string) interface{},
config config.KeybindingConfig,
guards KeybindingGuards,
) []*Binding
}
type IController interface {
HasKeybindings
Context() Context
}
type IListContext interface {
HasKeybindings
GetSelectedItem() (ListItem, bool)
GetSelectedItemId() string
HandlePrevLine() error
HandleNextLine() error
HandleScrollLeft() error
HandleScrollRight() error
HandleNextPage() error
HandleGotoTop() error
HandleGotoBottom() error
HandlePrevPage() error
HandleClick(onClick func() error) error
OnSearchSelect(selectedLineIdx int) error
FocusLine()
HandleRenderToMain() error
GetPanelState() IListPanelState
Context
}
type IListPanelState interface {
SetSelectedLineIdx(int)
GetSelectedLineIdx() int
}
type ListItem interface {
// ID is a SHA when the item is a commit, a filename when the item is a file, 'stash@{4}' when it's a stash entry, 'my_branch' when it's a branch
ID() string
// Description is something we would show in a message e.g. '123as14: push blah' for a commit
Description() string
}

View File

@ -16,3 +16,12 @@ type Binding struct {
Tag string // e.g. 'navigation'. Used for grouping things in the cheatsheet
OpensMenu bool
}
// A guard is a decorator which checks something before executing a handler
// and potentially early-exits if some precondition hasn't been met.
type Guard func(func() error) func() error
type KeybindingGuards struct {
OutsideFilterMode Guard
NoPopupPanel Guard
}

View File

@ -5,6 +5,7 @@ type RefreshableView int
const (
COMMITS RefreshableView = iota
REBASE_COMMITS
BRANCHES
FILES
STASH

View File

@ -8,7 +8,7 @@ import (
)
func (gui *Gui) showUpdatePrompt(newVersion string) error {
return gui.PopupHandler.Ask(popup.AskOpts{
return gui.c.Ask(popup.AskOpts{
Title: "New version available!",
Prompt: fmt.Sprintf("Download version %s? (enter/esc)", newVersion),
HandleConfirm: func() error {
@ -20,10 +20,10 @@ func (gui *Gui) showUpdatePrompt(newVersion string) error {
func (gui *Gui) onUserUpdateCheckFinish(newVersion string, err error) error {
if err != nil {
return gui.PopupHandler.Error(err)
return gui.c.Error(err)
}
if newVersion == "" {
return gui.PopupHandler.ErrorMsg("New version not found")
return gui.c.ErrorMsg("New version not found")
}
return gui.showUpdatePrompt(newVersion)
}
@ -31,13 +31,13 @@ func (gui *Gui) onUserUpdateCheckFinish(newVersion string, err error) error {
func (gui *Gui) onBackgroundUpdateCheckFinish(newVersion string, err error) error {
if err != nil {
// ignoring the error for now so that I'm not annoying users
gui.Log.Error(err.Error())
gui.c.Log.Error(err.Error())
return nil
}
if newVersion == "" {
return nil
}
if gui.UserConfig.Update.Method == "background" {
if gui.c.UserConfig.Update.Method == "background" {
gui.startUpdating(newVersion)
return nil
}
@ -56,7 +56,7 @@ func (gui *Gui) onUpdateFinish(statusId int, err error) error {
gui.OnUIThread(func() error {
_ = gui.renderString(gui.Views.AppStatus, "")
if err != nil {
return gui.PopupHandler.ErrorMsg("Update failed: " + err.Error())
return gui.c.ErrorMsg("Update failed: " + err.Error())
}
return nil
})
@ -65,7 +65,7 @@ func (gui *Gui) onUpdateFinish(statusId int, err error) error {
}
func (gui *Gui) createUpdateQuitConfirmation() error {
return gui.PopupHandler.Ask(popup.AskOpts{
return gui.c.Ask(popup.AskOpts{
Title: "Currently Updating",
Prompt: "An update is in progress. Are you sure you want to quit?",
HandleConfirm: func() error {

View File

@ -59,14 +59,14 @@ func arrToMap(arr []types.RefreshableView) map[types.RefreshableView]bool {
return output
}
func (gui *Gui) refreshSidePanels(options types.RefreshOptions) error {
func (gui *Gui) Refresh(options types.RefreshOptions) error {
if options.Scope == nil {
gui.Log.Infof(
gui.c.Log.Infof(
"refreshing all scopes in %s mode",
getModeName(options.Mode),
)
} else {
gui.Log.Infof(
gui.c.Log.Infof(
"refreshing the following scopes in %s mode: %s",
getModeName(options.Mode),
strings.Join(getScopeNames(options.Scope), ","),
@ -78,69 +78,55 @@ func (gui *Gui) refreshSidePanels(options types.RefreshOptions) error {
f := func() {
var scopeMap map[types.RefreshableView]bool
if len(options.Scope) == 0 {
scopeMap = arrToMap([]types.RefreshableView{types.COMMITS, types.BRANCHES, types.FILES, types.STASH, types.REFLOG, types.TAGS, types.REMOTES, types.STATUS, types.BISECT_INFO})
scopeMap = arrToMap([]types.RefreshableView{
types.COMMITS,
types.BRANCHES,
types.FILES,
types.STASH,
types.REFLOG,
types.TAGS,
types.REMOTES,
types.STATUS,
types.BISECT_INFO,
})
} else {
scopeMap = arrToMap(options.Scope)
}
if scopeMap[types.COMMITS] || scopeMap[types.BRANCHES] || scopeMap[types.REFLOG] || scopeMap[types.BISECT_INFO] {
refresh := func(f func()) {
wg.Add(1)
func() {
if options.Mode == types.ASYNC {
go utils.Safe(func() { gui.refreshCommits() })
go utils.Safe(f)
} else {
gui.refreshCommits()
f()
}
wg.Done()
}()
}
if scopeMap[types.COMMITS] || scopeMap[types.BRANCHES] || scopeMap[types.REFLOG] || scopeMap[types.BISECT_INFO] {
refresh(gui.refreshCommits)
} else if scopeMap[types.REBASE_COMMITS] {
// the above block handles rebase commits so we only need to call this one
// if we've asked specifically for rebase commits and not those other things
refresh(func() { _ = gui.refreshRebaseCommits() })
}
if scopeMap[types.FILES] || scopeMap[types.SUBMODULES] {
wg.Add(1)
func() {
if options.Mode == types.ASYNC {
go utils.Safe(func() { _ = gui.refreshFilesAndSubmodules() })
} else {
_ = gui.refreshFilesAndSubmodules()
}
wg.Done()
}()
refresh(func() { _ = gui.refreshFilesAndSubmodules() })
}
if scopeMap[types.STASH] {
wg.Add(1)
func() {
if options.Mode == types.ASYNC {
go utils.Safe(func() { _ = gui.refreshStashEntries() })
} else {
_ = gui.refreshStashEntries()
}
wg.Done()
}()
refresh(func() { _ = gui.refreshStashEntries() })
}
if scopeMap[types.TAGS] {
wg.Add(1)
func() {
if options.Mode == types.ASYNC {
go utils.Safe(func() { _ = gui.refreshTags() })
} else {
_ = gui.refreshTags()
}
wg.Done()
}()
refresh(func() { _ = gui.refreshTags() })
}
if scopeMap[types.REMOTES] {
wg.Add(1)
func() {
if options.Mode == types.ASYNC {
go utils.Safe(func() { _ = gui.refreshRemotes() })
} else {
_ = gui.refreshRemotes()
}
wg.Done()
}()
refresh(func() { _ = gui.refreshRemotes() })
}
wg.Wait()
@ -234,7 +220,7 @@ func (gui *Gui) resizePopupPanel(v *gocui.View, content string) error {
return err
}
func (gui *Gui) changeSelectedLine(panelState IListPanelState, total int, change int) {
func (gui *Gui) changeSelectedLine(panelState types.IListPanelState, total int, change int) {
// TODO: find out why we're doing this
line := panelState.GetSelectedLineIdx()
@ -253,7 +239,7 @@ func (gui *Gui) changeSelectedLine(panelState IListPanelState, total int, change
panelState.SetSelectedLineIdx(newLine)
}
func (gui *Gui) refreshSelectedLine(panelState IListPanelState, total int) {
func (gui *Gui) refreshSelectedLine(panelState types.IListPanelState, total int) {
line := panelState.GetSelectedLineIdx()
if line == -1 && total > 0 {
@ -274,16 +260,16 @@ func (gui *Gui) renderDisplayStringsAtPos(v *gocui.View, y int, displayStrings [
}
func (gui *Gui) globalOptionsMap() map[string]string {
keybindingConfig := gui.UserConfig.Keybinding
keybindingConfig := gui.c.UserConfig.Keybinding
return map[string]string{
fmt.Sprintf("%s/%s", gui.getKeyDisplay(keybindingConfig.Universal.ScrollUpMain), gui.getKeyDisplay(keybindingConfig.Universal.ScrollDownMain)): gui.Tr.LcScroll,
fmt.Sprintf("%s %s %s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevBlock), gui.getKeyDisplay(keybindingConfig.Universal.NextBlock), gui.getKeyDisplay(keybindingConfig.Universal.PrevItem), gui.getKeyDisplay(keybindingConfig.Universal.NextItem)): gui.Tr.LcNavigate,
gui.getKeyDisplay(keybindingConfig.Universal.Return): gui.Tr.LcCancel,
gui.getKeyDisplay(keybindingConfig.Universal.Quit): gui.Tr.LcQuit,
gui.getKeyDisplay(keybindingConfig.Universal.OptionMenu): gui.Tr.LcMenu,
fmt.Sprintf("%s-%s", gui.getKeyDisplay(keybindingConfig.Universal.JumpToBlock[0]), gui.getKeyDisplay(keybindingConfig.Universal.JumpToBlock[len(keybindingConfig.Universal.JumpToBlock)-1])): gui.Tr.LcJump,
fmt.Sprintf("%s/%s", gui.getKeyDisplay(keybindingConfig.Universal.ScrollLeft), gui.getKeyDisplay(keybindingConfig.Universal.ScrollRight)): gui.Tr.LcScrollLeftRight,
fmt.Sprintf("%s/%s", gui.getKeyDisplay(keybindingConfig.Universal.ScrollUpMain), gui.getKeyDisplay(keybindingConfig.Universal.ScrollDownMain)): gui.c.Tr.LcScroll,
fmt.Sprintf("%s %s %s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevBlock), gui.getKeyDisplay(keybindingConfig.Universal.NextBlock), gui.getKeyDisplay(keybindingConfig.Universal.PrevItem), gui.getKeyDisplay(keybindingConfig.Universal.NextItem)): gui.c.Tr.LcNavigate,
gui.getKeyDisplay(keybindingConfig.Universal.Return): gui.c.Tr.LcCancel,
gui.getKeyDisplay(keybindingConfig.Universal.Quit): gui.c.Tr.LcQuit,
gui.getKeyDisplay(keybindingConfig.Universal.OptionMenu): gui.c.Tr.LcMenu,
fmt.Sprintf("%s-%s", gui.getKeyDisplay(keybindingConfig.Universal.JumpToBlock[0]), gui.getKeyDisplay(keybindingConfig.Universal.JumpToBlock[len(keybindingConfig.Universal.JumpToBlock)-1])): gui.c.Tr.LcJump,
fmt.Sprintf("%s/%s", gui.getKeyDisplay(keybindingConfig.Universal.ScrollLeft), gui.getKeyDisplay(keybindingConfig.Universal.ScrollRight)): gui.c.Tr.LcScrollLeftRight,
}
}
@ -302,9 +288,9 @@ func (gui *Gui) secondaryViewFocused() bool {
}
func (gui *Gui) onViewTabClick(viewName string, tabIndex int) error {
context := gui.State.ViewTabContextMap[viewName][tabIndex].contexts[0]
context := gui.State.ViewTabContextMap[viewName][tabIndex].Contexts[0]
return gui.pushContext(context)
return gui.c.PushContext(context)
}
func (gui *Gui) handleNextTab() error {

View File

@ -3,11 +3,11 @@ package gui
func (gui *Gui) toggleWhitespaceInDiffView() error {
gui.IgnoreWhitespaceInDiffView = !gui.IgnoreWhitespaceInDiffView
toastMessage := gui.Tr.ShowingWhitespaceInDiffView
toastMessage := gui.c.Tr.ShowingWhitespaceInDiffView
if gui.IgnoreWhitespaceInDiffView {
toastMessage = gui.Tr.IgnoringWhitespaceInDiffView
toastMessage = gui.c.Tr.IgnoringWhitespaceInDiffView
}
gui.raiseToast(toastMessage)
gui.c.Toast(toastMessage)
return gui.refreshFilesAndSubmodules()
}

Some files were not shown because too many files have changed in this diff Show More