Skip to content

Commit

Permalink
Show all submodules recursively (#3341)
Browse files Browse the repository at this point in the history
- **PR Description**

Extend the submodules tab to show not only the top-level submodules, but
also their nested submodules, recursively.

Fixes #3306.
  • Loading branch information
stefanhaller authored Mar 7, 2024
2 parents ad01745 + 3b72328 commit 44f553b
Show file tree
Hide file tree
Showing 18 changed files with 308 additions and 47 deletions.
8 changes: 8 additions & 0 deletions pkg/commands/git_commands/git_command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ func (self *GitCommandBuilder) Dir(path string) *GitCommandBuilder {
return self
}

func (self *GitCommandBuilder) DirIf(condition bool, path string) *GitCommandBuilder {
if condition {
return self.Dir(path)
}

return self
}

// Note, you may prefer to use the Dir method instead of this one
func (self *GitCommandBuilder) Worktree(path string) *GitCommandBuilder {
// worktree arg comes before the command
Expand Down
71 changes: 57 additions & 14 deletions pkg/commands/git_commands/submodule.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ func NewSubmoduleCommands(gitCommon *GitCommon) *SubmoduleCommands {
}
}

func (self *SubmoduleCommands) GetConfigs() ([]*models.SubmoduleConfig, error) {
file, err := os.Open(".gitmodules")
func (self *SubmoduleCommands) GetConfigs(parentModule *models.SubmoduleConfig) ([]*models.SubmoduleConfig, error) {
gitModulesPath := ".gitmodules"
if parentModule != nil {
gitModulesPath = filepath.Join(parentModule.FullPath(), gitModulesPath)
}
file, err := os.Open(gitModulesPath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
Expand All @@ -51,21 +55,27 @@ func (self *SubmoduleCommands) GetConfigs() ([]*models.SubmoduleConfig, error) {
}

configs := []*models.SubmoduleConfig{}
lastConfigIdx := -1
for scanner.Scan() {
line := scanner.Text()

if name, ok := firstMatch(line, `\[submodule "(.*)"\]`); ok {
configs = append(configs, &models.SubmoduleConfig{Name: name})
configs = append(configs, &models.SubmoduleConfig{
Name: name, ParentModule: parentModule,
})
lastConfigIdx = len(configs) - 1
continue
}

if len(configs) > 0 {
lastConfig := configs[len(configs)-1]

if lastConfigIdx != -1 {
if path, ok := firstMatch(line, `\s*path\s*=\s*(.*)\s*`); ok {
lastConfig.Path = path
configs[lastConfigIdx].Path = path
nestedConfigs, err := self.GetConfigs(configs[lastConfigIdx])
if err == nil {
configs = append(configs, nestedConfigs...)
}
} else if url, ok := firstMatch(line, `\s*url\s*=\s*(.*)\s*`); ok {
lastConfig.Url = url
configs[lastConfigIdx].Url = url
}
}
}
Expand All @@ -77,21 +87,26 @@ func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
// if the path does not exist then it hasn't yet been initialized so we'll swallow the error
// because the intention here is to have no dirty worktree state
if _, err := os.Stat(submodule.Path); os.IsNotExist(err) {
self.Log.Infof("submodule path %s does not exist, returning", submodule.Path)
self.Log.Infof("submodule path %s does not exist, returning", submodule.FullPath())
return nil
}

cmdArgs := NewGitCmd("stash").
Dir(submodule.Path).
Dir(submodule.FullPath()).
Arg("--include-untracked").
ToArgv()

return self.cmd.New(cmdArgs).Run()
}

func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error {
parentDir := ""
if submodule.ParentModule != nil {
parentDir = submodule.ParentModule.FullPath()
}
cmdArgs := NewGitCmd("submodule").
Arg("update", "--init", "--force", "--", submodule.Path).
DirIf(parentDir != "", parentDir).
ToArgv()

return self.cmd.New(cmdArgs).Run()
Expand All @@ -107,6 +122,20 @@ func (self *SubmoduleCommands) UpdateAll() error {
func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677

if submodule.ParentModule != nil {
wd, err := os.Getwd()
if err != nil {
return err
}

err = os.Chdir(submodule.ParentModule.FullPath())
if err != nil {
return err
}

defer func() { _ = os.Chdir(wd) }()
}

if err := self.cmd.New(
NewGitCmd("submodule").
Arg("deinit", "--force", "--", submodule.Path).ToArgv(),
Expand Down Expand Up @@ -141,7 +170,7 @@ func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {

// We may in fact want to use the repo's git dir path but git docs say not to
// mix submodules and worktrees anyway.
return os.RemoveAll(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "modules", submodule.Path))
return os.RemoveAll(submodule.GitDirPath(self.repoPaths.repoGitDirPath))
}

func (self *SubmoduleCommands) Add(name string, path string, url string) error {
Expand All @@ -158,10 +187,24 @@ func (self *SubmoduleCommands) Add(name string, path string, url string) error {
return self.cmd.New(cmdArgs).Run()
}

func (self *SubmoduleCommands) UpdateUrl(name string, path string, newUrl string) error {
func (self *SubmoduleCommands) UpdateUrl(submodule *models.SubmoduleConfig, newUrl string) error {
if submodule.ParentModule != nil {
wd, err := os.Getwd()
if err != nil {
return err
}

err = os.Chdir(submodule.ParentModule.FullPath())
if err != nil {
return err
}

defer func() { _ = os.Chdir(wd) }()
}

setUrlCmdStr := NewGitCmd("config").
Arg(
"--file", ".gitmodules", "submodule."+name+".url", newUrl,
"--file", ".gitmodules", "submodule."+submodule.Name+".url", newUrl,
).
ToArgv()

Expand All @@ -170,7 +213,7 @@ func (self *SubmoduleCommands) UpdateUrl(name string, path string, newUrl string
return err
}

syncCmdStr := NewGitCmd("submodule").Arg("sync", "--", path).
syncCmdStr := NewGitCmd("submodule").Arg("sync", "--", submodule.Path).
ToArgv()

if err := self.cmd.New(syncCmdStr).Run(); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/commands/git_commands/working_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ func (self *WorkingTreeCommands) RemoveUntrackedFiles() error {

// ResetAndClean removes all unstaged changes and removes all untracked files
func (self *WorkingTreeCommands) ResetAndClean() error {
submoduleConfigs, err := self.submodule.GetConfigs()
submoduleConfigs, err := self.submodule.GetConfigs(nil)
if err != nil {
return err
}
Expand Down
31 changes: 30 additions & 1 deletion pkg/commands/models/submodule_config.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,48 @@
package models

import "path/filepath"

type SubmoduleConfig struct {
Name string
Path string
Url string

ParentModule *SubmoduleConfig // nil if top-level
}

func (r *SubmoduleConfig) RefName() string {
func (r *SubmoduleConfig) FullName() string {
if r.ParentModule != nil {
return r.ParentModule.FullName() + "/" + r.Name
}

return r.Name
}

func (r *SubmoduleConfig) FullPath() string {
if r.ParentModule != nil {
return r.ParentModule.FullPath() + "/" + r.Path
}

return r.Path
}

func (r *SubmoduleConfig) RefName() string {
return r.FullName()
}

func (r *SubmoduleConfig) ID() string {
return r.RefName()
}

func (r *SubmoduleConfig) Description() string {
return r.RefName()
}

func (r *SubmoduleConfig) GitDirPath(repoGitDirPath string) string {
parentPath := repoGitDirPath
if r.ParentModule != nil {
parentPath = r.ParentModule.GitDirPath(repoGitDirPath)
}

return filepath.Join(parentPath, "modules", r.Name)
}
2 changes: 1 addition & 1 deletion pkg/gui/context/submodules_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext {
viewModel := NewFilteredListViewModel(
func() []*models.SubmoduleConfig { return c.Model().Submodules },
func(submodule *models.SubmoduleConfig) []string {
return []string{submodule.Name}
return []string{submodule.FullName()}
},
nil,
)
Expand Down
2 changes: 1 addition & 1 deletion pkg/gui/controllers/helpers/refresh_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ func (self *RefreshHelper) refreshTags() error {
}

func (self *RefreshHelper) refreshStateSubmoduleConfigs() error {
configs, err := self.c.Git().Submodule.GetConfigs()
configs, err := self.c.Git().Submodule.GetConfigs(nil)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/gui/controllers/helpers/repos_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error
}
self.c.State().GetRepoPathStack().Push(wd)

return self.DispatchSwitchToRepo(submodule.Path, context.NO_CONTEXT)
return self.DispatchSwitchToRepo(submodule.FullPath(), context.NO_CONTEXT)
}

func (self *ReposHelper) getCurrentBranch(path string) string {
Expand Down
10 changes: 5 additions & 5 deletions pkg/gui/controllers/submodules_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ func (self *SubmodulesController) GetOnRenderToMain() func() error {
} else {
prefix := fmt.Sprintf(
"Name: %s\nPath: %s\nUrl: %s\n\n",
style.FgGreen.Sprint(submodule.Name),
style.FgYellow.Sprint(submodule.Path),
style.FgGreen.Sprint(submodule.FullName()),
style.FgYellow.Sprint(submodule.FullPath()),
style.FgCyan.Sprint(submodule.Url),
)

Expand Down Expand Up @@ -178,12 +178,12 @@ func (self *SubmodulesController) add() error {

func (self *SubmodulesController) editURL(submodule *models.SubmoduleConfig) error {
return self.c.Prompt(types.PromptOpts{
Title: fmt.Sprintf(self.c.Tr.UpdateSubmoduleUrl, submodule.Name),
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.Name, submodule.Path, newUrl)
err := self.c.Git().Submodule.UpdateUrl(submodule, newUrl)
if err != nil {
_ = self.c.Error(err)
}
Expand Down Expand Up @@ -272,7 +272,7 @@ func (self *SubmodulesController) update(submodule *models.SubmoduleConfig) erro
func (self *SubmodulesController) remove(submodule *models.SubmoduleConfig) error {
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.RemoveSubmodule,
Prompt: fmt.Sprintf(self.c.Tr.RemoveSubmodulePrompt, submodule.Name),
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 {
Expand Down
12 changes: 11 additions & 1 deletion pkg/gui/presentation/submodules.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,15 @@ func GetSubmoduleListDisplayStrings(submodules []*models.SubmoduleConfig) [][]st
}

func getSubmoduleDisplayStrings(s *models.SubmoduleConfig) []string {
return []string{theme.DefaultTextColor.Sprint(s.Name)}
name := s.Name
if s.ParentModule != nil {
indentation := ""
for p := s.ParentModule; p != nil; p = p.ParentModule {
indentation += " "
}

name = indentation + "- " + s.Name
}

return []string{theme.DefaultTextColor.Sprint(name)}
}
11 changes: 11 additions & 0 deletions pkg/integration/components/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package components

import (
"fmt"
"log"
"strings"

"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
)

type Git struct {
Expand Down Expand Up @@ -44,3 +47,11 @@ func (self *Git) expect(cmdArgs []string, condition func(string) (bool, string))

return self
}

func (self *Git) Version() *git_commands.GitVersion {
version, err := getGitVersion()
if err != nil {
log.Fatalf("Could not get git version: %v", err)
}
return version
}
4 changes: 2 additions & 2 deletions pkg/integration/components/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,9 @@ func (self *Shell) CloneIntoRemote(name string) *Shell {
return self
}

func (self *Shell) CloneIntoSubmodule(submoduleName string) *Shell {
func (self *Shell) CloneIntoSubmodule(submoduleName string, submodulePath string) *Shell {
self.Clone("other_repo")
self.RunCommand([]string{"git", "submodule", "add", "../other_repo", submoduleName})
self.RunCommand([]string{"git", "submodule", "add", "--name", submoduleName, "../other_repo", submodulePath})

return self
}
Expand Down
12 changes: 8 additions & 4 deletions pkg/integration/tests/submodule/enter.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var Enter = NewIntegrationTest(NewIntegrationTestArgs{
},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("first commit")
shell.CloneIntoSubmodule("my_submodule")
shell.CloneIntoSubmodule("my_submodule_name", "my_submodule_path")
shell.GitAddAll()
shell.Commit("add submodule")
},
Expand All @@ -29,14 +29,18 @@ var Enter = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Status().Content(Contains("repo"))
}
assertInSubmodule := func() {
t.Views().Status().Content(Contains("my_submodule"))
if t.Git().Version().IsAtLeast(2, 22, 0) {
t.Views().Status().Content(Contains("my_submodule_path(my_submodule_name)"))
} else {
t.Views().Status().Content(Contains("my_submodule_path"))
}
}

assertInParentRepo()

t.Views().Submodules().Focus().
Lines(
Contains("my_submodule").IsSelected(),
Contains("my_submodule_name").IsSelected(),
).
// enter the submodule
PressEnter()
Expand All @@ -60,7 +64,7 @@ var Enter = NewIntegrationTest(NewIntegrationTestArgs{

t.Views().Files().Focus().
Lines(
MatchesRegexp(` M.*my_submodule \(submodule\)`).IsSelected(),
MatchesRegexp(` M.*my_submodule_path \(submodule\)`).IsSelected(),
).
Tap(func() {
// main view also shows the new commit when we're looking at the submodule within the files view
Expand Down
Loading

0 comments on commit 44f553b

Please sign in to comment.