mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-31 14:24:25 +03:00
add submodules context
This commit is contained in:
@ -5,3 +5,15 @@ type SubmoduleConfig struct {
|
|||||||
Path string
|
Path string
|
||||||
Url string
|
Url string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *SubmoduleConfig) RefName() string {
|
||||||
|
return r.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SubmoduleConfig) ID() string {
|
||||||
|
return r.RefName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SubmoduleConfig) Description() string {
|
||||||
|
return r.RefName()
|
||||||
|
}
|
||||||
|
@ -34,6 +34,7 @@ const (
|
|||||||
CONFIRMATION_CONTEXT_KEY = "confirmation"
|
CONFIRMATION_CONTEXT_KEY = "confirmation"
|
||||||
SEARCH_CONTEXT_KEY = "search"
|
SEARCH_CONTEXT_KEY = "search"
|
||||||
COMMIT_MESSAGE_CONTEXT_KEY = "commitMessage"
|
COMMIT_MESSAGE_CONTEXT_KEY = "commitMessage"
|
||||||
|
SUBMODULES_CONTEXT_KEY = "submodules"
|
||||||
)
|
)
|
||||||
|
|
||||||
var allContextKeys = []string{
|
var allContextKeys = []string{
|
||||||
@ -57,6 +58,64 @@ var allContextKeys = []string{
|
|||||||
CONFIRMATION_CONTEXT_KEY,
|
CONFIRMATION_CONTEXT_KEY,
|
||||||
SEARCH_CONTEXT_KEY,
|
SEARCH_CONTEXT_KEY,
|
||||||
COMMIT_MESSAGE_CONTEXT_KEY,
|
COMMIT_MESSAGE_CONTEXT_KEY,
|
||||||
|
SUBMODULES_CONTEXT_KEY,
|
||||||
|
}
|
||||||
|
|
||||||
|
type SimpleContextNode struct {
|
||||||
|
Context Context
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemotesContextNode struct {
|
||||||
|
Context Context
|
||||||
|
Branches SimpleContextNode
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextTree struct {
|
||||||
|
Status SimpleContextNode
|
||||||
|
Files SimpleContextNode
|
||||||
|
Submodules SimpleContextNode
|
||||||
|
Menu SimpleContextNode
|
||||||
|
Branches SimpleContextNode
|
||||||
|
Remotes RemotesContextNode
|
||||||
|
Tags SimpleContextNode
|
||||||
|
BranchCommits SimpleContextNode
|
||||||
|
CommitFiles SimpleContextNode
|
||||||
|
ReflogCommits SimpleContextNode
|
||||||
|
SubCommits SimpleContextNode
|
||||||
|
Stash SimpleContextNode
|
||||||
|
Normal SimpleContextNode
|
||||||
|
Staging SimpleContextNode
|
||||||
|
PatchBuilding SimpleContextNode
|
||||||
|
Merging SimpleContextNode
|
||||||
|
Credentials SimpleContextNode
|
||||||
|
Confirmation SimpleContextNode
|
||||||
|
CommitMessage SimpleContextNode
|
||||||
|
Search SimpleContextNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) allContexts() []Context {
|
||||||
|
return []Context{
|
||||||
|
gui.Contexts.Status.Context,
|
||||||
|
gui.Contexts.Files.Context,
|
||||||
|
gui.Contexts.Submodules.Context,
|
||||||
|
gui.Contexts.Branches.Context,
|
||||||
|
gui.Contexts.Remotes.Context,
|
||||||
|
gui.Contexts.Remotes.Branches.Context,
|
||||||
|
gui.Contexts.Tags.Context,
|
||||||
|
gui.Contexts.BranchCommits.Context,
|
||||||
|
gui.Contexts.CommitFiles.Context,
|
||||||
|
gui.Contexts.ReflogCommits.Context,
|
||||||
|
gui.Contexts.Stash.Context,
|
||||||
|
gui.Contexts.Menu.Context,
|
||||||
|
gui.Contexts.Confirmation.Context,
|
||||||
|
gui.Contexts.Credentials.Context,
|
||||||
|
gui.Contexts.CommitMessage.Context,
|
||||||
|
gui.Contexts.Normal.Context,
|
||||||
|
gui.Contexts.Staging.Context,
|
||||||
|
gui.Contexts.Merging.Context,
|
||||||
|
gui.Contexts.PatchBuilding.Context,
|
||||||
|
gui.Contexts.SubCommits.Context,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Context interface {
|
type Context interface {
|
||||||
@ -139,61 +198,6 @@ func (c BasicContext) GetKey() string {
|
|||||||
return c.Key
|
return c.Key
|
||||||
}
|
}
|
||||||
|
|
||||||
type SimpleContextNode struct {
|
|
||||||
Context Context
|
|
||||||
}
|
|
||||||
|
|
||||||
type RemotesContextNode struct {
|
|
||||||
Context Context
|
|
||||||
Branches SimpleContextNode
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContextTree struct {
|
|
||||||
Status SimpleContextNode
|
|
||||||
Files SimpleContextNode
|
|
||||||
Menu SimpleContextNode
|
|
||||||
Branches SimpleContextNode
|
|
||||||
Remotes RemotesContextNode
|
|
||||||
Tags SimpleContextNode
|
|
||||||
BranchCommits SimpleContextNode
|
|
||||||
CommitFiles SimpleContextNode
|
|
||||||
ReflogCommits SimpleContextNode
|
|
||||||
SubCommits SimpleContextNode
|
|
||||||
Stash SimpleContextNode
|
|
||||||
Normal SimpleContextNode
|
|
||||||
Staging SimpleContextNode
|
|
||||||
PatchBuilding SimpleContextNode
|
|
||||||
Merging SimpleContextNode
|
|
||||||
Credentials SimpleContextNode
|
|
||||||
Confirmation SimpleContextNode
|
|
||||||
CommitMessage SimpleContextNode
|
|
||||||
Search SimpleContextNode
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) allContexts() []Context {
|
|
||||||
return []Context{
|
|
||||||
gui.Contexts.Status.Context,
|
|
||||||
gui.Contexts.Files.Context,
|
|
||||||
gui.Contexts.Branches.Context,
|
|
||||||
gui.Contexts.Remotes.Context,
|
|
||||||
gui.Contexts.Remotes.Branches.Context,
|
|
||||||
gui.Contexts.Tags.Context,
|
|
||||||
gui.Contexts.BranchCommits.Context,
|
|
||||||
gui.Contexts.CommitFiles.Context,
|
|
||||||
gui.Contexts.ReflogCommits.Context,
|
|
||||||
gui.Contexts.Stash.Context,
|
|
||||||
gui.Contexts.Menu.Context,
|
|
||||||
gui.Contexts.Confirmation.Context,
|
|
||||||
gui.Contexts.Credentials.Context,
|
|
||||||
gui.Contexts.CommitMessage.Context,
|
|
||||||
gui.Contexts.Normal.Context,
|
|
||||||
gui.Contexts.Staging.Context,
|
|
||||||
gui.Contexts.Merging.Context,
|
|
||||||
gui.Contexts.PatchBuilding.Context,
|
|
||||||
gui.Contexts.SubCommits.Context,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) contextTree() ContextTree {
|
func (gui *Gui) contextTree() ContextTree {
|
||||||
return ContextTree{
|
return ContextTree{
|
||||||
Status: SimpleContextNode{
|
Status: SimpleContextNode{
|
||||||
@ -207,6 +211,9 @@ func (gui *Gui) contextTree() ContextTree {
|
|||||||
Files: SimpleContextNode{
|
Files: SimpleContextNode{
|
||||||
Context: gui.filesListContext(),
|
Context: gui.filesListContext(),
|
||||||
},
|
},
|
||||||
|
Submodules: SimpleContextNode{
|
||||||
|
Context: gui.submodulesListContext(),
|
||||||
|
},
|
||||||
Menu: SimpleContextNode{
|
Menu: SimpleContextNode{
|
||||||
Context: gui.menuListContext(),
|
Context: gui.menuListContext(),
|
||||||
},
|
},
|
||||||
@ -365,6 +372,18 @@ func (gui *Gui) viewTabContextMap() map[string][]tabContext {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"files": {
|
||||||
|
{
|
||||||
|
tab: "Files",
|
||||||
|
contexts: []Context{gui.Contexts.Files.Context},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: "Submodules",
|
||||||
|
contexts: []Context{
|
||||||
|
gui.Contexts.Submodules.Context,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,8 @@ func (gui *Gui) currentDiffTerminals() []string {
|
|||||||
switch gui.currentContext().GetKey() {
|
switch gui.currentContext().GetKey() {
|
||||||
case "":
|
case "":
|
||||||
return nil
|
return nil
|
||||||
case FILES_CONTEXT_KEY:
|
case FILES_CONTEXT_KEY, SUBMODULES_CONTEXT_KEY:
|
||||||
|
// TODO: should we just return nil here?
|
||||||
return []string{""}
|
return []string{""}
|
||||||
case COMMIT_FILES_CONTEXT_KEY:
|
case COMMIT_FILES_CONTEXT_KEY:
|
||||||
return []string{gui.State.Panels.CommitFiles.refName}
|
return []string{gui.State.Panels.CommitFiles.refName}
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (gui *Gui) submoduleFromFile(file *models.File) *models.SubmoduleConfig {
|
func (gui *Gui) submoduleFromFile(file *models.File) *models.SubmoduleConfig {
|
||||||
for _, config := range gui.State.SubmoduleConfigs {
|
for _, config := range gui.State.Submodules {
|
||||||
if config.Name == file.Name {
|
if config.Name == file.Name {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
@ -23,7 +23,7 @@ func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
|
|||||||
|
|
||||||
var menuItems []*menuItem
|
var menuItems []*menuItem
|
||||||
|
|
||||||
submoduleConfigs := gui.State.SubmoduleConfigs
|
submoduleConfigs := gui.State.Submodules
|
||||||
if file.IsSubmodule(submoduleConfigs) {
|
if file.IsSubmodule(submoduleConfigs) {
|
||||||
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
|
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
// "strings"
|
// "strings"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -80,7 +79,7 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
|
|||||||
return gui.refreshMainViews(refreshOpts)
|
return gui.refreshMainViews(refreshOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) refreshFiles() error {
|
func (gui *Gui) refreshFilesAndSubmodules() error {
|
||||||
gui.State.RefreshingFilesMutex.Lock()
|
gui.State.RefreshingFilesMutex.Lock()
|
||||||
gui.State.IsRefreshingFiles = true
|
gui.State.IsRefreshingFiles = true
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -103,15 +102,25 @@ func (gui *Gui) refreshFiles() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gui.g.Update(func(g *gocui.Gui) error {
|
gui.g.Update(func(g *gocui.Gui) error {
|
||||||
if err := gui.Contexts.Files.Context.HandleRender(); err != nil {
|
if err := gui.postRefreshUpdate(gui.Contexts.Submodules.Context); err != nil {
|
||||||
return err
|
gui.Log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if g.CurrentView() == filesView || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == MAIN_MERGING_CONTEXT_KEY) {
|
if gui.getFilesView().Context == FILES_CONTEXT_KEY {
|
||||||
|
// doing this a little custom (as opposed to using gui.postRefreshUpdate) because we handle selecting the file explicitly below
|
||||||
|
if err := gui.Contexts.Files.Context.HandleRender(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gui.currentContext().GetKey() == FILES_CONTEXT_KEY || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == MAIN_MERGING_CONTEXT_KEY) {
|
||||||
newSelectedFile := gui.getSelectedFile()
|
newSelectedFile := gui.getSelectedFile()
|
||||||
alreadySelected := selectedFile != nil && newSelectedFile != nil && newSelectedFile.Name == selectedFile.Name
|
alreadySelected := selectedFile != nil && newSelectedFile != nil && newSelectedFile.Name == selectedFile.Name
|
||||||
return gui.selectFile(alreadySelected)
|
if err := gui.selectFile(alreadySelected); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -161,15 +170,10 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
submoduleConfigs := gui.State.SubmoduleConfigs
|
submoduleConfigs := gui.State.Submodules
|
||||||
if file.IsSubmodule(submoduleConfigs) {
|
if file.IsSubmodule(submoduleConfigs) {
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
gui.State.RepoPathStack = append(gui.State.RepoPathStack, wd)
|
|
||||||
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
|
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
|
||||||
return gui.dispatchSwitchToRepo(submoduleConfig.Path)
|
return gui.enterSubmodule(submoduleConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
if file.HasInlineMergeConflicts {
|
if file.HasInlineMergeConflicts {
|
||||||
@ -325,7 +329,7 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
|
|||||||
if err := gui.GitCommand.StageAll(); err != nil {
|
if err := gui.GitCommand.StageAll(); err != nil {
|
||||||
return gui.surfaceError(err)
|
return gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
if err := gui.refreshFiles(); err != nil {
|
if err := gui.refreshFilesAndSubmodules(); err != nil {
|
||||||
return gui.surfaceError(err)
|
return gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -448,7 +452,7 @@ func (gui *Gui) refreshStateSubmoduleConfigs() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.State.SubmoduleConfigs = configs
|
gui.State.Submodules = configs
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -209,6 +209,10 @@ type commitFilesPanelState struct {
|
|||||||
canRebase bool
|
canRebase bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type submodulePanelState struct {
|
||||||
|
listPanelState
|
||||||
|
}
|
||||||
|
|
||||||
type panelStates struct {
|
type panelStates struct {
|
||||||
Files *filePanelState
|
Files *filePanelState
|
||||||
Branches *branchPanelState
|
Branches *branchPanelState
|
||||||
@ -223,6 +227,7 @@ type panelStates struct {
|
|||||||
LineByLine *lineByLinePanelState
|
LineByLine *lineByLinePanelState
|
||||||
Merging *mergingPanelState
|
Merging *mergingPanelState
|
||||||
CommitFiles *commitFilesPanelState
|
CommitFiles *commitFilesPanelState
|
||||||
|
Submodules *submodulePanelState
|
||||||
}
|
}
|
||||||
|
|
||||||
type searchingState struct {
|
type searchingState struct {
|
||||||
@ -273,12 +278,12 @@ type Modes struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type guiState struct {
|
type guiState struct {
|
||||||
Files []*models.File
|
Files []*models.File
|
||||||
SubmoduleConfigs []*models.SubmoduleConfig
|
Submodules []*models.SubmoduleConfig
|
||||||
Branches []*models.Branch
|
Branches []*models.Branch
|
||||||
Commits []*models.Commit
|
Commits []*models.Commit
|
||||||
StashEntries []*models.StashEntry
|
StashEntries []*models.StashEntry
|
||||||
CommitFiles []*models.CommitFile
|
CommitFiles []*models.CommitFile
|
||||||
// FilteredReflogCommits are the ones that appear in the reflog panel.
|
// FilteredReflogCommits are the ones that appear in the reflog panel.
|
||||||
// when in filtering mode we only include the ones that match the given path
|
// when in filtering mode we only include the ones that match the given path
|
||||||
FilteredReflogCommits []*models.Commit
|
FilteredReflogCommits []*models.Commit
|
||||||
@ -358,6 +363,7 @@ func (gui *Gui) resetState() {
|
|||||||
Panels: &panelStates{
|
Panels: &panelStates{
|
||||||
// TODO: work out why some of these are -1 and some are 0. Last time I checked there was a good reason but I'm less certain now
|
// TODO: work out why some of these are -1 and some are 0. Last time I checked there was a good reason but I'm less certain now
|
||||||
Files: &filePanelState{listPanelState{SelectedLineIdx: -1}},
|
Files: &filePanelState{listPanelState{SelectedLineIdx: -1}},
|
||||||
|
Submodules: &submodulePanelState{listPanelState{SelectedLineIdx: -1}},
|
||||||
Branches: &branchPanelState{listPanelState{SelectedLineIdx: 0}},
|
Branches: &branchPanelState{listPanelState{SelectedLineIdx: 0}},
|
||||||
Remotes: &remotePanelState{listPanelState{SelectedLineIdx: 0}},
|
Remotes: &remotePanelState{listPanelState{SelectedLineIdx: 0}},
|
||||||
RemoteBranches: &remoteBranchesState{listPanelState{SelectedLineIdx: -1}},
|
RemoteBranches: &remoteBranchesState{listPanelState{SelectedLineIdx: -1}},
|
||||||
@ -456,7 +462,7 @@ func (gui *Gui) Run() error {
|
|||||||
go gui.startBackgroundFetch()
|
go gui.startBackgroundFetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.goEvery(time.Second*10, gui.stopChan, gui.refreshFiles)
|
gui.goEvery(time.Second*10, gui.stopChan, gui.refreshFilesAndSubmodules)
|
||||||
|
|
||||||
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
|
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
|
||||||
|
|
||||||
|
@ -363,102 +363,119 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("files.commitChanges"),
|
Key: gui.getKey("files.commitChanges"),
|
||||||
Handler: gui.wrappedHandler(gui.handleCommitPress),
|
Handler: gui.wrappedHandler(gui.handleCommitPress),
|
||||||
Description: gui.Tr.SLocalize("CommitChanges"),
|
Description: gui.Tr.SLocalize("CommitChanges"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("files.commitChangesWithoutHook"),
|
Key: gui.getKey("files.commitChangesWithoutHook"),
|
||||||
Handler: gui.handleWIPCommitPress,
|
Handler: gui.handleWIPCommitPress,
|
||||||
Description: gui.Tr.SLocalize("commitChangesWithoutHook"),
|
Description: gui.Tr.SLocalize("commitChangesWithoutHook"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("files.amendLastCommit"),
|
Key: gui.getKey("files.amendLastCommit"),
|
||||||
Handler: gui.wrappedHandler(gui.handleAmendCommitPress),
|
Handler: gui.wrappedHandler(gui.handleAmendCommitPress),
|
||||||
Description: gui.Tr.SLocalize("AmendLastCommit"),
|
Description: gui.Tr.SLocalize("AmendLastCommit"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("files.commitChangesWithEditor"),
|
Key: gui.getKey("files.commitChangesWithEditor"),
|
||||||
Handler: gui.wrappedHandler(gui.handleCommitEditorPress),
|
Handler: gui.wrappedHandler(gui.handleCommitEditorPress),
|
||||||
Description: gui.Tr.SLocalize("CommitChangesWithEditor"),
|
Description: gui.Tr.SLocalize("CommitChangesWithEditor"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("universal.select"),
|
Key: gui.getKey("universal.select"),
|
||||||
Handler: gui.wrappedHandler(gui.handleFilePress),
|
Handler: gui.wrappedHandler(gui.handleFilePress),
|
||||||
Description: gui.Tr.SLocalize("toggleStaged"),
|
Description: gui.Tr.SLocalize("toggleStaged"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("universal.remove"),
|
Key: gui.getKey("universal.remove"),
|
||||||
Handler: gui.handleCreateDiscardMenu,
|
Handler: gui.handleCreateDiscardMenu,
|
||||||
Description: gui.Tr.SLocalize("viewDiscardOptions"),
|
Description: gui.Tr.SLocalize("viewDiscardOptions"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("universal.edit"),
|
Key: gui.getKey("universal.edit"),
|
||||||
Handler: gui.handleFileEdit,
|
Handler: gui.handleFileEdit,
|
||||||
Description: gui.Tr.SLocalize("editFile"),
|
Description: gui.Tr.SLocalize("editFile"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("universal.openFile"),
|
Key: gui.getKey("universal.openFile"),
|
||||||
Handler: gui.handleFileOpen,
|
Handler: gui.handleFileOpen,
|
||||||
Description: gui.Tr.SLocalize("openFile"),
|
Description: gui.Tr.SLocalize("openFile"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("files.ignoreFile"),
|
Key: gui.getKey("files.ignoreFile"),
|
||||||
Handler: gui.handleIgnoreFile,
|
Handler: gui.handleIgnoreFile,
|
||||||
Description: gui.Tr.SLocalize("ignoreFile"),
|
Description: gui.Tr.SLocalize("ignoreFile"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("files.refreshFiles"),
|
Key: gui.getKey("files.refreshFiles"),
|
||||||
Handler: gui.handleRefreshFiles,
|
Handler: gui.handleRefreshFiles,
|
||||||
Description: gui.Tr.SLocalize("refreshFiles"),
|
Description: gui.Tr.SLocalize("refreshFiles"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("files.stashAllChanges"),
|
Key: gui.getKey("files.stashAllChanges"),
|
||||||
Handler: gui.handleStashChanges,
|
Handler: gui.handleStashChanges,
|
||||||
Description: gui.Tr.SLocalize("stashAllChanges"),
|
Description: gui.Tr.SLocalize("stashAllChanges"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("files.viewStashOptions"),
|
Key: gui.getKey("files.viewStashOptions"),
|
||||||
Handler: gui.handleCreateStashMenu,
|
Handler: gui.handleCreateStashMenu,
|
||||||
Description: gui.Tr.SLocalize("viewStashOptions"),
|
Description: gui.Tr.SLocalize("viewStashOptions"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("files.toggleStagedAll"),
|
Key: gui.getKey("files.toggleStagedAll"),
|
||||||
Handler: gui.handleStageAll,
|
Handler: gui.handleStageAll,
|
||||||
Description: gui.Tr.SLocalize("toggleStagedAll"),
|
Description: gui.Tr.SLocalize("toggleStagedAll"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("files.viewResetOptions"),
|
Key: gui.getKey("files.viewResetOptions"),
|
||||||
Handler: gui.handleCreateResetMenu,
|
Handler: gui.handleCreateResetMenu,
|
||||||
Description: gui.Tr.SLocalize("viewResetOptions"),
|
Description: gui.Tr.SLocalize("viewResetOptions"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("universal.goInto"),
|
Key: gui.getKey("universal.goInto"),
|
||||||
Handler: gui.handleEnterFile,
|
Handler: gui.handleEnterFile,
|
||||||
Description: gui.Tr.SLocalize("StageLines"),
|
Description: gui.Tr.SLocalize("StageLines"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("files.fetch"),
|
Key: gui.getKey("files.fetch"),
|
||||||
Handler: gui.handleGitFetch,
|
Handler: gui.handleGitFetch,
|
||||||
Description: gui.Tr.SLocalize("fetch"),
|
Description: gui.Tr.SLocalize("fetch"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("universal.copyToClipboard"),
|
Key: gui.getKey("universal.copyToClipboard"),
|
||||||
Handler: gui.wrappedHandler(gui.handleCopySelectedSideContextItemToClipboard),
|
Handler: gui.wrappedHandler(gui.handleCopySelectedSideContextItemToClipboard),
|
||||||
Description: gui.Tr.SLocalize("copyFileNameToClipboard"),
|
Description: gui.Tr.SLocalize("copyFileNameToClipboard"),
|
||||||
@ -471,6 +488,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "files",
|
ViewName: "files",
|
||||||
|
Contexts: []string{FILES_CONTEXT_KEY},
|
||||||
Key: gui.getKey("commits.viewResetOptions"),
|
Key: gui.getKey("commits.viewResetOptions"),
|
||||||
Handler: gui.handleCreateResetToUpstreamMenu,
|
Handler: gui.handleCreateResetToUpstreamMenu,
|
||||||
Description: gui.Tr.SLocalize("viewResetToUpstreamOptions"),
|
Description: gui.Tr.SLocalize("viewResetToUpstreamOptions"),
|
||||||
@ -1542,6 +1560,32 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.wrappedHandler(gui.onMenuPress),
|
Handler: gui.wrappedHandler(gui.onMenuPress),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ViewName: "files",
|
||||||
|
Contexts: []string{SUBMODULES_CONTEXT_KEY},
|
||||||
|
Key: gui.getKey("universal.goInto"),
|
||||||
|
Handler: gui.wrappedHandler(gui.handleSubmoduleEnter),
|
||||||
|
Description: gui.Tr.SLocalize("enterSubmodule"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ViewName: "files",
|
||||||
|
Key: gui.getKey("universal.nextTab"),
|
||||||
|
Handler: gui.handleNextTab,
|
||||||
|
Description: gui.Tr.SLocalize("nextTab"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ViewName: "files",
|
||||||
|
Key: gui.getKey("universal.prevTab"),
|
||||||
|
Handler: gui.handlePrevTab,
|
||||||
|
Description: gui.Tr.SLocalize("prevTab"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ViewName: "files",
|
||||||
|
Contexts: []string{SUBMODULES_CONTEXT_KEY},
|
||||||
|
Key: gui.getKey("universal.copyToClipboard"),
|
||||||
|
Handler: gui.wrappedHandler(gui.handleCopySelectedSideContextItemToClipboard),
|
||||||
|
Description: gui.Tr.SLocalize("copySubmoduleNameToClipboard"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} {
|
for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} {
|
||||||
|
@ -285,8 +285,10 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
|||||||
listContext *ListContext
|
listContext *ListContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: don't we already have the view included in the context object itself? Or might that change in a way we don't want reflected here?
|
||||||
listContextStates := []listContextState{
|
listContextStates := []listContextState{
|
||||||
{view: filesView, listContext: gui.filesListContext()},
|
{view: filesView, listContext: gui.filesListContext()},
|
||||||
|
{view: filesView, listContext: gui.submodulesListContext()},
|
||||||
{view: branchesView, listContext: gui.branchesListContext()},
|
{view: branchesView, listContext: gui.branchesListContext()},
|
||||||
{view: branchesView, listContext: gui.remotesListContext()},
|
{view: branchesView, listContext: gui.remotesListContext()},
|
||||||
{view: branchesView, listContext: gui.remoteBranchesListContext()},
|
{view: branchesView, listContext: gui.remoteBranchesListContext()},
|
||||||
|
@ -270,7 +270,7 @@ func (gui *Gui) filesListContext() *ListContext {
|
|||||||
ResetMainViewOriginOnFocus: false,
|
ResetMainViewOriginOnFocus: false,
|
||||||
Kind: SIDE_CONTEXT,
|
Kind: SIDE_CONTEXT,
|
||||||
GetDisplayStrings: func() [][]string {
|
GetDisplayStrings: func() [][]string {
|
||||||
return presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Modes.Diffing.Ref, gui.State.SubmoduleConfigs)
|
return presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Modes.Diffing.Ref, gui.State.Submodules)
|
||||||
},
|
},
|
||||||
SelectedItem: func() (ListItem, bool) {
|
SelectedItem: func() (ListItem, bool) {
|
||||||
item := gui.getSelectedFile()
|
item := gui.getSelectedFile()
|
||||||
@ -462,6 +462,27 @@ func (gui *Gui) commitFilesListContext() *ListContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) submodulesListContext() *ListContext {
|
||||||
|
return &ListContext{
|
||||||
|
ViewName: "files",
|
||||||
|
WindowName: "files",
|
||||||
|
ContextKey: SUBMODULES_CONTEXT_KEY,
|
||||||
|
GetItemsLength: func() int { return len(gui.State.Submodules) },
|
||||||
|
GetPanelState: func() IListPanelState { return gui.State.Panels.Submodules },
|
||||||
|
OnFocus: gui.handleSubmoduleSelect,
|
||||||
|
Gui: gui,
|
||||||
|
ResetMainViewOriginOnFocus: true,
|
||||||
|
Kind: SIDE_CONTEXT,
|
||||||
|
GetDisplayStrings: func() [][]string {
|
||||||
|
return presentation.GetSubmoduleListDisplayStrings(gui.State.Submodules)
|
||||||
|
},
|
||||||
|
SelectedItem: func() (ListItem, bool) {
|
||||||
|
item := gui.getSelectedSubmodule()
|
||||||
|
return item, item != nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (gui *Gui) getListContexts() []*ListContext {
|
func (gui *Gui) getListContexts() []*ListContext {
|
||||||
return []*ListContext{
|
return []*ListContext{
|
||||||
gui.menuListContext(),
|
gui.menuListContext(),
|
||||||
@ -475,6 +496,7 @@ func (gui *Gui) getListContexts() []*ListContext {
|
|||||||
gui.subCommitsListContext(),
|
gui.subCommitsListContext(),
|
||||||
gui.stashListContext(),
|
gui.stashListContext(),
|
||||||
gui.commitFilesListContext(),
|
gui.commitFilesListContext(),
|
||||||
|
gui.submodulesListContext(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
21
pkg/gui/presentation/submodules.go
Normal file
21
pkg/gui/presentation/submodules.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package presentation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetSubmoduleListDisplayStrings(submodules []*models.SubmoduleConfig) [][]string {
|
||||||
|
lines := make([][]string, len(submodules))
|
||||||
|
|
||||||
|
for i := range submodules {
|
||||||
|
lines[i] = getSubmoduleDisplayStrings(submodules[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSubmoduleDisplayStrings(s *models.SubmoduleConfig) []string {
|
||||||
|
return []string{utils.ColoredString(s.Name, theme.DefaultTextColor)}
|
||||||
|
}
|
135
pkg/gui/submodules_panel.go
Normal file
135
pkg/gui/submodules_panel.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) getSelectedSubmodule() *models.SubmoduleConfig {
|
||||||
|
selectedLine := gui.State.Panels.Submodules.SelectedLineIdx
|
||||||
|
if selectedLine == -1 || len(gui.State.Submodules) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.State.Submodules[selectedLine]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleSubmoduleSelect() error {
|
||||||
|
var task updateTask
|
||||||
|
submodule := gui.getSelectedSubmodule()
|
||||||
|
if submodule == nil {
|
||||||
|
task = gui.createRenderStringTask("No submodules")
|
||||||
|
} else {
|
||||||
|
// TODO: we want to display the path, name, url, and a diff. We really need to be able to pipe commands together. We can always pipe commands together and just not do it asynchronously, but what if it's an expensive diff to obtain? I think that makes the most sense now though.
|
||||||
|
task = gui.createRenderStringTask(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Name: %s\nPath: %s\nUrl: %s\n",
|
||||||
|
utils.ColoredString(submodule.Name, color.FgGreen),
|
||||||
|
utils.ColoredString(submodule.Path, color.FgYellow),
|
||||||
|
utils.ColoredString(submodule.Url, color.FgCyan),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.refreshMainViews(refreshMainOpts{
|
||||||
|
main: &viewUpdateOpts{
|
||||||
|
title: "Submodule",
|
||||||
|
task: task,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleSubmoduleEnter() error {
|
||||||
|
submodule := gui.getSelectedSubmodule()
|
||||||
|
if submodule == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.enterSubmodule(submodule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) enterSubmodule(submodule *models.SubmoduleConfig) error {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.State.RepoPathStack = append(gui.State.RepoPathStack, wd)
|
||||||
|
|
||||||
|
return gui.dispatchSwitchToRepo(submodule.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (gui *Gui) handleAddRemote(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
// return gui.prompt(gui.Tr.SLocalize("newRemoteName"), "", func(remoteName string) error {
|
||||||
|
// return gui.prompt(gui.Tr.SLocalize("newRemoteUrl"), "", func(remoteUrl string) error {
|
||||||
|
// if err := gui.GitCommand.AddRemote(remoteName, remoteUrl); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// return gui.refreshSidePanels(refreshOptions{scope: []int{REMOTES}})
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (gui *Gui) handleRemoveRemote(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
// remote := gui.getSelectedSubmodule()
|
||||||
|
// if remote == nil {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return gui.ask(askOpts{
|
||||||
|
// title: gui.Tr.SLocalize("removeRemote"),
|
||||||
|
// prompt: gui.Tr.SLocalize("removeRemotePrompt") + " '" + remote.Name + "'?",
|
||||||
|
// handleConfirm: func() error {
|
||||||
|
// if err := gui.GitCommand.RemoveRemote(remote.Name); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
// remote := gui.getSelectedSubmodule()
|
||||||
|
// if remote == nil {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// editNameMessage := gui.Tr.TemplateLocalize(
|
||||||
|
// "editRemoteName",
|
||||||
|
// Teml{
|
||||||
|
// "remoteName": remote.Name,
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
|
||||||
|
// return gui.prompt(editNameMessage, remote.Name, func(updatedRemoteName string) error {
|
||||||
|
// if updatedRemoteName != remote.Name {
|
||||||
|
// if err := gui.GitCommand.RenameRemote(remote.Name, updatedRemoteName); err != nil {
|
||||||
|
// return gui.surfaceError(err)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// editUrlMessage := gui.Tr.TemplateLocalize(
|
||||||
|
// "editRemoteUrl",
|
||||||
|
// Teml{
|
||||||
|
// "remoteName": updatedRemoteName,
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
|
||||||
|
// urls := remote.Urls
|
||||||
|
// url := ""
|
||||||
|
// if len(urls) > 0 {
|
||||||
|
// url = urls[0]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return gui.prompt(editUrlMessage, url, func(updatedRemoteUrl string) error {
|
||||||
|
// if err := gui.GitCommand.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
|
||||||
|
// return gui.surfaceError(err)
|
||||||
|
// }
|
||||||
|
// return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// }
|
@ -25,18 +25,20 @@ const (
|
|||||||
TAGS
|
TAGS
|
||||||
REMOTES
|
REMOTES
|
||||||
STATUS
|
STATUS
|
||||||
|
SUBMODULES
|
||||||
)
|
)
|
||||||
|
|
||||||
func getScopeNames(scopes []int) []string {
|
func getScopeNames(scopes []int) []string {
|
||||||
scopeNameMap := map[int]string{
|
scopeNameMap := map[int]string{
|
||||||
COMMITS: "commits",
|
COMMITS: "commits",
|
||||||
BRANCHES: "branches",
|
BRANCHES: "branches",
|
||||||
FILES: "files",
|
FILES: "files",
|
||||||
STASH: "stash",
|
SUBMODULES: "submodules",
|
||||||
REFLOG: "reflog",
|
STASH: "stash",
|
||||||
TAGS: "tags",
|
REFLOG: "reflog",
|
||||||
REMOTES: "remotes",
|
TAGS: "tags",
|
||||||
STATUS: "status",
|
REMOTES: "remotes",
|
||||||
|
STATUS: "status",
|
||||||
}
|
}
|
||||||
|
|
||||||
scopeNames := make([]string, len(scopes))
|
scopeNames := make([]string, len(scopes))
|
||||||
@ -116,13 +118,13 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if scopeMap[FILES] {
|
if scopeMap[FILES] || scopeMap[SUBMODULES] {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
func() {
|
func() {
|
||||||
if options.mode == ASYNC {
|
if options.mode == ASYNC {
|
||||||
go gui.refreshFiles()
|
go gui.refreshFilesAndSubmodules()
|
||||||
} else {
|
} else {
|
||||||
gui.refreshFiles()
|
gui.refreshFilesAndSubmodules()
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
@ -11,7 +11,7 @@ func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
|
|||||||
red := color.New(color.FgRed)
|
red := color.New(color.FgRed)
|
||||||
|
|
||||||
nukeStr := "reset --hard HEAD && git clean -fd"
|
nukeStr := "reset --hard HEAD && git clean -fd"
|
||||||
if len(gui.State.SubmoduleConfigs) > 0 {
|
if len(gui.State.Submodules) > 0 {
|
||||||
nukeStr = fmt.Sprintf("%s (%s)", nukeStr, gui.Tr.SLocalize("andResetSubmodules"))
|
nukeStr = fmt.Sprintf("%s (%s)", nukeStr, gui.Tr.SLocalize("andResetSubmodules"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1197,6 +1197,12 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
|||||||
}, &i18n.Message{
|
}, &i18n.Message{
|
||||||
ID: "andResetSubmodules",
|
ID: "andResetSubmodules",
|
||||||
Other: "and reset submodules",
|
Other: "and reset submodules",
|
||||||
|
}, &i18n.Message{
|
||||||
|
ID: "enterSubmodule",
|
||||||
|
Other: "enter submodule",
|
||||||
|
}, &i18n.Message{
|
||||||
|
ID: "copySubmoduleNameToClipboard",
|
||||||
|
Other: "copy submodule name to clipboard",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user