1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-30 03:23:08 +03:00

Begin refactoring gui

This begins a big refactor of moving more code out of the Gui struct into contexts, controllers, and helpers. We also move some code into structs in the
gui package purely for the sake of better encapsulation
This commit is contained in:
Jesse Duffield
2022-12-30 23:24:24 +11:00
parent 826128a8e0
commit 8edad826ca
101 changed files with 3331 additions and 2877 deletions

View File

@ -7,5 +7,8 @@ func AttachControllers(context types.Context, controllers ...types.IController)
context.AddKeybindingsFn(controller.GetKeybindings)
context.AddMouseKeybindingsFn(controller.GetMouseKeybindings)
context.AddOnClickFn(controller.GetOnClick())
context.AddOnRenderToMainFn(controller.GetOnRenderToMain())
context.AddOnFocusFn(controller.GetOnFocus())
context.AddOnFocusLostFn(controller.GetOnFocusLost())
}
}

View File

@ -18,3 +18,15 @@ func (self *baseController) GetMouseKeybindings(opts types.KeybindingsOpts) []*g
func (self *baseController) GetOnClick() func() error {
return nil
}
func (self *baseController) GetOnRenderToMain() func() error {
return nil
}
func (self *baseController) GetOnFocus() func(types.OnFocusOpts) error {
return nil
}
func (self *baseController) GetOnFocusLost() func(types.OnFocusLostOpts) error {
return nil
}

View File

@ -111,6 +111,30 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
}
}
func (self *BranchesController) GetOnRenderToMain() func() error {
return func() error {
return self.helpers.Diff.WithDiffModeCheck(func() error {
var task types.UpdateTask
branch := self.context().GetSelected()
if branch == nil {
task = types.NewRenderStringTask(self.c.Tr.NoBranchesThisRepo)
} else {
cmdObj := self.c.Git().Branch.GetGraphCmdObj(branch.FullRefName())
task = types.NewRunPtyTask(cmdObj.GetCmd())
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: self.c.Tr.LogTitle,
Task: task,
},
})
})
}
}
func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error {
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.Actions.SetUnsetUpstream,

View File

@ -0,0 +1,53 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type ConfirmationController struct {
baseController
*controllerCommon
}
var _ types.IController = &ConfirmationController{}
func NewConfirmationController(
common *controllerCommon,
) *ConfirmationController {
return &ConfirmationController{
baseController: baseController{},
controllerCommon: common,
}
}
func (self *ConfirmationController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{}
return bindings
}
func (self *ConfirmationController) GetOnFocusLost() func(types.OnFocusLostOpts) error {
return func(types.OnFocusLostOpts) error {
deactivateConfirmationPrompt(self.controllerCommon)
return nil
}
}
func (self *ConfirmationController) Context() types.Context {
return self.context()
}
func (self *ConfirmationController) context() types.Context {
return self.contexts.Confirmation
}
func deactivateConfirmationPrompt(c *controllerCommon) {
c.mutexes.PopupMutex.Lock()
c.c.State().GetRepoState().SetCurrentPopupOpts(nil)
c.mutexes.PopupMutex.Unlock()
c.c.Views().Confirmation.Visible = false
c.c.Views().Suggestions.Visible = false
gui.clearConfirmationViewKeyBindings()
}

View File

@ -15,7 +15,6 @@ type FilesController struct {
baseController // nolint: unused
*controllerCommon
enterSubmodule func(submodule *models.SubmoduleConfig) error
setCommitMessage func(message string)
getSavedCommitMessage func() string
}
@ -24,13 +23,11 @@ var _ types.IController = &FilesController{}
func NewFilesController(
common *controllerCommon,
enterSubmodule func(submodule *models.SubmoduleConfig) error,
setCommitMessage func(message string),
getSavedCommitMessage func() string,
) *FilesController {
return &FilesController{
controllerCommon: common,
enterSubmodule: enterSubmodule,
setCommitMessage: setCommitMessage,
getSavedCommitMessage: getSavedCommitMessage,
}
@ -175,6 +172,74 @@ func (self *FilesController) GetMouseKeybindings(opts types.KeybindingsOpts) []*
}
}
func (self *FilesController) GetOnRenderToMain() func() error {
return func() error {
return self.helpers.Diff.WithDiffModeCheck(func() error {
node := self.context().GetSelected()
if node == nil {
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: self.c.Tr.DiffTitle,
Task: types.NewRenderStringTask(self.c.Tr.NoChangedFiles),
},
})
}
if node.File != nil && node.File.HasInlineMergeConflicts {
hasConflicts, err := self.helpers.MergeConflicts.SetMergeState(node.GetPath())
if err != nil {
return err
}
if hasConflicts {
return self.helpers.MergeConflicts.Render(false)
}
}
self.helpers.MergeConflicts.ResetMergeState()
pair := self.c.MainViewPairs().Normal
if node.File != nil {
pair = self.c.MainViewPairs().Staging
}
split := self.c.UserConfig.Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges())
mainShowsStaged := !split && node.GetHasStagedChanges()
cmdObj := self.git.WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, self.c.State().GetIgnoreWhitespaceInDiffView())
title := self.c.Tr.UnstagedChanges
if mainShowsStaged {
title = self.c.Tr.StagedChanges
}
refreshOpts := types.RefreshMainOpts{
Pair: pair,
Main: &types.ViewUpdateOpts{
Task: types.NewRunPtyTask(cmdObj.GetCmd()),
Title: title,
},
}
if split {
cmdObj := self.git.WorkingTree.WorktreeFileDiffCmdObj(node, false, true, self.c.State().GetIgnoreWhitespaceInDiffView())
title := self.c.Tr.StagedChanges
if mainShowsStaged {
title = self.c.Tr.UnstagedChanges
}
refreshOpts.Secondary = &types.ViewUpdateOpts{
Title: title,
Task: types.NewRunPtyTask(cmdObj.GetCmd()),
}
}
return self.c.RenderToMainViews(refreshOpts)
})
}
}
func (self *FilesController) GetOnClick() func() error {
return self.checkSelectedFileNode(self.press)
}
@ -379,7 +444,7 @@ func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
submoduleConfigs := self.model.Submodules
if file.IsSubmodule(submoduleConfigs) {
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
return self.enterSubmodule(submoduleConfig)
return self.helpers.Repos.EnterSubmodule(submoduleConfig)
}
if file.HasInlineMergeConflicts {

View File

@ -0,0 +1,114 @@
package helpers
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
type DiffHelper struct {
c *types.HelperCommon
}
func NewDiffHelper(c *types.HelperCommon) *DiffHelper {
return &DiffHelper{
c: c,
}
}
func (self *DiffHelper) DiffStr() string {
output := self.c.Modes().Diffing.Ref
right := self.currentDiffTerminal()
if right != "" {
output += " " + right
}
if self.c.Modes().Diffing.Reverse {
output += " -R"
}
if self.c.State().GetIgnoreWhitespaceInDiffView() {
output += " --ignore-all-space"
}
file := self.currentlySelectedFilename()
if file != "" {
output += " -- " + file
} else if self.c.Modes().Filtering.Active() {
output += " -- " + self.c.Modes().Filtering.GetPath()
}
return output
}
func (self *DiffHelper) ExitDiffMode() error {
self.c.Modes().Diffing = diffing.New()
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
}
func (self *DiffHelper) RenderDiff() error {
cmdObj := self.c.OS().Cmd.New(
fmt.Sprintf("git diff --submodule --no-ext-diff --color %s", self.DiffStr()),
)
task := types.NewRunPtyTask(cmdObj.GetCmd())
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: "Diff",
Task: task,
},
})
}
// CurrentDiffTerminals returns the current diff terminals of the currently selected item.
// in the case of a branch it returns both the branch and it's upstream name,
// which becomes an option when you bring up the diff menu, but when you're just
// flicking through branches it will be using the local branch name.
func (self *DiffHelper) CurrentDiffTerminals() []string {
c := self.c.CurrentSideContext()
if c.GetKey() == "" {
return nil
}
switch v := c.(type) {
case types.DiffableContext:
return v.GetDiffTerminals()
}
return nil
}
func (self *DiffHelper) currentDiffTerminal() string {
names := self.CurrentDiffTerminals()
if len(names) == 0 {
return ""
}
return names[0]
}
func (self *DiffHelper) currentlySelectedFilename() string {
currentContext := self.c.CurrentContext()
switch currentContext := currentContext.(type) {
case types.IListContext:
if lo.Contains([]types.ContextKey{context.FILES_CONTEXT_KEY, context.COMMIT_FILES_CONTEXT_KEY}, currentContext.GetKey()) {
return currentContext.GetSelectedItemId()
}
}
return ""
}
func (self *DiffHelper) WithDiffModeCheck(f func() error) error {
if self.c.Modes().Diffing.Active() {
return self.RenderDiff()
}
return f()
}

View File

@ -12,26 +12,45 @@ type Helpers struct {
CherryPick *CherryPickHelper
Host *HostHelper
PatchBuilding *PatchBuildingHelper
Staging *StagingHelper
GPG *GpgHelper
Upstream *UpstreamHelper
AmendHelper *AmendHelper
Snake *SnakeHelper
// lives in context package because our contexts need it to render to main
Diff *DiffHelper
Repos *ReposHelper
RecordDirectory *RecordDirectoryHelper
Update *UpdateHelper
Window *WindowHelper
View *ViewHelper
Refresh *RefreshHelper
}
func NewStubHelpers() *Helpers {
return &Helpers{
Refs: &RefsHelper{},
Bisect: &BisectHelper{},
Suggestions: &SuggestionsHelper{},
Files: &FilesHelper{},
WorkingTree: &WorkingTreeHelper{},
Tags: &TagsHelper{},
MergeAndRebase: &MergeAndRebaseHelper{},
MergeConflicts: &MergeConflictsHelper{},
CherryPick: &CherryPickHelper{},
Host: &HostHelper{},
PatchBuilding: &PatchBuildingHelper{},
GPG: &GpgHelper{},
Upstream: &UpstreamHelper{},
AmendHelper: &AmendHelper{},
Refs: &RefsHelper{},
Bisect: &BisectHelper{},
Suggestions: &SuggestionsHelper{},
Files: &FilesHelper{},
WorkingTree: &WorkingTreeHelper{},
Tags: &TagsHelper{},
MergeAndRebase: &MergeAndRebaseHelper{},
MergeConflicts: &MergeConflictsHelper{},
CherryPick: &CherryPickHelper{},
Host: &HostHelper{},
PatchBuilding: &PatchBuildingHelper{},
Staging: &StagingHelper{},
GPG: &GpgHelper{},
Upstream: &UpstreamHelper{},
AmendHelper: &AmendHelper{},
Snake: &SnakeHelper{},
Diff: &DiffHelper{},
Repos: &ReposHelper{},
RecordDirectory: &RecordDirectoryHelper{},
Update: &UpdateHelper{},
Window: &WindowHelper{},
View: &ViewHelper{},
Refresh: &RefreshHelper{},
}
}

View File

@ -113,3 +113,42 @@ func (self *MergeConflictsHelper) SwitchToMerge(path string) error {
func (self *MergeConflictsHelper) context() *context.MergeConflictsContext {
return self.contexts.MergeConflicts
}
func (self *MergeConflictsHelper) Render(isFocused bool) error {
content := self.context().GetContentToRender(isFocused)
var task types.UpdateTask
if self.context().IsUserScrolling() {
task = types.NewRenderStringWithoutScrollTask(content)
} else {
originY := self.context().GetOriginY()
task = types.NewRenderStringWithScrollTask(content, 0, originY)
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().MergeConflicts,
Main: &types.ViewUpdateOpts{
Task: task,
},
})
}
func (self *MergeConflictsHelper) RefreshMergeState() error {
self.contexts.MergeConflicts.GetMutex().Lock()
defer self.contexts.MergeConflicts.GetMutex().Unlock()
if self.c.CurrentContext().GetKey() != context.MERGE_CONFLICTS_CONTEXT_KEY {
return nil
}
hasConflicts, err := self.SetConflictsAndRender(self.contexts.MergeConflicts.GetState().GetPath(), true)
if err != nil {
return self.c.Error(err)
}
if !hasConflicts {
return self.EscapeMerge()
}
return nil
}

View File

@ -4,6 +4,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@ -60,3 +61,59 @@ func (self *PatchBuildingHelper) Reset() error {
// refreshing the current context so that the secondary panel is hidden if necessary.
return self.c.PostRefreshUpdate(self.c.CurrentContext())
}
func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpts) error {
selectedLineIdx := -1
if opts.ClickedWindowName == "main" {
selectedLineIdx = opts.ClickedViewLineIdx
}
if !self.git.Patch.PatchBuilder.Active() {
return self.Escape()
}
// get diff from commit file that's currently selected
path := self.contexts.CommitFiles.GetSelectedPath()
if path == "" {
return nil
}
ref := self.contexts.CommitFiles.CommitFileTreeViewModel.GetRef()
to := ref.RefName()
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
diff, err := self.git.WorkingTree.ShowFileDiff(from, to, reverse, path, true, self.c.State().GetIgnoreWhitespaceInDiffView())
if err != nil {
return err
}
secondaryDiff := self.git.Patch.PatchBuilder.RenderPatchForFile(path, false, false)
if err != nil {
return err
}
context := self.contexts.CustomPatchBuilder
oldState := context.GetState()
state := patch_exploring.NewState(diff, selectedLineIdx, oldState, self.c.Log)
context.SetState(state)
if state == nil {
return self.Escape()
}
mainContent := context.GetContentToRender(true)
self.contexts.CustomPatchBuilder.FocusSelection()
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().PatchBuilding,
Main: &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(mainContent),
Title: self.c.Tr.Patch,
},
Secondary: &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(secondaryDiff),
Title: self.c.Tr.CustomPatch,
},
})
}

View File

@ -0,0 +1,38 @@
package helpers
import (
"os"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type RecordDirectoryHelper struct {
c *types.HelperCommon
}
func NewRecordDirectoryHelper(c *types.HelperCommon) *RecordDirectoryHelper {
return &RecordDirectoryHelper{
c: c,
}
}
// when a user runs lazygit with the LAZYGIT_NEW_DIR_FILE env variable defined
// we will write the current directory to that file on exit so that their
// shell can then change to that directory. That means you don't get kicked
// back to the directory that you started with.
func (self *RecordDirectoryHelper) RecordCurrentDirectory() error {
// determine current directory, set it in LAZYGIT_NEW_DIR_FILE
dirName, err := os.Getwd()
if err != nil {
return err
}
return self.RecordDirectory(dirName)
}
func (self *RecordDirectoryHelper) RecordDirectory(dirName string) error {
newDirFilePath := os.Getenv("LAZYGIT_NEW_DIR_FILE")
if newDirFilePath == "" {
return nil
}
return self.c.OS().CreateFileWithContent(newDirFilePath, dirName)
}

View File

@ -0,0 +1,617 @@
package helpers
import (
"fmt"
"strings"
"sync"
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/generics/slices"
"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/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type RefreshHelper struct {
c *types.HelperCommon
contexts *context.ContextTree
git *commands.GitCommand
refsHelper *RefsHelper
mergeAndRebaseHelper *MergeAndRebaseHelper
patchBuildingHelper *PatchBuildingHelper
stagingHelper *StagingHelper
mergeConflictsHelper *MergeConflictsHelper
fileWatcher types.IFileWatcher
}
func NewRefreshHelper(
c *types.HelperCommon,
contexts *context.ContextTree,
git *commands.GitCommand,
refsHelper *RefsHelper,
mergeAndRebaseHelper *MergeAndRebaseHelper,
patchBuildingHelper *PatchBuildingHelper,
stagingHelper *StagingHelper,
mergeConflictsHelper *MergeConflictsHelper,
fileWatcher types.IFileWatcher,
) *RefreshHelper {
return &RefreshHelper{
c: c,
contexts: contexts,
git: git,
refsHelper: refsHelper,
mergeAndRebaseHelper: mergeAndRebaseHelper,
patchBuildingHelper: patchBuildingHelper,
stagingHelper: stagingHelper,
mergeConflictsHelper: mergeConflictsHelper,
fileWatcher: fileWatcher,
}
}
func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
if options.Scope == nil {
self.c.Log.Infof(
"refreshing all scopes in %s mode",
getModeName(options.Mode),
)
} else {
self.c.Log.Infof(
"refreshing the following scopes in %s mode: %s",
getModeName(options.Mode),
strings.Join(getScopeNames(options.Scope), ","),
)
}
wg := sync.WaitGroup{}
f := func() {
var scopeSet *set.Set[types.RefreshableView]
if len(options.Scope) == 0 {
// not refreshing staging/patch-building unless explicitly requested because we only need
// to refresh those while focused.
scopeSet = set.NewFromSlice([]types.RefreshableView{
types.COMMITS,
types.BRANCHES,
types.FILES,
types.STASH,
types.REFLOG,
types.TAGS,
types.REMOTES,
types.STATUS,
types.BISECT_INFO,
})
} else {
scopeSet = set.NewFromSlice(options.Scope)
}
refresh := func(f func()) {
wg.Add(1)
func() {
if options.Mode == types.ASYNC {
go utils.Safe(f)
} else {
f()
}
wg.Done()
}()
}
if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) || scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) {
refresh(self.refreshCommits)
} else if scopeSet.Includes(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() { _ = self.refreshRebaseCommits() })
}
if scopeSet.Includes(types.SUB_COMMITS) {
refresh(func() { _ = self.refreshSubCommitsWithLimit() })
}
// reason we're not doing this if the COMMITS type is included is that if the COMMITS type _is_ included we will refresh the commit files context anyway
if scopeSet.Includes(types.COMMIT_FILES) && !scopeSet.Includes(types.COMMITS) {
refresh(func() { _ = self.refreshCommitFilesContext() })
}
if scopeSet.Includes(types.FILES) || scopeSet.Includes(types.SUBMODULES) {
refresh(func() { _ = self.refreshFilesAndSubmodules() })
}
if scopeSet.Includes(types.STASH) {
refresh(func() { _ = self.refreshStashEntries() })
}
if scopeSet.Includes(types.TAGS) {
refresh(func() { _ = self.refreshTags() })
}
if scopeSet.Includes(types.REMOTES) {
refresh(func() { _ = self.refreshRemotes() })
}
if scopeSet.Includes(types.STAGING) {
refresh(func() { _ = self.stagingHelper.RefreshStagingPanel(types.OnFocusOpts{}) })
}
if scopeSet.Includes(types.PATCH_BUILDING) {
refresh(func() { _ = self.patchBuildingHelper.RefreshPatchBuildingPanel(types.OnFocusOpts{}) })
}
if scopeSet.Includes(types.MERGE_CONFLICTS) || scopeSet.Includes(types.FILES) {
refresh(func() { _ = self.mergeConflictsHelper.RefreshMergeState() })
}
wg.Wait()
self.refreshStatus()
if options.Then != nil {
options.Then()
}
}
if options.Mode == types.BLOCK_UI {
self.c.OnUIThread(func() error {
f()
return nil
})
} else {
f()
}
return nil
}
func getScopeNames(scopes []types.RefreshableView) []string {
scopeNameMap := map[types.RefreshableView]string{
types.COMMITS: "commits",
types.BRANCHES: "branches",
types.FILES: "files",
types.SUBMODULES: "submodules",
types.SUB_COMMITS: "subCommits",
types.STASH: "stash",
types.REFLOG: "reflog",
types.TAGS: "tags",
types.REMOTES: "remotes",
types.STATUS: "status",
types.BISECT_INFO: "bisect",
types.STAGING: "staging",
types.MERGE_CONFLICTS: "mergeConflicts",
}
return slices.Map(scopes, func(scope types.RefreshableView) string {
return scopeNameMap[scope]
})
}
func getModeName(mode types.RefreshMode) string {
switch mode {
case types.SYNC:
return "sync"
case types.ASYNC:
return "async"
case types.BLOCK_UI:
return "block-ui"
default:
return "unknown mode"
}
}
// during startup, the bottleneck is fetching the reflog entries. We need these
// on startup to sort the branches by recency. So we have two phases: INITIAL, and COMPLETE.
// In the initial phase we don't get any reflog commits, but we asynchronously get them
// and refresh the branches after that
func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() {
switch self.c.State().GetRepoState().GetStartupStage() {
case types.INITIAL:
go utils.Safe(func() {
_ = self.refreshReflogCommits()
self.refreshBranches()
self.c.State().GetRepoState().SetStartupStage(types.COMPLETE)
})
case types.COMPLETE:
_ = self.refreshReflogCommits()
}
}
// whenever we change commits, we should update branches because the upstream/downstream
// counts can change. Whenever we change branches we should probably also change commits
// e.g. in the case of switching branches.
func (self *RefreshHelper) refreshCommits() {
wg := sync.WaitGroup{}
wg.Add(2)
go utils.Safe(func() {
self.refreshReflogCommitsConsideringStartup()
self.refreshBranches()
wg.Done()
})
go utils.Safe(func() {
_ = self.refreshCommitsWithLimit()
ctx, ok := self.contexts.CommitFiles.GetParentContext()
if ok && ctx.GetKey() == context.LOCAL_COMMITS_CONTEXT_KEY {
// This makes sense when we've e.g. just amended a commit, meaning we get a new commit SHA at the same position.
// However if we've just added a brand new commit, it pushes the list down by one and so we would end up
// showing the contents of a different commit than the one we initially entered.
// Ideally we would know when to refresh the commit files context and when not to,
// or perhaps we could just pop that context off the stack whenever cycling windows.
// For now the awkwardness remains.
commit := self.contexts.LocalCommits.GetSelected()
if commit != nil {
self.contexts.CommitFiles.SetRef(commit)
self.contexts.CommitFiles.SetTitleRef(commit.RefName())
_ = self.refreshCommitFilesContext()
}
}
wg.Done()
})
wg.Wait()
}
func (self *RefreshHelper) refreshCommitsWithLimit() error {
self.c.Mutexes().LocalCommitsMutex.Lock()
defer self.c.Mutexes().LocalCommitsMutex.Unlock()
commits, err := self.git.Loaders.CommitLoader.GetCommits(
git_commands.GetCommitsOptions{
Limit: self.contexts.LocalCommits.GetLimitCommits(),
FilterPath: self.c.Modes().Filtering.GetPath(),
IncludeRebaseCommits: true,
RefName: self.refForLog(),
All: self.contexts.LocalCommits.GetShowWholeGitGraph(),
},
)
if err != nil {
return err
}
self.c.Model().Commits = commits
self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.git.Status.WorkingTreeState()
return self.c.PostRefreshUpdate(self.contexts.LocalCommits)
}
func (self *RefreshHelper) refreshSubCommitsWithLimit() error {
self.c.Mutexes().SubCommitsMutex.Lock()
defer self.c.Mutexes().SubCommitsMutex.Unlock()
commits, err := self.git.Loaders.CommitLoader.GetCommits(
git_commands.GetCommitsOptions{
Limit: self.contexts.SubCommits.GetLimitCommits(),
FilterPath: self.c.Modes().Filtering.GetPath(),
IncludeRebaseCommits: false,
RefName: self.contexts.SubCommits.GetRef().FullRefName(),
},
)
if err != nil {
return err
}
self.c.Model().SubCommits = commits
return self.c.PostRefreshUpdate(self.contexts.SubCommits)
}
func (self *RefreshHelper) refreshCommitFilesContext() error {
ref := self.contexts.CommitFiles.GetRef()
to := ref.RefName()
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
files, err := self.git.Loaders.CommitFileLoader.GetFilesInDiff(from, to, reverse)
if err != nil {
return self.c.Error(err)
}
self.c.Model().CommitFiles = files
self.contexts.CommitFiles.CommitFileTreeViewModel.SetTree()
return self.c.PostRefreshUpdate(self.contexts.CommitFiles)
}
func (self *RefreshHelper) refreshRebaseCommits() error {
self.c.Mutexes().LocalCommitsMutex.Lock()
defer self.c.Mutexes().LocalCommitsMutex.Unlock()
updatedCommits, err := self.git.Loaders.CommitLoader.MergeRebasingCommits(self.c.Model().Commits)
if err != nil {
return err
}
self.c.Model().Commits = updatedCommits
self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.git.Status.WorkingTreeState()
return self.c.PostRefreshUpdate(self.contexts.LocalCommits)
}
func (self *RefreshHelper) refreshTags() error {
tags, err := self.git.Loaders.TagLoader.GetTags()
if err != nil {
return self.c.Error(err)
}
self.c.Model().Tags = tags
return self.c.PostRefreshUpdate(self.contexts.Tags)
}
func (self *RefreshHelper) refreshStateSubmoduleConfigs() error {
configs, err := self.git.Submodule.GetConfigs()
if err != nil {
return err
}
self.c.Model().Submodules = configs
return nil
}
// self.refreshStatus is called at the end of this because that's when we can
// be sure there is a State.Model.Branches array to pick the current branch from
func (self *RefreshHelper) refreshBranches() {
reflogCommits := self.c.Model().FilteredReflogCommits
if self.c.Modes().Filtering.Active() {
// in filter mode we filter our reflog commits to just those containing the path
// however we need all the reflog entries to populate the recencies of our branches
// which allows us to order them correctly. So if we're filtering we'll just
// manually load all the reflog commits here
var err error
reflogCommits, _, err = self.git.Loaders.ReflogCommitLoader.GetReflogCommits(nil, "")
if err != nil {
self.c.Log.Error(err)
}
}
branches, err := self.git.Loaders.BranchLoader.Load(reflogCommits)
if err != nil {
_ = self.c.Error(err)
}
self.c.Model().Branches = branches
if err := self.c.PostRefreshUpdate(self.contexts.Branches); err != nil {
self.c.Log.Error(err)
}
self.refreshStatus()
}
func (self *RefreshHelper) refreshFilesAndSubmodules() error {
self.c.Mutexes().RefreshingFilesMutex.Lock()
self.c.State().SetIsRefreshingFiles(true)
defer func() {
self.c.State().SetIsRefreshingFiles(false)
self.c.Mutexes().RefreshingFilesMutex.Unlock()
}()
if err := self.refreshStateSubmoduleConfigs(); err != nil {
return err
}
if err := self.refreshStateFiles(); err != nil {
return err
}
self.c.OnUIThread(func() error {
if err := self.c.PostRefreshUpdate(self.contexts.Submodules); err != nil {
self.c.Log.Error(err)
}
if err := self.c.PostRefreshUpdate(self.contexts.Files); err != nil {
self.c.Log.Error(err)
}
return nil
})
return nil
}
func (self *RefreshHelper) refreshStateFiles() error {
fileTreeViewModel := self.contexts.Files.FileTreeViewModel
// If git thinks any of our files have inline merge conflicts, but they actually don't,
// we stage them.
// Note that if files with merge conflicts have both arisen and have been resolved
// between refreshes, we won't stage them here. This is super unlikely though,
// and this approach spares us from having to call `git status` twice in a row.
// Although this also means that at startup we won't be staging anything until
// we call git status again.
pathsToStage := []string{}
prevConflictFileCount := 0
for _, file := range self.c.Model().Files {
if file.HasMergeConflicts {
prevConflictFileCount++
}
if file.HasInlineMergeConflicts {
hasConflicts, err := mergeconflicts.FileHasConflictMarkers(file.Name)
if err != nil {
self.c.Log.Error(err)
} else if !hasConflicts {
pathsToStage = append(pathsToStage, file.Name)
}
}
}
if len(pathsToStage) > 0 {
self.c.LogAction(self.c.Tr.Actions.StageResolvedFiles)
if err := self.git.WorkingTree.StageFiles(pathsToStage); err != nil {
return self.c.Error(err)
}
}
files := self.git.Loaders.FileLoader.
GetStatusFiles(git_commands.GetStatusFileOptions{})
conflictFileCount := 0
for _, file := range files {
if file.HasMergeConflicts {
conflictFileCount++
}
}
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && conflictFileCount == 0 && prevConflictFileCount > 0 {
self.c.OnUIThread(func() error { return self.mergeAndRebaseHelper.PromptToContinueRebase() })
}
fileTreeViewModel.RWMutex.Lock()
// only taking over the filter if it hasn't already been set by the user.
// Though this does make it impossible for the user to actually say they want to display all if
// conflicts are currently being shown. Hmm. Worth it I reckon. If we need to add some
// extra state here to see if the user's set the filter themselves we can do that, but
// I'd prefer to maintain as little state as possible.
if conflictFileCount > 0 {
if fileTreeViewModel.GetFilter() == filetree.DisplayAll {
fileTreeViewModel.SetFilter(filetree.DisplayConflicted)
}
} else if fileTreeViewModel.GetFilter() == filetree.DisplayConflicted {
fileTreeViewModel.SetFilter(filetree.DisplayAll)
}
self.c.Model().Files = files
fileTreeViewModel.SetTree()
fileTreeViewModel.RWMutex.Unlock()
if err := self.fileWatcher.AddFilesToFileWatcher(files); err != nil {
return err
}
return nil
}
// the reflogs panel is the only panel where we cache data, in that we only
// load entries that have been created since we last ran the call. This means
// we need to be more careful with how we use this, and to ensure we're emptying
// the reflogs array when changing contexts.
// This method also manages two things: ReflogCommits and FilteredReflogCommits.
// FilteredReflogCommits are rendered in the reflogs panel, and ReflogCommits
// are used by the branches panel to obtain recency values for sorting.
func (self *RefreshHelper) refreshReflogCommits() error {
// pulling state into its own variable incase it gets swapped out for another state
// and we get an out of bounds exception
model := self.c.Model()
var lastReflogCommit *models.Commit
if len(model.ReflogCommits) > 0 {
lastReflogCommit = model.ReflogCommits[0]
}
refresh := func(stateCommits *[]*models.Commit, filterPath string) error {
commits, onlyObtainedNewReflogCommits, err := self.git.Loaders.ReflogCommitLoader.
GetReflogCommits(lastReflogCommit, filterPath)
if err != nil {
return self.c.Error(err)
}
if onlyObtainedNewReflogCommits {
*stateCommits = append(commits, *stateCommits...)
} else {
*stateCommits = commits
}
return nil
}
if err := refresh(&model.ReflogCommits, ""); err != nil {
return err
}
if self.c.Modes().Filtering.Active() {
if err := refresh(&model.FilteredReflogCommits, self.c.Modes().Filtering.GetPath()); err != nil {
return err
}
} else {
model.FilteredReflogCommits = model.ReflogCommits
}
return self.c.PostRefreshUpdate(self.contexts.ReflogCommits)
}
func (self *RefreshHelper) refreshRemotes() error {
prevSelectedRemote := self.contexts.Remotes.GetSelected()
remotes, err := self.git.Loaders.RemoteLoader.GetRemotes()
if err != nil {
return self.c.Error(err)
}
self.c.Model().Remotes = remotes
// we need to ensure our selected remote branches aren't now outdated
if prevSelectedRemote != nil && self.c.Model().RemoteBranches != nil {
// find remote now
for _, remote := range remotes {
if remote.Name == prevSelectedRemote.Name {
self.c.Model().RemoteBranches = remote.Branches
break
}
}
}
if err := self.c.PostRefreshUpdate(self.contexts.Remotes); err != nil {
return err
}
if err := self.c.PostRefreshUpdate(self.contexts.RemoteBranches); err != nil {
return err
}
return nil
}
func (self *RefreshHelper) refreshStashEntries() error {
self.c.Model().StashEntries = self.git.Loaders.StashLoader.
GetStashEntries(self.c.Modes().Filtering.GetPath())
return self.c.PostRefreshUpdate(self.contexts.Stash)
}
// never call this on its own, it should only be called from within refreshCommits()
func (self *RefreshHelper) refreshStatus() {
self.c.Mutexes().RefreshingStatusMutex.Lock()
defer self.c.Mutexes().RefreshingStatusMutex.Unlock()
currentBranch := self.refsHelper.GetCheckedOutRef()
if currentBranch == nil {
// need to wait for branches to refresh
return
}
status := ""
if currentBranch.IsRealBranch() {
status += presentation.ColoredBranchStatus(currentBranch, self.c.Tr) + " "
}
workingTreeState := self.git.Status.WorkingTreeState()
if workingTreeState != enums.REBASE_MODE_NONE {
status += style.FgYellow.Sprintf("(%s) ", presentation.FormatWorkingTreeState(workingTreeState))
}
name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name)
repoName := utils.GetCurrentRepoName()
status += fmt.Sprintf("%s → %s ", repoName, name)
self.c.SetViewContent(self.c.Views().Status, status)
}
func (self *RefreshHelper) refForLog() string {
bisectInfo := self.git.Bisect.GetInfo()
self.c.Model().BisectInfo = bisectInfo
if !bisectInfo.Started() {
return "HEAD"
}
// need to see if our bisect's current commit is reachable from our 'new' ref.
if bisectInfo.Bisecting() && !self.git.Bisect.ReachableFromStart(bisectInfo) {
return bisectInfo.GetNewSha()
}
return bisectInfo.GetStartSha()
}

View File

@ -0,0 +1,175 @@
package helpers
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"github.com/jesseduffield/generics/slices"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type onNewRepoFn func(startArgs appTypes.StartArgs, reuseState bool) error
// helps switch back and forth between repos
type ReposHelper struct {
c *types.HelperCommon
recordDirectoryHelper *RecordDirectoryHelper
onNewRepo onNewRepoFn
}
func NewRecentReposHelper(
c *types.HelperCommon,
recordDirectoryHelper *RecordDirectoryHelper,
onNewRepo onNewRepoFn,
) *ReposHelper {
return &ReposHelper{
c: c,
recordDirectoryHelper: recordDirectoryHelper,
onNewRepo: onNewRepo,
}
}
func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error {
wd, err := os.Getwd()
if err != nil {
return err
}
self.c.State().GetRepoPathStack().Push(wd)
return self.DispatchSwitchToRepo(submodule.Path, true)
}
func (self *ReposHelper) getCurrentBranch(path string) string {
readHeadFile := func(path string) (string, error) {
headFile, err := os.ReadFile(filepath.Join(path, "HEAD"))
if err == nil {
content := strings.TrimSpace(string(headFile))
refsPrefix := "ref: refs/heads/"
var branchDisplay string
if strings.HasPrefix(content, refsPrefix) {
// is a branch
branchDisplay = strings.TrimPrefix(content, refsPrefix)
} else {
// detached HEAD state, displaying short SHA
branchDisplay = utils.ShortSha(content)
}
return branchDisplay, nil
}
return "", err
}
gitDirPath := filepath.Join(path, ".git")
if gitDir, err := os.Stat(gitDirPath); err == nil {
if gitDir.IsDir() {
// ordinary repo
if branch, err := readHeadFile(gitDirPath); err == nil {
return branch
}
} else {
// worktree
if worktreeGitDir, err := os.ReadFile(gitDirPath); err == nil {
content := strings.TrimSpace(string(worktreeGitDir))
worktreePath := strings.TrimPrefix(content, "gitdir: ")
if branch, err := readHeadFile(worktreePath); err == nil {
return branch
}
}
}
}
return self.c.Tr.LcBranchUnknown
}
func (self *ReposHelper) CreateRecentReposMenu() error {
// we'll show an empty panel if there are no recent repos
recentRepoPaths := []string{}
if len(self.c.GetAppState().RecentRepos) > 0 {
// we skip the first one because we're currently in it
recentRepoPaths = self.c.GetAppState().RecentRepos[1:]
}
currentBranches := sync.Map{}
wg := sync.WaitGroup{}
wg.Add(len(recentRepoPaths))
for _, path := range recentRepoPaths {
go func(path string) {
defer wg.Done()
currentBranches.Store(path, self.getCurrentBranch(path))
}(path)
}
wg.Wait()
menuItems := slices.Map(recentRepoPaths, func(path string) *types.MenuItem {
branchName, _ := currentBranches.Load(path)
if icons.IsIconEnabled() {
branchName = icons.BRANCH_ICON + " " + fmt.Sprintf("%v", branchName)
}
return &types.MenuItem{
LabelColumns: []string{
filepath.Base(path),
style.FgCyan.Sprint(branchName),
style.FgMagenta.Sprint(path),
},
OnPress: func() error {
// if we were in a submodule, we want to forget about that stack of repos
// so that hitting escape in the new repo does nothing
self.c.State().GetRepoPathStack().Clear()
return self.DispatchSwitchToRepo(path, false)
},
}
})
return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.RecentRepos, Items: menuItems})
}
func (self *ReposHelper) DispatchSwitchToRepo(path string, reuse bool) error {
env.UnsetGitDirEnvs()
originalPath, err := os.Getwd()
if err != nil {
return nil
}
if err := os.Chdir(path); err != nil {
if os.IsNotExist(err) {
return self.c.ErrorMsg(self.c.Tr.ErrRepositoryMovedOrDeleted)
}
return err
}
if err := commands.VerifyInGitRepo(self.c.OS()); err != nil {
if err := os.Chdir(originalPath); err != nil {
return err
}
return err
}
if err := self.recordDirectoryHelper.RecordCurrentDirectory(); err != nil {
return err
}
// these two mutexes are used by our background goroutines (triggered via `self.goEvery`. We don't want to
// switch to a repo while one of these goroutines is in the process of updating something
self.c.Mutexes().SyncMutex.Lock()
defer self.c.Mutexes().SyncMutex.Unlock()
self.c.Mutexes().RefreshingFilesMutex.Lock()
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
return self.onNewRepo(appTypes.StartArgs{}, reuse)
}

View File

@ -0,0 +1,76 @@
package helpers
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/snake"
)
type SnakeHelper struct {
c *types.HelperCommon
game *snake.Game
}
func NewSnakeHelper(c *types.HelperCommon) *SnakeHelper {
return &SnakeHelper{
c: c,
}
}
func (self *SnakeHelper) StartGame() {
view := self.c.Views().Snake
game := snake.NewGame(view.Width(), view.Height(), self.renderSnakeGame, self.c.LogAction)
self.game = game
game.Start()
}
func (self *SnakeHelper) ExitGame() {
self.game.Exit()
}
func (self *SnakeHelper) SetDirection(direction snake.Direction) {
self.game.SetDirection(direction)
}
func (self *SnakeHelper) renderSnakeGame(cells [][]snake.CellType, alive bool) {
view := self.c.Views().Snake
if !alive {
_ = self.c.ErrorMsg(self.c.Tr.YouDied)
return
}
output := self.drawSnakeGame(cells)
view.Clear()
fmt.Fprint(view, output)
self.c.Render()
}
func (self *SnakeHelper) drawSnakeGame(cells [][]snake.CellType) string {
writer := &strings.Builder{}
for i, row := range cells {
for _, cell := range row {
switch cell {
case snake.None:
writer.WriteString(" ")
case snake.Snake:
writer.WriteString("█")
case snake.Food:
writer.WriteString(style.FgMagenta.Sprint("█"))
}
}
if i < len(cells) {
writer.WriteString("\n")
}
}
output := writer.String()
return output
}

View File

@ -0,0 +1,119 @@
package helpers
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type StagingHelper struct {
c *types.HelperCommon
git *commands.GitCommand
contexts *context.ContextTree
}
func NewStagingHelper(
c *types.HelperCommon,
git *commands.GitCommand,
contexts *context.ContextTree,
) *StagingHelper {
return &StagingHelper{
c: c,
git: git,
contexts: contexts,
}
}
// NOTE: used from outside this file
func (self *StagingHelper) RefreshStagingPanel(focusOpts types.OnFocusOpts) error {
secondaryFocused := self.secondaryStagingFocused()
mainSelectedLineIdx := -1
secondarySelectedLineIdx := -1
if focusOpts.ClickedViewLineIdx > 0 {
if secondaryFocused {
secondarySelectedLineIdx = focusOpts.ClickedViewLineIdx
} else {
mainSelectedLineIdx = focusOpts.ClickedViewLineIdx
}
}
mainContext := self.contexts.Staging
secondaryContext := self.contexts.StagingSecondary
var file *models.File
node := self.contexts.Files.GetSelected()
if node != nil {
file = node.File
}
if file == nil || (!file.HasUnstagedChanges && !file.HasStagedChanges) {
return self.handleStagingEscape()
}
mainDiff := self.git.WorkingTree.WorktreeFileDiff(file, true, false, false)
secondaryDiff := self.git.WorkingTree.WorktreeFileDiff(file, true, true, false)
// grabbing locks here and releasing before we finish the function
// because pushing say the secondary context could mean entering this function
// again, and we don't want to have a deadlock
mainContext.GetMutex().Lock()
secondaryContext.GetMutex().Lock()
mainContext.SetState(
patch_exploring.NewState(mainDiff, mainSelectedLineIdx, mainContext.GetState(), self.c.Log),
)
secondaryContext.SetState(
patch_exploring.NewState(secondaryDiff, secondarySelectedLineIdx, secondaryContext.GetState(), self.c.Log),
)
mainState := mainContext.GetState()
secondaryState := secondaryContext.GetState()
mainContent := mainContext.GetContentToRender(!secondaryFocused)
secondaryContent := secondaryContext.GetContentToRender(secondaryFocused)
mainContext.GetMutex().Unlock()
secondaryContext.GetMutex().Unlock()
if mainState == nil && secondaryState == nil {
return self.handleStagingEscape()
}
if mainState == nil && !secondaryFocused {
return self.c.PushContext(secondaryContext, focusOpts)
}
if secondaryState == nil && secondaryFocused {
return self.c.PushContext(mainContext, focusOpts)
}
if secondaryFocused {
self.contexts.StagingSecondary.FocusSelection()
} else {
self.contexts.Staging.FocusSelection()
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Staging,
Main: &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(mainContent),
Title: self.c.Tr.UnstagedChanges,
},
Secondary: &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(secondaryContent),
Title: self.c.Tr.StagedChanges,
},
})
}
func (self *StagingHelper) handleStagingEscape() error {
return self.c.PushContext(self.contexts.Files)
}
func (self *StagingHelper) secondaryStagingFocused() bool {
return self.c.CurrentStaticContext().GetKey() == self.contexts.StagingSecondary.GetKey()
}

View File

@ -0,0 +1,98 @@
package helpers
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type UpdateHelper struct {
c *types.HelperCommon
updater *updates.Updater
}
func NewUpdateHelper(c *types.HelperCommon, updater *updates.Updater) *UpdateHelper {
return &UpdateHelper{
c: c,
updater: updater,
}
}
func (self *UpdateHelper) CheckForUpdateInBackground() error {
self.updater.CheckForNewUpdate(func(newVersion string, err error) error {
if err != nil {
// ignoring the error for now so that I'm not annoying users
self.c.Log.Error(err.Error())
return nil
}
if newVersion == "" {
return nil
}
if self.c.UserConfig.Update.Method == "background" {
self.startUpdating(newVersion)
return nil
}
return self.showUpdatePrompt(newVersion)
}, false)
return nil
}
func (self *UpdateHelper) CheckForUpdateInForeground() error {
return self.c.WithWaitingStatus(self.c.Tr.CheckingForUpdates, func() error {
self.updater.CheckForNewUpdate(func(newVersion string, err error) error {
if err != nil {
return self.c.Error(err)
}
if newVersion == "" {
return self.c.ErrorMsg(self.c.Tr.FailedToRetrieveLatestVersionErr)
}
return self.showUpdatePrompt(newVersion)
}, true)
return nil
})
}
func (self *UpdateHelper) startUpdating(newVersion string) {
_ = self.c.WithWaitingStatus(self.c.Tr.UpdateInProgressWaitingStatus, func() error {
self.c.State().SetUpdating(true)
err := self.updater.Update(newVersion)
return self.onUpdateFinish(err)
})
}
func (self *UpdateHelper) onUpdateFinish(err error) error {
self.c.State().SetUpdating(false)
self.c.OnUIThread(func() error {
self.c.SetViewContent(self.c.Views().AppStatus, "")
if err != nil {
errMessage := utils.ResolvePlaceholderString(
self.c.Tr.UpdateFailedErr, map[string]string{
"errMessage": err.Error(),
},
)
return self.c.ErrorMsg(errMessage)
}
return self.c.Alert(self.c.Tr.UpdateCompletedTitle, self.c.Tr.UpdateCompleted)
})
return nil
}
func (self *UpdateHelper) showUpdatePrompt(newVersion string) error {
message := utils.ResolvePlaceholderString(
self.c.Tr.UpdateAvailable, map[string]string{
"newVersion": newVersion,
},
)
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.UpdateAvailableTitle,
Prompt: message,
HandleConfirm: func() error {
self.startUpdating(newVersion)
return nil
},
})
}

View File

@ -0,0 +1,33 @@
package helpers
import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type ViewHelper struct {
c *types.HelperCommon
contexts *context.ContextTree
}
func NewViewHelper(c *types.HelperCommon, contexts *context.ContextTree) *ViewHelper {
return &ViewHelper{
c: c,
contexts: contexts,
}
}
func (self *ViewHelper) ContextForView(viewName string) (types.Context, bool) {
view, err := self.c.GocuiGui().View(viewName)
if err != nil {
return nil, false
}
for _, context := range self.contexts.Flatten() {
if context.GetViewName() == view.Name() {
return context, true
}
}
return nil, false
}

View File

@ -0,0 +1,138 @@
package helpers
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type WindowHelper struct {
c *types.HelperCommon
viewHelper *ViewHelper
contexts *context.ContextTree
}
func NewWindowHelper(c *types.HelperCommon, viewHelper *ViewHelper, contexts *context.ContextTree) *WindowHelper {
return &WindowHelper{
c: c,
viewHelper: viewHelper,
contexts: contexts,
}
}
// A window refers to a place on the screen which can hold one or more views.
// A view is a box that renders content, and within a window only one view will
// appear at a time. When a view appears within a window, it occupies the whole
// space. Right now most windows are 1:1 with views, except for commitFiles which
// is a view that moves between windows
func (self *WindowHelper) GetViewNameForWindow(window string) string {
viewName, ok := self.windowViewNameMap().Get(window)
if !ok {
panic(fmt.Sprintf("Viewname not found for window: %s", window))
}
return viewName
}
func (self *WindowHelper) GetContextForWindow(window string) types.Context {
viewName := self.GetViewNameForWindow(window)
context, ok := self.viewHelper.ContextForView(viewName)
if !ok {
panic("TODO: fix this")
}
return context
}
// for now all we actually care about is the context's view so we're storing that
func (self *WindowHelper) SetWindowContext(c types.Context) {
if c.IsTransient() {
self.resetWindowContext(c)
}
self.windowViewNameMap().Set(c.GetWindowName(), c.GetViewName())
}
func (self *WindowHelper) windowViewNameMap() *utils.ThreadSafeMap[string, string] {
return self.c.State().GetRepoState().GetWindowViewNameMap()
}
func (self *WindowHelper) CurrentWindow() string {
return self.c.CurrentContext().GetWindowName()
}
// assumes the context's windowName has been set to the new window if necessary
func (self *WindowHelper) resetWindowContext(c types.Context) {
for _, windowName := range self.windowViewNameMap().Keys() {
viewName, ok := self.windowViewNameMap().Get(windowName)
if !ok {
continue
}
if viewName == c.GetViewName() && windowName != c.GetWindowName() {
for _, context := range self.contexts.Flatten() {
if context.GetKey() != c.GetKey() && context.GetWindowName() == windowName {
self.windowViewNameMap().Set(windowName, context.GetViewName())
}
}
}
}
}
// moves given context's view to the top of the window
func (self *WindowHelper) MoveToTopOfWindow(context types.Context) {
view := context.GetView()
if view == nil {
return
}
window := context.GetWindowName()
topView := self.TopViewInWindow(window)
if view.Name() != topView.Name() {
if err := self.c.GocuiGui().SetViewOnTopOf(view.Name(), topView.Name()); err != nil {
self.c.Log.Error(err)
}
}
}
func (self *WindowHelper) TopViewInWindow(windowName string) *gocui.View {
// now I need to find all views in that same window, via contexts. And I guess then I need to find the index of the highest view in that list.
viewNamesInWindow := self.viewNamesInWindow(windowName)
// The views list is ordered highest-last, so we're grabbing the last view of the window
var topView *gocui.View
for _, currentView := range self.c.GocuiGui().Views() {
if lo.Contains(viewNamesInWindow, currentView.Name()) {
topView = currentView
}
}
return topView
}
func (self *WindowHelper) viewNamesInWindow(windowName string) []string {
result := []string{}
for _, context := range self.contexts.Flatten() {
if context.GetWindowName() == windowName {
result = append(result, context.GetViewName())
}
}
return result
}
func (self *WindowHelper) WindowForView(viewName string) string {
context, ok := self.viewHelper.ContextForView(viewName)
if !ok {
panic("todo: deal with this")
}
return context.GetWindowName()
}

View File

@ -12,6 +12,9 @@ import (
"github.com/samber/lo"
)
// after selecting the 200th commit, we'll load in all the rest
const COMMIT_THRESHOLD = 200
type (
PullFilesFn func() error
)
@ -150,6 +153,50 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
return bindings
}
func (self *LocalCommitsController) GetOnRenderToMain() func() error {
return func() error {
return self.helpers.Diff.WithDiffModeCheck(func() error {
var task types.UpdateTask
commit := self.context().GetSelected()
if commit == nil {
task = types.NewRenderStringTask(self.c.Tr.NoCommitsThisBranch)
} else if commit.Action == todo.UpdateRef {
task = types.NewRenderStringTask(
utils.ResolvePlaceholderString(
self.c.Tr.UpdateRefHere,
map[string]string{
"ref": commit.Name,
}))
} else {
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Sha, self.c.Modes().Filtering.GetPath(), self.c.State().GetIgnoreWhitespaceInDiffView())
task = types.NewRunPtyTask(cmdObj.GetCmd())
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: "Patch",
Task: task,
},
Secondary: secondaryPatchPanelUpdateOpts(self.c),
})
})
}
}
func secondaryPatchPanelUpdateOpts(c *types.HelperCommon) *types.ViewUpdateOpts {
if c.Git().Patch.PatchBuilder.Active() {
patch := c.Git().Patch.PatchBuilder.RenderAggregatedPatch(false)
return &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(patch),
Title: c.Tr.CustomPatch,
}
}
return nil
}
func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
if self.context().GetSelectedLineIdx() >= len(self.model.Commits)-1 {
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
@ -753,6 +800,22 @@ func (self *LocalCommitsController) checkSelected(callback func(*models.Commit)
}
}
func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) error {
return func(types.OnFocusOpts) error {
context := self.context()
if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() {
context.SetLimitCommits(false)
go utils.Safe(func() {
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}}); err != nil {
_ = self.c.Error(err)
}
})
}
return nil
}
}
func (self *LocalCommitsController) Context() types.Context {
return self.context()
}

View File

@ -44,6 +44,14 @@ func (self *MenuController) GetOnClick() func() error {
return self.press
}
func (self *MenuController) GetOnFocus() func(types.OnFocusOpts) error {
return func(types.OnFocusOpts) error {
selectedMenuItem := self.context().GetSelected()
self.c.Views().Tooltip.SetContent(selectedMenuItem.Tooltip)
return nil
}
}
func (self *MenuController) press() error {
return self.context().OnMenuPress(self.context().GetSelected())
}

View File

@ -134,6 +134,24 @@ func (self *MergeConflictsController) GetMouseKeybindings(opts types.Keybindings
}
}
func (self *MergeConflictsController) GetOnFocus() func(types.OnFocusOpts) error {
return func(types.OnFocusOpts) error {
self.c.Views().MergeConflicts.Wrap = false
return self.helpers.MergeConflicts.Render(true)
}
}
func (self *MergeConflictsController) GetOnFocusLost() func(types.OnFocusLostOpts) error {
return func(types.OnFocusLostOpts) error {
self.context().SetUserScrolling(false)
self.context().GetState().ResetConflictSelection()
self.c.Views().MergeConflicts.Wrap = true
return nil
}
}
func (self *MergeConflictsController) HandleScrollUp() error {
self.context().SetUserScrolling(true)
self.context().GetViewTrait().ScrollUp(self.c.UserConfig.Gui.ScrollHeight)

View File

@ -0,0 +1,53 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type ReflogCommitsController struct {
baseController
*controllerCommon
context *context.ReflogCommitsContext
}
var _ types.IController = &ReflogCommitsController{}
func NewReflogCommitsController(
common *controllerCommon,
context *context.ReflogCommitsContext,
) *ReflogCommitsController {
return &ReflogCommitsController{
baseController: baseController{},
controllerCommon: common,
context: context,
}
}
func (self *ReflogCommitsController) Context() types.Context {
return self.context
}
func (self *ReflogCommitsController) GetOnRenderToMain() func() error {
return func() error {
return self.helpers.Diff.WithDiffModeCheck(func() error {
commit := self.context.GetSelected()
var task types.UpdateTask
if commit == nil {
task = types.NewRenderStringTask("No reflog history")
} else {
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Sha, self.c.Modes().Filtering.GetPath(), self.c.State().GetIgnoreWhitespaceInDiffView())
task = types.NewRunPtyTask(cmdObj.GetCmd())
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: "Reflog Entry",
Task: task,
},
})
})
}
}

View File

@ -73,6 +73,29 @@ func (self *RemoteBranchesController) GetKeybindings(opts types.KeybindingsOpts)
}
}
func (self *RemoteBranchesController) GetOnRenderToMain() func() error {
return func() error {
return self.helpers.Diff.WithDiffModeCheck(func() error {
var task types.UpdateTask
remoteBranch := self.context().GetSelected()
if remoteBranch == nil {
task = types.NewRenderStringTask("No branches for this remote")
} else {
cmdObj := self.git.Branch.GetGraphCmdObj(remoteBranch.FullRefName())
task = types.NewRunCommandTask(cmdObj.GetCmd())
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: "Remote Branch",
Task: task,
},
})
})
}
}
func (self *RemoteBranchesController) Context() types.Context {
return self.context()
}

View File

@ -1,8 +1,12 @@
package controllers
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@ -60,6 +64,28 @@ func (self *RemotesController) GetKeybindings(opts types.KeybindingsOpts) []*typ
return bindings
}
func (self *RemotesController) GetOnRenderToMain() func() error {
return func() error {
return self.helpers.Diff.WithDiffModeCheck(func() error {
var task types.UpdateTask
remote := self.context.GetSelected()
if remote == nil {
task = types.NewRenderStringTask("No remotes")
} else {
task = types.NewRenderStringTask(fmt.Sprintf("%s\nUrls:\n%s", style.FgGreen.Sprint(remote.Name), strings.Join(remote.Urls, "\n")))
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: "Remote",
Task: task,
},
})
})
}
}
func (self *RemotesController) GetOnClick() func() error {
return self.checkSelected(self.enter)
}

View File

@ -8,20 +8,16 @@ import (
type SnakeController struct {
baseController
*controllerCommon
getGame func() *snake.Game
}
var _ types.IController = &SnakeController{}
func NewSnakeController(
common *controllerCommon,
getGame func() *snake.Game,
) *SnakeController {
return &SnakeController{
baseController: baseController{},
controllerCommon: common,
getGame: getGame,
}
}
@ -56,9 +52,24 @@ func (self *SnakeController) Context() types.Context {
return self.contexts.Snake
}
func (self *SnakeController) GetOnFocus() func(types.OnFocusOpts) error {
return func(types.OnFocusOpts) error {
self.helpers.Snake.StartGame()
return nil
}
}
func (self *SnakeController) GetOnFocusLost() func(types.OnFocusLostOpts) error {
return func(types.OnFocusLostOpts) error {
self.helpers.Snake.ExitGame()
self.helpers.Window.MoveToTopOfWindow(self.contexts.Submodules)
return nil
}
}
func (self *SnakeController) SetDirection(direction snake.Direction) func() error {
return func() error {
self.getGame().SetDirection(direction)
self.helpers.Snake.SetDirection(direction)
return nil
}
}

View File

@ -55,6 +55,28 @@ func (self *StashController) GetKeybindings(opts types.KeybindingsOpts) []*types
return bindings
}
func (self *StashController) GetOnRenderToMain() func() error {
return func() error {
return self.helpers.Diff.WithDiffModeCheck(func() error {
var task types.UpdateTask
stashEntry := self.context().GetSelected()
if stashEntry == nil {
task = types.NewRenderStringTask(self.c.Tr.NoStashEntries)
} else {
task = types.NewRunPtyTask(self.git.Stash.ShowStashEntryCmdObj(stashEntry.Index).GetCmd())
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: "Stash",
Task: task,
},
})
})
}
}
func (self *StashController) checkSelected(callback func(*models.StashEntry) error) func() error {
return func() error {
item := self.context().GetSelected()

View File

@ -0,0 +1,198 @@
package controllers
import (
"errors"
"fmt"
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type StatusController struct {
baseController
*controllerCommon
}
var _ types.IController = &StatusController{}
func NewStatusController(
common *controllerCommon,
) *StatusController {
return &StatusController{
baseController: baseController{},
controllerCommon: common,
}
}
func (self *StatusController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.OpenFile),
Handler: self.openConfig,
Description: self.c.Tr.OpenConfig,
},
{
Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: self.editConfig,
Description: self.c.Tr.EditConfig,
},
{
Key: opts.GetKey(opts.Config.Status.CheckForUpdate),
Handler: self.handleCheckForUpdate,
Description: self.c.Tr.LcCheckForUpdate,
},
{
Key: opts.GetKey(opts.Config.Status.RecentRepos),
Handler: self.helpers.Repos.CreateRecentReposMenu,
Description: self.c.Tr.SwitchRepo,
},
{
Key: opts.GetKey(opts.Config.Status.AllBranchesLogGraph),
Handler: self.showAllBranchLogs,
Description: self.c.Tr.LcAllBranchesLogGraph,
},
}
return bindings
}
func (self *StatusController) GetOnRenderToMain() func() error {
return func() error {
dashboardString := strings.Join(
[]string{
lazygitTitle(),
"Copyright 2022 Jesse Duffield",
fmt.Sprintf("Keybindings: %s", constants.Links.Docs.Keybindings),
fmt.Sprintf("Config Options: %s", constants.Links.Docs.Config),
fmt.Sprintf("Tutorial: %s", constants.Links.Docs.Tutorial),
fmt.Sprintf("Raise an Issue: %s", constants.Links.Issues),
fmt.Sprintf("Release Notes: %s", constants.Links.Releases),
style.FgMagenta.Sprintf("Become a sponsor: %s", constants.Links.Donate), // caffeine ain't free
}, "\n\n")
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: self.c.Tr.StatusTitle,
Task: types.NewRenderStringTask(dashboardString),
},
})
}
}
func (self *StatusController) GetOnClick() func() error {
return self.onClick
}
func (self *StatusController) Context() types.Context {
return self.contexts.Status
}
func (self *StatusController) onClick() error {
// TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives)
currentBranch := self.helpers.Refs.GetCheckedOutRef()
if currentBranch == nil {
// need to wait for branches to refresh
return nil
}
if err := self.c.PushContext(self.Context()); err != nil {
return err
}
cx, _ := self.c.Views().Status.Cursor()
upstreamStatus := presentation.BranchStatus(currentBranch, self.c.Tr)
repoName := utils.GetCurrentRepoName()
workingTreeState := self.git.Status.WorkingTreeState()
switch workingTreeState {
case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING:
workingTreeStatus := fmt.Sprintf("(%s)", presentation.FormatWorkingTreeState(workingTreeState))
if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) {
return self.helpers.MergeAndRebase.CreateRebaseOptionsMenu()
}
if cursorInSubstring(cx, upstreamStatus+" "+workingTreeStatus+" ", repoName) {
return self.helpers.Repos.CreateRecentReposMenu()
}
default:
if cursorInSubstring(cx, upstreamStatus+" ", repoName) {
return self.helpers.Repos.CreateRecentReposMenu()
}
}
return nil
}
func runeCount(str string) int {
return len([]rune(str))
}
func cursorInSubstring(cx int, prefix string, substring string) bool {
return cx >= runeCount(prefix) && cx < runeCount(prefix+substring)
}
func lazygitTitle() string {
return `
_ _ _
| | (_) |
| | __ _ _____ _ __ _ _| |_
| |/ _` + "`" + ` |_ / | | |/ _` + "`" + ` | | __|
| | (_| |/ /| |_| | (_| | | |_
|_|\__,_/___|\__, |\__, |_|\__|
__/ | __/ |
|___/ |___/ `
}
func (self *StatusController) askForConfigFile(action func(file string) error) error {
confPaths := self.c.GetConfig().GetUserConfigPaths()
switch len(confPaths) {
case 0:
return errors.New(self.c.Tr.NoConfigFileFoundErr)
case 1:
return action(confPaths[0])
default:
menuItems := slices.Map(confPaths, func(path string) *types.MenuItem {
return &types.MenuItem{
Label: path,
OnPress: func() error {
return action(path)
},
}
})
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.SelectConfigFile,
Items: menuItems,
})
}
}
func (self *StatusController) openConfig() error {
return self.askForConfigFile(self.helpers.Files.OpenFile)
}
func (self *StatusController) editConfig() error {
return self.askForConfigFile(self.helpers.Files.EditFile)
}
func (self *StatusController) showAllBranchLogs() error {
cmdObj := self.git.Branch.AllBranchesLogCmdObj()
task := types.NewRunPtyTask(cmdObj.GetCmd())
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: self.c.Tr.LogTitle,
Task: task,
},
})
}
func (self *StatusController) handleCheckForUpdate() error {
return self.helpers.Update.CheckForUpdateInForeground()
}

View File

@ -0,0 +1,70 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type SubCommitsController struct {
baseController
*controllerCommon
context *context.SubCommitsContext
}
var _ types.IController = &SubCommitsController{}
func NewSubCommitsController(
common *controllerCommon,
context *context.SubCommitsContext,
) *SubCommitsController {
return &SubCommitsController{
baseController: baseController{},
controllerCommon: common,
context: context,
}
}
func (self *SubCommitsController) Context() types.Context {
return self.context
}
func (self *SubCommitsController) GetOnRenderToMain() func() error {
return func() error {
return self.helpers.Diff.WithDiffModeCheck(func() error {
commit := self.context.GetSelected()
var task types.UpdateTask
if commit == nil {
task = types.NewRenderStringTask("No commits")
} else {
cmdObj := self.git.Commit.ShowCmdObj(commit.Sha, self.modes.Filtering.GetPath(), self.c.State().GetIgnoreWhitespaceInDiffView())
task = types.NewRunPtyTask(cmdObj.GetCmd())
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: "Commit",
Task: task,
},
})
})
}
}
func (self *SubCommitsController) GetOnFocus() func() error {
return func() error {
context := self.context
if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() {
context.SetLimitCommits(false)
go utils.Safe(func() {
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUB_COMMITS}}); err != nil {
_ = self.c.Error(err)
}
})
}
return nil
}
}

View File

@ -14,20 +14,16 @@ import (
type SubmodulesController struct {
baseController
*controllerCommon
enterSubmodule func(submodule *models.SubmoduleConfig) error
}
var _ types.IController = &SubmodulesController{}
func NewSubmodulesController(
controllerCommon *controllerCommon,
enterSubmodule func(submodule *models.SubmoduleConfig) error,
) *SubmodulesController {
return &SubmodulesController{
baseController: baseController{},
controllerCommon: controllerCommon,
enterSubmodule: enterSubmodule,
}
}
@ -81,8 +77,43 @@ func (self *SubmodulesController) GetOnClick() func() error {
return self.checkSelected(self.enter)
}
func (self *SubmodulesController) GetOnRenderToMain() func() error {
return func() error {
return self.helpers.Diff.WithDiffModeCheck(func() error {
var task types.UpdateTask
submodule := self.context().GetSelected()
if submodule == nil {
task = types.NewRenderStringTask("No submodules")
} else {
prefix := fmt.Sprintf(
"Name: %s\nPath: %s\nUrl: %s\n\n",
style.FgGreen.Sprint(submodule.Name),
style.FgYellow.Sprint(submodule.Path),
style.FgCyan.Sprint(submodule.Url),
)
file := self.helpers.WorkingTree.FileForSubmodule(submodule)
if file == nil {
task = types.NewRenderStringTask(prefix)
} else {
cmdObj := self.git.WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, self.c.State().GetIgnoreWhitespaceInDiffView())
task = types.NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix)
}
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: "Submodule",
Task: task,
},
})
})
}
}
func (self *SubmodulesController) enter(submodule *models.SubmoduleConfig) error {
return self.enterSubmodule(submodule)
return self.helpers.Repos.EnterSubmodule(submodule)
}
func (self *SubmodulesController) add() error {

View File

@ -0,0 +1,43 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type SuggestionsController struct {
baseController
*controllerCommon
}
var _ types.IController = &SuggestionsController{}
func NewSuggestionsController(
common *controllerCommon,
) *SuggestionsController {
return &SuggestionsController{
baseController: baseController{},
controllerCommon: common,
}
}
func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{}
return bindings
}
func (self *SuggestionsController) GetOnFocusLost() func(types.OnFocusLostOpts) error {
return func(types.OnFocusLostOpts) error {
deactivateConfirmationPrompt
return nil
}
}
func (self *SuggestionsController) Context() types.Context {
return self.context()
}
func (self *SuggestionsController) context() *context.SuggestionsContext {
return self.contexts.Suggestions
}

View File

@ -1,6 +1,7 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@ -17,20 +18,20 @@ type CanSwitchToDiffFiles interface {
type SwitchToDiffFilesController struct {
baseController
*controllerCommon
context CanSwitchToDiffFiles
viewFiles func(SwitchToCommitFilesContextOpts) error
context CanSwitchToDiffFiles
diffFilesContext *context.CommitFilesContext
}
func NewSwitchToDiffFilesController(
controllerCommon *controllerCommon,
viewFiles func(SwitchToCommitFilesContextOpts) error,
context CanSwitchToDiffFiles,
diffFilesContext *context.CommitFilesContext,
) *SwitchToDiffFilesController {
return &SwitchToDiffFilesController{
baseController: baseController{},
controllerCommon: controllerCommon,
context: context,
viewFiles: viewFiles,
diffFilesContext: diffFilesContext,
}
}
@ -72,3 +73,22 @@ func (self *SwitchToDiffFilesController) enter(ref types.Ref) error {
func (self *SwitchToDiffFilesController) Context() types.Context {
return self.context
}
func (self *SwitchToDiffFilesController) viewFiles(opts SwitchToCommitFilesContextOpts) error {
diffFilesContext := self.diffFilesContext
diffFilesContext.SetSelectedLineIdx(0)
diffFilesContext.SetRef(opts.Ref)
diffFilesContext.SetTitleRef(opts.Ref.Description())
diffFilesContext.SetCanRebase(opts.CanRebase)
diffFilesContext.SetParentContext(opts.Context)
diffFilesContext.SetWindowName(opts.Context.GetWindowName())
if err := self.c.Refresh(types.RefreshOptions{
Scope: []types.RefreshableView{types.COMMIT_FILES},
}); err != nil {
return err
}
return self.c.PushContext(diffFilesContext)
}

View File

@ -56,6 +56,29 @@ func (self *TagsController) GetKeybindings(opts types.KeybindingsOpts) []*types.
return bindings
}
func (self *TagsController) GetOnRenderToMain() func() error {
return func() error {
return self.helpers.Diff.WithDiffModeCheck(func() error {
var task types.UpdateTask
tag := self.context().GetSelected()
if tag == nil {
task = types.NewRenderStringTask("No tags")
} else {
cmdObj := self.git.Branch.GetGraphCmdObj(tag.FullRefName())
task = types.NewRunCommandTask(cmdObj.GetCmd())
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: "Tag",
Task: task,
},
})
})
}
}
func (self *TagsController) checkout(tag *models.Tag) error {
self.c.LogAction(self.c.Tr.Actions.CheckoutTag)
if err := self.helpers.Refs.CheckoutRef(tag.Name, types.CheckoutRefOptions{}); err != nil {