mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-11 16:16:28 -04:00
The focused main view is the staging surface, so it has to resolve the diff it shows to patch-space. A pager that restructures the diff without emitting our metadata (stock delta-default, plain difftastic, `cat -n`) produces output the buffer parser can't read, which would leave its diff unstageable. When such a diff is focused, re-render it raw — git's own colour, no pager — the same content a no-pager setup shows, which the buffer parser handles. Browsing keeps the pretty pager output; only focusing to act switches to raw. Whether the pager is usable is decided by probing it: run it on empty input and look for the version-only OSC 1717 handshake a metadata-aware pager emits first. This is a pager-level, content-independent fact (a binary file, which has no change lines under any pager, can't mislead it) and it's known before we render, so we never render pretty only to discover mid-flight that we should have rendered raw. The verdict is cached per pager (reset when the pager changes). No PTY is needed — git needs a terminal to decide to invoke a pager, but the pager emits the handshake regardless. A git-config external diff driver (useExternalDiffGitConfig) is chosen per file via .gitattributes and a single diff can mix drivers, so there's no one pager to probe; it's treated as unsupported (always raw). Bypassing the pager needs two things, since a pager reaches the diff by two routes: an external diff command (suppressed in the cmd via the new ignoreExternalDiff arg, keeping git's colour, unlike plain) and a stdin pager (GIT_PAGER, applied by the pty task — so the raw render uses a plain command task instead). This wires the files panel; the commit/stash/patch-building panels follow. 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, false, 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
|
|
}
|