1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-05-27 23:08:02 +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) ## Commits Panel (Commits)
<pre> <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>c</kbd>: copy commit (cherry-pick)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard <kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: copy commit range (cherry-pick) <kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (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>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit <kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag 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>ctrl+y</kbd>: copy commit message to clipboard
<kbd>o</kbd>: open commit in browser <kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options <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>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: amend last commit <kbd>A</kbd>: amend last commit
<kbd>C</kbd>: commit changes using git editor <kbd>C</kbd>: commit changes using git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: view 'discard changes' options <kbd>d</kbd>: view 'discard changes' options
<kbd>e</kbd>: edit file <kbd>e</kbd>: edit file
<kbd>o</kbd>: open 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>`</kbd>: toggle file tree view
<kbd>M</kbd>: open external merge tool (git mergetool) <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>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>space</kbd>: toggle staged
</pre> </pre>
## Files Panel (Submodules) ## 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) ## Commits Paneel (Commits)
<pre> <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>c</kbd>: kopieer commit (cherry-pick)
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord <kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
<kbd>C</kbd>: kopieer commit reeks (cherry-pick) <kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>v</kbd>: plak commits (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>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit <kbd>space</kbd>: checkout commit
<kbd>n</kbd>: creëer nieuwe branch van commit
<kbd>T</kbd>: tag 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>ctrl+y</kbd>: kopieer commit bericht naar klembord
<kbd>o</kbd>: open commit in browser <kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options <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>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>A</kbd>: wijzig laatste commit <kbd>A</kbd>: wijzig laatste commit
<kbd>C</kbd>: commit veranderingen met de git editor <kbd>C</kbd>: commit veranderingen met de git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties <kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
<kbd>e</kbd>: verander bestand <kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open 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>`</kbd>: toggle bestandsboom weergave
<kbd>M</kbd>: open external merge tool (git mergetool) <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>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>space</kbd>: toggle staged
</pre> </pre>
## Bestanden Paneel (Submodules) ## 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) ## Commity Panel (Commity)
<pre> <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>c</kbd>: kopiuj commit (przebieranie)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard <kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie) <kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
<kbd>v</kbd>: wklej commity (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>enter</kbd>: przeglądaj pliki commita
<kbd>space</kbd>: checkout commit <kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag 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>ctrl+y</kbd>: copy commit message to clipboard
<kbd>o</kbd>: open commit in browser <kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options <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>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
<kbd>A</kbd>: Zmień ostatni commit <kbd>A</kbd>: Zmień ostatni commit
<kbd>C</kbd>: Zatwierdź zmiany używając edytora <kbd>C</kbd>: Zatwierdź zmiany używając edytora
<kbd>space</kbd>: przełącz stan poczekalni
<kbd>d</kbd>: pokaż opcje porzucania zmian <kbd>d</kbd>: pokaż opcje porzucania zmian
<kbd>e</kbd>: edytuj plik <kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz 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>`</kbd>: toggle file tree view
<kbd>M</kbd>: open external merge tool (git mergetool) <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>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>space</kbd>: przełącz stan poczekalni
</pre> </pre>
## Pliki Panel (Submodules) ## 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> <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>c</kbd>: 复制提交(拣选)
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板 <kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
<kbd>C</kbd>: 复制提交范围(拣选) <kbd>C</kbd>: 复制提交范围(拣选)
<kbd>v</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>enter</kbd>: 查看提交的文件
<kbd>space</kbd>: 检出提交 <kbd>space</kbd>: 检出提交
<kbd>n</kbd>: 从提交创建新分支
<kbd>T</kbd>: 标签提交 <kbd>T</kbd>: 标签提交
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
<kbd>ctrl+y</kbd>: 将提交消息复制到剪贴板 <kbd>ctrl+y</kbd>: 将提交消息复制到剪贴板
<kbd>o</kbd>: open commit in browser <kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options <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>w</kbd>: 提交更改而无需预先提交钩子
<kbd>A</kbd>: 修补最后一次提交 <kbd>A</kbd>: 修补最后一次提交
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息) <kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
<kbd>space</kbd>: 切换暂存状态
<kbd>d</kbd>: 查看'放弃更改‘选项 <kbd>d</kbd>: 查看'放弃更改‘选项
<kbd>e</kbd>: 编辑文件 <kbd>e</kbd>: 编辑文件
<kbd>o</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>`</kbd>: 切换文件树视图
<kbd>M</kbd>: 打开合并工具 <kbd>M</kbd>: 打开合并工具
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白更改 <kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白更改
<kbd>space</kbd>: 切换暂存状态
</pre> </pre>
## 文件 面板 (子模块) ## 文件 面板 (子模块)

View File

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

View File

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

View File

@ -1,6 +1,8 @@
package git_commands package git_commands
import ( import (
"sync"
gogit "github.com/jesseduffield/go-git/v5" gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/common"
@ -13,6 +15,8 @@ type GitCommon struct {
dotGitDir string dotGitDir string
repo *gogit.Repository repo *gogit.Repository
config *ConfigCommands config *ConfigCommands
// mutex for doing things like push/pull/fetch
syncMutex *sync.Mutex
} }
func NewGitCommon( func NewGitCommon(
@ -22,6 +26,7 @@ func NewGitCommon(
dotGitDir string, dotGitDir string,
repo *gogit.Repository, repo *gogit.Repository,
config *ConfigCommands, config *ConfigCommands,
syncMutex *sync.Mutex,
) *GitCommon { ) *GitCommon {
return &GitCommon{ return &GitCommon{
Common: cmn, Common: cmn,
@ -30,5 +35,6 @@ func NewGitCommon(
dotGitDir: dotGitDir, dotGitDir: dotGitDir,
repo: repo, repo: repo,
config: config, 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 { 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)) 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 // 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) cmdStr += " " + self.cmd.Quote(opts.UpstreamBranch)
} }
cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest() cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex)
return cmdObj, nil return cmdObj, nil
} }
@ -83,7 +83,7 @@ func (self *SyncCommands) Fetch(opts FetchOptions) error {
} else { } else {
cmdObj.PromptOnCredentialRequest() cmdObj.PromptOnCredentialRequest()
} }
return cmdObj.Run() return cmdObj.WithMutex(self.syncMutex).Run()
} }
type PullOptions struct { 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 // setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
// has 'pull.rebase = interactive' configured. // 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 { 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)) 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 { func (self *SyncCommands) FetchRemote(remoteName string) error {
cmdStr := fmt.Sprintf("git fetch %s", self.cmd.Quote(remoteName)) 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 { 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 ( import (
"fmt" "fmt"
"os" "os"
"sync"
"testing" "testing"
"time" "time"
@ -211,7 +212,12 @@ func TestNewGitCommand(t *testing.T) {
s := s s := s
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
s.setup() 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 ( import (
"os/exec" "os/exec"
"sync"
) )
// A command object is a general way to represent a command to be run on the // 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 PromptOnCredentialRequest() ICmdObj
FailOnCredentialRequest() ICmdObj FailOnCredentialRequest() ICmdObj
WithMutex(mutex *sync.Mutex) ICmdObj
Mutex() *sync.Mutex
GetCredentialStrategy() CredentialStrategy 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. // if set to true, it means we might be asked to enter a username/password by this command.
credentialStrategy CredentialStrategy credentialStrategy CredentialStrategy
// can be set so that we don't run certain commands simultaneously
mutex *sync.Mutex
} }
type CredentialStrategy int type CredentialStrategy int
@ -132,6 +139,16 @@ func (self *CmdObj) IgnoreEmptyError() ICmdObj {
return self 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 { func (self *CmdObj) ShouldIgnoreEmptyError() bool {
return self.ignoreEmptyError return self.ignoreEmptyError
} }

View File

@ -34,6 +34,11 @@ type cmdObjRunner struct {
var _ ICmdObjRunner = &cmdObjRunner{} var _ ICmdObjRunner = &cmdObjRunner{}
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error { func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
}
if cmdObj.GetCredentialStrategy() != NONE { if cmdObj.GetCredentialStrategy() != NONE {
return self.runWithCredentialHandling(cmdObj) return self.runWithCredentialHandling(cmdObj)
} }
@ -42,17 +47,14 @@ func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
return self.runAndStream(cmdObj) return self.runAndStream(cmdObj)
} }
_, err := self.RunWithOutput(cmdObj) _, err := self.RunWithOutputAux(cmdObj)
return err return err
} }
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) { func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
if cmdObj.ShouldStreamOutput() { if cmdObj.Mutex() != nil {
err := self.runAndStream(cmdObj) cmdObj.Mutex().Lock()
// for now we're not capturing output, just because it would take a little more defer cmdObj.Mutex().Unlock()
// 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.GetCredentialStrategy() != NONE { if cmdObj.GetCredentialStrategy() != NONE {
@ -63,6 +65,18 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
return "", err 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") self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
if cmdObj.ShouldLog() { 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 { 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 { 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") 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 return topStatus.message
} }
func (gui *Gui) raiseToast(message string) { func (gui *Gui) toast(message string) {
gui.statusManager.addToastStatus(message) gui.statusManager.addToastStatus(message)
gui.renderAppStatus() gui.renderAppStatus()
@ -119,7 +119,7 @@ func (gui *Gui) withWaitingStatus(message string, f func() error) error {
if err := f(); err != nil { if err := f(); err != nil {
gui.OnUIThread(func() error { gui.OnUIThread(func() error {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
}) })
} }
}) })

View File

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

View File

@ -1,22 +1,28 @@
package gui package gui
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type BasicContext struct { type BasicContext struct {
OnFocus func(opts ...OnFocusOpts) error OnFocus func(opts ...types.OnFocusOpts) error
OnFocusLost func() error OnFocusLost func() error
OnRender func() error OnRender func() error
// this is for pushing some content to the main view // this is for pushing some content to the main view
OnRenderToMain func(opts ...OnFocusOpts) error OnRenderToMain func(opts ...types.OnFocusOpts) error
Kind ContextKind Kind types.ContextKind
Key ContextKey Key types.ContextKey
ViewName string ViewName string
WindowName string WindowName string
OnGetOptionsMap func() map[string]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 // 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 hasParent bool
} }
var _ types.Context = &BasicContext{}
func (self *BasicContext) GetOptionsMap() map[string]string { func (self *BasicContext) GetOptionsMap() map[string]string {
if self.OnGetOptionsMap != nil { if self.OnGetOptionsMap != nil {
return self.OnGetOptionsMap() return self.OnGetOptionsMap()
@ -24,12 +30,12 @@ func (self *BasicContext) GetOptionsMap() map[string]string {
return nil return nil
} }
func (self *BasicContext) SetParentContext(context Context) { func (self *BasicContext) SetParentContext(context types.Context) {
self.ParentContext = context self.ParentContext = context
self.hasParent = true self.hasParent = true
} }
func (self *BasicContext) GetParentContext() (Context, bool) { func (self *BasicContext) GetParentContext() (types.Context, bool) {
return self.ParentContext, self.hasParent return self.ParentContext, self.hasParent
} }
@ -59,7 +65,7 @@ func (self *BasicContext) GetViewName() string {
return self.ViewName return self.ViewName
} }
func (self *BasicContext) HandleFocus(opts ...OnFocusOpts) error { func (self *BasicContext) HandleFocus(opts ...types.OnFocusOpts) error {
if self.OnFocus != nil { if self.OnFocus != nil {
if err := self.OnFocus(opts...); err != nil { if err := self.OnFocus(opts...); err != nil {
return err return err
@ -90,10 +96,10 @@ func (self *BasicContext) HandleRenderToMain() error {
return nil return nil
} }
func (self *BasicContext) GetKind() ContextKind { func (self *BasicContext) GetKind() types.ContextKind {
return self.Kind return self.Kind
} }
func (self *BasicContext) GetKey() ContextKey { func (self *BasicContext) GetKey() types.ContextKey {
return self.Key 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/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models" "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/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
@ -31,9 +32,9 @@ func (gui *Gui) branchesRenderToMain() error {
var task updateTask var task updateTask
branch := gui.getSelectedBranch() branch := gui.getSelectedBranch()
if branch == nil { if branch == nil {
task = NewRenderStringTask(gui.Tr.NoBranchesThisRepo) task = NewRenderStringTask(gui.c.Tr.NoBranchesThisRepo)
} else { } else {
cmdObj := gui.Git.Branch.GetGraphCmdObj(branch.Name) cmdObj := gui.git.Branch.GetGraphCmdObj(branch.Name)
task = NewRunPtyTask(cmdObj.GetCmd()) 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 // which allows us to order them correctly. So if we're filtering we'll just
// manually load all the reflog commits here // manually load all the reflog commits here
var err error var err error
reflogCommits, _, err = gui.Git.Loaders.ReflogCommits.GetReflogCommits(nil, "") reflogCommits, _, err = gui.git.Loaders.ReflogCommits.GetReflogCommits(nil, "")
if err != 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 { if err != nil {
_ = gui.PopupHandler.Error(err) _ = gui.c.Error(err)
} }
gui.State.Branches = branches gui.State.Branches = branches
if err := gui.postRefreshUpdate(gui.State.Contexts.Branches); err != nil { if err := gui.c.PostRefreshUpdate(gui.State.Contexts.Branches); err != nil {
gui.Log.Error(err) gui.c.Log.Error(err)
} }
gui.refreshStatus() gui.refreshStatus()
@ -83,11 +84,11 @@ func (gui *Gui) handleBranchPress() error {
return nil return nil
} }
if gui.State.Panels.Branches.SelectedLineIdx == 0 { 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() branch := gui.getSelectedBranch()
gui.logAction(gui.Tr.Actions.CheckoutBranch) gui.c.LogAction(gui.c.Tr.Actions.CheckoutBranch)
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{}) return gui.refHelper.CheckoutRef(branch.Name, types.CheckoutRefOptions{})
} }
func (gui *Gui) handleCreatePullRequestPress() error { func (gui *Gui) handleCreatePullRequestPress() error {
@ -110,129 +111,64 @@ func (gui *Gui) handleCopyPullRequestURLPress() error {
branch := gui.getSelectedBranch() branch := gui.getSelectedBranch()
branchExistsOnRemote := gui.Git.Remote.CheckRemoteBranchExists(branch.Name) branchExistsOnRemote := gui.git.Remote.CheckRemoteBranchExists(branch.Name)
if !branchExistsOnRemote { 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, "") url, err := hostingServiceMgr.GetPullRequestURL(branch.Name, "")
if err != nil { 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 { 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 return nil
} }
func (gui *Gui) handleGitFetch() error { 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 { 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 { func (gui *Gui) handleForceCheckout() error {
branch := gui.getSelectedBranch() branch := gui.getSelectedBranch()
message := gui.Tr.SureForceCheckout message := gui.c.Tr.SureForceCheckout
title := gui.Tr.ForceCheckoutBranch title := gui.c.Tr.ForceCheckoutBranch
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: title, Title: title,
Prompt: message, Prompt: message,
HandleConfirm: func() error { HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.ForceCheckoutBranch) gui.c.LogAction(gui.c.Tr.Actions.ForceCheckoutBranch)
if err := gui.Git.Branch.Checkout(branch.Name, git_commands.CheckoutOptions{Force: true}); err != nil { if err := gui.git.Branch.Checkout(branch.Name, git_commands.CheckoutOptions{Force: true}); 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})
}, },
}) })
} }
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 { func (gui *Gui) handleCheckoutByName() error {
return gui.PopupHandler.Prompt(popup.PromptOpts{ return gui.c.Prompt(popup.PromptOpts{
Title: gui.Tr.BranchName + ":", Title: gui.c.Tr.BranchName + ":",
FindSuggestionsFunc: gui.getRefsSuggestionsFunc(), FindSuggestionsFunc: gui.suggestionsHelper.GetRefsSuggestionsFunc(),
HandleConfirm: func(response string) error { HandleConfirm: func(response string) error {
gui.logAction("Checkout branch") gui.c.LogAction("Checkout branch")
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{ return gui.refHelper.CheckoutRef(response, types.CheckoutRefOptions{
onRefNotFound: func(ref string) error { OnRefNotFound: func(ref string) error {
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.BranchNotFoundTitle, Title: gui.c.Tr.BranchNotFoundTitle,
Prompt: fmt.Sprintf("%s %s%s", gui.Tr.BranchNotFoundPrompt, ref, "?"), Prompt: fmt.Sprintf("%s %s%s", gui.c.Tr.BranchNotFoundPrompt, ref, "?"),
HandleConfirm: func() error { HandleConfirm: func() error {
return gui.createNewBranchWithName(ref) return gui.createNewBranchWithName(ref)
}, },
@ -257,12 +193,12 @@ func (gui *Gui) createNewBranchWithName(newBranchName string) error {
return nil return nil
} }
if err := gui.Git.Branch.New(newBranchName, branch.Name); err != nil { if err := gui.git.Branch.New(newBranchName, branch.Name); err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
gui.State.Panels.Branches.SelectedLineIdx = 0 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 { func (gui *Gui) handleDeleteBranch() error {
@ -276,18 +212,18 @@ func (gui *Gui) deleteBranch(force bool) error {
} }
checkedOutBranch := gui.getCheckedOutBranch() checkedOutBranch := gui.getCheckedOutBranch()
if checkedOutBranch.Name == selectedBranch.Name { 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) return gui.deleteNamedBranch(selectedBranch, force)
} }
func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) error { func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) error {
title := gui.Tr.DeleteBranch title := gui.c.Tr.DeleteBranch
var templateStr string var templateStr string
if force { if force {
templateStr = gui.Tr.ForceDeleteBranchMessage templateStr = gui.c.Tr.ForceDeleteBranchMessage
} else { } else {
templateStr = gui.Tr.DeleteBranchMessage templateStr = gui.c.Tr.DeleteBranchMessage
} }
message := utils.ResolvePlaceholderString( message := utils.ResolvePlaceholderString(
templateStr, 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, Title: title,
Prompt: message, Prompt: message,
HandleConfirm: func() error { HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.DeleteBranch) gui.c.LogAction(gui.c.Tr.Actions.DeleteBranch)
if err := gui.Git.Branch.Delete(selectedBranch.Name, force); err != nil { if err := gui.git.Branch.Delete(selectedBranch.Name, force); err != nil {
errMessage := err.Error() errMessage := err.Error()
if !force && strings.Contains(errMessage, "git branch -D ") { if !force && strings.Contains(errMessage, "git branch -D ") {
return gui.deleteNamedBranch(selectedBranch, true) 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 { func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok { if gui.git.Branch.IsHeadDetached() {
return err 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")
}
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")
} }
checkedOutBranchName := gui.getCheckedOutBranch().Name checkedOutBranchName := gui.getCheckedOutBranch().Name
if checkedOutBranchName == branchName { if checkedOutBranchName == branchName {
return gui.PopupHandler.ErrorMsg(gui.Tr.CantMergeBranchIntoItself) return gui.c.ErrorMsg(gui.c.Tr.CantMergeBranchIntoItself)
} }
prompt := utils.ResolvePlaceholderString( prompt := utils.ResolvePlaceholderString(
gui.Tr.ConfirmMerge, gui.c.Tr.ConfirmMerge,
map[string]string{ map[string]string{
"checkedOutBranch": checkedOutBranchName, "checkedOutBranch": checkedOutBranchName,
"selectedBranch": branchName, "selectedBranch": branchName,
}, },
) )
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.MergingTitle, Title: gui.c.Tr.MergingTitle,
Prompt: prompt, Prompt: prompt,
HandleConfirm: func() error { HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.Merge) gui.c.LogAction(gui.c.Tr.Actions.Merge)
err := gui.Git.Branch.Merge(branchName, git_commands.MergeOpts{}) err := gui.git.Branch.Merge(branchName, git_commands.MergeOpts{})
return gui.handleGenericMergeCommandResult(err) return gui.checkMergeOrRebase(err)
}, },
}) })
} }
func (gui *Gui) handleMerge() error { func (gui *Gui) handleMerge() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
selectedBranchName := gui.getSelectedBranch().Name selectedBranchName := gui.getSelectedBranch().Name
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName) return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
} }
@ -359,29 +287,25 @@ func (gui *Gui) handleRebaseOntoLocalBranch() error {
} }
func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error { func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
checkedOutBranch := gui.getCheckedOutBranch().Name checkedOutBranch := gui.getCheckedOutBranch().Name
if selectedBranchName == checkedOutBranch { if selectedBranchName == checkedOutBranch {
return gui.PopupHandler.ErrorMsg(gui.Tr.CantRebaseOntoSelf) return gui.c.ErrorMsg(gui.c.Tr.CantRebaseOntoSelf)
} }
prompt := utils.ResolvePlaceholderString( prompt := utils.ResolvePlaceholderString(
gui.Tr.ConfirmRebase, gui.c.Tr.ConfirmRebase,
map[string]string{ map[string]string{
"checkedOutBranch": checkedOutBranch, "checkedOutBranch": checkedOutBranch,
"selectedBranch": selectedBranchName, "selectedBranch": selectedBranchName,
}, },
) )
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.RebasingTitle, Title: gui.c.Tr.RebasingTitle,
Prompt: prompt, Prompt: prompt,
HandleConfirm: func() error { HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.RebaseBranch) gui.c.LogAction(gui.c.Tr.Actions.RebaseBranch)
err := gui.Git.Rebase.RebaseBranch(selectedBranchName) err := gui.git.Rebase.RebaseBranch(selectedBranchName)
return gui.handleGenericMergeCommandResult(err) return gui.checkMergeOrRebase(err)
}, },
}) })
} }
@ -393,35 +317,35 @@ func (gui *Gui) handleFastForward() error {
} }
if !branch.IsTrackingRemote() { if !branch.IsTrackingRemote() {
return gui.PopupHandler.ErrorMsg(gui.Tr.FwdNoUpstream) return gui.c.ErrorMsg(gui.c.Tr.FwdNoUpstream)
} }
if !branch.RemoteBranchStoredLocally() { if !branch.RemoteBranchStoredLocally() {
return gui.PopupHandler.ErrorMsg(gui.Tr.FwdNoLocalUpstream) return gui.c.ErrorMsg(gui.c.Tr.FwdNoLocalUpstream)
} }
if branch.HasCommitsToPush() { 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( message := utils.ResolvePlaceholderString(
gui.Tr.Fetching, gui.c.Tr.Fetching,
map[string]string{ map[string]string{
"from": fmt.Sprintf("%s/%s", branch.UpstreamRemote, branch.UpstreamBranch), "from": fmt.Sprintf("%s/%s", branch.UpstreamRemote, branch.UpstreamBranch),
"to": branch.Name, "to": branch.Name,
}, },
) )
return gui.PopupHandler.WithLoaderPanel(message, func() error { return gui.c.WithLoaderPanel(message, func() error {
if gui.State.Panels.Branches.SelectedLineIdx == 0 { 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 { } else {
gui.logAction(action) gui.c.LogAction(action)
err := gui.Git.Sync.FastForward(branch.Name, branch.UpstreamRemote, branch.UpstreamBranch) err := gui.git.Sync.FastForward(branch.Name, branch.UpstreamRemote, branch.UpstreamBranch)
if err != nil { 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 return nil
@ -434,7 +358,7 @@ func (gui *Gui) handleCreateResetToBranchMenu() error {
return nil return nil
} }
return gui.createResetMenu(branch.Name) return gui.refHelper.CreateGitResetMenu(branch.Name)
} }
func (gui *Gui) handleRenameBranch() error { func (gui *Gui) handleRenameBranch() error {
@ -444,13 +368,13 @@ func (gui *Gui) handleRenameBranch() error {
} }
promptForNewName := func() error { promptForNewName := func() error {
return gui.PopupHandler.Prompt(popup.PromptOpts{ return gui.c.Prompt(popup.PromptOpts{
Title: gui.Tr.NewBranchNamePrompt + " " + branch.Name + ":", Title: gui.c.Tr.NewBranchNamePrompt + " " + branch.Name + ":",
InitialContent: branch.Name, InitialContent: branch.Name,
HandleConfirm: func(newBranchName string) error { HandleConfirm: func(newBranchName string) error {
gui.logAction(gui.Tr.Actions.RenameBranch) gui.c.LogAction(gui.c.Tr.Actions.RenameBranch)
if err := gui.Git.Branch.Rename(branch.Name, newBranchName); err != nil { if err := gui.git.Branch.Rename(branch.Name, newBranchName); err != nil {
return gui.PopupHandler.Error(err) 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 // 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 promptForNewName()
} }
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.LcRenameBranch, Title: gui.c.Tr.LcRenameBranch,
Prompt: gui.Tr.RenameBranchWarning, Prompt: gui.c.Tr.RenameBranchWarning,
HandleConfirm: promptForNewName, 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 { func (gui *Gui) handleNewBranchOffCurrentItem() error {
context := gui.currentSideListContext() context := gui.currentSideListContext()
@ -501,7 +418,7 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
} }
message := utils.ResolvePlaceholderString( message := utils.ResolvePlaceholderString(
gui.Tr.NewBranchNameBranchOff, gui.c.Tr.NewBranchNameBranchOff,
map[string]string{ map[string]string{
"branchName": item.Description(), "branchName": item.Description(),
}, },
@ -513,12 +430,12 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
prefilledName = strings.SplitAfterN(item.ID(), "/", 2)[1] prefilledName = strings.SplitAfterN(item.ID(), "/", 2)[1]
} }
return gui.PopupHandler.Prompt(popup.PromptOpts{ return gui.c.Prompt(popup.PromptOpts{
Title: message, Title: message,
InitialContent: prefilledName, InitialContent: prefilledName,
HandleConfirm: func(response string) error { HandleConfirm: func(response string) error {
gui.logAction(gui.Tr.Actions.CreateBranch) gui.c.LogAction(gui.c.Tr.Actions.CreateBranch)
if err := gui.Git.Branch.New(sanitizedBranchName(response), item.ID()); err != nil { if err := gui.git.Branch.New(sanitizedBranchName(response), item.ID()); err != nil {
return err return err
} }
@ -529,14 +446,14 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
} }
if context.GetKey() != gui.State.Contexts.Branches.GetKey() { 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 return err
} }
} }
gui.State.Panels.Branches.SelectedLineIdx = 0 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 ( import (
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/popup" "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 // 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 { func (gui *Gui) resetCherryPickingIfNecessary(context types.Context) error {
oldContextKey := ContextKey(gui.State.Modes.CherryPicking.ContextKey) oldContextKey := types.ContextKey(gui.State.Modes.CherryPicking.ContextKey)
if oldContextKey != context.GetKey() { if oldContextKey != context.GetKey() {
// need to reset the cherry picking mode // need to reset the cherry picking mode
@ -22,10 +23,6 @@ func (gui *Gui) resetCherryPickingIfNecessary(context Context) error {
} }
func (gui *Gui) handleCopyCommit() 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. // get currently selected commit, add the sha to state.
context := gui.currentSideListContext() context := gui.currentSideListContext()
if context == nil { if context == nil {
@ -80,7 +77,7 @@ func (gui *Gui) commitsListForContext() []*models.Commit {
case SUB_COMMITS_CONTEXT_KEY: case SUB_COMMITS_CONTEXT_KEY:
return gui.State.SubCommits return gui.State.SubCommits
default: 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 return nil
} }
} }
@ -102,10 +99,6 @@ func (gui *Gui) addCommitToCherryPickedCommits(index int) {
} }
func (gui *Gui) handleCopyCommitRange() error { func (gui *Gui) handleCopyCommitRange() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
// get currently selected commit, add the sha to state. // get currently selected commit, add the sha to state.
context := gui.currentSideListContext() context := gui.currentSideListContext()
if context == nil { 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 // HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
func (gui *Gui) HandlePasteCommits() error { func (gui *Gui) HandlePasteCommits() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok { return gui.c.Ask(popup.AskOpts{
return err Title: gui.c.Tr.CherryPick,
} Prompt: gui.c.Tr.SureCherryPick,
return gui.PopupHandler.Ask(popup.AskOpts{
Title: gui.Tr.CherryPick,
Prompt: gui.Tr.SureCherryPick,
HandleConfirm: func() error { HandleConfirm: func() error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.CherryPickingStatus, func() error { return gui.c.WithWaitingStatus(gui.c.Tr.CherryPickingStatus, func() error {
gui.logAction(gui.Tr.Actions.CherryPick) gui.c.LogAction(gui.c.Tr.Actions.CherryPick)
err := gui.Git.Rebase.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits) err := gui.git.Rebase.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
return gui.handleGenericMergeCommandResult(err) return gui.checkMergeOrRebase(err)
}) })
}, },
}) })
} }
func (gui *Gui) exitCherryPickingMode() error { 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.ContextKey = ""
gui.State.Modes.CherryPicking.CherryPickedCommits = nil gui.State.Modes.CherryPicking.CherryPickedCommits = nil
if contextKey == "" { 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 nil
} }
return gui.rerenderContextViewIfPresent(contextKey) return gui.rerenderContextViewIfPresent(contextKey)
} }
func (gui *Gui) rerenderContextViewIfPresent(contextKey ContextKey) error { func (gui *Gui) rerenderContextViewIfPresent(contextKey types.ContextKey) error {
if contextKey == "" { if contextKey == "" {
return nil return nil
} }
@ -184,11 +173,11 @@ func (gui *Gui) rerenderContextViewIfPresent(contextKey ContextKey) error {
view, err := gui.g.View(viewName) view, err := gui.g.View(viewName)
if err != nil { if err != nil {
gui.Log.Error(err) gui.c.Log.Error(err)
return nil return nil
} }
if ContextKey(view.Context) == contextKey { if types.ContextKey(view.Context) == contextKey {
if err := context.HandleRender(); err != nil { if err := context.HandleRender(); err != nil {
return err 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. // 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 // We pass logCommand to our OSCommand struct so that it can handle logging commands
// for us. // for us.
func (gui *Gui) logAction(action string) { func (gui *Gui) LogAction(action string) {
if gui.Views.Extras == nil { if gui.Views.Extras == nil {
return return
} }
@ -32,7 +32,7 @@ func (gui *Gui) logAction(action string) {
fmt.Fprint(gui.Views.Extras, "\n"+style.FgYellow.Sprint(action)) 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 { if gui.Views.Extras == nil {
return return
} }
@ -52,23 +52,23 @@ func (gui *Gui) logCommand(cmdStr string, commandLine bool) {
func (gui *Gui) printCommandLogHeader() { func (gui *Gui) printCommandLogHeader() {
introStr := fmt.Sprintf( introStr := fmt.Sprintf(
gui.Tr.CommandLogHeader, gui.c.Tr.CommandLogHeader,
gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.ExtrasMenu), gui.getKeyDisplay(gui.c.UserConfig.Keybinding.Universal.ExtrasMenu),
) )
fmt.Fprintln(gui.Views.Extras, style.FgCyan.Sprint(introStr)) fmt.Fprintln(gui.Views.Extras, style.FgCyan.Sprint(introStr))
if gui.UserConfig.Gui.ShowRandomTip { if gui.c.UserConfig.Gui.ShowRandomTip {
fmt.Fprintf( fmt.Fprintf(
gui.Views.Extras, gui.Views.Extras,
"%s: %s", "%s: %s",
style.FgYellow.Sprint(gui.Tr.RandomTip), style.FgYellow.Sprint(gui.c.Tr.RandomTip),
style.FgGreen.Sprint(gui.getRandomTip()), style.FgGreen.Sprint(gui.getRandomTip()),
) )
} }
} }
func (gui *Gui) getRandomTip() string { func (gui *Gui) getRandomTip() string {
config := gui.UserConfig.Keybinding config := gui.c.UserConfig.Keybinding
formattedKey := func(key string) string { formattedKey := func(key string) string {
return gui.getKeyDisplay(key) return gui.getKeyDisplay(key)

View File

@ -3,6 +3,7 @@ package gui
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch" "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/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
@ -47,7 +48,7 @@ func (gui *Gui) commitFilesRenderToMain() error {
to := gui.State.CommitFileTreeViewModel.GetParent() to := gui.State.CommitFileTreeViewModel.GetParent()
from, reverse := gui.getFromAndReverseArgsForDiff(to) 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()) task := NewRunPtyTask(cmdObj.GetCmd())
return gui.refreshMainViews(refreshMainOpts{ return gui.refreshMainViews(refreshMainOpts{
@ -65,12 +66,12 @@ func (gui *Gui) handleCheckoutCommitFile() error {
return nil return nil
} }
gui.logAction(gui.Tr.Actions.CheckoutFile) gui.c.LogAction(gui.c.Tr.Actions.CheckoutFile)
if err := gui.Git.WorkingTree.CheckoutFile(gui.State.CommitFileTreeViewModel.GetParent(), node.GetPath()); err != nil { if err := gui.git.WorkingTree.CheckoutFile(gui.State.CommitFileTreeViewModel.GetParent(), node.GetPath()); err != nil {
return gui.PopupHandler.Error(err) 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 { func (gui *Gui) handleDiscardOldFileChange() error {
@ -80,19 +81,19 @@ func (gui *Gui) handleDiscardOldFileChange() error {
fileName := gui.getSelectedCommitFileName() fileName := gui.getSelectedCommitFileName()
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.DiscardFileChangesTitle, Title: gui.c.Tr.DiscardFileChangesTitle,
Prompt: gui.Tr.DiscardFileChangesPrompt, Prompt: gui.c.Tr.DiscardFileChangesPrompt,
HandleConfirm: func() error { HandleConfirm: func() error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error {
gui.logAction(gui.Tr.Actions.DiscardOldFileChange) 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.git.Rebase.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil {
if err := gui.handleGenericMergeCommandResult(err); err != nil { if err := gui.checkMergeOrRebase(err); err != nil {
return err 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 to := gui.State.Panels.CommitFiles.refName
from, reverse := gui.getFromAndReverseArgsForDiff(to) 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 { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
gui.State.CommitFileTreeViewModel.SetParent(to) gui.State.CommitFileTreeViewModel.SetParent(to)
gui.State.CommitFileTreeViewModel.SetFiles(files) 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 { func (gui *Gui) handleOpenOldCommitFile() error {
@ -125,7 +126,7 @@ func (gui *Gui) handleOpenOldCommitFile() error {
return nil return nil
} }
return gui.openFile(node.GetPath()) return gui.fileHelper.OpenFile(node.GetPath())
} }
func (gui *Gui) handleEditCommitFile() error { func (gui *Gui) handleEditCommitFile() error {
@ -135,10 +136,10 @@ func (gui *Gui) handleEditCommitFile() error {
} }
if node.File == nil { 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 { func (gui *Gui) handleToggleFileForPatch() error {
@ -148,7 +149,7 @@ func (gui *Gui) handleToggleFileForPatch() error {
} }
toggleTheFile := func() error { toggleTheFile := func() error {
if !gui.Git.Patch.PatchManager.Active() { if !gui.git.Patch.PatchManager.Active() {
if err := gui.startPatchManager(); err != nil { if err := gui.startPatchManager(); err != nil {
return err 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, // if there is any file that hasn't been fully added we'll fully add everything,
// otherwise we'll remove everything // otherwise we'll remove everything
adding := node.AnyFile(func(file *models.CommitFile) bool { 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 { err := node.ForEachFile(func(file *models.CommitFile) error {
if adding { if adding {
return gui.Git.Patch.PatchManager.AddFileWhole(file.Name) return gui.git.Patch.PatchManager.AddFileWhole(file.Name)
} else { } else {
return gui.Git.Patch.PatchManager.RemoveFile(file.Name) return gui.git.Patch.PatchManager.RemoveFile(file.Name)
} }
}) })
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
if gui.Git.Patch.PatchManager.IsEmpty() { if gui.git.Patch.PatchManager.IsEmpty() {
gui.Git.Patch.PatchManager.Reset() 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() { if gui.git.Patch.PatchManager.Active() && gui.git.Patch.PatchManager.To != gui.State.CommitFileTreeViewModel.GetParent() {
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.DiscardPatch, Title: gui.c.Tr.DiscardPatch,
Prompt: gui.Tr.DiscardPatchConfirm, Prompt: gui.c.Tr.DiscardPatchConfirm,
HandleConfirm: func() error { HandleConfirm: func() error {
gui.Git.Patch.PatchManager.Reset() gui.git.Patch.PatchManager.Reset()
return toggleTheFile() return toggleTheFile()
}, },
}) })
@ -199,15 +200,15 @@ func (gui *Gui) startPatchManager() error {
to := gui.State.Panels.CommitFiles.refName to := gui.State.Panels.CommitFiles.refName
from, reverse := gui.getFromAndReverseArgsForDiff(to) 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 return nil
} }
func (gui *Gui) handleEnterCommitFile() error { 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() node := gui.getSelectedCommitFileNode()
if node == nil { if node == nil {
return nil return nil
@ -218,21 +219,21 @@ func (gui *Gui) enterCommitFile(opts OnFocusOpts) error {
} }
enterTheFile := func() error { enterTheFile := func() error {
if !gui.Git.Patch.PatchManager.Active() { if !gui.git.Patch.PatchManager.Active() {
if err := gui.startPatchManager(); err != nil { if err := gui.startPatchManager(); err != nil {
return err 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() { if gui.git.Patch.PatchManager.Active() && gui.git.Patch.PatchManager.To != gui.State.CommitFileTreeViewModel.GetParent() {
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.DiscardPatch, Title: gui.c.Tr.DiscardPatch,
Prompt: gui.Tr.DiscardPatchConfirm, Prompt: gui.c.Tr.DiscardPatchConfirm,
HandleConfirm: func() error { HandleConfirm: func() error {
gui.Git.Patch.PatchManager.Reset() gui.git.Patch.PatchManager.Reset()
return enterTheFile() return enterTheFile()
}, },
}) })
@ -249,29 +250,29 @@ func (gui *Gui) handleToggleCommitFileDirCollapsed() error {
gui.State.CommitFileTreeViewModel.ToggleCollapsed(node.GetPath()) gui.State.CommitFileTreeViewModel.ToggleCollapsed(node.GetPath())
if err := gui.postRefreshUpdate(gui.State.Contexts.CommitFiles); err != nil { if err := gui.c.PostRefreshUpdate(gui.State.Contexts.CommitFiles); err != nil {
gui.Log.Error(err) gui.c.Log.Error(err)
} }
return nil 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 // 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. // no longer considers the commitFiles view as its main view.
gui.resetWindowForView(gui.Views.CommitFiles) gui.resetWindowForView(gui.Views.CommitFiles)
gui.State.Panels.CommitFiles.SelectedLineIdx = 0 gui.State.Panels.CommitFiles.SelectedLineIdx = 0
gui.State.Panels.CommitFiles.refName = refName gui.State.Panels.CommitFiles.refName = opts.RefName
gui.State.Panels.CommitFiles.canRebase = canRebase gui.State.Panels.CommitFiles.canRebase = opts.CanRebase
gui.State.Contexts.CommitFiles.SetParentContext(context) gui.State.Contexts.CommitFiles.SetParentContext(opts.Context)
gui.State.Contexts.CommitFiles.SetWindowName(windowName) gui.State.Contexts.CommitFiles.SetWindowName(opts.WindowName)
if err := gui.refreshCommitFilesView(); err != nil { if err := gui.refreshCommitFilesView(); err != nil {
return err 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 // 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 gui.c.PostRefreshUpdate(gui.State.Contexts.CommitFiles)
return err
}
if err := gui.State.Contexts.CommitFiles.HandleFocus(); err != nil {
return err
}
return nil
} }

View File

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

View File

@ -1,13 +1,10 @@
package gui package gui
import ( import (
"fmt"
"sync" "sync"
"github.com/jesseduffield/lazygit/pkg/commands/loaders" "github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models" "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" "github.com/jesseduffield/lazygit/pkg/utils"
) )
@ -31,7 +28,7 @@ func (gui *Gui) onCommitFocus() error {
state.LimitCommits = false state.LimitCommits = false
go utils.Safe(func() { go utils.Safe(func() {
if err := gui.refreshCommitsWithLimit(); err != nil { 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 var task updateTask
commit := gui.getSelectedLocalCommit() commit := gui.getSelectedLocalCommit()
if commit == nil { if commit == nil {
task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch) task = NewRenderStringTask(gui.c.Tr.NoCommitsThisBranch)
} else { } 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()) task = NewRunPtyTask(cmdObj.GetCmd())
} }
@ -118,7 +115,7 @@ func (gui *Gui) refreshCommitsWithLimit() error {
gui.Mutexes.BranchCommitsMutex.Lock() gui.Mutexes.BranchCommitsMutex.Lock()
defer gui.Mutexes.BranchCommitsMutex.Unlock() defer gui.Mutexes.BranchCommitsMutex.Unlock()
commits, err := gui.Git.Loaders.Commits.GetCommits( commits, err := gui.git.Loaders.Commits.GetCommits(
loaders.GetCommitsOptions{ loaders.GetCommitsOptions{
Limit: gui.State.Panels.Commits.LimitCommits, Limit: gui.State.Panels.Commits.LimitCommits,
FilterPath: gui.State.Modes.Filtering.GetPath(), FilterPath: gui.State.Modes.Filtering.GetPath(),
@ -132,11 +129,11 @@ func (gui *Gui) refreshCommitsWithLimit() error {
} }
gui.State.Commits = commits gui.State.Commits = commits
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits) return gui.c.PostRefreshUpdate(gui.State.Contexts.BranchCommits)
} }
func (gui *Gui) refForLog() string { func (gui *Gui) refForLog() string {
bisectInfo := gui.Git.Bisect.GetInfo() bisectInfo := gui.git.Bisect.GetInfo()
gui.State.BisectInfo = bisectInfo gui.State.BisectInfo = bisectInfo
if !bisectInfo.Started() { 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. // 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() return bisectInfo.GetNewSha()
} }
@ -155,691 +152,11 @@ func (gui *Gui) refreshRebaseCommits() error {
gui.Mutexes.BranchCommitsMutex.Lock() gui.Mutexes.BranchCommitsMutex.Lock()
defer gui.Mutexes.BranchCommitsMutex.Unlock() 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 { if err != nil {
return err return err
} }
gui.State.Commits = updatedCommits gui.State.Commits = updatedCommits
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits) return gui.c.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
} }

View File

@ -19,7 +19,7 @@ func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function f
if function != nil { if function != nil {
if err := function(); err != 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 function != nil {
if err := function(getResponse()); err != 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 suggestionsView.FgColor = theme.GocuiDefaultTextColor
gui.setSuggestions(findSuggestionsFunc("")) gui.setSuggestions(findSuggestionsFunc(""))
suggestionsView.Visible = true 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 return nil
@ -171,12 +171,12 @@ func (gui *Gui) createPopupPanel(opts popup.CreatePopupPanelOpts) error {
return err 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 { func (gui *Gui) setKeyBindings(opts popup.CreatePopupPanelOpts) error {
actions := utils.ResolvePlaceholderString( actions := utils.ResolvePlaceholderString(
gui.Tr.CloseConfirm, gui.c.Tr.CloseConfirm,
map[string]string{ map[string]string{
"keyBindClose": "esc", "keyBindClose": "esc",
"keyBindConfirm": "enter", "keyBindConfirm": "enter",
@ -197,7 +197,7 @@ func (gui *Gui) setKeyBindings(opts popup.CreatePopupPanelOpts) error {
handler func() error handler func() error
} }
keybindingConfig := gui.UserConfig.Keybinding keybindingConfig := gui.c.UserConfig.Keybinding
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction( onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(
opts.HandlersManageFocus, opts.HandlersManageFocus,
opts.HandleConfirmPrompt, opts.HandleConfirmPrompt,
@ -262,7 +262,7 @@ func (gui *Gui) setKeyBindings(opts popup.CreatePopupPanelOpts) error {
} }
func (gui *Gui) clearConfirmationViewKeyBindings() { 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.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone) _ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), 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() 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" "fmt"
"github.com/jesseduffield/gocui" "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 { func (gui *Gui) popupViewNames() []string {
result := []string{} result := []string{}
for _, context := range gui.allContexts() { 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()) result = append(result, context.GetViewName())
} }
} }
@ -50,7 +19,7 @@ func (gui *Gui) popupViewNames() []string {
return result return result
} }
func (gui *Gui) currentContextKeyIgnoringPopups() ContextKey { func (gui *Gui) currentContextKeyIgnoringPopups() types.ContextKey {
gui.State.ContextManager.RLock() gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock() defer gui.State.ContextManager.RUnlock()
@ -60,7 +29,7 @@ func (gui *Gui) currentContextKeyIgnoringPopups() ContextKey {
reversedIndex := len(stack) - 1 - i reversedIndex := len(stack) - 1 - i
context := stack[reversedIndex] context := stack[reversedIndex]
kind := stack[reversedIndex].GetKind() kind := stack[reversedIndex].GetKind()
if kind != TEMPORARY_POPUP && kind != PERSISTENT_POPUP { if kind != types.TEMPORARY_POPUP && kind != types.PERSISTENT_POPUP {
return context.GetKey() 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 // 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. // 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() gui.State.ContextManager.Lock()
defer gui.State.ContextManager.Unlock() defer gui.State.ContextManager.Unlock()
if len(gui.State.ContextManager.ContextStack) == 0 { if len(gui.State.ContextManager.ContextStack) == 0 {
gui.State.ContextManager.ContextStack = []Context{c} gui.State.ContextManager.ContextStack = []types.Context{c}
} else { } else {
// replace the last item with the given item // 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) 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) 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 // using triple dot but you should only ever pass one of these opt structs
if len(opts) > 1 { if len(opts) > 1 {
return errors.New("cannot pass multiple opts to pushContext") 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 // push onto stack
// if we are switching to a side context, remove all other contexts in the 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 { for _, stackContext := range gui.State.ContextManager.ContextStack {
if stackContext.GetKey() != c.GetKey() { if stackContext.GetKey() != c.GetKey() {
if err := gui.deactivateContext(stackContext); err != nil { 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() { } 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) // 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. // 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 // 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 // look up the context currently active for that view and switch to that context
func (gui *Gui) pushContextWithView(viewName string) error { 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 { func (gui *Gui) returnFromContext() error {
@ -151,7 +120,7 @@ func (gui *Gui) returnFromContext() error {
return gui.activateContext(newContext) 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()) view, _ := gui.g.View(c.GetViewName())
if view != nil && view.IsSearching() { 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 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 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 // 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 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. // 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()) v, err := gui.g.View(c.GetViewName())
if err != nil { if err != nil {
return nil return nil
} }
if ContextKey(v.Context) != c.GetKey() { if types.ContextKey(v.Context) != c.GetKey() {
return nil return nil
} }
@ -198,13 +167,13 @@ func (gui *Gui) postRefreshUpdate(c Context) error {
return nil 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() viewName := c.GetViewName()
v, err := gui.g.View(viewName) v, err := gui.g.View(viewName)
if err != nil { if err != nil {
return err 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. // ensure that any other window for which this view was active is now set to the default for that window.
gui.setViewAsActiveForWindow(v) gui.setViewAsActiveForWindow(v)
@ -260,14 +229,14 @@ func (gui *Gui) activateContext(c Context, opts ...OnFocusOpts) error {
// return result // return result
// } // }
func (gui *Gui) currentContext() Context { func (gui *Gui) currentContext() types.Context {
gui.State.ContextManager.RLock() gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock() defer gui.State.ContextManager.RUnlock()
return gui.currentContextWithoutLock() return gui.currentContextWithoutLock()
} }
func (gui *Gui) currentContextWithoutLock() Context { func (gui *Gui) currentContextWithoutLock() types.Context {
if len(gui.State.ContextManager.ContextStack) == 0 { if len(gui.State.ContextManager.ContextStack) == 0 {
return gui.defaultSideContext() 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 // the status panel is not yet a list context (and may never be), so this method is not
// quite the same as currentSideContext() // quite the same as currentSideContext()
func (gui *Gui) currentSideListContext() IListContext { func (gui *Gui) currentSideListContext() types.IListContext {
context := gui.currentSideContext() context := gui.currentSideContext()
listContext, ok := context.(IListContext) listContext, ok := context.(types.IListContext)
if !ok { if !ok {
return nil return nil
} }
return listContext return listContext
} }
func (gui *Gui) currentSideContext() Context { func (gui *Gui) currentSideContext() types.Context {
gui.State.ContextManager.RLock() gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock() defer gui.State.ContextManager.RUnlock()
@ -297,11 +266,11 @@ func (gui *Gui) currentSideContext() Context {
return gui.defaultSideContext() 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 { for i := range stack {
context := stack[len(stack)-1-i] context := stack[len(stack)-1-i]
if context.GetKind() == SIDE_CONTEXT { if context.GetKind() == types.SIDE_CONTEXT {
return context return context
} }
} }
@ -310,7 +279,7 @@ func (gui *Gui) currentSideContext() Context {
} }
// static as opposed to popup // static as opposed to popup
func (gui *Gui) currentStaticContext() Context { func (gui *Gui) currentStaticContext() types.Context {
gui.State.ContextManager.RLock() gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock() defer gui.State.ContextManager.RUnlock()
@ -324,7 +293,7 @@ func (gui *Gui) currentStaticContext() Context {
for i := range stack { for i := range stack {
context := stack[len(stack)-1-i] 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 return context
} }
} }
@ -332,7 +301,7 @@ func (gui *Gui) currentStaticContext() Context {
return gui.defaultSideContext() return gui.defaultSideContext()
} }
func (gui *Gui) defaultSideContext() Context { func (gui *Gui) defaultSideContext() types.Context {
if gui.State.Modes.Filtering.Active() { if gui.State.Modes.Filtering.Active() {
return gui.State.Contexts.BranchCommits return gui.State.Contexts.BranchCommits
} else { } 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 // 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 // other views can have their context changed directly but this function helps
// keep the main and secondary views in sync // 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 { if gui.State.MainContext == contextKey {
return return
} }
@ -432,13 +401,13 @@ func (gui *Gui) viewTabNames(viewName string) []string {
result := make([]string, len(tabContexts)) result := make([]string, len(tabContexts))
for i, tabContext := range tabContexts { for i, tabContext := range tabContexts {
result[i] = tabContext.tab result[i] = tabContext.Tab
} }
return result 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 // 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()] tabContexts, ok := gui.State.ViewTabContextMap[c.GetViewName()]
if !ok { if !ok {
@ -446,12 +415,12 @@ func (gui *Gui) setViewTabForContext(c Context) {
} }
for tabIndex, tabContext := range tabContexts { for tabIndex, tabContext := range tabContexts {
for _, context := range tabContext.contexts { for _, context := range tabContext.Contexts {
if context.GetKey() == c.GetKey() { if context.GetKey() == c.GetKey() {
// get the view, set the tab // get the view, set the tab
v, err := gui.g.View(c.GetViewName()) v, err := gui.g.View(c.GetViewName())
if err != nil { if err != nil {
gui.Log.Error(err) gui.c.Log.Error(err)
return return
} }
v.TabIndex = tabIndex v.TabIndex = tabIndex
@ -461,12 +430,7 @@ func (gui *Gui) setViewTabForContext(c Context) {
} }
} }
type tabContext struct { func (gui *Gui) mustContextForContextKey(contextKey types.ContextKey) types.Context {
tab string
contexts []Context
}
func (gui *Gui) mustContextForContextKey(contextKey ContextKey) Context {
context, ok := gui.contextForContextKey(contextKey) context, ok := gui.contextForContextKey(contextKey)
if !ok { if !ok {
@ -476,7 +440,7 @@ func (gui *Gui) mustContextForContextKey(contextKey ContextKey) Context {
return 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() { for _, context := range gui.allContexts() {
if context.GetKey() == contextKey { if context.GetKey() == contextKey {
return context, true return context, true
@ -487,7 +451,7 @@ func (gui *Gui) contextForContextKey(contextKey ContextKey) (Context, bool) {
} }
func (gui *Gui) rerenderView(view *gocui.View) error { func (gui *Gui) rerenderView(view *gocui.View) error {
contextKey := ContextKey(view.Context) contextKey := types.ContextKey(view.Context)
context := gui.mustContextForContextKey(contextKey) context := gui.mustContextForContextKey(contextKey)
return context.HandleRender() 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 package gui
type ContextKey string import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
const ( "github.com/jesseduffield/lazygit/pkg/gui/types"
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"
) )
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, STATUS_CONTEXT_KEY,
FILES_CONTEXT_KEY, FILES_CONTEXT_KEY,
LOCAL_BRANCHES_CONTEXT_KEY, LOCAL_BRANCHES_CONTEXT_KEY,
@ -54,34 +57,8 @@ var allContextKeys = []ContextKey{
COMMAND_LOG_CONTEXT_KEY, COMMAND_LOG_CONTEXT_KEY,
} }
type ContextTree struct { func (gui *Gui) allContexts() []types.Context {
Status Context return []types.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{
gui.State.Contexts.Status, gui.State.Contexts.Status,
gui.State.Contexts.Files, gui.State.Contexts.Files,
gui.State.Contexts.Submodules, gui.State.Contexts.Submodules,
@ -107,11 +84,11 @@ func (gui *Gui) allContexts() []Context {
} }
} }
func (gui *Gui) contextTree() ContextTree { func (gui *Gui) contextTree() context.ContextTree {
return ContextTree{ return context.ContextTree{
Status: &BasicContext{ Status: &BasicContext{
OnRenderToMain: OnFocusWrapper(gui.statusRenderToMain), OnRenderToMain: OnFocusWrapper(gui.statusRenderToMain),
Kind: SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
ViewName: "status", ViewName: "status",
Key: STATUS_CONTEXT_KEY, Key: STATUS_CONTEXT_KEY,
}, },
@ -128,15 +105,15 @@ func (gui *Gui) contextTree() ContextTree {
Tags: gui.tagsListContext(), Tags: gui.tagsListContext(),
Stash: gui.stashListContext(), Stash: gui.stashListContext(),
Normal: &BasicContext{ 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 return nil // TODO: should we do something here? We should allow for scrolling the panel
}, },
Kind: MAIN_CONTEXT, Kind: types.MAIN_CONTEXT,
ViewName: "main", ViewName: "main",
Key: MAIN_NORMAL_CONTEXT_KEY, Key: MAIN_NORMAL_CONTEXT_KEY,
}, },
Staging: &BasicContext{ Staging: &BasicContext{
OnFocus: func(opts ...OnFocusOpts) error { OnFocus: func(opts ...types.OnFocusOpts) error {
forceSecondaryFocused := false forceSecondaryFocused := false
selectedLineIdx := -1 selectedLineIdx := -1
if len(opts) > 0 && opts[0].ClickedViewName != "" { if len(opts) > 0 && opts[0].ClickedViewName != "" {
@ -149,12 +126,12 @@ func (gui *Gui) contextTree() ContextTree {
} }
return gui.onStagingFocus(forceSecondaryFocused, selectedLineIdx) return gui.onStagingFocus(forceSecondaryFocused, selectedLineIdx)
}, },
Kind: MAIN_CONTEXT, Kind: types.MAIN_CONTEXT,
ViewName: "main", ViewName: "main",
Key: MAIN_STAGING_CONTEXT_KEY, Key: MAIN_STAGING_CONTEXT_KEY,
}, },
PatchBuilding: &BasicContext{ PatchBuilding: &BasicContext{
OnFocus: func(opts ...OnFocusOpts) error { OnFocus: func(opts ...types.OnFocusOpts) error {
selectedLineIdx := -1 selectedLineIdx := -1
if len(opts) > 0 && (opts[0].ClickedViewName == "main" || opts[0].ClickedViewName == "secondary") { if len(opts) > 0 && (opts[0].ClickedViewName == "main" || opts[0].ClickedViewName == "secondary") {
selectedLineIdx = opts[0].ClickedViewLineIdx selectedLineIdx = opts[0].ClickedViewLineIdx
@ -162,7 +139,7 @@ func (gui *Gui) contextTree() ContextTree {
return gui.onPatchBuildingFocus(selectedLineIdx) return gui.onPatchBuildingFocus(selectedLineIdx)
}, },
Kind: MAIN_CONTEXT, Kind: types.MAIN_CONTEXT,
ViewName: "main", ViewName: "main",
Key: MAIN_PATCH_BUILDING_CONTEXT_KEY, Key: MAIN_PATCH_BUILDING_CONTEXT_KEY,
}, },
@ -175,30 +152,30 @@ func (gui *Gui) contextTree() ContextTree {
}, },
Credentials: &BasicContext{ Credentials: &BasicContext{
OnFocus: OnFocusWrapper(gui.handleAskFocused), OnFocus: OnFocusWrapper(gui.handleAskFocused),
Kind: PERSISTENT_POPUP, Kind: types.PERSISTENT_POPUP,
ViewName: "credentials", ViewName: "credentials",
Key: CREDENTIALS_CONTEXT_KEY, Key: CREDENTIALS_CONTEXT_KEY,
}, },
Confirmation: &BasicContext{ Confirmation: &BasicContext{
OnFocus: OnFocusWrapper(gui.handleAskFocused), OnFocus: OnFocusWrapper(gui.handleAskFocused),
Kind: TEMPORARY_POPUP, Kind: types.TEMPORARY_POPUP,
ViewName: "confirmation", ViewName: "confirmation",
Key: CONFIRMATION_CONTEXT_KEY, Key: CONFIRMATION_CONTEXT_KEY,
}, },
Suggestions: gui.suggestionsListContext(), Suggestions: gui.suggestionsListContext(),
CommitMessage: &BasicContext{ CommitMessage: &BasicContext{
OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused), OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused),
Kind: PERSISTENT_POPUP, Kind: types.PERSISTENT_POPUP,
ViewName: "commitMessage", ViewName: "commitMessage",
Key: COMMIT_MESSAGE_CONTEXT_KEY, Key: COMMIT_MESSAGE_CONTEXT_KEY,
}, },
Search: &BasicContext{ Search: &BasicContext{
Kind: PERSISTENT_POPUP, Kind: types.PERSISTENT_POPUP,
ViewName: "search", ViewName: "search",
Key: SEARCH_CONTEXT_KEY, Key: SEARCH_CONTEXT_KEY,
}, },
CommandLog: &BasicContext{ CommandLog: &BasicContext{
Kind: EXTRAS_CONTEXT, Kind: types.EXTRAS_CONTEXT,
ViewName: "extras", ViewName: "extras",
Key: COMMAND_LOG_CONTEXT_KEY, Key: COMMAND_LOG_CONTEXT_KEY,
OnGetOptionsMap: gui.getMergingOptions, 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 // using this wrapper for when an onFocus function doesn't care about any potential
// props that could be passed // props that could be passed
func OnFocusWrapper(f func() error) func(opts ...OnFocusOpts) error { func OnFocusWrapper(f func() error) func(opts ...types.OnFocusOpts) error {
return func(opts ...OnFocusOpts) error { return func(opts ...types.OnFocusOpts) error {
return f() 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" "path/filepath"
"strings" "strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
// if Go let me do private struct embedding of structs with public fields (which it should) type SubmodulesController struct {
// I would just do that. But alas. c *ControllerCommon
type ControllerCommon struct { context types.IListContext
*common.Common git *commands.GitCommand
IGuiCommon
enterSubmodule func(submodule *models.SubmoduleConfig) error
getSelectedSubmodule func() *models.SubmoduleConfig
} }
type SubmodulesController struct { var _ types.IController = &SubmodulesController{}
// 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
}
func NewSubmodulesController( func NewSubmodulesController(
c *ControllerCommon, c *ControllerCommon,
enterSubmoduleFn func(submodule *models.SubmoduleConfig) error, context types.IListContext,
git *commands.GitCommand, git *commands.GitCommand,
submodules []*models.SubmoduleConfig, enterSubmodule func(submodule *models.SubmoduleConfig) error,
getSelectedSubmodule func() *models.SubmoduleConfig, getSelectedSubmodule func() *models.SubmoduleConfig,
) *SubmodulesController { ) *SubmodulesController {
return &SubmodulesController{ return &SubmodulesController{
c: c, c: c,
enterSubmoduleFn: enterSubmoduleFn, context: context,
git: git, git: git,
submodules: submodules, enterSubmodule: enterSubmodule,
getSelectedSubmodule: getSelectedSubmodule, getSelectedSubmodule: getSelectedSubmodule,
} }
} }
func (self *SubmodulesController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig) []*types.Binding { func (self *SubmodulesController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
return []*types.Binding{ bindings := []*types.Binding{
{ {
Key: getKey(config.Universal.GoInto), Key: getKey(config.Universal.GoInto),
Handler: self.forSubmodule(self.enter), Handler: self.checkSelected(self.enter),
Description: self.c.Tr.LcEnterSubmodule, Description: self.c.Tr.LcEnterSubmodule,
}, },
{ {
Key: getKey(config.Universal.Remove), Key: getKey(config.Universal.Remove),
Handler: self.forSubmodule(self.remove), Handler: self.checkSelected(self.remove),
Description: self.c.Tr.LcRemoveSubmodule, Description: self.c.Tr.LcRemoveSubmodule,
}, },
{ {
Key: getKey(config.Submodules.Update), Key: getKey(config.Submodules.Update),
Handler: self.forSubmodule(self.update), Handler: self.checkSelected(self.update),
Description: self.c.Tr.LcSubmoduleUpdate, Description: self.c.Tr.LcSubmoduleUpdate,
}, },
{ {
@ -73,12 +65,12 @@ func (self *SubmodulesController) Keybindings(getKey func(key string) interface{
}, },
{ {
Key: getKey(config.Universal.Edit), Key: getKey(config.Universal.Edit),
Handler: self.forSubmodule(self.editURL), Handler: self.checkSelected(self.editURL),
Description: self.c.Tr.LcEditSubmoduleUrl, Description: self.c.Tr.LcEditSubmoduleUrl,
}, },
{ {
Key: getKey(config.Submodules.Init), Key: getKey(config.Submodules.Init),
Handler: self.forSubmodule(self.init), Handler: self.checkSelected(self.init),
Description: self.c.Tr.LcInitSubmodule, Description: self.c.Tr.LcInitSubmodule,
}, },
{ {
@ -87,11 +79,17 @@ func (self *SubmodulesController) Keybindings(getKey func(key string) interface{
Description: self.c.Tr.LcViewBulkSubmoduleOptions, Description: self.c.Tr.LcViewBulkSubmoduleOptions,
OpensMenu: true, 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 { func (self *SubmodulesController) enter(submodule *models.SubmoduleConfig) error {
return self.enterSubmoduleFn(submodule) return self.enterSubmodule(submodule)
} }
func (self *SubmodulesController) add() error { 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 { return func() error {
submodule := self.getSelectedSubmodule() submodule := self.getSelectedSubmodule()
if submodule == nil { if submodule == nil {
@ -241,3 +239,7 @@ func (self *SubmodulesController) forSubmodule(callback func(*models.SubmoduleCo
return callback(submodule) 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 package controllers
import ( 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/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -8,6 +11,54 @@ import (
type IGuiCommon interface { type IGuiCommon interface {
popup.IPopupHandler 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 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 ( 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/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/gui/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "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 // 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. // 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 type ReflogActionKind int
const ( const (
@ -32,15 +65,113 @@ type reflogAction struct {
to string 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 // 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 // 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. // 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 // 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. // 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. // 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 counter := 0
reflogCommits := gui.State.FilteredReflogCommits reflogCommits := self.getFilteredReflogCommits()
rebaseFinishCommitSha := "" rebaseFinishCommitSha := ""
var action *reflogAction var action *reflogAction
for reflogCommitIdx, reflogCommit := range reflogCommits { for reflogCommitIdx, reflogCommit := range reflogCommits {
@ -86,115 +217,42 @@ func (gui *Gui) parseReflogForActions(onUserAction func(counter int, action refl
return nil return nil
} }
func (gui *Gui) reflogUndo() error { type hardResetOptions struct {
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 {
WaitingStatus string WaitingStatus string
EnvVars []string EnvVars []string
} }
// only to be used in the undo flow for now // only to be used in the undo flow for now (does an autostash)
func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHardResetWithAutoStashOptions) error { func (self *UndoController) hardResetWithAutoStash(commitSha string, options hardResetOptions) error {
reset := func() error { reset := func() error {
if err := gui.resetToRef(commitSha, "hard", options.EnvVars); err != nil { if err := self.refHelper.ResetToRef(commitSha, "hard", options.EnvVars); err != nil {
return gui.PopupHandler.Error(err) return self.c.Error(err)
} }
return nil return nil
} }
// if we have any modified tracked files we need to ask the user if they want us to stash for them // 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 { if dirtyWorkingTree {
// offer to autostash changes // offer to autostash changes
return gui.PopupHandler.Ask(popup.AskOpts{ return self.c.Ask(popup.AskOpts{
Title: gui.Tr.AutoStashTitle, Title: self.c.Tr.AutoStashTitle,
Prompt: gui.Tr.AutoStashPrompt, Prompt: self.c.Tr.AutoStashPrompt,
HandleConfirm: func() error { HandleConfirm: func() error {
return gui.PopupHandler.WithWaitingStatus(options.WaitingStatus, func() error { return self.c.WithWaitingStatus(options.WaitingStatus, func() error {
if err := gui.Git.Stash.Save(gui.Tr.StashPrefix + commitSha); err != nil { if err := self.git.Stash.Save(self.c.Tr.StashPrefix + commitSha); err != nil {
return gui.PopupHandler.Error(err) return self.c.Error(err)
} }
if err := reset(); err != nil { if err := reset(); err != nil {
return err return err
} }
err := gui.Git.Stash.Pop(0) err := self.git.Stash.Pop(0)
if err := gui.refreshSidePanels(types.RefreshOptions{}); err != nil { if err := self.c.Refresh(types.RefreshOptions{}); err != nil {
return err return err
} }
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return self.c.Error(err)
} }
return nil 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() return reset()
}) })
} }

View File

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

View File

@ -54,7 +54,7 @@ func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (s
SelectedCommitFile: gui.getSelectedCommitFile(), SelectedCommitFile: gui.getSelectedCommitFile(),
SelectedCommitFilePath: gui.getSelectedCommitFilePath(), SelectedCommitFilePath: gui.getSelectedCommitFilePath(),
SelectedSubCommit: gui.getSelectedSubCommit(), SelectedSubCommit: gui.getSelectedSubCommit(),
CheckedOutBranch: gui.currentBranch(), CheckedOutBranch: gui.getCheckedOutBranch(),
PromptResponses: promptResponses, 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 { func (gui *Gui) inputPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
title, err := gui.resolveTemplate(prompt.Title, promptResponses) title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses) initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses)
if err != nil { 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, Title: title,
InitialContent: initialValue, InitialContent: initialValue,
HandleConfirm: func(str string) error { HandleConfirm: func(str string) error {
@ -95,17 +95,17 @@ func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses []
} }
name, err := gui.resolveTemplate(nameTemplate, promptResponses) name, err := gui.resolveTemplate(nameTemplate, promptResponses)
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
description, err := gui.resolveTemplate(option.Description, promptResponses) description, err := gui.resolveTemplate(option.Description, promptResponses)
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
value, err := gui.resolveTemplate(option.Value, promptResponses) value, err := gui.resolveTemplate(option.Value, promptResponses)
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
menuItems[i] = &popup.MenuItem{ menuItems[i] = &popup.MenuItem{
@ -119,30 +119,30 @@ func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses []
title, err := gui.resolveTemplate(prompt.Title, promptResponses) title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil { 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) { func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, labelFormat string) ([]commandMenuEntry, error) {
reg, err := regexp.Compile(filter) reg, err := regexp.Compile(filter)
if err != nil { 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) buff := bytes.NewBuffer(nil)
valueTemp, err := template.New("format").Parse(valueFormat) valueTemp, err := template.New("format").Parse(valueFormat)
if err != nil { 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{}) colorFuncMap := style.TemplateFuncMapAddColors(template.FuncMap{})
descTemp, err := template.New("format").Funcs(colorFuncMap).Parse(labelFormat) descTemp, err := template.New("format").Funcs(colorFuncMap).Parse(labelFormat)
if err != nil { 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{} candidates := []commandMenuEntry{}
@ -167,7 +167,7 @@ func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, label
err = valueTemp.Execute(buff, tmplData) err = valueTemp.Execute(buff, tmplData)
if err != nil { if err != nil {
return candidates, gui.PopupHandler.Error(err) return candidates, gui.c.Error(err)
} }
entry := commandMenuEntry{ entry := commandMenuEntry{
value: strings.TrimSpace(buff.String()), value: strings.TrimSpace(buff.String()),
@ -177,7 +177,7 @@ func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, label
buff.Reset() buff.Reset()
err = descTemp.Execute(buff, tmplData) err = descTemp.Execute(buff, tmplData)
if err != nil { if err != nil {
return candidates, gui.PopupHandler.Error(err) return candidates, gui.c.Error(err)
} }
entry.label = strings.TrimSpace(buff.String()) entry.label = strings.TrimSpace(buff.String())
} else { } else {
@ -195,25 +195,25 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR
// Collect cmd to run from config // Collect cmd to run from config
cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses) cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses)
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
// Collect Filter regexp // Collect Filter regexp
filter, err := gui.resolveTemplate(prompt.Filter, promptResponses) filter, err := gui.resolveTemplate(prompt.Filter, promptResponses)
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
// Run and save output // Run and save output
message, err := gui.Git.Custom.RunWithOutput(cmdStr) message, err := gui.git.Custom.RunWithOutput(cmdStr)
if err != nil { 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 // Need to make a menu out of what the cmd has displayed
candidates, err := gui.GenerateMenuCandidates(message, filter, prompt.ValueFormat, prompt.LabelFormat) candidates, err := gui.GenerateMenuCandidates(message, filter, prompt.ValueFormat, prompt.LabelFormat)
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
menuItems := make([]*popup.MenuItem, len(candidates)) 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) title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil { 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 { func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand) func() error {
@ -243,7 +243,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
f := func() error { f := func() error {
cmdStr, err := gui.resolveTemplate(customCommand.Command, promptResponses) cmdStr, err := gui.resolveTemplate(customCommand.Command, promptResponses)
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
if customCommand.Subprocess { if customCommand.Subprocess {
@ -252,19 +252,19 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
loadingText := customCommand.LoadingText loadingText := customCommand.LoadingText
if loadingText == "" { if loadingText == "" {
loadingText = gui.Tr.LcRunningCustomCommandStatus loadingText = gui.c.Tr.LcRunningCustomCommandStatus
} }
return gui.PopupHandler.WithWaitingStatus(loadingText, func() error { return gui.c.WithWaitingStatus(loadingText, func() error {
gui.logAction(gui.Tr.Actions.CustomCommand) gui.c.LogAction(gui.c.Tr.Actions.CustomCommand)
cmdObj := gui.OSCommand.Cmd.NewShell(cmdStr) cmdObj := gui.OSCommand.Cmd.NewShell(cmdStr)
if customCommand.Stream { if customCommand.Stream {
cmdObj.StreamOutput() cmdObj.StreamOutput()
} }
err := cmdObj.Run() err := cmdObj.Run()
if err != nil { 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) return gui.menuPromptFromCommand(prompt, promptResponses, idx, wrappedF)
} }
default: 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 { func (gui *Gui) GetCustomCommandKeybindings() []*types.Binding {
bindings := []*types.Binding{} bindings := []*types.Binding{}
customCommands := gui.UserConfig.CustomCommands customCommands := gui.c.UserConfig.CustomCommands
for _, customCommand := range customCommands { for _, customCommand := range customCommands {
var viewName string var viewName string
@ -315,11 +315,11 @@ func (gui *Gui) GetCustomCommandKeybindings() []*types.Binding {
case "": 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) 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: 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. // stupid golang making me build an array of strings for this.
allContextKeyStrings := make([]string, len(allContextKeys)) allContextKeyStrings := make([]string, len(AllContextKeys))
for i := range allContextKeys { for i := range AllContextKeys {
allContextKeyStrings[i] = string(allContextKeys[i]) allContextKeyStrings[i] = string(AllContextKeys[i])
} }
if !ok { 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, ", ")) 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 ( import (
"errors" "errors"
"github.com/jesseduffield/lazygit/pkg/gui/types"
) )
var CONTEXT_KEYS_SHOWING_DIFFS = []ContextKey{ var CONTEXT_KEYS_SHOWING_DIFFS = []types.ContextKey{
FILES_CONTEXT_KEY, FILES_CONTEXT_KEY,
COMMIT_FILES_CONTEXT_KEY, COMMIT_FILES_CONTEXT_KEY,
STASH_CONTEXT_KEY, STASH_CONTEXT_KEY,
@ -28,10 +30,10 @@ func isShowingDiff(gui *Gui) bool {
func (gui *Gui) IncreaseContextInDiffView() error { func (gui *Gui) IncreaseContextInDiffView() error {
if isShowingDiff(gui) { if isShowingDiff(gui) {
if err := gui.CheckCanChangeContext(); err != nil { 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() return gui.handleDiffContextSizeChange()
} }
@ -39,14 +41,14 @@ func (gui *Gui) IncreaseContextInDiffView() error {
} }
func (gui *Gui) DecreaseContextInDiffView() 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 isShowingDiff(gui) && old_size > 1 {
if err := gui.CheckCanChangeContext(); err != nil { 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() return gui.handleDiffContextSizeChange()
} }
@ -67,8 +69,8 @@ func (gui *Gui) handleDiffContextSizeChange() error {
} }
func (gui *Gui) CheckCanChangeContext() error { func (gui *Gui) CheckCanChangeContext() error {
if gui.Git.Patch.PatchManager.Active() { if gui.git.Patch.PatchManager.Active() {
return errors.New(gui.Tr.CantChangeContextSizeError) return errors.New(gui.c.Tr.CantChangeContextSizeError)
} }
return nil return nil

View File

@ -29,7 +29,7 @@ func setupGuiForTest(gui *Gui) {
gui.Views.Main, _ = gui.prepareView("main") gui.Views.Main, _ = gui.prepareView("main")
gui.Views.Secondary, _ = gui.prepareView("secondary") gui.Views.Secondary, _ = gui.prepareView("secondary")
gui.Views.Options, _ = gui.prepareView("options") gui.Views.Options, _ = gui.prepareView("options")
gui.Git.Patch.PatchManager = &patch.PatchManager{} gui.git.Patch.PatchManager = &patch.PatchManager{}
_, _ = gui.refreshLineByLinePanel(diffForTest, "", false, 11) _, _ = gui.refreshLineByLinePanel(diffForTest, "", false, 11)
} }
@ -48,12 +48,12 @@ func TestIncreasesContextInDiffViewByOneInContextWithDiff(t *testing.T) {
gui := NewDummyGui() gui := NewDummyGui()
context := c(gui) context := c(gui)
setupGuiForTest(gui) setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 1 gui.c.UserConfig.Git.DiffContextSize = 1
_ = gui.pushContext(context) _ = gui.c.PushContext(context)
_ = gui.IncreaseContextInDiffView() _ = 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() gui := NewDummyGui()
context := c(gui) context := c(gui)
setupGuiForTest(gui) setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 1 gui.c.UserConfig.Git.DiffContextSize = 1
_ = gui.pushContext(context) _ = gui.c.PushContext(context)
_ = gui.IncreaseContextInDiffView() _ = 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() gui := NewDummyGui()
context := c(gui) context := c(gui)
setupGuiForTest(gui) setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2 gui.c.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContext(context) _ = gui.c.PushContext(context)
_ = gui.DecreaseContextInDiffView() _ = 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() gui := NewDummyGui()
context := c(gui) context := c(gui)
setupGuiForTest(gui) setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2 gui.c.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContext(context) _ = gui.c.PushContext(context)
_ = gui.DecreaseContextInDiffView() _ = 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) { func TestDoesntIncreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) {
gui := NewDummyGui() gui := NewDummyGui()
setupGuiForTest(gui) setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2 gui.c.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContext(gui.State.Contexts.CommitFiles) _ = gui.c.PushContext(gui.State.Contexts.CommitFiles)
gui.Git.Patch.PatchManager.Start("from", "to", false, false) gui.git.Patch.PatchManager.Start("from", "to", false, false)
errorCount := 0 errorCount := 0
gui.PopupHandler = &popup.TestPopupHandler{ gui.PopupHandler = &popup.TestPopupHandler{
OnErrorMsg: func(message string) error { OnErrorMsg: func(message string) error {
assert.Equal(t, gui.Tr.CantChangeContextSizeError, message) assert.Equal(t, gui.c.Tr.CantChangeContextSizeError, message)
errorCount += 1 errorCount += 1
return nil return nil
}, },
@ -156,20 +156,20 @@ func TestDoesntIncreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *test
_ = gui.IncreaseContextInDiffView() _ = gui.IncreaseContextInDiffView()
assert.Equal(t, 1, errorCount) 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) { func TestDoesntDecreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) {
gui := NewDummyGui() gui := NewDummyGui()
setupGuiForTest(gui) setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2 gui.c.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContext(gui.State.Contexts.CommitFiles) _ = gui.c.PushContext(gui.State.Contexts.CommitFiles)
gui.Git.Patch.PatchManager.Start("from", "to", false, false) gui.git.Patch.PatchManager.Start("from", "to", false, false)
errorCount := 0 errorCount := 0
gui.PopupHandler = &popup.TestPopupHandler{ gui.PopupHandler = &popup.TestPopupHandler{
OnErrorMsg: func(message string) error { OnErrorMsg: func(message string) error {
assert.Equal(t, gui.Tr.CantChangeContextSizeError, message) assert.Equal(t, gui.c.Tr.CantChangeContextSizeError, message)
errorCount += 1 errorCount += 1
return nil return nil
}, },
@ -177,15 +177,15 @@ func TestDoesntDecreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *test
_ = gui.DecreaseContextInDiffView() _ = gui.DecreaseContextInDiffView()
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize) assert.Equal(t, 2, gui.c.UserConfig.Git.DiffContextSize)
} }
func TestDecreasesContextInDiffViewNoFurtherThanOne(t *testing.T) { func TestDecreasesContextInDiffViewNoFurtherThanOne(t *testing.T) {
gui := NewDummyGui() gui := NewDummyGui()
setupGuiForTest(gui) setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 1 gui.c.UserConfig.Git.DiffContextSize = 1
_ = gui.DecreaseContextInDiffView() _ = 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 { func (gui *Gui) exitDiffMode() error {
gui.State.Modes.Diffing = diffing.New() 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 { func (gui *Gui) renderDiff() error {
@ -112,11 +112,11 @@ func (gui *Gui) handleCreateDiffingMenuPanel() error {
name := name name := name
menuItems = append(menuItems, []*popup.MenuItem{ 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 { OnPress: func() error {
gui.State.Modes.Diffing.Ref = name gui.State.Modes.Diffing.Ref = name
// can scope this down based on current view but too lazy right now // 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{ menuItems = append(menuItems, []*popup.MenuItem{
{ {
DisplayString: gui.Tr.LcEnterRefToDiff, DisplayString: gui.c.Tr.LcEnterRefToDiff,
OnPress: func() error { OnPress: func() error {
return gui.PopupHandler.Prompt(popup.PromptOpts{ return gui.c.Prompt(popup.PromptOpts{
Title: gui.Tr.LcEnteRefName, Title: gui.c.Tr.LcEnteRefName,
FindSuggestionsFunc: gui.getRefsSuggestionsFunc(), FindSuggestionsFunc: gui.suggestionsHelper.GetRefsSuggestionsFunc(),
HandleConfirm: func(response string) error { HandleConfirm: func(response string) error {
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response) 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() { if gui.State.Modes.Diffing.Active() {
menuItems = append(menuItems, []*popup.MenuItem{ menuItems = append(menuItems, []*popup.MenuItem{
{ {
DisplayString: gui.Tr.LcSwapDiff, DisplayString: gui.c.Tr.LcSwapDiff,
OnPress: func() error { OnPress: func() error {
gui.State.Modes.Diffing.Reverse = !gui.State.Modes.Diffing.Reverse 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 { OnPress: func() error {
gui.State.Modes.Diffing = diffing.New() 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 { if node.File == nil {
menuItems = []*popup.MenuItem{ menuItems = []*popup.MenuItem{
{ {
DisplayString: gui.Tr.LcDiscardAllChanges, DisplayString: gui.c.Tr.LcDiscardAllChanges,
OnPress: func() error { OnPress: func() error {
gui.logAction(gui.Tr.Actions.DiscardAllChangesInDirectory) gui.c.LogAction(gui.c.Tr.Actions.DiscardAllChangesInDirectory)
if err := gui.Git.WorkingTree.DiscardAllDirChanges(node); err != nil { if err := gui.git.WorkingTree.DiscardAllDirChanges(node); err != nil {
return gui.PopupHandler.Error(err) 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() { if node.GetHasStagedChanges() && node.GetHasUnstagedChanges() {
menuItems = append(menuItems, &popup.MenuItem{ menuItems = append(menuItems, &popup.MenuItem{
DisplayString: gui.Tr.LcDiscardUnstagedChanges, DisplayString: gui.c.Tr.LcDiscardUnstagedChanges,
OnPress: func() error { OnPress: func() error {
gui.logAction(gui.Tr.Actions.DiscardUnstagedChangesInDirectory) gui.c.LogAction(gui.c.Tr.Actions.DiscardUnstagedChangesInDirectory)
if err := gui.Git.WorkingTree.DiscardUnstagedDirChanges(node); err != nil { if err := gui.git.WorkingTree.DiscardUnstagedDirChanges(node); err != nil {
return gui.PopupHandler.Error(err) 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{ menuItems = []*popup.MenuItem{
{ {
DisplayString: gui.Tr.LcSubmoduleStashAndReset, DisplayString: gui.c.Tr.LcSubmoduleStashAndReset,
OnPress: func() error { OnPress: func() error {
return gui.resetSubmodule(submodule) return gui.Controllers.Files.ResetSubmodule(submodule)
}, },
}, },
} }
} else { } else {
menuItems = []*popup.MenuItem{ menuItems = []*popup.MenuItem{
{ {
DisplayString: gui.Tr.LcDiscardAllChanges, DisplayString: gui.c.Tr.LcDiscardAllChanges,
OnPress: func() error { OnPress: func() error {
gui.logAction(gui.Tr.Actions.DiscardAllChangesInFile) gui.c.LogAction(gui.c.Tr.Actions.DiscardAllChangesInFile)
if err := gui.Git.WorkingTree.DiscardAllFileChanges(file); err != nil { if err := gui.git.WorkingTree.DiscardAllFileChanges(file); err != nil {
return gui.PopupHandler.Error(err) 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 { if file.HasStagedChanges && file.HasUnstagedChanges {
menuItems = append(menuItems, &popup.MenuItem{ menuItems = append(menuItems, &popup.MenuItem{
DisplayString: gui.Tr.LcDiscardUnstagedChanges, DisplayString: gui.c.Tr.LcDiscardUnstagedChanges,
OnPress: func() error { OnPress: func() error {
gui.logAction(gui.Tr.Actions.DiscardAllUnstagedChangesInFile) gui.c.LogAction(gui.c.Tr.Actions.DiscardAllUnstagedChangesInFile)
if err := gui.Git.WorkingTree.DiscardUnstagedFileChanges(file); err != nil { if err := gui.git.WorkingTree.DiscardUnstagedFileChanges(file); err != nil {
return gui.PopupHandler.Error(err) 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 { 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 { if !ok {
newlineKey = gocui.KeyAltEnter 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. // considered out of bounds to add a newline, meaning we can avoid unnecessary scrolling.
err := gui.resizePopupPanel(v, v.TextArea.GetContent()) err := gui.resizePopupPanel(v, v.TextArea.GetContent())
if err != nil { if err != nil {
gui.Log.Error(err) gui.c.Log.Error(err)
} }
v.RenderTextArea() v.RenderTextArea()
gui.RenderCommitLength() gui.RenderCommitLength()

View File

@ -8,11 +8,11 @@ import (
) )
func (gui *Gui) handleCreateExtrasMenuPanel() error { func (gui *Gui) handleCreateExtrasMenuPanel() error {
return gui.PopupHandler.Menu(popup.CreateMenuOptions{ return gui.c.Menu(popup.CreateMenuOptions{
Title: gui.Tr.CommandLog, Title: gui.c.Tr.CommandLog,
Items: []*popup.MenuItem{ Items: []*popup.MenuItem{
{ {
DisplayString: gui.Tr.ToggleShowCommandLog, DisplayString: gui.c.Tr.ToggleShowCommandLog,
OnPress: func() error { OnPress: func() error {
currentContext := gui.currentStaticContext() currentContext := gui.currentStaticContext()
if gui.ShowExtrasWindow && currentContext.GetKey() == COMMAND_LOG_CONTEXT_KEY { if gui.ShowExtrasWindow && currentContext.GetKey() == COMMAND_LOG_CONTEXT_KEY {
@ -22,13 +22,13 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error {
} }
show := !gui.ShowExtrasWindow show := !gui.ShowExtrasWindow
gui.ShowExtrasWindow = show gui.ShowExtrasWindow = show
gui.Config.GetAppState().HideCommandLog = !show gui.c.GetAppState().HideCommandLog = !show
_ = gui.Config.SaveAppState() _ = gui.c.SaveAppState()
return nil return nil
}, },
}, },
{ {
DisplayString: gui.Tr.FocusCommandLog, DisplayString: gui.c.Tr.FocusCommandLog,
OnPress: gui.handleFocusCommandLog, OnPress: gui.handleFocusCommandLog,
}, },
}, },
@ -37,8 +37,9 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error {
func (gui *Gui) handleFocusCommandLog() error { func (gui *Gui) handleFocusCommandLog() error {
gui.ShowExtrasWindow = true gui.ShowExtrasWindow = true
// TODO: is this necessary? Can't I just call 'return from context'?
gui.State.Contexts.CommandLog.SetParentContext(gui.currentSideContext()) 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 { func (gui *Gui) scrollUpExtra() error {
@ -58,7 +59,7 @@ func (gui *Gui) scrollDownExtra() error {
} }
func (gui *Gui) getCmdWriter() io.Writer { 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. // 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 // only refresh if we're not already
if !gui.State.IsRefreshingFiles { 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 // watch for errors
case err := <-gui.fileWatcher.Watcher.Errors: case err := <-gui.fileWatcher.Watcher.Errors:
if err != nil { if err != nil {
gui.Log.Error(err) gui.c.Log.Error(err)
} }
} }
} }

View File

@ -1,15 +1,10 @@
package gui package gui
import ( import (
"fmt" "github.com/jesseduffield/gocui"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/loaders" "github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums" "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/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
"github.com/jesseduffield/lazygit/pkg/gui/popup" "github.com/jesseduffield/lazygit/pkg/gui/popup"
@ -52,7 +47,7 @@ func (gui *Gui) filesRenderToMain() error {
return gui.refreshMainViews(refreshMainOpts{ return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{ main: &viewUpdateOpts{
title: "", title: "",
task: NewRenderStringTask(gui.Tr.NoChangedFiles), task: NewRenderStringTask(gui.c.Tr.NoChangedFiles),
}, },
}) })
} }
@ -69,24 +64,24 @@ func (gui *Gui) filesRenderToMain() error {
gui.resetMergeStateWithLock() 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{ refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
title: gui.Tr.UnstagedChanges, title: gui.c.Tr.UnstagedChanges,
task: NewRunPtyTask(cmdObj.GetCmd()), task: NewRunPtyTask(cmdObj.GetCmd()),
}} }}
if node.GetHasUnstagedChanges() { if node.GetHasUnstagedChanges() {
if node.GetHasStagedChanges() { 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{ refreshOpts.secondary = &viewUpdateOpts{
title: gui.Tr.StagedChanges, title: gui.c.Tr.StagedChanges,
task: NewRunPtyTask(cmdObj.GetCmd()), task: NewRunPtyTask(cmdObj.GetCmd()),
} }
} }
} else { } else {
refreshOpts.main.title = gui.Tr.StagedChanges refreshOpts.main.title = gui.c.Tr.StagedChanges
} }
return gui.refreshMainViews(refreshOpts) return gui.refreshMainViews(refreshOpts)
@ -115,12 +110,12 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
} }
gui.OnUIThread(func() error { gui.OnUIThread(func() error {
if err := gui.postRefreshUpdate(gui.State.Contexts.Submodules); err != nil { if err := gui.c.PostRefreshUpdate(gui.State.Contexts.Submodules); err != nil {
gui.Log.Error(err) gui.c.Log.Error(err)
} }
if ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY { if types.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 // 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 { if err := gui.State.Contexts.Files.HandleRender(); err != nil {
return err return err
} }
@ -143,418 +138,6 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
return nil 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 { func (gui *Gui) refreshStateFiles() error {
state := gui.State state := gui.State
@ -591,13 +174,13 @@ func (gui *Gui) refreshStateFiles() error {
} }
if len(pathsToStage) > 0 { if len(pathsToStage) > 0 {
gui.logAction(gui.Tr.Actions.StageResolvedFiles) gui.c.LogAction(gui.Tr.Actions.StageResolvedFiles)
if err := gui.Git.WorkingTree.StageFiles(pathsToStage); err != nil { if err := gui.git.WorkingTree.StageFiles(pathsToStage); err != nil {
return gui.surfaceError(err) return gui.c.Error(err)
} }
} }
files := gui.Git.Loaders.Files. files := gui.git.Loaders.Files.
GetStatusFiles(loaders.GetStatusFileOptions{}) GetStatusFiles(loaders.GetStatusFileOptions{})
conflictFileCount := 0 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() }) gui.OnUIThread(func() error { return gui.promptToContinueRebase() })
} }
@ -716,218 +299,7 @@ func (gui *Gui) findNewSelectedIdx(prevNodes []*filetree.FileNode, currNodes []*
return -1 return -1
} }
func (gui *Gui) handlePullFiles() error { func (gui *Gui) onFocusFile() 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
}
gui.takeOverMergeConflictScrolling() gui.takeOverMergeConflictScrolling()
if gui.State.Panels.Merging.GetPath() != file.Name { 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) return gui.pushContext(gui.State.Contexts.Merging)
} }
func (gui *Gui) openFile(filename string) error { func (gui *Gui) getSetTextareaTextFn(view *gocui.View) func(string) {
gui.logAction(gui.Tr.Actions.OpenFile) return func(text string) {
if err := gui.OSCommand.OpenFile(filename); err != nil { view.ClearTextArea()
return gui.PopupHandler.Error(err) 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 ( import (
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/types"
) )
type CommitFileNode struct { type CommitFileNode struct {
@ -12,8 +13,7 @@ type CommitFileNode struct {
} }
var _ INode = &CommitFileNode{} var _ INode = &CommitFileNode{}
var _ types.ListItem = &CommitFileNode{}
// methods satisfying ListItem interface
func (s *CommitFileNode) ID() string { func (s *CommitFileNode) ID() string {
return s.GetPath() return s.GetPath()

View File

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

View File

@ -5,17 +5,27 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
func (gui *Gui) validateNotInFilterMode() (bool, error) { func (gui *Gui) validateNotInFilterMode() bool {
if gui.State.Modes.Filtering.Active() { if gui.State.Modes.Filtering.Active() {
err := gui.PopupHandler.Ask(popup.AskOpts{ _ = gui.c.Ask(popup.AskOpts{
Title: gui.Tr.MustExitFilterModeTitle, Title: gui.c.Tr.MustExitFilterModeTitle,
Prompt: gui.Tr.MustExitFilterModePrompt, Prompt: gui.c.Tr.MustExitFilterModePrompt,
HandleConfirm: gui.exitFilterMode, 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 { func (gui *Gui) exitFilterMode() error {
@ -28,7 +38,7 @@ func (gui *Gui) clearFiltering() error {
gui.State.ScreenMode = SCREEN_NORMAL 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 { func (gui *Gui) setFiltering(path string) error {
@ -37,11 +47,11 @@ func (gui *Gui) setFiltering(path string) error {
gui.State.ScreenMode = SCREEN_HALF 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 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) gui.State.Contexts.BranchCommits.GetPanelState().SetSelectedLineIdx(0)
}}) }})
} }

View File

@ -26,7 +26,7 @@ func (gui *Gui) handleCreateFilteringMenuPanel() error {
if fileName != "" { if fileName != "" {
menuItems = append(menuItems, &popup.MenuItem{ 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 { OnPress: func() error {
return gui.setFiltering(fileName) return gui.setFiltering(fileName)
}, },
@ -34,11 +34,11 @@ func (gui *Gui) handleCreateFilteringMenuPanel() error {
} }
menuItems = append(menuItems, &popup.MenuItem{ menuItems = append(menuItems, &popup.MenuItem{
DisplayString: gui.Tr.LcFilterPathOption, DisplayString: gui.c.Tr.LcFilterPathOption,
OnPress: func() error { OnPress: func() error {
return gui.PopupHandler.Prompt(popup.PromptOpts{ return gui.c.Prompt(popup.PromptOpts{
FindSuggestionsFunc: gui.getFilePathSuggestionsFunc(), FindSuggestionsFunc: gui.suggestionsHelper.GetFilePathSuggestionsFunc(),
Title: gui.Tr.EnterFileName, Title: gui.c.Tr.EnterFileName,
HandleConfirm: func(response string) error { HandleConfirm: func(response string) error {
return gui.setFiltering(strings.TrimSpace(response)) return gui.setFiltering(strings.TrimSpace(response))
}, },
@ -48,10 +48,10 @@ func (gui *Gui) handleCreateFilteringMenuPanel() error {
if gui.State.Modes.Filtering.Active() { if gui.State.Modes.Filtering.Active() {
menuItems = append(menuItems, &popup.MenuItem{ menuItems = append(menuItems, &popup.MenuItem{
DisplayString: gui.Tr.LcExitFilterMode, DisplayString: gui.c.Tr.LcExitFilterMode,
OnPress: gui.clearFiltering, 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 return nil
} }
if !gui.Git.Flow.GitFlowEnabled() { 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") 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 { startHandler := func(branchType string) func() error {
return 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, Title: title,
HandleConfirm: func(name string) error { HandleConfirm: func(name string) error {
gui.logAction(gui.Tr.Actions.GitFlowStart) gui.c.LogAction(gui.c.Tr.Actions.GitFlowStart)
return gui.runSubprocessWithSuspenseAndRefresh( 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", Title: "git flow",
Items: []*popup.MenuItem{ Items: []*popup.MenuItem{
{ {
@ -64,11 +64,11 @@ func (gui *Gui) handleCreateGitFlowMenu() error {
} }
func (gui *Gui) gitFlowFinishBranch(branchName string) 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 { 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) return gui.runSubprocessWithSuspenseAndRefresh(cmdObj)
} }

View File

@ -65,7 +65,7 @@ func (gui *Gui) prevScreenMode() error {
func (gui *Gui) scrollUpView(view *gocui.View) error { func (gui *Gui) scrollUpView(view *gocui.View) error {
ox, oy := view.Origin() 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) 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 { func (gui *Gui) linesToScrollDown(view *gocui.View) int {
_, oy := view.Origin() _, oy := view.Origin()
y := oy y := oy
canScrollPastBottom := gui.UserConfig.Gui.ScrollPastBottom canScrollPastBottom := gui.c.UserConfig.Gui.ScrollPastBottom
if !canScrollPastBottom { if !canScrollPastBottom {
_, sy := view.Size() _, sy := view.Size()
y += sy y += sy
} }
scrollHeight := gui.UserConfig.Gui.ScrollHeight scrollHeight := gui.c.UserConfig.Gui.ScrollHeight
scrollableLines := view.ViewLinesHeight() - y scrollableLines := view.ViewLinesHeight() - y
if scrollableLines < 0 { if scrollableLines < 0 {
return 0 return 0
@ -177,7 +177,7 @@ func (gui *Gui) scrollDownConfirmationPanel() error {
} }
func (gui *Gui) handleRefresh() 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 { 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 // set filename, set primary/secondary selected, set line number, then switch context
// I'll need to know it was changed though. // I'll need to know it was changed though.
// Could I pass something along to the context change? // 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: 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 return nil
@ -205,35 +205,29 @@ func (gui *Gui) handleMouseDownSecondary() error {
switch gui.g.CurrentView() { switch gui.g.CurrentView() {
case gui.Views.Files: 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 return nil
} }
func (gui *Gui) fetch() (err error) { func (gui *Gui) fetch() (err error) {
gui.Mutexes.FetchMutex.Lock() gui.c.LogAction("Fetch")
defer gui.Mutexes.FetchMutex.Unlock() err = gui.git.Sync.Fetch(git_commands.FetchOptions{})
gui.logAction("Fetch")
err = gui.Git.Sync.Fetch(git_commands.FetchOptions{})
if err != nil && strings.Contains(err.Error(), "exit status 128") { 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 return err
} }
func (gui *Gui) backgroundFetch() (err error) { func (gui *Gui) backgroundFetch() (err error) {
gui.Mutexes.FetchMutex.Lock() err = gui.git.Sync.Fetch(git_commands.FetchOptions{Background: true})
defer gui.Mutexes.FetchMutex.Unlock()
err = gui.Git.Sync.Fetch(git_commands.FetchOptions{Background: true}) _ = gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
_ = gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
return err return err
} }
@ -246,14 +240,14 @@ func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error {
return nil return nil
} }
gui.logAction(gui.Tr.Actions.CopyToClipboard) gui.c.LogAction(gui.c.Tr.Actions.CopyToClipboard)
if err := gui.OSCommand.CopyToClipboard(itemId); err != nil { 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) 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 return nil
} }

View File

@ -14,9 +14,9 @@ import (
// we don't need to see a loading status if we're in a subprocess. // 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 // 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 { 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 { if useSubprocess {
success, err := gui.runSubprocessWithSuspense(gui.OSCommand.Cmd.NewShell(cmdObj.ToString())) success, err := gui.runSubprocessWithSuspense(gui.OSCommand.Cmd.NewShell(cmdObj.ToString()))
if success && onSuccess != nil { if success && onSuccess != nil {
@ -24,7 +24,7 @@ func (gui *Gui) withGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string,
return err 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 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 { 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 := gui.OSCommand.Cmd.NewShell(cmdObj.ToString())
cmdObj.AddEnvVars("TERM=dumb") cmdObj.AddEnvVars("TERM=dumb")
cmdWriter := gui.getCmdWriter() 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.Run(); err != nil {
if _, err := cmd.Stdout.Write([]byte(fmt.Sprintf("%s\n", style.FgRed.Sprint(err.Error())))); 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}) _ = gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
return gui.PopupHandler.Error( return gui.c.Error(
fmt.Errorf( 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/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config" "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/controllers"
"github.com/jesseduffield/lazygit/pkg/gui/filetree" "github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/lbl" "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/cherrypicking"
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing" "github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
"github.com/jesseduffield/lazygit/pkg/gui/modes/filtering" "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"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors" "github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph" "github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
@ -55,13 +57,13 @@ const StartupPopupVersion = 5
var OverlappingEdges = false var OverlappingEdges = false
type ContextManager struct { type ContextManager struct {
ContextStack []Context ContextStack []types.Context
sync.RWMutex sync.RWMutex
} }
func NewContextManager(initialContext Context) ContextManager { func NewContextManager(initialContext types.Context) ContextManager {
return ContextManager{ return ContextManager{
ContextStack: []Context{initialContext}, ContextStack: []types.Context{initialContext},
RWMutex: sync.RWMutex{}, RWMutex: sync.RWMutex{},
} }
} }
@ -72,7 +74,7 @@ type Repo string
type Gui struct { type Gui struct {
*common.Common *common.Common
g *gocui.Gui g *gocui.Gui
Git *commands.GitCommand git *commands.GitCommand
OSCommand *oscommands.OSCommand OSCommand *oscommands.OSCommand
// this is the state of the GUI for the current repo // this is the state of the GUI for the current repo
@ -126,6 +128,7 @@ type Gui struct {
IsNewRepo bool IsNewRepo bool
// controllers define keybindings for a given context
Controllers Controllers Controllers Controllers
// flag as to whether or not the diff view should ignore whitespace // 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` // if this is true, we'll load our commits using `git log --all`
ShowWholeGitGraph bool 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 RetainOriginalDir bool
PrevLayout PrevLayout 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 // 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 // in case we want to restore it before quitting for users who have set up
// the feature for changing directory upon quit. // the feature for changing directory upon quit.
@ -182,7 +194,7 @@ type GuiRepoState struct {
Updating bool Updating bool
Panels *panelStates Panels *panelStates
SplitMainPanel bool 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 IsRefreshingFiles bool
Searching searchingState Searching searchingState
@ -192,9 +204,9 @@ type GuiRepoState struct {
Modes Modes Modes Modes
ContextManager ContextManager ContextManager ContextManager
Contexts ContextTree Contexts context.ContextTree
ViewContextMap map[string]Context ViewContextMap map[string]types.Context
ViewTabContextMap map[string][]tabContext ViewTabContextMap map[string][]context.TabContext
// WindowViewNameMap is a mapping of windows to the current view of that window. // 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 // 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 // this is the message of the last failed commit attempt
failedCommitMessage string failedCommitMessage string
// TODO: move these into the gui struct
ScreenMode WindowMaximisation ScreenMode WindowMaximisation
} }
type Controllers struct { 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 { type listPanelState struct {
@ -373,13 +392,15 @@ type Modes struct {
Diffing diffing.Diffing 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 { type guiMutexes struct {
RefreshingFilesMutex sync.Mutex RefreshingFilesMutex *sync.Mutex
RefreshingStatusMutex sync.Mutex RefreshingStatusMutex *sync.Mutex
FetchMutex sync.Mutex FetchMutex *sync.Mutex
BranchCommitsMutex sync.Mutex BranchCommitsMutex *sync.Mutex
LineByLinePanelMutex sync.Mutex LineByLinePanelMutex *sync.Mutex
SubprocessMutex sync.Mutex SubprocessMutex *sync.Mutex
} }
// reuseState determines if we pull the repo state from our repo state map or // 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 return
} }
} else { } 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), FilteredReflogCommits: make([]*models.Commit, 0),
ReflogCommits: make([]*models.Commit, 0), ReflogCommits: make([]*models.Commit, 0),
StashEntries: make([]*models.StashEntry, 0), StashEntries: make([]*models.StashEntry, 0),
BisectInfo: git_commands.NewNullBisectInfo(),
Panels: &panelStates{ 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 // 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}}, Files: &filePanelState{listPanelState{SelectedLineIdx: -1}},
@ -450,8 +472,8 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
CherryPicking: cherrypicking.New(), CherryPicking: cherrypicking.New(),
Diffing: diffing.New(), Diffing: diffing.New(),
}, },
ViewContextMap: contexts.initialViewContextMap(), ViewContextMap: contexts.InitialViewContextMap(),
ViewTabContextMap: contexts.initialViewTabContextMap(), ViewTabContextMap: contexts.InitialViewTabContextMap(),
ScreenMode: screenMode, ScreenMode: screenMode,
// TODO: put contexts in the context manager // TODO: put contexts in the context manager
ContextManager: NewContextManager(initialContext), ContextManager: NewContextManager(initialContext),
@ -462,21 +484,6 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
gui.RepoStateMap[Repo(currentDir)] = gui.State 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 // for now the split view will always be on
// NewGui builds a new gui handler // NewGui builds a new gui handler
func NewGui( 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 // 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 // sake of backwards compatibility. We're making use of short circuiting here
ShowExtrasWindow: cmn.UserConfig.Gui.ShowCommandLog && !config.GetAppState().HideCommandLog, 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, InitialDir: initialDir,
} }
guiIO := oscommands.NewGuiIO( guiIO := oscommands.NewGuiIO(
cmn.Log, cmn.Log,
gui.logCommand, gui.LogCommand,
gui.getCmdWriter, gui.getCmdWriter,
gui.promptUserForCredential, gui.promptUserForCredential,
) )
@ -519,42 +533,151 @@ func NewGui(
gui.OSCommand = osCommand gui.OSCommand = osCommand
var err error var err error
gui.Git, err = commands.NewGitCommand( gui.git, err = commands.NewGitCommand(
cmn, cmn,
osCommand, osCommand,
gitConfig, gitConfig,
gui.Mutexes.FetchMutex,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
gui.resetState(filterPath, false)
gui.watchFilesForChanges() gui.watchFilesForChanges()
gui.PopupHandler = popup.NewPopupHandler( gui.PopupHandler = popup.NewPopupHandler(
cmn, cmn,
gui.createPopupPanel, 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) }, func() error { return gui.closeConfirmationPrompt(false) },
gui.createMenu, gui.createMenu,
gui.withWaitingStatus, 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} guiCommon := &guiCommon{gui: gui, IPopupHandler: gui.PopupHandler}
controllerCommon := &controllers.ControllerCommon{IGuiCommon: guiCommon, Common: cmn} 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{ gui.Controllers = Controllers{
Submodules: controllers.NewSubmodulesController( Submodules: controllers.NewSubmodulesController(
controllerCommon, controllerCommon,
gui.State.Contexts.Submodules,
gui.git,
gui.enterSubmodule, gui.enterSubmodule,
gui.Git,
gui.State.Submodules,
gui.getSelectedSubmodule, 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 return gui, nil
@ -621,7 +744,7 @@ func (gui *Gui) Run() error {
} }
gui.waitForIntro.Add(1) gui.waitForIntro.Add(1)
if gui.UserConfig.Git.AutoFetch { if gui.c.UserConfig.Git.AutoFetch {
go utils.Safe(gui.startBackgroundFetch) go utils.Safe(gui.startBackgroundFetch)
} }
@ -629,7 +752,7 @@ func (gui *Gui) Run() error {
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout())) 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() err = g.MainLoop()
return err return err
@ -684,7 +807,7 @@ func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess oscommands.ICmdOb
return err 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 return err
} }
@ -705,7 +828,7 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool,
} }
if err := gui.g.Suspend(); err != nil { if err := gui.g.Suspend(); err != nil {
return false, gui.PopupHandler.Error(err) return false, gui.c.Error(err)
} }
gui.PauseBackgroundThreads = true gui.PauseBackgroundThreads = true
@ -719,14 +842,14 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool,
gui.PauseBackgroundThreads = false gui.PauseBackgroundThreads = false
if cmdErr != nil { if cmdErr != nil {
return false, gui.PopupHandler.Error(cmdErr) return false, gui.c.Error(cmdErr)
} }
return true, nil return true, nil
} }
func (gui *Gui) runSubprocess(cmdObj oscommands.ICmdObj) error { //nolint:unparam func (gui *Gui) runSubprocess(cmdObj oscommands.ICmdObj) error { //nolint:unparam
gui.logCommand(cmdObj.ToString(), true) gui.LogCommand(cmdObj.ToString(), true)
subprocess := cmdObj.GetCmd() subprocess := cmdObj.GetCmd()
subprocess.Stdout = os.Stdout subprocess.Stdout = os.Stdout
@ -754,7 +877,7 @@ func (gui *Gui) loadNewRepo() error {
return err 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 return err
} }
@ -774,7 +897,7 @@ func (gui *Gui) showInitialPopups(tasks []func(chan struct{}) error) {
task := task task := task
go utils.Safe(func() { go utils.Safe(func() {
if err := task(done); err != nil { 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 { func (gui *Gui) showIntroPopupMessage(done chan struct{}) error {
onConfirm := func() error { onConfirm := func() error {
done <- struct{}{} done <- struct{}{}
gui.Config.GetAppState().StartupPopupVersion = StartupPopupVersion gui.c.GetAppState().StartupPopupVersion = StartupPopupVersion
return gui.Config.SaveAppState() return gui.c.SaveAppState()
} }
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: "", Title: "",
Prompt: gui.Tr.IntroPopupMessage, Prompt: gui.c.Tr.IntroPopupMessage,
HandleConfirm: onConfirm, HandleConfirm: onConfirm,
HandleClose: onConfirm, HandleClose: onConfirm,
}) })
@ -826,9 +949,9 @@ func (gui *Gui) startBackgroundFetch() {
} }
err := gui.backgroundFetch() err := gui.backgroundFetch()
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew { if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
_ = gui.PopupHandler.Ask(popup.AskOpts{ _ = gui.c.Ask(popup.AskOpts{
Title: gui.Tr.NoAutomaticGitFetchTitle, Title: gui.c.Tr.NoAutomaticGitFetchTitle,
Prompt: gui.Tr.NoAutomaticGitFetchBody, Prompt: gui.c.Tr.NoAutomaticGitFetchBody,
}) })
} else { } else {
gui.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), gui.stopChan, func() error { 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 { if gui.g.Mouse {
donate := style.FgMagenta.SetUnderline().Sprint(gui.Tr.Donate) donate := style.FgMagenta.SetUnderline().Sprint(gui.c.Tr.Donate)
askQuestion := style.FgYellow.SetUnderline().Sprint(gui.Tr.AskQuestion) askQuestion := style.FgYellow.SetUnderline().Sprint(gui.c.Tr.AskQuestion)
return fmt.Sprintf("%s %s %s", donate, askQuestion, gui.Config.GetVersion()) return fmt.Sprintf("%s %s %s", donate, askQuestion, gui.Config.GetVersion())
} else { } else {
return gui.Config.GetVersion() return gui.Config.GetVersion()
@ -35,7 +35,7 @@ func (gui *Gui) handleInfoClick() error {
for _, mode := range gui.modeStatuses() { for _, mode := range gui.modeStatuses() {
if mode.isActive() { if mode.isActive() {
if width-cx > len(gui.Tr.ResetInParentheses) { if width-cx > len(gui.c.Tr.ResetInParentheses) {
return nil return nil
} }
return mode.reset() 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 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) 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 gui.OSCommand.OpenLink(constants.Links.Discussions)
} }
return nil return nil

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ package gui
import ( import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/theme"
) )
@ -50,36 +51,36 @@ func (gui *Gui) createAllViews() error {
gui.Views.SearchPrefix.Frame = false gui.Views.SearchPrefix.Frame = false
gui.setViewContent(gui.Views.SearchPrefix, SEARCH_PREFIX) 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.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.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.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.Branches.FgColor = theme.GocuiDefaultTextColor
gui.Views.Files.Highlight = true 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.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.Wrap = true
gui.Views.Secondary.FgColor = theme.GocuiDefaultTextColor gui.Views.Secondary.FgColor = theme.GocuiDefaultTextColor
gui.Views.Secondary.IgnoreCarriageReturns = true 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.Wrap = true
gui.Views.Main.FgColor = theme.GocuiDefaultTextColor gui.Views.Main.FgColor = theme.GocuiDefaultTextColor
gui.Views.Main.IgnoreCarriageReturns = true 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.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.Status.FgColor = theme.GocuiDefaultTextColor
gui.Views.Search.BgColor = gocui.ColorDefault gui.Views.Search.BgColor = gocui.ColorDefault
@ -93,7 +94,7 @@ func (gui *Gui) createAllViews() error {
gui.Views.AppStatus.Visible = false gui.Views.AppStatus.Visible = false
gui.Views.CommitMessage.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.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitMessage.Editable = true gui.Views.CommitMessage.Editable = true
gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor) gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor)
@ -101,7 +102,7 @@ func (gui *Gui) createAllViews() error {
gui.Views.Confirmation.Visible = false gui.Views.Confirmation.Visible = false
gui.Views.Credentials.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.FgColor = theme.GocuiDefaultTextColor
gui.Views.Credentials.Editable = true gui.Views.Credentials.Editable = true
@ -113,7 +114,7 @@ func (gui *Gui) createAllViews() error {
gui.Views.Information.FgColor = gocui.ColorGreen gui.Views.Information.FgColor = gocui.ColorGreen
gui.Views.Information.Frame = false 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.FgColor = theme.GocuiDefaultTextColor
gui.Views.Extras.Autoscroll = true gui.Views.Extras.Autoscroll = true
gui.Views.Extras.Wrap = 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 // 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 continue
} }
@ -271,7 +272,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
view.SelBgColor = theme.GocuiSelectedLineBgColor view.SelBgColor = theme.GocuiSelectedLineBgColor
// I doubt this is expensive though it's admittedly redundant after the first render // 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)) 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 // here is a good place log some stuff
// if you run `lazygit --logs` // if you run `lazygit --logs`
// this will let you see these branches as prettified json // 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() return gui.resizeCurrentPopupPanel()
} }
@ -310,7 +311,7 @@ func (gui *Gui) onInitialViewsCreationForRepo() error {
} }
initialContext := gui.currentSideContext() initialContext := gui.currentSideContext()
if err := gui.pushContext(initialContext); err != nil { if err := gui.c.PushContext(initialContext); err != nil {
return err return err
} }
@ -372,9 +373,9 @@ func (gui *Gui) onInitialViewsCreation() error {
return err return err
} }
if !gui.UserConfig.DisableStartupPopups { if !gui.c.UserConfig.DisableStartupPopups {
popupTasks := []func(chan struct{}) error{} popupTasks := []func(chan struct{}) error{}
storedPopupVersion := gui.Config.GetAppState().StartupPopupVersion storedPopupVersion := gui.c.GetAppState().StartupPopupVersion
if storedPopupVersion < StartupPopupVersion { if storedPopupVersion < StartupPopupVersion {
popupTasks = append(popupTasks, gui.showIntroPopupMessage) popupTasks = append(popupTasks, gui.showIntroPopupMessage)
} }

View File

@ -87,9 +87,9 @@ func (gui *Gui) copySelectedToClipboard() error {
return gui.withLBLActiveCheck(func(state *LblPanelState) error { return gui.withLBLActiveCheck(func(state *LblPanelState) error {
selected := state.PlainRenderSelected() 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 { if err := gui.OSCommand.CopyToClipboard(selected); err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
return nil return nil
@ -141,7 +141,7 @@ func (gui *Gui) refreshMainViewForLineByLine(state *LblPanelState) error {
if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() { if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() {
filename := gui.getSelectedCommitFileName() filename := gui.getSelectedCommitFileName()
var err error var err error
includedLineIndices, err = gui.Git.Patch.PatchManager.GetFileIncLineIndices(filename) includedLineIndices, err = gui.git.Patch.PatchManager.GetFileIncLineIndices(filename)
if err != nil { if err != nil {
return err return err
} }
@ -285,5 +285,5 @@ func (gui *Gui) handleLineByLineEdit() error {
} }
lineNumber := gui.State.Panels.LineByLine.CurrentLineNumber() 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" "fmt"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/types"
) )
type ListContext struct { type ListContext struct {
GetItemsLength func() int GetItemsLength func() int
GetDisplayStrings func(startIdx int, length int) [][]string GetDisplayStrings func(startIdx int, length int) [][]string
OnFocus func(...OnFocusOpts) error OnFocus func(...types.OnFocusOpts) error
OnRenderToMain func(...OnFocusOpts) error OnRenderToMain func(...types.OnFocusOpts) error
OnFocusLost func() error OnFocusLost func() error
OnClickSelectedItem 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) // 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) SelectedItem func() (types.ListItem, bool)
OnGetPanelState func() IListPanelState OnGetPanelState func() types.IListPanelState
// if this is true, we'll call GetDisplayStrings for just the visible part of the // 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 // 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) // content based on the selection (e.g. for showing the selected commit)
@ -27,45 +28,12 @@ type ListContext struct {
*BasicContext *BasicContext
} }
type IListContext interface { var _ types.IListContext = &ListContext{}
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
GetPanelState() IListPanelState func (self *ListContext) GetPanelState() types.IListPanelState {
Context
}
func (self *ListContext) GetPanelState() IListPanelState {
return self.OnGetPanelState() 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() { func (self *ListContext) FocusLine() {
view, err := self.Gui.g.View(self.ViewName) view, err := self.Gui.g.View(self.ViewName)
if err != nil { if err != nil {
@ -87,7 +55,7 @@ func formatListFooter(selectedLineIdx int, length int) string {
return fmt.Sprintf("%d of %d", selectedLineIdx+1, length) 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() return self.SelectedItem()
} }
@ -132,7 +100,7 @@ func (self *ListContext) HandleFocusLost() error {
return nil return nil
} }
func (self *ListContext) HandleFocus(opts ...OnFocusOpts) error { func (self *ListContext) HandleFocus(opts ...types.OnFocusOpts) error {
if self.Gui.popupPanelFocused() { if self.Gui.popupPanelFocused() {
return nil return nil
} }
@ -158,19 +126,19 @@ func (self *ListContext) HandleFocus(opts ...OnFocusOpts) error {
return nil return nil
} }
func (self *ListContext) handlePrevLine() error { func (self *ListContext) HandlePrevLine() error {
return self.handleLineChange(-1) return self.handleLineChange(-1)
} }
func (self *ListContext) handleNextLine() error { func (self *ListContext) HandleNextLine() error {
return self.handleLineChange(1) return self.handleLineChange(1)
} }
func (self *ListContext) handleScrollLeft() error { func (self *ListContext) HandleScrollLeft() error {
return self.scroll(self.Gui.scrollLeft) return self.scroll(self.Gui.scrollLeft)
} }
func (self *ListContext) handleScrollRight() error { func (self *ListContext) HandleScrollRight() error {
return self.scroll(self.Gui.scrollRight) return self.scroll(self.Gui.scrollRight)
} }
@ -209,7 +177,7 @@ func (self *ListContext) handleLineChange(change int) error {
return self.HandleFocus() return self.HandleFocus()
} }
func (self *ListContext) handleNextPage() error { func (self *ListContext) HandleNextPage() error {
view, err := self.Gui.g.View(self.ViewName) view, err := self.Gui.g.View(self.ViewName)
if err != nil { if err != nil {
return nil return nil
@ -219,15 +187,15 @@ func (self *ListContext) handleNextPage() error {
return self.handleLineChange(delta) return self.handleLineChange(delta)
} }
func (self *ListContext) handleGotoTop() error { func (self *ListContext) HandleGotoTop() error {
return self.handleLineChange(-self.GetItemsLength()) return self.handleLineChange(-self.GetItemsLength())
} }
func (self *ListContext) handleGotoBottom() error { func (self *ListContext) HandleGotoBottom() error {
return self.handleLineChange(self.GetItemsLength()) return self.handleLineChange(self.GetItemsLength())
} }
func (self *ListContext) handlePrevPage() error { func (self *ListContext) HandlePrevPage() error {
view, err := self.Gui.g.View(self.ViewName) view, err := self.Gui.g.View(self.ViewName)
if err != nil { if err != nil {
return nil return nil
@ -238,7 +206,7 @@ func (self *ListContext) handlePrevPage() error {
return self.handleLineChange(-delta) return self.handleLineChange(-delta)
} }
func (self *ListContext) handleClick() error { func (self *ListContext) HandleClick(onClick func() error) error {
if self.ignoreKeybinding() { if self.ignoreKeybinding() {
return nil return nil
} }
@ -252,7 +220,7 @@ func (self *ListContext) handleClick() error {
newSelectedLineIdx := view.SelectedLineIdx() newSelectedLineIdx := view.SelectedLineIdx()
// we need to focus the view // 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 return err
} }
@ -263,13 +231,13 @@ func (self *ListContext) handleClick() error {
self.GetPanelState().SetSelectedLineIdx(newSelectedLineIdx) self.GetPanelState().SetSelectedLineIdx(newSelectedLineIdx)
prevViewName := self.Gui.currentViewName() prevViewName := self.Gui.currentViewName()
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == self.ViewName && self.OnClickSelectedItem != nil { if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == self.ViewName && onClick != nil {
return self.OnClickSelectedItem() return onClick()
} }
return self.HandleFocus() return self.HandleFocus()
} }
func (self *ListContext) onSearchSelect(selectedLineIdx int) error { func (self *ListContext) OnSearchSelect(selectedLineIdx int) error {
self.GetPanelState().SetSelectedLineIdx(selectedLineIdx) self.GetPanelState().SetSelectedLineIdx(selectedLineIdx)
return self.HandleFocus() return self.HandleFocus()
} }
@ -281,3 +249,35 @@ func (self *ListContext) HandleRenderToMain() error {
return nil 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 ( import (
"log" "log"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
func (gui *Gui) menuListContext() IListContext { func (gui *Gui) menuListContext() types.IListContext {
return &ListContext{ return &ListContext{
BasicContext: &BasicContext{ BasicContext: &BasicContext{
ViewName: "menu", ViewName: "menu",
Key: "menu", Key: "menu",
Kind: PERSISTENT_POPUP, Kind: types.PERSISTENT_POPUP,
OnGetOptionsMap: gui.getMenuOptions, OnGetOptionsMap: gui.getMenuOptions,
}, },
GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() }, GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Menu }, OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.Menu },
OnClickSelectedItem: gui.onMenuPress, Gui: gui,
Gui: gui,
// no GetDisplayStrings field because we do a custom render on menu creation // 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{ return &ListContext{
BasicContext: &BasicContext{ BasicContext: &BasicContext{
ViewName: "files", ViewName: "files",
WindowName: "files", WindowName: "files",
Key: FILES_CONTEXT_KEY, Key: FILES_CONTEXT_KEY,
Kind: SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
}, },
GetItemsLength: func() int { return gui.State.FileTreeViewModel.GetItemsLength() }, GetItemsLength: func() int { return gui.State.FileTreeViewModel.GetItemsLength() },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Files }, OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.Files },
OnFocus: OnFocusWrapper(gui.onFocusFile), OnFocus: OnFocusWrapper(gui.onFocusFile),
OnRenderToMain: OnFocusWrapper(gui.filesRenderToMain), OnRenderToMain: OnFocusWrapper(gui.filesRenderToMain),
OnClickSelectedItem: gui.handleFilePress, Gui: gui,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string { GetDisplayStrings: func(startIdx int, length int) [][]string {
lines := presentation.RenderFileTree(gui.State.FileTreeViewModel, gui.State.Modes.Diffing.Ref, gui.State.Submodules) lines := presentation.RenderFileTree(gui.State.FileTreeViewModel, gui.State.Modes.Diffing.Ref, gui.State.Submodules)
mappedLines := make([][]string, len(lines)) mappedLines := make([][]string, len(lines))
@ -50,117 +47,115 @@ func (gui *Gui) filesListContext() IListContext {
return mappedLines return mappedLines
}, },
SelectedItem: func() (ListItem, bool) { SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedFileNode() item := gui.getSelectedFileNode()
return item, item != nil return item, item != nil
}, },
} }
} }
func (gui *Gui) branchesListContext() IListContext { func (gui *Gui) branchesListContext() types.IListContext {
return &ListContext{ return &ListContext{
BasicContext: &BasicContext{ BasicContext: &BasicContext{
ViewName: "branches", ViewName: "branches",
WindowName: "branches", WindowName: "branches",
Key: LOCAL_BRANCHES_CONTEXT_KEY, Key: LOCAL_BRANCHES_CONTEXT_KEY,
Kind: SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
}, },
GetItemsLength: func() int { return len(gui.State.Branches) }, 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), OnRenderToMain: OnFocusWrapper(gui.branchesRenderToMain),
Gui: gui, Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string { GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref) 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() item := gui.getSelectedBranch()
return item, item != nil return item, item != nil
}, },
} }
} }
func (gui *Gui) remotesListContext() IListContext { func (gui *Gui) remotesListContext() types.IListContext {
return &ListContext{ return &ListContext{
BasicContext: &BasicContext{ BasicContext: &BasicContext{
ViewName: "branches", ViewName: "branches",
WindowName: "branches", WindowName: "branches",
Key: REMOTES_CONTEXT_KEY, Key: REMOTES_CONTEXT_KEY,
Kind: SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
}, },
GetItemsLength: func() int { return len(gui.State.Remotes) }, GetItemsLength: func() int { return len(gui.State.Remotes) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Remotes }, OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.Remotes },
OnRenderToMain: OnFocusWrapper(gui.remotesRenderToMain), OnRenderToMain: OnFocusWrapper(gui.remotesRenderToMain),
OnClickSelectedItem: gui.handleRemoteEnter, Gui: gui,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string { GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Modes.Diffing.Ref) return presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Modes.Diffing.Ref)
}, },
SelectedItem: func() (ListItem, bool) { SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedRemote() item := gui.getSelectedRemote()
return item, item != nil return item, item != nil
}, },
} }
} }
func (gui *Gui) remoteBranchesListContext() IListContext { func (gui *Gui) remoteBranchesListContext() types.IListContext {
return &ListContext{ return &ListContext{
BasicContext: &BasicContext{ BasicContext: &BasicContext{
ViewName: "branches", ViewName: "branches",
WindowName: "branches", WindowName: "branches",
Key: REMOTE_BRANCHES_CONTEXT_KEY, Key: REMOTE_BRANCHES_CONTEXT_KEY,
Kind: SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
}, },
GetItemsLength: func() int { return len(gui.State.RemoteBranches) }, 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), OnRenderToMain: OnFocusWrapper(gui.remoteBranchesRenderToMain),
Gui: gui, Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string { GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Modes.Diffing.Ref) return presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Modes.Diffing.Ref)
}, },
SelectedItem: func() (ListItem, bool) { SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedRemoteBranch() item := gui.getSelectedRemoteBranch()
return item, item != nil return item, item != nil
}, },
} }
} }
func (gui *Gui) tagsListContext() IListContext { func (gui *Gui) tagsListContext() types.IListContext {
return &ListContext{ return &ListContext{
BasicContext: &BasicContext{ BasicContext: &BasicContext{
ViewName: "branches", ViewName: "branches",
WindowName: "branches", WindowName: "branches",
Key: TAGS_CONTEXT_KEY, Key: TAGS_CONTEXT_KEY,
Kind: SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
}, },
GetItemsLength: func() int { return len(gui.State.Tags) }, 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), OnRenderToMain: OnFocusWrapper(gui.tagsRenderToMain),
Gui: gui, Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string { GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Modes.Diffing.Ref) return presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Modes.Diffing.Ref)
}, },
SelectedItem: func() (ListItem, bool) { SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedTag() item := gui.getSelectedTag()
return item, item != nil return item, item != nil
}, },
} }
} }
func (gui *Gui) branchCommitsListContext() IListContext { func (gui *Gui) branchCommitsListContext() types.IListContext {
parseEmoji := gui.UserConfig.Git.ParseEmoji parseEmoji := gui.c.UserConfig.Git.ParseEmoji
return &ListContext{ return &ListContext{
BasicContext: &BasicContext{ BasicContext: &BasicContext{
ViewName: "commits", ViewName: "commits",
WindowName: "commits", WindowName: "commits",
Key: BRANCH_COMMITS_CONTEXT_KEY, Key: BRANCH_COMMITS_CONTEXT_KEY,
Kind: SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
}, },
GetItemsLength: func() int { return len(gui.State.Commits) }, GetItemsLength: func() int { return len(gui.State.Commits) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Commits }, OnGetPanelState: func() types.IListPanelState { return gui.State.Panels.Commits },
OnFocus: OnFocusWrapper(gui.onCommitFocus), OnFocus: OnFocusWrapper(gui.onCommitFocus),
OnRenderToMain: OnFocusWrapper(gui.branchCommitsRenderToMain), OnRenderToMain: OnFocusWrapper(gui.branchCommitsRenderToMain),
OnClickSelectedItem: gui.handleViewCommitFiles, Gui: gui,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string { GetDisplayStrings: func(startIdx int, length int) [][]string {
selectedCommitSha := "" selectedCommitSha := ""
if gui.currentContext().GetKey() == BRANCH_COMMITS_CONTEXT_KEY { if gui.currentContext().GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
@ -182,7 +177,7 @@ func (gui *Gui) branchCommitsListContext() IListContext {
gui.State.BisectInfo, gui.State.BisectInfo,
) )
}, },
SelectedItem: func() (ListItem, bool) { SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedLocalCommit() item := gui.getSelectedLocalCommit()
return item, item != nil return item, item != nil
}, },
@ -190,17 +185,17 @@ func (gui *Gui) branchCommitsListContext() IListContext {
} }
} }
func (gui *Gui) subCommitsListContext() IListContext { func (gui *Gui) subCommitsListContext() types.IListContext {
parseEmoji := gui.UserConfig.Git.ParseEmoji parseEmoji := gui.c.UserConfig.Git.ParseEmoji
return &ListContext{ return &ListContext{
BasicContext: &BasicContext{ BasicContext: &BasicContext{
ViewName: "branches", ViewName: "branches",
WindowName: "branches", WindowName: "branches",
Key: SUB_COMMITS_CONTEXT_KEY, Key: SUB_COMMITS_CONTEXT_KEY,
Kind: SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
}, },
GetItemsLength: func() int { return len(gui.State.SubCommits) }, 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), OnRenderToMain: OnFocusWrapper(gui.subCommitsRenderToMain),
Gui: gui, Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string { GetDisplayStrings: func(startIdx int, length int) [][]string {
@ -224,7 +219,7 @@ func (gui *Gui) subCommitsListContext() IListContext {
git_commands.NewNullBisectInfo(), git_commands.NewNullBisectInfo(),
) )
}, },
SelectedItem: func() (ListItem, bool) { SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedSubCommit() item := gui.getSelectedSubCommit()
return item, item != nil return item, item != nil
}, },
@ -237,7 +232,7 @@ func (gui *Gui) shouldShowGraph() bool {
return false return false
} }
value := gui.UserConfig.Git.Log.ShowGraph value := gui.c.UserConfig.Git.Log.ShowGraph
switch value { switch value {
case "always": case "always":
return true return true
@ -251,17 +246,17 @@ func (gui *Gui) shouldShowGraph() bool {
return false return false
} }
func (gui *Gui) reflogCommitsListContext() IListContext { func (gui *Gui) reflogCommitsListContext() types.IListContext {
parseEmoji := gui.UserConfig.Git.ParseEmoji parseEmoji := gui.c.UserConfig.Git.ParseEmoji
return &ListContext{ return &ListContext{
BasicContext: &BasicContext{ BasicContext: &BasicContext{
ViewName: "commits", ViewName: "commits",
WindowName: "commits", WindowName: "commits",
Key: REFLOG_COMMITS_CONTEXT_KEY, Key: REFLOG_COMMITS_CONTEXT_KEY,
Kind: SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
}, },
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) }, 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), OnRenderToMain: OnFocusWrapper(gui.reflogCommitsRenderToMain),
Gui: gui, Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string { GetDisplayStrings: func(startIdx int, length int) [][]string {
@ -273,45 +268,45 @@ func (gui *Gui) reflogCommitsListContext() IListContext {
parseEmoji, parseEmoji,
) )
}, },
SelectedItem: func() (ListItem, bool) { SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedReflogCommit() item := gui.getSelectedReflogCommit()
return item, item != nil return item, item != nil
}, },
} }
} }
func (gui *Gui) stashListContext() IListContext { func (gui *Gui) stashListContext() types.IListContext {
return &ListContext{ return &ListContext{
BasicContext: &BasicContext{ BasicContext: &BasicContext{
ViewName: "stash", ViewName: "stash",
WindowName: "stash", WindowName: "stash",
Key: STASH_CONTEXT_KEY, Key: STASH_CONTEXT_KEY,
Kind: SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
}, },
GetItemsLength: func() int { return len(gui.State.StashEntries) }, 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), OnRenderToMain: OnFocusWrapper(gui.stashRenderToMain),
Gui: gui, Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string { GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Modes.Diffing.Ref) return presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Modes.Diffing.Ref)
}, },
SelectedItem: func() (ListItem, bool) { SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedStashEntry() item := gui.getSelectedStashEntry()
return item, item != nil return item, item != nil
}, },
} }
} }
func (gui *Gui) commitFilesListContext() IListContext { func (gui *Gui) commitFilesListContext() types.IListContext {
return &ListContext{ return &ListContext{
BasicContext: &BasicContext{ BasicContext: &BasicContext{
ViewName: "commitFiles", ViewName: "commitFiles",
WindowName: "commits", WindowName: "commits",
Key: COMMIT_FILES_CONTEXT_KEY, Key: COMMIT_FILES_CONTEXT_KEY,
Kind: SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
}, },
GetItemsLength: func() int { return gui.State.CommitFileTreeViewModel.GetItemsLength() }, 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), OnFocus: OnFocusWrapper(gui.onCommitFileFocus),
OnRenderToMain: OnFocusWrapper(gui.commitFilesRenderToMain), OnRenderToMain: OnFocusWrapper(gui.commitFilesRenderToMain),
Gui: gui, Gui: gui,
@ -320,7 +315,7 @@ func (gui *Gui) commitFilesListContext() IListContext {
return [][]string{{style.FgRed.Sprint("(none)")}} 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)) mappedLines := make([][]string, len(lines))
for i, line := range lines { for i, line := range lines {
mappedLines[i] = []string{line} mappedLines[i] = []string{line}
@ -328,45 +323,45 @@ func (gui *Gui) commitFilesListContext() IListContext {
return mappedLines return mappedLines
}, },
SelectedItem: func() (ListItem, bool) { SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedCommitFileNode() item := gui.getSelectedCommitFileNode()
return item, item != nil return item, item != nil
}, },
} }
} }
func (gui *Gui) submodulesListContext() IListContext { func (gui *Gui) submodulesListContext() types.IListContext {
return &ListContext{ return &ListContext{
BasicContext: &BasicContext{ BasicContext: &BasicContext{
ViewName: "files", ViewName: "files",
WindowName: "files", WindowName: "files",
Key: SUBMODULES_CONTEXT_KEY, Key: SUBMODULES_CONTEXT_KEY,
Kind: SIDE_CONTEXT, Kind: types.SIDE_CONTEXT,
}, },
GetItemsLength: func() int { return len(gui.State.Submodules) }, 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), OnRenderToMain: OnFocusWrapper(gui.submodulesRenderToMain),
Gui: gui, Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string { GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetSubmoduleListDisplayStrings(gui.State.Submodules) return presentation.GetSubmoduleListDisplayStrings(gui.State.Submodules)
}, },
SelectedItem: func() (ListItem, bool) { SelectedItem: func() (types.ListItem, bool) {
item := gui.getSelectedSubmodule() item := gui.getSelectedSubmodule()
return item, item != nil return item, item != nil
}, },
} }
} }
func (gui *Gui) suggestionsListContext() IListContext { func (gui *Gui) suggestionsListContext() types.IListContext {
return &ListContext{ return &ListContext{
BasicContext: &BasicContext{ BasicContext: &BasicContext{
ViewName: "suggestions", ViewName: "suggestions",
WindowName: "suggestions", WindowName: "suggestions",
Key: SUGGESTIONS_CONTEXT_KEY, Key: SUGGESTIONS_CONTEXT_KEY,
Kind: PERSISTENT_POPUP, Kind: types.PERSISTENT_POPUP,
}, },
GetItemsLength: func() int { return len(gui.State.Suggestions) }, 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, Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string { GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions) return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions)
@ -374,8 +369,8 @@ func (gui *Gui) suggestionsListContext() IListContext {
} }
} }
func (gui *Gui) getListContexts() []IListContext { func (gui *Gui) getListContexts() []types.IListContext {
return []IListContext{ return []types.IListContext{
gui.State.Contexts.Menu, gui.State.Contexts.Menu,
gui.State.Contexts.Files, gui.State.Contexts.Files,
gui.State.Contexts.Branches, gui.State.Contexts.Branches,
@ -391,58 +386,3 @@ func (gui *Gui) getListContexts() []IListContext {
gui.State.Contexts.Suggestions, 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 view.Highlight = opts.highlight
if err := gui.runTaskForView(view, opts.task); err != nil { if err := gui.runTaskForView(view, opts.task); err != nil {
gui.Log.Error(err) gui.c.Log.Error(err)
return nil return nil
} }

View File

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

View File

@ -52,8 +52,8 @@ func (gui *Gui) handleMergeConflictUndo() error {
return nil return nil
} }
gui.logAction("Restoring file to previous state") gui.c.LogAction("Restoring file to previous state")
gui.logCommand("Undoing last conflict resolution", false) gui.LogCommand("Undoing last conflict resolution", false)
if err := ioutil.WriteFile(state.GetPath(), []byte(state.GetContent()), 0644); err != nil { if err := ioutil.WriteFile(state.GetPath(), []byte(state.GetContent()), 0644); err != nil {
return err return err
} }
@ -124,8 +124,8 @@ func (gui *Gui) resolveConflict(selection mergeconflicts.Selection) (bool, error
case mergeconflicts.ALL: case mergeconflicts.ALL:
logStr = "Picking all hunks" logStr = "Picking all hunks"
} }
gui.logAction("Resolve merge conflict") gui.c.LogAction("Resolve merge conflict")
gui.logCommand(logStr, false) gui.LogCommand(logStr, false)
state.PushContent(content) state.PushContent(content)
return true, ioutil.WriteFile(state.GetPath(), []byte(content), 0644) return true, ioutil.WriteFile(state.GetPath(), []byte(content), 0644)
} }
@ -153,7 +153,7 @@ func (gui *Gui) renderConflicts(hasFocus bool) error {
return gui.refreshMainViews(refreshMainOpts{ return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{ main: &viewUpdateOpts{
title: gui.Tr.MergeConflictsTitle, title: gui.c.Tr.MergeConflictsTitle,
task: NewRenderStringWithoutScrollTask(content), task: NewRenderStringWithoutScrollTask(content),
noWrap: true, noWrap: true,
}, },
@ -178,19 +178,19 @@ func (gui *Gui) centerYPos(view *gocui.View, y int) {
} }
func (gui *Gui) getMergingOptions() map[string]string { func (gui *Gui) getMergingOptions() map[string]string {
keybindingConfig := gui.UserConfig.Keybinding keybindingConfig := gui.c.UserConfig.Keybinding
return map[string]string{ 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.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.Tr.LcNavigateConflicts, fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevBlock), gui.getKeyDisplay(keybindingConfig.Universal.NextBlock)): gui.c.Tr.LcNavigateConflicts,
gui.getKeyDisplay(keybindingConfig.Universal.Select): gui.Tr.LcPickHunk, gui.getKeyDisplay(keybindingConfig.Universal.Select): gui.c.Tr.LcPickHunk,
gui.getKeyDisplay(keybindingConfig.Main.PickBothHunks): gui.Tr.LcPickAllHunks, gui.getKeyDisplay(keybindingConfig.Main.PickBothHunks): gui.c.Tr.LcPickAllHunks,
gui.getKeyDisplay(keybindingConfig.Universal.Undo): gui.Tr.LcUndo, gui.getKeyDisplay(keybindingConfig.Universal.Undo): gui.c.Tr.LcUndo,
} }
} }
func (gui *Gui) handleEscapeMerge() error { 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 return err
} }
@ -200,7 +200,7 @@ func (gui *Gui) handleEscapeMerge() error {
func (gui *Gui) onLastConflictResolved() error { func (gui *Gui) onLastConflictResolved() error {
// as part of refreshing files, we handle the situation where a file has had // as part of refreshing files, we handle the situation where a file has had
// its merge conflicts resolved. // 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() { func (gui *Gui) resetMergeState() {
@ -209,7 +209,7 @@ func (gui *Gui) resetMergeState() {
} }
func (gui *Gui) setMergeState(path string) (bool, error) { 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 { if err != nil {
return false, err return false, err
} }
@ -269,7 +269,6 @@ func (gui *Gui) setConflictsAndRender(path string, hasFocus bool) (bool, error)
return false, err return false, err
} }
// if we don't have conflicts we'll fall through and show the diff
if hasConflicts { if hasConflicts {
return true, gui.renderConflicts(hasFocus) return true, gui.renderConflicts(hasFocus)
} }
@ -294,7 +293,7 @@ func (gui *Gui) refreshMergeState() error {
hasConflicts, err := gui.setConflictsAndRender(gui.State.Panels.Merging.GetPath(), true) hasConflicts, err := gui.setConflictsAndRender(gui.State.Panels.Merging.GetPath(), true)
if err != nil { if err != nil {
return gui.surfaceError(err) return gui.c.Error(err)
} }
if !hasConflicts { if !hasConflicts {
@ -303,3 +302,19 @@ func (gui *Gui) refreshMergeState() error {
return nil 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( return gui.withResetButton(
fmt.Sprintf( fmt.Sprintf(
"%s %s", "%s %s",
gui.Tr.LcShowingGitDiff, gui.c.Tr.LcShowingGitDiff,
"git diff "+gui.diffStr(), "git diff "+gui.diffStr(),
), ),
style.FgMagenta, style.FgMagenta,
@ -30,9 +30,9 @@ func (gui *Gui) modeStatuses() []modeStatus {
reset: gui.exitDiffMode, reset: gui.exitDiffMode,
}, },
{ {
isActive: gui.Git.Patch.PatchManager.Active, isActive: gui.git.Patch.PatchManager.Active,
description: func() string { 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, reset: gui.handleResetPatch,
}, },
@ -42,7 +42,7 @@ func (gui *Gui) modeStatuses() []modeStatus {
return gui.withResetButton( return gui.withResetButton(
fmt.Sprintf( fmt.Sprintf(
"%s '%s'", "%s '%s'",
gui.Tr.LcFilteringBy, gui.c.Tr.LcFilteringBy,
gui.State.Modes.Filtering.GetPath(), gui.State.Modes.Filtering.GetPath(),
), ),
style.FgRed, style.FgRed,
@ -65,10 +65,10 @@ func (gui *Gui) modeStatuses() []modeStatus {
}, },
{ {
isActive: func() bool { isActive: func() bool {
return gui.Git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE return gui.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE
}, },
description: func() string { description: func() string {
workingTreeState := gui.Git.Status.WorkingTreeState() workingTreeState := gui.git.Status.WorkingTreeState()
return gui.withResetButton( return gui.withResetButton(
formatWorkingTreeState(workingTreeState), style.FgYellow, formatWorkingTreeState(workingTreeState), style.FgYellow,
) )
@ -82,7 +82,7 @@ func (gui *Gui) modeStatuses() []modeStatus {
description: func() string { description: func() string {
return gui.withResetButton("bisecting", style.FgGreen) 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( return textStyle.Sprintf(
"%s %s", "%s %s",
content, 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{ return gui.c.Menu(popup.CreateMenuOptions{
Title: strings.Title(gui.Tr.LcMenu), Title: strings.Title(gui.c.Tr.LcMenu),
Items: menuItems, Items: menuItems,
HideCancel: true, HideCancel: true,
}) })

View File

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

View File

@ -9,8 +9,8 @@ import (
) )
func (gui *Gui) handleCreatePatchOptionsMenu() error { func (gui *Gui) handleCreatePatchOptionsMenu() error {
if !gui.Git.Patch.PatchManager.Active() { if !gui.git.Patch.PatchManager.Active() {
return gui.PopupHandler.ErrorMsg(gui.Tr.NoPatchError) return gui.c.ErrorMsg(gui.c.Tr.NoPatchError)
} }
menuItems := []*popup.MenuItem{ 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{ 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, OnPress: gui.handleDeletePatchFromCommit,
}, },
{ {
@ -46,7 +46,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error {
if gui.currentContext().GetKey() == gui.State.Contexts.BranchCommits.GetKey() { if gui.currentContext().GetKey() == gui.State.Contexts.BranchCommits.GetKey() {
selectedCommit := gui.getSelectedLocalCommit() 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 // adding this option to index 1
menuItems = append( menuItems = append(
menuItems[:1], 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 { func (gui *Gui) getPatchCommitIndex() int {
for index, commit := range gui.State.Commits { 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 return index
} }
} }
@ -76,8 +76,8 @@ func (gui *Gui) getPatchCommitIndex() int {
} }
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) { func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
if gui.Git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { if gui.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return false, gui.PopupHandler.ErrorMsg(gui.Tr.CantPatchWhileRebasingError) return false, gui.c.ErrorMsg(gui.c.Tr.CantPatchWhileRebasingError)
} }
return true, nil return true, nil
} }
@ -98,11 +98,11 @@ func (gui *Gui) handleDeletePatchFromCommit() error {
return err return err
} }
return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error {
commitIndex := gui.getPatchCommitIndex() commitIndex := gui.getPatchCommitIndex()
gui.logAction(gui.Tr.Actions.RemovePatchFromCommit) gui.c.LogAction(gui.c.Tr.Actions.RemovePatchFromCommit)
err := gui.Git.Patch.DeletePatchesFromCommit(gui.State.Commits, commitIndex) err := gui.git.Patch.DeletePatchesFromCommit(gui.State.Commits, commitIndex)
return gui.handleGenericMergeCommandResult(err) return gui.checkMergeOrRebase(err)
}) })
} }
@ -115,11 +115,11 @@ func (gui *Gui) handleMovePatchToSelectedCommit() error {
return err return err
} }
return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error {
commitIndex := gui.getPatchCommitIndex() commitIndex := gui.getPatchCommitIndex()
gui.logAction(gui.Tr.Actions.MovePatchToSelectedCommit) gui.c.LogAction(gui.c.Tr.Actions.MovePatchToSelectedCommit)
err := gui.Git.Patch.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx) err := gui.git.Patch.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx)
return gui.handleGenericMergeCommandResult(err) return gui.checkMergeOrRebase(err)
}) })
} }
@ -133,18 +133,18 @@ func (gui *Gui) handleMovePatchIntoWorkingTree() error {
} }
pull := func(stash bool) 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() commitIndex := gui.getPatchCommitIndex()
gui.logAction(gui.Tr.Actions.MovePatchIntoIndex) gui.c.LogAction(gui.c.Tr.Actions.MovePatchIntoIndex)
err := gui.Git.Patch.MovePatchIntoIndex(gui.State.Commits, commitIndex, stash) err := gui.git.Patch.MovePatchIntoIndex(gui.State.Commits, commitIndex, stash)
return gui.handleGenericMergeCommandResult(err) return gui.checkMergeOrRebase(err)
}) })
} }
if len(gui.trackedFiles()) > 0 { if gui.workingTreeHelper.IsWorkingTreeDirty() {
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.MustStashTitle, Title: gui.c.Tr.MustStashTitle,
Prompt: gui.Tr.MustStashWarning, Prompt: gui.c.Tr.MustStashWarning,
HandleConfirm: func() error { HandleConfirm: func() error {
return pull(true) return pull(true)
}, },
@ -163,11 +163,11 @@ func (gui *Gui) handlePullPatchIntoNewCommit() error {
return err return err
} }
return gui.PopupHandler.WithWaitingStatus(gui.Tr.RebasingStatus, func() error { return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error {
commitIndex := gui.getPatchCommitIndex() commitIndex := gui.getPatchCommitIndex()
gui.logAction(gui.Tr.Actions.MovePatchIntoNewCommit) gui.c.LogAction(gui.c.Tr.Actions.MovePatchIntoNewCommit)
err := gui.Git.Patch.PullPatchIntoNewCommit(gui.State.Commits, commitIndex) err := gui.git.Patch.PullPatchIntoNewCommit(gui.State.Commits, commitIndex)
return gui.handleGenericMergeCommandResult(err) return gui.checkMergeOrRebase(err)
}) })
} }
@ -176,21 +176,21 @@ func (gui *Gui) handleApplyPatch(reverse bool) error {
return err return err
} }
action := gui.Tr.Actions.ApplyPatch action := gui.c.Tr.Actions.ApplyPatch
if reverse { if reverse {
action = "Apply patch in reverse" action = "Apply patch in reverse"
} }
gui.logAction(action) gui.c.LogAction(action)
if err := gui.Git.Patch.PatchManager.ApplyPatches(reverse); err != nil { if err := gui.git.Patch.PatchManager.ApplyPatches(reverse); err != nil {
return gui.PopupHandler.Error(err) 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 { func (gui *Gui) handleResetPatch() error {
gui.Git.Patch.PatchManager.Reset() gui.git.Patch.PatchManager.Reset()
if gui.currentContextKeyIgnoringPopups() == MAIN_PATCH_BUILDING_CONTEXT_KEY { 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 return err
} }
} }

View File

@ -19,6 +19,8 @@ type IPopupHandler interface {
WithLoaderPanel(message string, f func() error) error WithLoaderPanel(message string, f func() error) error
WithWaitingStatus(message string, f func() error) error WithWaitingStatus(message string, f func() error) error
Menu(opts CreateMenuOptions) error Menu(opts CreateMenuOptions) error
Toast(message string)
GetPromptInput() string
} }
type CreateMenuOptions struct { type CreateMenuOptions struct {
@ -74,6 +76,8 @@ type RealPopupHandler struct {
closePopupFn func() error closePopupFn func() error
createMenuFn func(CreateMenuOptions) error createMenuFn func(CreateMenuOptions) error
withWaitingStatusFn func(message string, f func() error) error withWaitingStatusFn func(message string, f func() error) error
toastFn func(message string)
getPromptInputFn func() string
} }
var _ IPopupHandler = &RealPopupHandler{} var _ IPopupHandler = &RealPopupHandler{}
@ -85,6 +89,8 @@ func NewPopupHandler(
closePopupFn func() error, closePopupFn func() error,
createMenuFn func(CreateMenuOptions) error, createMenuFn func(CreateMenuOptions) error,
withWaitingStatusFn func(message string, f func() error) error, withWaitingStatusFn func(message string, f func() error) error,
toastFn func(message string),
getPromptInputFn func() string,
) *RealPopupHandler { ) *RealPopupHandler {
return &RealPopupHandler{ return &RealPopupHandler{
Common: common, Common: common,
@ -94,6 +100,8 @@ func NewPopupHandler(
closePopupFn: closePopupFn, closePopupFn: closePopupFn,
createMenuFn: createMenuFn, createMenuFn: createMenuFn,
withWaitingStatusFn: withWaitingStatusFn, withWaitingStatusFn: withWaitingStatusFn,
toastFn: toastFn,
getPromptInputFn: getPromptInputFn,
} }
} }
@ -101,6 +109,10 @@ func (self *RealPopupHandler) Menu(opts CreateMenuOptions) error {
return self.createMenuFn(opts) return self.createMenuFn(opts)
} }
func (self *RealPopupHandler) Toast(message string) {
self.toastFn(message)
}
func (self *RealPopupHandler) WithWaitingStatus(message string, f func() error) error { func (self *RealPopupHandler) WithWaitingStatus(message string, f func() error) error {
return self.withWaitingStatusFn(message, f) return self.withWaitingStatusFn(message, f)
} }
@ -188,6 +200,12 @@ func (self *RealPopupHandler) WithLoaderPanel(message string, f func() error) er
return nil 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 { type TestPopupHandler struct {
OnErrorMsg func(message string) error OnErrorMsg func(message string) error
OnAsk func(opts AskOpts) 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 { func (self *TestPopupHandler) Menu(opts CreateMenuOptions) error {
panic("not yet implemented") 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. // command.
func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error { func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error {
width, _ := gui.Views.Main.Size() width, _ := gui.Views.Main.Size()
pager := gui.Git.Config.GetPager(width) pager := gui.git.Config.GetPager(width)
if pager == "" { if pager == "" {
// if we're not using a custom pager we don't need to use a pty // 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) { start := func() (*exec.Cmd, io.Reader) {
ptmx, err := pty.StartWithSize(cmd, gui.desiredPtySize()) ptmx, err := pty.StartWithSize(cmd, gui.desiredPtySize())
if err != nil { if err != nil {
gui.Log.Error(err) gui.c.Log.Error(err)
} }
gui.State.Ptmx = ptmx 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 { menuItemsForBranch := func(branch *models.Branch) []*popup.MenuItem {
return []*popup.MenuItem{ return []*popup.MenuItem{
{ {
DisplayStrings: fromToDisplayStrings(branch.Name, gui.Tr.LcDefaultBranch), DisplayStrings: fromToDisplayStrings(branch.Name, gui.c.Tr.LcDefaultBranch),
OnPress: func() error { OnPress: func() error {
return gui.createPullRequest(branch.Name, "") return gui.createPullRequest(branch.Name, "")
}, },
}, },
{ {
DisplayStrings: fromToDisplayStrings(branch.Name, gui.Tr.LcSelectBranch), DisplayStrings: fromToDisplayStrings(branch.Name, gui.c.Tr.LcSelectBranch),
OnPress: func() error { OnPress: func() error {
return gui.PopupHandler.Prompt(popup.PromptOpts{ return gui.c.Prompt(popup.PromptOpts{
Title: branch.Name + " →", Title: branch.Name + " →",
FindSuggestionsFunc: gui.getBranchNameSuggestionsFunc(), FindSuggestionsFunc: gui.suggestionsHelper.GetBranchNameSuggestionsFunc(),
HandleConfirm: func(targetBranchName string) error { HandleConfirm: func(targetBranchName string) error {
return gui.createPullRequest(branch.Name, targetBranchName) return gui.createPullRequest(branch.Name, targetBranchName)
}}, }},
@ -52,27 +52,27 @@ func (gui *Gui) createPullRequestMenu(selectedBranch *models.Branch, checkedOutB
menuItems = append(menuItems, menuItemsForBranch(selectedBranch)...) 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 { func (gui *Gui) createPullRequest(from string, to string) error {
hostingServiceMgr := gui.getHostingServiceMgr() hostingServiceMgr := gui.getHostingServiceMgr()
url, err := hostingServiceMgr.GetPullRequestURL(from, to) url, err := hostingServiceMgr.GetPullRequestURL(from, to)
if err != nil { 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 { if err := gui.OSCommand.OpenLink(url); err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
return nil return nil
} }
func (gui *Gui) getHostingServiceMgr() *hosting_service.HostingServiceMgr { func (gui *Gui) getHostingServiceMgr() *hosting_service.HostingServiceMgr {
remoteUrl := gui.Git.Config.GetRemoteURL() remoteUrl := gui.git.Config.GetRemoteURL()
configServices := gui.UserConfig.Services configServices := gui.c.UserConfig.Services
return hosting_service.NewHostingServiceMgr(gui.Log, gui.Tr, remoteUrl, configServices) 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() parentContext, hasParent := currentContext.GetParentContext()
if hasParent && currentContext != nil && parentContext != nil { if hasParent && currentContext != nil && parentContext != nil {
// TODO: think about whether this should be marked as a return rather than adding to the stack // 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() { for _, mode := range gui.modeStatuses() {
@ -60,7 +60,7 @@ func (gui *Gui) handleTopLevelReturn() error {
return gui.dispatchSwitchToRepo(path, true) return gui.dispatchSwitchToRepo(path, true)
} }
if gui.UserConfig.QuitOnTopLevelReturn { if gui.c.UserConfig.QuitOnTopLevelReturn {
return gui.handleQuit() return gui.handleQuit()
} }
@ -72,10 +72,10 @@ func (gui *Gui) quit() error {
return gui.createUpdateQuitConfirmation() return gui.createUpdateQuitConfirmation()
} }
if gui.UserConfig.ConfirmOnQuit { if gui.c.UserConfig.ConfirmOnQuit {
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: "", Title: "",
Prompt: gui.Tr.ConfirmQuit, Prompt: gui.c.Tr.ConfirmQuit,
HandleConfirm: func() error { HandleConfirm: func() error {
return gocui.ErrQuit return gocui.ErrQuit
}, },

View File

@ -20,7 +20,7 @@ const (
func (gui *Gui) handleCreateRebaseOptionsMenu() error { func (gui *Gui) handleCreateRebaseOptionsMenu() error {
options := []string{REBASE_OPTION_CONTINUE, REBASE_OPTION_ABORT} 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) options = append(options, REBASE_OPTION_SKIP)
} }
@ -37,23 +37,23 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error {
} }
var title string var title string
if gui.Git.Status.WorkingTreeState() == enums.REBASE_MODE_MERGING { if gui.git.Status.WorkingTreeState() == enums.REBASE_MODE_MERGING {
title = gui.Tr.MergeOptionsTitle title = gui.c.Tr.MergeOptionsTitle
} else { } 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 { 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 { 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 := "" commandType := ""
switch status { switch status {
@ -68,14 +68,14 @@ func (gui *Gui) genericMergeCommand(command string) error {
// we should end up with a command like 'git merge --continue' // 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 // 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 // TODO: see if we should be calling more of the code from gui.Git.Rebase.GenericMergeOrRebaseAction
return gui.runSubprocessWithSuspenseAndRefresh( return gui.runSubprocessWithSuspenseAndRefresh(
gui.Git.Rebase.GenericMergeOrRebaseActionCmdObj(commandType, command), gui.git.Rebase.GenericMergeOrRebaseActionCmdObj(commandType, command),
) )
} }
result := gui.Git.Rebase.GenericMergeOrRebaseAction(commandType, command) result := gui.git.Rebase.GenericMergeOrRebaseAction(commandType, command)
if err := gui.handleGenericMergeCommandResult(result); err != nil { if err := gui.checkMergeOrRebase(result); err != nil {
return err return err
} }
return nil return nil
@ -98,8 +98,8 @@ func isMergeConflictErr(errStr string) bool {
return false return false
} }
func (gui *Gui) handleGenericMergeCommandResult(result error) error { func (gui *Gui) checkMergeOrRebase(result error) error {
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 return err
} }
if result == nil { if result == nil {
@ -112,12 +112,12 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
// assume in this case that we're already done // assume in this case that we're already done
return nil return nil
} else if isMergeConflictErr(result.Error()) { } else if isMergeConflictErr(result.Error()) {
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.FoundConflictsTitle, Title: gui.c.Tr.FoundConflictsTitle,
Prompt: gui.Tr.FoundConflicts, Prompt: gui.c.Tr.FoundConflicts,
HandlersManageFocus: true, HandlersManageFocus: true,
HandleConfirm: func() error { HandleConfirm: func() error {
return gui.pushContext(gui.State.Contexts.Files) return gui.c.PushContext(gui.State.Contexts.Files)
}, },
HandleClose: func() error { HandleClose: func() error {
if err := gui.returnFromContext(); err != nil { if err := gui.returnFromContext(); err != nil {
@ -128,16 +128,16 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
}, },
}) })
} else { } else {
return gui.PopupHandler.ErrorMsg(result.Error()) return gui.c.ErrorMsg(result.Error())
} }
} }
func (gui *Gui) abortMergeOrRebaseWithConfirm() error { func (gui *Gui) abortMergeOrRebaseWithConfirm() error {
// prompt user to confirm that they want to abort, then do it // prompt user to confirm that they want to abort, then do it
mode := gui.workingTreeStateNoun() mode := gui.workingTreeStateNoun()
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: fmt.Sprintf(gui.Tr.AbortTitle, mode), Title: fmt.Sprintf(gui.c.Tr.AbortTitle, mode),
Prompt: fmt.Sprintf(gui.Tr.AbortPrompt, mode), Prompt: fmt.Sprintf(gui.c.Tr.AbortPrompt, mode),
HandleConfirm: func() error { HandleConfirm: func() error {
return gui.genericMergeCommand(REBASE_OPTION_ABORT) return gui.genericMergeCommand(REBASE_OPTION_ABORT)
}, },
@ -145,7 +145,7 @@ func (gui *Gui) abortMergeOrRebaseWithConfirm() error {
} }
func (gui *Gui) workingTreeStateNoun() string { func (gui *Gui) workingTreeStateNoun() string {
workingTreeState := gui.Git.Status.WorkingTreeState() workingTreeState := gui.git.Status.WorkingTreeState()
switch workingTreeState { switch workingTreeState {
case enums.REBASE_MODE_NONE: case enums.REBASE_MODE_NONE:
return "" return ""

View File

@ -13,7 +13,7 @@ import (
) )
func (gui *Gui) handleCreateRecentReposMenu() error { func (gui *Gui) handleCreateRecentReposMenu() error {
recentRepoPaths := gui.Config.GetAppState().RecentRepos recentRepoPaths := gui.c.GetAppState().RecentRepos
reposCount := utils.Min(len(recentRepoPaths), 20) reposCount := utils.Min(len(recentRepoPaths), 20)
// we won't show the current repo hence the -1 // 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 { func (gui *Gui) handleShowAllBranchLogs() error {
cmdObj := gui.Git.Branch.AllBranchesLogCmdObj() cmdObj := gui.git.Branch.AllBranchesLogCmdObj()
task := NewRunPtyTask(cmdObj.GetCmd()) task := NewRunPtyTask(cmdObj.GetCmd())
return gui.refreshMainViews(refreshMainOpts{ 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 err := os.Chdir(path); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return gui.PopupHandler.ErrorMsg(gui.Tr.ErrRepositoryMovedOrDeleted) return gui.c.ErrorMsg(gui.c.Tr.ErrRepositoryMovedOrDeleted)
} }
return err return err
} }
@ -71,11 +71,16 @@ func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error {
return err 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 { if err != nil {
return err 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 // 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 // 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, // updateRecentRepoList registers the fact that we opened lazygit in this repo,
// so that we can open the same repo via the 'recent repos' menu // so that we can open the same repo via the 'recent repos' menu
func (gui *Gui) updateRecentRepoList() error { 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 // 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 // worktree in our recent repos list, which is a change that would need to be
// backwards compatible // 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 return nil
} }
recentRepos := gui.Config.GetAppState().RecentRepos recentRepos := gui.c.GetAppState().RecentRepos
currentRepo, err := os.Getwd() currentRepo, err := os.Getwd()
if err != nil { if err != nil {
return err return err
} }
known, recentRepos := newRecentReposList(recentRepos, currentRepo) known, recentRepos := newRecentReposList(recentRepos, currentRepo)
gui.IsNewRepo = known gui.IsNewRepo = known
gui.Config.GetAppState().RecentRepos = recentRepos gui.c.GetAppState().RecentRepos = recentRepos
return gui.Config.SaveAppState() return gui.c.SaveAppState()
} }
// newRecentReposList returns a new repo list with a new entry but only when it doesn't exist yet // 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 ( import (
"github.com/jesseduffield/lazygit/pkg/commands/models" "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/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types"
) )
// list panel functions // list panel functions
@ -23,7 +25,7 @@ func (gui *Gui) reflogCommitsRenderToMain() error {
if commit == nil { if commit == nil {
task = NewRenderStringTask("No reflog history") task = NewRenderStringTask("No reflog history")
} else { } 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()) task = NewRunPtyTask(cmdObj.GetCmd())
} }
@ -53,10 +55,10 @@ func (gui *Gui) refreshReflogCommits() error {
} }
refresh := func(stateCommits *[]*models.Commit, filterPath string) 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) GetReflogCommits(lastReflogCommit, filterPath)
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
if onlyObtainedNewReflogCommits { if onlyObtainedNewReflogCommits {
@ -79,21 +81,21 @@ func (gui *Gui) refreshReflogCommits() error {
state.FilteredReflogCommits = state.ReflogCommits 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() commit := gui.getSelectedReflogCommit()
if commit == nil { if commit == nil {
return nil return nil
} }
err := gui.PopupHandler.Ask(popup.AskOpts{ err := gui.c.Ask(popup.AskOpts{
Title: gui.Tr.LcCheckoutCommit, Title: gui.c.Tr.LcCheckoutCommit,
Prompt: gui.Tr.SureCheckoutThisCommit, Prompt: gui.c.Tr.SureCheckoutThisCommit,
HandleConfirm: func() error { HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.CheckoutReflogCommit) gui.c.LogAction(gui.c.Tr.Actions.CheckoutReflogCommit)
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{}) return gui.refHelper.CheckoutRef(commit.Sha, types.CheckoutRefOptions{})
}, },
}) })
if err != nil { if err != nil {
@ -108,7 +110,7 @@ func (gui *Gui) handleCheckoutReflogCommit() error {
func (gui *Gui) handleCreateReflogResetMenu() error { func (gui *Gui) handleCreateReflogResetMenu() error {
commit := gui.getSelectedReflogCommit() commit := gui.getSelectedReflogCommit()
return gui.createResetMenu(commit.Sha) return gui.refHelper.CreateGitResetMenu(commit.Sha)
} }
func (gui *Gui) handleViewReflogCommitFiles() error { func (gui *Gui) handleViewReflogCommitFiles() error {
@ -117,5 +119,10 @@ func (gui *Gui) handleViewReflogCommitFiles() error {
return nil 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 { if remoteBranch == nil {
task = NewRenderStringTask("No branches for this remote") task = NewRenderStringTask("No branches for this remote")
} else { } else {
cmdObj := gui.Git.Branch.GetGraphCmdObj(remoteBranch.FullName()) cmdObj := gui.git.Branch.GetGraphCmdObj(remoteBranch.FullName())
task = NewRunCommandTask(cmdObj.GetCmd()) task = NewRunCommandTask(cmdObj.GetCmd())
} }
@ -39,7 +39,7 @@ func (gui *Gui) remoteBranchesRenderToMain() error {
} }
func (gui *Gui) handleRemoteBranchesEscape() 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 { func (gui *Gui) handleMergeRemoteBranch() error {
@ -52,20 +52,20 @@ func (gui *Gui) handleDeleteRemoteBranch() error {
if remoteBranch == nil { if remoteBranch == nil {
return 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{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.DeleteRemoteBranch, Title: gui.c.Tr.DeleteRemoteBranch,
Prompt: message, Prompt: message,
HandleConfirm: func() error { HandleConfirm: func() error {
return gui.PopupHandler.WithWaitingStatus(gui.Tr.DeletingStatus, func() error { return gui.c.WithWaitingStatus(gui.c.Tr.DeletingStatus, func() error {
gui.logAction(gui.Tr.Actions.DeleteRemoteBranch) gui.c.LogAction(gui.c.Tr.Actions.DeleteRemoteBranch)
err := gui.Git.Remote.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name) err := gui.git.Remote.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name)
if err != nil { 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() checkedOutBranch := gui.getCheckedOutBranch()
message := utils.ResolvePlaceholderString( message := utils.ResolvePlaceholderString(
gui.Tr.SetUpstreamMessage, gui.c.Tr.SetUpstreamMessage,
map[string]string{ map[string]string{
"checkedOut": checkedOutBranch.Name, "checkedOut": checkedOutBranch.Name,
"selected": selectedBranch.FullName(), "selected": selectedBranch.FullName(),
}, },
) )
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.SetUpstreamTitle, Title: gui.c.Tr.SetUpstreamTitle,
Prompt: message, Prompt: message,
HandleConfirm: func() error { HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.SetBranchUpstream) gui.c.LogAction(gui.c.Tr.Actions.SetBranchUpstream)
if err := gui.Git.Branch.SetUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil { if err := gui.git.Branch.SetUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil {
return gui.PopupHandler.Error(err) 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 nil
} }
return gui.createResetMenu(selectedBranch.FullName()) return gui.refHelper.CreateGitResetMenu(selectedBranch.FullName())
} }

View File

@ -5,10 +5,8 @@ import (
"strings" "strings"
"github.com/jesseduffield/lazygit/pkg/commands/models" "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/style"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
// list panel functions // list panel functions
@ -42,9 +40,9 @@ func (gui *Gui) remotesRenderToMain() error {
func (gui *Gui) refreshRemotes() error { func (gui *Gui) refreshRemotes() error {
prevSelectedRemote := gui.getSelectedRemote() prevSelectedRemote := gui.getSelectedRemote()
remotes, err := gui.Git.Loaders.Remotes.GetRemotes() remotes, err := gui.git.Loaders.Remotes.GetRemotes()
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
gui.State.Remotes = remotes gui.State.Remotes = remotes
@ -59,133 +57,5 @@ func (gui *Gui) refreshRemotes() error {
} }
} }
return gui.postRefreshUpdate(gui.mustContextForContextKey(ContextKey(gui.Views.Branches.Context))) return gui.c.PostRefreshUpdate(gui.mustContextForContextKey(types.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}})
})
} }

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() 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 return err
} }
@ -43,7 +43,7 @@ func (gui *Gui) handleSearch() error {
} }
func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) 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 { return func(y int, index int, total int) error {
if total == 0 { if total == 0 {

View File

@ -28,16 +28,16 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
} }
if secondaryFocused { if secondaryFocused {
gui.Views.Main.Title = gui.Tr.StagedChanges gui.Views.Main.Title = gui.c.Tr.StagedChanges
gui.Views.Secondary.Title = gui.Tr.UnstagedChanges gui.Views.Secondary.Title = gui.c.Tr.UnstagedChanges
} else { } else {
gui.Views.Main.Title = gui.Tr.UnstagedChanges gui.Views.Main.Title = gui.c.Tr.UnstagedChanges
gui.Views.Secondary.Title = gui.Tr.StagedChanges 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 // 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) diff := gui.git.WorkingTree.WorktreeFileDiff(file, true, secondaryFocused, false)
secondaryDiff := 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 // 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 // 4-5 lines in which case we'll swap panels
@ -97,7 +97,7 @@ func (gui *Gui) handleTogglePanel() error {
func (gui *Gui) handleStagingEscape() error { func (gui *Gui) handleStagingEscape() error {
gui.escapeLineByLinePanel() gui.escapeLineByLinePanel()
return gui.pushContext(gui.State.Contexts.Files) return gui.c.PushContext(gui.State.Contexts.Files)
} }
func (gui *Gui) handleToggleStagedSelection() error { func (gui *Gui) handleToggleStagedSelection() error {
@ -113,10 +113,10 @@ func (gui *Gui) handleResetSelection() error {
return gui.applySelection(true, state) return gui.applySelection(true, state)
} }
if !gui.UserConfig.Gui.SkipUnstageLineWarning { if !gui.c.UserConfig.Gui.SkipUnstageLineWarning {
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.UnstageLinesTitle, Title: gui.c.Tr.UnstageLinesTitle,
Prompt: gui.Tr.UnstageLinesPrompt, Prompt: gui.c.Tr.UnstageLinesPrompt,
HandleConfirm: func() error { HandleConfirm: func() error {
return gui.withLBLActiveCheck(func(state *LblPanelState) error { return gui.withLBLActiveCheck(func(state *LblPanelState) error {
return gui.applySelection(true, state) return gui.applySelection(true, state)
@ -148,17 +148,17 @@ func (gui *Gui) applySelection(reverse bool, state *LblPanelState) error {
if !reverse || state.SecondaryFocused { if !reverse || state.SecondaryFocused {
applyFlags = append(applyFlags, "cached") applyFlags = append(applyFlags, "cached")
} }
gui.logAction(gui.Tr.Actions.ApplyPatch) gui.c.LogAction(gui.c.Tr.Actions.ApplyPatch)
err := gui.Git.WorkingTree.ApplyPatch(patch, applyFlags...) err := gui.git.WorkingTree.ApplyPatch(patch, applyFlags...)
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
if state.SelectingRange() { if state.SelectingRange() {
state.SetLineSelectMode() 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 return err
} }
if err := gui.refreshStagingPanel(false, -1); err != nil { if err := gui.refreshStagingPanel(false, -1); err != nil {

View File

@ -2,6 +2,7 @@ package gui
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands/models" "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/popup"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -21,9 +22,9 @@ func (gui *Gui) stashRenderToMain() error {
var task updateTask var task updateTask
stashEntry := gui.getSelectedStashEntry() stashEntry := gui.getSelectedStashEntry()
if stashEntry == nil { if stashEntry == nil {
task = NewRenderStringTask(gui.Tr.NoStashEntries) task = NewRenderStringTask(gui.c.Tr.NoStashEntries)
} else { } else {
task = NewRunPtyTask(gui.Git.Stash.ShowStashEntryCmdObj(stashEntry.Index).GetCmd()) task = NewRunPtyTask(gui.git.Stash.ShowStashEntryCmdObj(stashEntry.Index).GetCmd())
} }
return gui.refreshMainViews(refreshMainOpts{ return gui.refreshMainViews(refreshMainOpts{
@ -35,7 +36,7 @@ func (gui *Gui) stashRenderToMain() error {
} }
func (gui *Gui) refreshStashEntries() 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()) GetStashEntries(gui.State.Modes.Filtering.GetPath())
return gui.postRefreshUpdate(gui.State.Contexts.Stash) return gui.postRefreshUpdate(gui.State.Contexts.Stash)
@ -49,14 +50,14 @@ func (gui *Gui) handleStashApply() error {
return nil return nil
} }
skipStashWarning := gui.UserConfig.Gui.SkipStashWarning skipStashWarning := gui.c.UserConfig.Gui.SkipStashWarning
apply := func() error { apply := func() error {
gui.logAction(gui.Tr.Actions.Stash) gui.c.LogAction(gui.c.Tr.Actions.Stash)
err := gui.Git.Stash.Apply(stashEntry.Index) err := gui.git.Stash.Apply(stashEntry.Index)
_ = gui.postStashRefresh() _ = gui.postStashRefresh()
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
return nil return nil
} }
@ -65,9 +66,9 @@ func (gui *Gui) handleStashApply() error {
return apply() return apply()
} }
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.StashApply, Title: gui.c.Tr.StashApply,
Prompt: gui.Tr.SureApplyStashEntry, Prompt: gui.c.Tr.SureApplyStashEntry,
HandleConfirm: func() error { HandleConfirm: func() error {
return apply() return apply()
}, },
@ -80,14 +81,14 @@ func (gui *Gui) handleStashPop() error {
return nil return nil
} }
skipStashWarning := gui.UserConfig.Gui.SkipStashWarning skipStashWarning := gui.c.UserConfig.Gui.SkipStashWarning
pop := func() error { pop := func() error {
gui.logAction(gui.Tr.Actions.Stash) gui.c.LogAction(gui.c.Tr.Actions.Stash)
err := gui.Git.Stash.Pop(stashEntry.Index) err := gui.git.Stash.Pop(stashEntry.Index)
_ = gui.postStashRefresh() _ = gui.postStashRefresh()
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
return nil return nil
} }
@ -96,9 +97,9 @@ func (gui *Gui) handleStashPop() error {
return pop() return pop()
} }
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.StashPop, Title: gui.c.Tr.StashPop,
Prompt: gui.Tr.SurePopStashEntry, Prompt: gui.c.Tr.SurePopStashEntry,
HandleConfirm: func() error { HandleConfirm: func() error {
return pop() return pop()
}, },
@ -111,15 +112,15 @@ func (gui *Gui) handleStashDrop() error {
return nil return nil
} }
return gui.PopupHandler.Ask(popup.AskOpts{ return gui.c.Ask(popup.AskOpts{
Title: gui.Tr.StashDrop, Title: gui.c.Tr.StashDrop,
Prompt: gui.Tr.SureDropStashEntry, Prompt: gui.c.Tr.SureDropStashEntry,
HandleConfirm: func() error { HandleConfirm: func() error {
gui.logAction(gui.Tr.Actions.Stash) gui.c.LogAction(gui.c.Tr.Actions.Stash)
err := gui.Git.Stash.Drop(stashEntry.Index) err := gui.git.Stash.Drop(stashEntry.Index)
_ = gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{STASH}}) _ = gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH}})
if err != nil { if err != nil {
return gui.PopupHandler.Error(err) return gui.c.Error(err)
} }
return nil return nil
}, },
@ -127,25 +128,7 @@ func (gui *Gui) handleStashDrop() error {
} }
func (gui *Gui) postStashRefresh() error { func (gui *Gui) postStashRefresh() error {
return gui.refreshSidePanels(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH, types.FILES}}) return gui.c.Refresh(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
},
})
} }
func (gui *Gui) handleViewStashFiles() error { func (gui *Gui) handleViewStashFiles() error {
@ -154,5 +137,10 @@ func (gui *Gui) handleViewStashFiles() error {
return nil 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() gui.Mutexes.RefreshingStatusMutex.Lock()
defer gui.Mutexes.RefreshingStatusMutex.Unlock() defer gui.Mutexes.RefreshingStatusMutex.Unlock()
currentBranch := gui.currentBranch() currentBranch := gui.getCheckedOutBranch()
if currentBranch == nil { if currentBranch == nil {
// need to wait for branches to refresh // need to wait for branches to refresh
return return
@ -29,7 +29,7 @@ func (gui *Gui) refreshStatus() {
status += presentation.ColoredBranchStatus(currentBranch) + " " status += presentation.ColoredBranchStatus(currentBranch) + " "
} }
workingTreeState := gui.Git.Status.WorkingTreeState() workingTreeState := gui.git.Status.WorkingTreeState()
if workingTreeState != enums.REBASE_MODE_NONE { if workingTreeState != enums.REBASE_MODE_NONE {
status += style.FgYellow.Sprintf("(%s) ", formatWorkingTreeState(workingTreeState)) 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 { 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) gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
return nil return nil
}) })
@ -62,20 +62,20 @@ func (gui *Gui) handleStatusClick() error {
return nil return nil
} }
currentBranch := gui.currentBranch() currentBranch := gui.getCheckedOutBranch()
if currentBranch == nil { if currentBranch == nil {
// need to wait for branches to refresh // need to wait for branches to refresh
return nil 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 return err
} }
cx, _ := gui.Views.Status.Cursor() cx, _ := gui.Views.Status.Cursor()
upstreamStatus := presentation.BranchStatus(currentBranch) upstreamStatus := presentation.BranchStatus(currentBranch)
repoName := utils.GetCurrentRepoName() repoName := utils.GetCurrentRepoName()
workingTreeState := gui.Git.Status.WorkingTreeState() workingTreeState := gui.git.Status.WorkingTreeState()
switch workingTreeState { switch workingTreeState {
case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING: case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING:
workingTreeStatus := fmt.Sprintf("(%s)", formatWorkingTreeState(workingTreeState)) workingTreeStatus := fmt.Sprintf("(%s)", formatWorkingTreeState(workingTreeState))
@ -135,7 +135,7 @@ func (gui *Gui) askForConfigFile(action func(file string) error) error {
confPaths := gui.Config.GetUserConfigPaths() confPaths := gui.Config.GetUserConfigPaths()
switch len(confPaths) { switch len(confPaths) {
case 0: case 0:
return errors.New(gui.Tr.NoConfigFileFoundErr) return errors.New(gui.c.Tr.NoConfigFileFoundErr)
case 1: case 1:
return action(confPaths[0]) return action(confPaths[0])
default: default:
@ -149,8 +149,8 @@ func (gui *Gui) askForConfigFile(action func(file string) error) error {
}, },
} }
} }
return gui.PopupHandler.Menu(popup.CreateMenuOptions{ return gui.c.Menu(popup.CreateMenuOptions{
Title: gui.Tr.SelectConfigFile, Title: gui.c.Tr.SelectConfigFile,
Items: menuItems, Items: menuItems,
HideCancel: true, HideCancel: true,
}) })
@ -158,11 +158,11 @@ func (gui *Gui) askForConfigFile(action func(file string) error) error {
} }
func (gui *Gui) handleOpenConfig() error { func (gui *Gui) handleOpenConfig() error {
return gui.askForConfigFile(gui.openFile) return gui.askForConfigFile(gui.fileHelper.OpenFile)
} }
func (gui *Gui) handleEditConfig() error { func (gui *Gui) handleEditConfig() error {
return gui.askForConfigFile(gui.editFile) return gui.askForConfigFile(gui.fileHelper.EditFile)
} }
func lazygitTitle() string { 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", "\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{New().SetFg(rgbYellow), BgRed},
TextStyle{ TextStyle{
fg: &rgbYellow, fg: &rgbYellow,
@ -147,6 +147,19 @@ func TestMerge(t *testing.T) {
}, },
"\x1b[38;2;255;255;0;48;2;197;30;20mfoo\x1b[0m", "\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 { for _, s := range scenarios {

View File

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

View File

@ -30,11 +30,11 @@ func (gui *Gui) submodulesRenderToMain() error {
style.FgCyan.Sprint(submodule.Url), style.FgCyan.Sprint(submodule.Url),
) )
file := gui.fileForSubmodule(submodule) file := gui.workingTreeHelper.FileForSubmodule(submodule)
if file == nil { if file == nil {
task = NewRenderStringTask(prefix) task = NewRenderStringTask(prefix)
} else { } 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) task = NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix)
} }
} }
@ -48,7 +48,7 @@ func (gui *Gui) submodulesRenderToMain() error {
} }
func (gui *Gui) refreshStateSubmoduleConfigs() error { func (gui *Gui) refreshStateSubmoduleConfigs() error {
configs, err := gui.Git.Submodule.GetConfigs() configs, err := gui.git.Submodule.GetConfigs()
if err != nil { if err != nil {
return err return err
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
"github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "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 // finding suggestions in this file, so that it's easy to see if a function already
// exists for fetching a particular model. // exists for fetching a particular model.
func (gui *Gui) getRemoteNames() []string { type SuggestionsHelper struct {
result := make([]string, len(gui.State.Remotes)) c *controllers.ControllerCommon
for i, remote := range gui.State.Remotes {
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 result[i] = remote.Name
} }
return result return result
@ -40,22 +62,22 @@ func matchesToSuggestions(matches []string) []*types.Suggestion {
return suggestions return suggestions
} }
func (gui *Gui) getRemoteSuggestionsFunc() func(string) []*types.Suggestion { func (self *SuggestionsHelper) GetRemoteSuggestionsFunc() func(string) []*types.Suggestion {
remoteNames := gui.getRemoteNames() remoteNames := self.getRemoteNames()
return fuzzySearchFunc(remoteNames) return fuzzySearchFunc(remoteNames)
} }
func (gui *Gui) getBranchNames() []string { func (self *SuggestionsHelper) getBranchNames() []string {
result := make([]string, len(gui.State.Branches)) result := make([]string, len(self.State.Branches))
for i, branch := range gui.State.Branches { for i, branch := range self.State.Branches {
result[i] = branch.Name result[i] = branch.Name
} }
return result return result
} }
func (gui *Gui) getBranchNameSuggestionsFunc() func(string) []*types.Suggestion { func (self *SuggestionsHelper) GetBranchNameSuggestionsFunc() func(string) []*types.Suggestion {
branchNames := gui.getBranchNames() branchNames := self.getBranchNames()
return func(input string) []*types.Suggestion { return func(input string) []*types.Suggestion {
var matchingBranchNames []string 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 // 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 // self.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. 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. // 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 // 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 // if nothing has been typed because there'll be too much to display efficiently
func (gui *Gui) getFilePathSuggestionsFunc() func(string) []*types.Suggestion { func (self *SuggestionsHelper) GetFilePathSuggestionsFunc() func(string) []*types.Suggestion {
_ = gui.PopupHandler.WithWaitingStatus(gui.Tr.LcLoadingFileSuggestions, func() error { _ = self.c.WithWaitingStatus(self.c.Tr.LcLoadingFileSuggestions, func() error {
trie := patricia.NewTrie() trie := patricia.NewTrie()
// load every non-gitignored file in the repo // load every non-gitignored file in the repo
ignore, err := gitignore.FromGit() ignore, err := gitignore.FromGit()
@ -101,22 +123,16 @@ func (gui *Gui) getFilePathSuggestionsFunc() func(string) []*types.Suggestion {
return nil return nil
}) })
// cache the trie for future use // cache the trie for future use
gui.State.FilesTrie = trie self.State.FilesTrie = trie
// refresh the selections view self.refreshSuggestionsFn()
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) }
})
return err return err
}) })
return func(input string) []*types.Suggestion { return func(input string) []*types.Suggestion {
matchingNames := []string{} 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)) matchingNames = append(matchingNames, item.(string))
return nil 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{} result := []string{}
for _, remote := range gui.State.Remotes { for _, remote := range self.State.Remotes {
for _, branch := range remote.Branches { for _, branch := range remote.Branches {
result = append(result, fmt.Sprintf("%s%s%s", remote.Name, separator, branch.Name)) 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 return result
} }
func (gui *Gui) getRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion { func (self *SuggestionsHelper) GetRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion {
return fuzzySearchFunc(gui.getRemoteBranchNames(separator)) return fuzzySearchFunc(self.getRemoteBranchNames(separator))
} }
func (gui *Gui) getTagNames() []string { func (self *SuggestionsHelper) getTagNames() []string {
result := make([]string, len(gui.State.Tags)) result := make([]string, len(self.State.Tags))
for i, tag := range gui.State.Tags { for i, tag := range self.State.Tags {
result[i] = tag.Name result[i] = tag.Name
} }
return result return result
} }
func (gui *Gui) getRefsSuggestionsFunc() func(string) []*types.Suggestion { func (self *SuggestionsHelper) GetRefsSuggestionsFunc() func(string) []*types.Suggestion {
remoteBranchNames := gui.getRemoteBranchNames("/") remoteBranchNames := self.getRemoteBranchNames("/")
localBranchNames := gui.getBranchNames() localBranchNames := self.getBranchNames()
tagNames := gui.getTagNames() tagNames := self.getTagNames()
additionalRefNames := []string{"HEAD", "FETCH_HEAD", "MERGE_HEAD", "ORIG_HEAD"} additionalRefNames := []string{"HEAD", "FETCH_HEAD", "MERGE_HEAD", "ORIG_HEAD"}
refNames := append(append(append(remoteBranchNames, localBranchNames...), tagNames...), additionalRefNames...) refNames := append(append(append(remoteBranchNames, localBranchNames...), tagNames...), additionalRefNames...)
@ -169,9 +185,9 @@ func (gui *Gui) getRefsSuggestionsFunc() func(string) []*types.Suggestion {
return fuzzySearchFunc(refNames) 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 // 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) return fuzzySearchFunc(history)
} }

View File

@ -2,36 +2,28 @@ package gui
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands/models" "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 { func (self *Gui) getSelectedTag() *models.Tag {
selectedLine := gui.State.Panels.Tags.SelectedLineIdx selectedLine := self.State.Panels.Tags.SelectedLineIdx
if selectedLine == -1 || len(gui.State.Tags) == 0 { if selectedLine == -1 || len(self.State.Tags) == 0 {
return nil return nil
} }
return gui.State.Tags[selectedLine] return self.State.Tags[selectedLine]
} }
func (gui *Gui) handleCreateTag() error { func (self *Gui) tagsRenderToMain() 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 {
var task updateTask var task updateTask
tag := gui.getSelectedTag() tag := self.getSelectedTag()
if tag == nil { if tag == nil {
task = NewRenderStringTask("No tags") task = NewRenderStringTask("No tags")
} else { } else {
cmdObj := gui.Git.Branch.GetGraphCmdObj(tag.Name) cmdObj := self.git.Branch.GetGraphCmdObj(tag.Name)
task = NewRunCommandTask(cmdObj.GetCmd()) task = NewRunCommandTask(cmdObj.GetCmd())
} }
return gui.refreshMainViews(refreshMainOpts{ return self.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{ main: &viewUpdateOpts{
title: "Tag", title: "Tag",
task: task, 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. // 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 { func (self *Gui) refreshTags() error {
tags, err := gui.Git.Loaders.Tags.GetTags() tags, err := self.git.Loaders.Tags.GetTags()
if err != nil { 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) return self.postRefreshUpdate(self.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)
} }

View File

@ -11,7 +11,7 @@ import (
func (gui *Gui) newCmdTask(view *gocui.View, cmd *exec.Cmd, prefix string) error { func (gui *Gui) newCmdTask(view *gocui.View, cmd *exec.Cmd, prefix string) error {
cmdStr := strings.Join(cmd.Args, " ") cmdStr := strings.Join(cmd.Args, " ")
gui.Log.WithField( gui.c.Log.WithField(
"command", "command",
cmdStr, cmdStr,
).Debug("RunCommand") ).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) { start := func() (*exec.Cmd, io.Reader) {
r, err := cmd.StdoutPipe() r, err := cmd.StdoutPipe()
if err != nil { if err != nil {
gui.Log.Warn(err) gui.c.Log.Warn(err)
} }
cmd.Stderr = cmd.Stdout cmd.Stderr = cmd.Stdout
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
gui.Log.Warn(err) gui.c.Log.Warn(err)
} }
return cmd, r return cmd, r
} }
if err := manager.NewTask(manager.NewCmdTask(start, prefix, height+oy+10, nil), cmdStr); err != nil { 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 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 Tag string // e.g. 'navigation'. Used for grouping things in the cheatsheet
OpensMenu bool 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 ( const (
COMMITS RefreshableView = iota COMMITS RefreshableView = iota
REBASE_COMMITS
BRANCHES BRANCHES
FILES FILES
STASH STASH

View File

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

View File

@ -59,14 +59,14 @@ func arrToMap(arr []types.RefreshableView) map[types.RefreshableView]bool {
return output return output
} }
func (gui *Gui) refreshSidePanels(options types.RefreshOptions) error { func (gui *Gui) Refresh(options types.RefreshOptions) error {
if options.Scope == nil { if options.Scope == nil {
gui.Log.Infof( gui.c.Log.Infof(
"refreshing all scopes in %s mode", "refreshing all scopes in %s mode",
getModeName(options.Mode), getModeName(options.Mode),
) )
} else { } else {
gui.Log.Infof( gui.c.Log.Infof(
"refreshing the following scopes in %s mode: %s", "refreshing the following scopes in %s mode: %s",
getModeName(options.Mode), getModeName(options.Mode),
strings.Join(getScopeNames(options.Scope), ","), strings.Join(getScopeNames(options.Scope), ","),
@ -78,69 +78,55 @@ func (gui *Gui) refreshSidePanels(options types.RefreshOptions) error {
f := func() { f := func() {
var scopeMap map[types.RefreshableView]bool var scopeMap map[types.RefreshableView]bool
if len(options.Scope) == 0 { 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 { } else {
scopeMap = arrToMap(options.Scope) 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) wg.Add(1)
func() { func() {
if options.Mode == types.ASYNC { if options.Mode == types.ASYNC {
go utils.Safe(func() { gui.refreshCommits() }) go utils.Safe(f)
} else { } else {
gui.refreshCommits() f()
} }
wg.Done() 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] { if scopeMap[types.FILES] || scopeMap[types.SUBMODULES] {
wg.Add(1) refresh(func() { _ = gui.refreshFilesAndSubmodules() })
func() {
if options.Mode == types.ASYNC {
go utils.Safe(func() { _ = gui.refreshFilesAndSubmodules() })
} else {
_ = gui.refreshFilesAndSubmodules()
}
wg.Done()
}()
} }
if scopeMap[types.STASH] { if scopeMap[types.STASH] {
wg.Add(1) refresh(func() { _ = gui.refreshStashEntries() })
func() {
if options.Mode == types.ASYNC {
go utils.Safe(func() { _ = gui.refreshStashEntries() })
} else {
_ = gui.refreshStashEntries()
}
wg.Done()
}()
} }
if scopeMap[types.TAGS] { if scopeMap[types.TAGS] {
wg.Add(1) refresh(func() { _ = gui.refreshTags() })
func() {
if options.Mode == types.ASYNC {
go utils.Safe(func() { _ = gui.refreshTags() })
} else {
_ = gui.refreshTags()
}
wg.Done()
}()
} }
if scopeMap[types.REMOTES] { if scopeMap[types.REMOTES] {
wg.Add(1) refresh(func() { _ = gui.refreshRemotes() })
func() {
if options.Mode == types.ASYNC {
go utils.Safe(func() { _ = gui.refreshRemotes() })
} else {
_ = gui.refreshRemotes()
}
wg.Done()
}()
} }
wg.Wait() wg.Wait()
@ -234,7 +220,7 @@ func (gui *Gui) resizePopupPanel(v *gocui.View, content string) error {
return err 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 // TODO: find out why we're doing this
line := panelState.GetSelectedLineIdx() line := panelState.GetSelectedLineIdx()
@ -253,7 +239,7 @@ func (gui *Gui) changeSelectedLine(panelState IListPanelState, total int, change
panelState.SetSelectedLineIdx(newLine) panelState.SetSelectedLineIdx(newLine)
} }
func (gui *Gui) refreshSelectedLine(panelState IListPanelState, total int) { func (gui *Gui) refreshSelectedLine(panelState types.IListPanelState, total int) {
line := panelState.GetSelectedLineIdx() line := panelState.GetSelectedLineIdx()
if line == -1 && total > 0 { 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 { func (gui *Gui) globalOptionsMap() map[string]string {
keybindingConfig := gui.UserConfig.Keybinding keybindingConfig := gui.c.UserConfig.Keybinding
return map[string]string{ 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", 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.Tr.LcNavigate, 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.Tr.LcCancel, gui.getKeyDisplay(keybindingConfig.Universal.Return): gui.c.Tr.LcCancel,
gui.getKeyDisplay(keybindingConfig.Universal.Quit): gui.Tr.LcQuit, gui.getKeyDisplay(keybindingConfig.Universal.Quit): gui.c.Tr.LcQuit,
gui.getKeyDisplay(keybindingConfig.Universal.OptionMenu): gui.Tr.LcMenu, 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.Tr.LcJump, 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.Tr.LcScrollLeftRight, 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 { 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 { func (gui *Gui) handleNextTab() error {

View File

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

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