mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 10:13:41 -05:00
The remaining refresh scopes each still read model, context, and mode state directly on their worker, racing the UI thread — the same class of race the commits refresh had: - files reads Model.Files (to detect resolved conflicts and drive the auto-stage) and the Files context's ForceShowUntracked; - reflog reads the existing reflog slices (for the incremental fetch), Model.HashPool and the filtering path/author; - branches reads Model.MainBranches and the previous branches (for the BehindBaseBranch carry-over); - stash reads the filtering path. Gather each scope's inputs into an immutable snapshot on the UI thread (via captureOnUIThread) before dispatching the git work, and have the refresh compute from the snapshot — for branches, threaded through both the immediate and the recency-sorted startup loads, which share one snapshot (the BehindBaseBranch carry-over is identical either way). Status, tags and worktrees read nothing UI-owned, so they're left alone. For the snapshots to actually run on the UI thread, the worker callers that reach these scopes must announce themselves: convert the submodule operations, the submodule stash-and-reset, and the background files poller to RefreshFromWorker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
326 lines
10 KiB
Go
326 lines
10 KiB
Go
package controllers
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
|
"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"
|
|
)
|
|
|
|
type SubmodulesController struct {
|
|
baseController
|
|
*ListControllerTrait[*models.SubmoduleConfig]
|
|
c *ControllerCommon
|
|
}
|
|
|
|
var _ types.IController = &SubmodulesController{}
|
|
|
|
func NewSubmodulesController(
|
|
c *ControllerCommon,
|
|
) *SubmodulesController {
|
|
return &SubmodulesController{
|
|
baseController: baseController{},
|
|
ListControllerTrait: NewListControllerTrait(
|
|
c,
|
|
c.Contexts().Submodules,
|
|
c.Contexts().Submodules.GetSelected,
|
|
c.Contexts().Submodules.GetSelectedItems,
|
|
),
|
|
c: c,
|
|
}
|
|
}
|
|
|
|
func (self *SubmodulesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
|
return []*types.Binding{
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.GoInto),
|
|
Handler: self.withItem(self.enter),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.Enter,
|
|
Tooltip: utils.ResolvePlaceholderString(self.c.Tr.EnterSubmoduleTooltip,
|
|
map[string]string{"escape": opts.Config.Universal.Return.String()}),
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.Select),
|
|
Handler: self.withItem(self.enter),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.Remove),
|
|
Handler: self.withItem(self.remove),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.Remove,
|
|
Tooltip: self.c.Tr.RemoveSubmoduleTooltip,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Submodules.Update),
|
|
Handler: self.withItem(self.update),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.Update,
|
|
Tooltip: self.c.Tr.SubmoduleUpdateTooltip,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.New),
|
|
Handler: self.add,
|
|
Description: self.c.Tr.NewSubmodule,
|
|
DisplayOnScreen: true,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.Edit),
|
|
Handler: self.withItem(self.editURL),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.EditSubmoduleUrl,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Submodules.Init),
|
|
Handler: self.withItem(self.init),
|
|
GetDisabledReason: self.require(self.singleItemSelected()),
|
|
Description: self.c.Tr.Initialize,
|
|
Tooltip: self.c.Tr.InitSubmoduleTooltip,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Submodules.BulkMenu),
|
|
Handler: self.openBulkActionsMenu,
|
|
Description: self.c.Tr.ViewBulkSubmoduleOptions,
|
|
OpensMenu: true,
|
|
},
|
|
{
|
|
Handler: self.easterEgg,
|
|
Description: self.c.Tr.EasterEgg,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (self *SubmodulesController) GetOnDoubleClick() func() error {
|
|
return self.withItemGraceful(self.enter)
|
|
}
|
|
|
|
func (self *SubmodulesController) GetOnRenderToMain() func() {
|
|
return func() {
|
|
self.c.Helpers().Diff.WithDiffModeCheck(func() {
|
|
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.FullName()),
|
|
style.FgYellow.Sprint(submodule.FullPath()),
|
|
style.FgCyan.Sprint(submodule.Url),
|
|
)
|
|
|
|
file := self.c.Helpers().WorkingTree.FileForSubmodule(submodule)
|
|
if file == nil {
|
|
task = types.NewRenderStringTask(prefix)
|
|
} else {
|
|
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, nil)
|
|
task = types.NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix)
|
|
}
|
|
}
|
|
|
|
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.c.Helpers().Repos.EnterSubmodule(submodule)
|
|
}
|
|
|
|
func (self *SubmodulesController) add() error {
|
|
self.c.Prompt(types.PromptOpts{
|
|
Title: self.c.Tr.NewSubmoduleUrl,
|
|
HandleConfirm: func(submoduleUrl string) error {
|
|
nameSuggestion := filepath.Base(strings.TrimSuffix(submoduleUrl, filepath.Ext(submoduleUrl)))
|
|
|
|
self.c.Prompt(types.PromptOpts{
|
|
Title: self.c.Tr.NewSubmoduleName,
|
|
InitialContent: nameSuggestion,
|
|
HandleConfirm: func(submoduleName string) error {
|
|
self.c.Prompt(types.PromptOpts{
|
|
Title: self.c.Tr.NewSubmodulePath,
|
|
InitialContent: submoduleName,
|
|
HandleConfirm: func(submodulePath string) error {
|
|
return self.c.WithWaitingStatus(self.c.Tr.AddingSubmoduleStatus, func(gocui.Task) error {
|
|
self.c.LogAction(self.c.Tr.Actions.AddSubmodule)
|
|
err := self.c.Git().Submodule.Add(submoduleName, submodulePath, submoduleUrl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
|
return nil
|
|
})
|
|
},
|
|
})
|
|
|
|
return nil
|
|
},
|
|
})
|
|
|
|
return nil
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *SubmodulesController) editURL(submodule *models.SubmoduleConfig) error {
|
|
self.c.Prompt(types.PromptOpts{
|
|
Title: fmt.Sprintf(self.c.Tr.UpdateSubmoduleUrl, submodule.FullName()),
|
|
InitialContent: submodule.Url,
|
|
HandleConfirm: func(newUrl string) error {
|
|
return self.c.WithWaitingStatus(self.c.Tr.UpdatingSubmoduleUrlStatus, func(gocui.Task) error {
|
|
self.c.LogAction(self.c.Tr.Actions.UpdateSubmoduleUrl)
|
|
err := self.c.Git().Submodule.UpdateUrl(submodule, newUrl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
|
return nil
|
|
})
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *SubmodulesController) init(submodule *models.SubmoduleConfig) error {
|
|
return self.c.WithWaitingStatus(self.c.Tr.InitializingSubmoduleStatus, func(gocui.Task) error {
|
|
self.c.LogAction(self.c.Tr.Actions.InitialiseSubmodule)
|
|
err := self.c.Git().Submodule.Init(submodule.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (self *SubmodulesController) openBulkActionsMenu() error {
|
|
return self.c.Menu(types.CreateMenuOptions{
|
|
Title: self.c.Tr.BulkSubmoduleOptions,
|
|
Items: []*types.MenuItem{
|
|
{
|
|
LabelColumns: []string{self.c.Tr.BulkInitSubmodules, style.FgGreen.Sprint(self.c.Git().Submodule.BulkInitCmdObj().ToString())},
|
|
OnPress: func() error {
|
|
return self.c.WithWaitingStatus(self.c.Tr.RunningCommand, func(gocui.Task) error {
|
|
self.c.LogAction(self.c.Tr.Actions.BulkInitialiseSubmodules)
|
|
err := self.c.Git().Submodule.BulkInitCmdObj().Run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
|
return nil
|
|
})
|
|
},
|
|
Keys: menuKey('i'),
|
|
},
|
|
{
|
|
LabelColumns: []string{self.c.Tr.BulkUpdateSubmodules, style.FgYellow.Sprint(self.c.Git().Submodule.BulkUpdateCmdObj().ToString())},
|
|
OnPress: func() error {
|
|
return self.c.WithWaitingStatus(self.c.Tr.RunningCommand, func(gocui.Task) error {
|
|
self.c.LogAction(self.c.Tr.Actions.BulkUpdateSubmodules)
|
|
if err := self.c.Git().Submodule.BulkUpdateCmdObj().Run(); err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
|
return nil
|
|
})
|
|
},
|
|
Keys: menuKey('u'),
|
|
},
|
|
{
|
|
LabelColumns: []string{self.c.Tr.BulkUpdateRecursiveSubmodules, style.FgYellow.Sprint(self.c.Git().Submodule.BulkUpdateRecursivelyCmdObj().ToString())},
|
|
OnPress: func() error {
|
|
return self.c.WithWaitingStatus(self.c.Tr.RunningCommand, func(gocui.Task) error {
|
|
self.c.LogAction(self.c.Tr.Actions.BulkUpdateRecursiveSubmodules)
|
|
if err := self.c.Git().Submodule.BulkUpdateRecursivelyCmdObj().Run(); err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
|
return nil
|
|
})
|
|
},
|
|
Keys: menuKey('r'),
|
|
},
|
|
{
|
|
LabelColumns: []string{self.c.Tr.BulkDeinitSubmodules, style.FgRed.Sprint(self.c.Git().Submodule.BulkDeinitCmdObj().ToString())},
|
|
OnPress: func() error {
|
|
return self.c.WithWaitingStatus(self.c.Tr.RunningCommand, func(gocui.Task) error {
|
|
self.c.LogAction(self.c.Tr.Actions.BulkDeinitialiseSubmodules)
|
|
if err := self.c.Git().Submodule.BulkDeinitCmdObj().Run(); err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
|
return nil
|
|
})
|
|
},
|
|
Keys: menuKey('d'),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func (self *SubmodulesController) update(submodule *models.SubmoduleConfig) error {
|
|
return self.c.WithWaitingStatus(self.c.Tr.UpdatingSubmoduleStatus, func(gocui.Task) error {
|
|
self.c.LogAction(self.c.Tr.Actions.UpdateSubmodule)
|
|
err := self.c.Git().Submodule.Update(submodule.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES}})
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (self *SubmodulesController) remove(submodule *models.SubmoduleConfig) error {
|
|
self.c.Confirm(types.ConfirmOpts{
|
|
Title: self.c.Tr.RemoveSubmodule,
|
|
Prompt: fmt.Sprintf(self.c.Tr.RemoveSubmodulePrompt, submodule.FullName()),
|
|
HandleConfirm: func() error {
|
|
self.c.LogAction(self.c.Tr.Actions.RemoveSubmodule)
|
|
if err := self.c.Git().Submodule.Delete(submodule); err != nil {
|
|
return err
|
|
}
|
|
|
|
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUBMODULES, types.FILES}})
|
|
return nil
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *SubmodulesController) easterEgg() error {
|
|
self.c.Context().Push(self.c.Contexts().Snake, types.OnFocusOpts{})
|
|
return nil
|
|
}
|
|
|
|
func (self *SubmodulesController) context() *context.SubmodulesContext {
|
|
return self.c.Contexts().Submodules
|
|
}
|