diff --git a/pkg/commands/file.go b/pkg/commands/file.go index 750b4e4f8..f030b4e67 100644 --- a/pkg/commands/file.go +++ b/pkg/commands/file.go @@ -19,7 +19,6 @@ type File struct { DisplayString string Type string // one of 'file', 'directory', and 'other' ShortStatus string // e.g. 'AD', ' A', 'M ', '??' - IsSubmodule bool } const RENAME_SEPARATOR = " -> " @@ -45,3 +44,17 @@ func (f *File) ID() string { func (f *File) Description() string { return f.Name } + +func (f *File) IsSubmodule(configs []*SubmoduleConfig) bool { + return f.SubmoduleConfig(configs) != nil +} + +func (f *File) SubmoduleConfig(configs []*SubmoduleConfig) *SubmoduleConfig { + for _, config := range configs { + if f.Name == config.Name { + return config + } + } + + return nil +} diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 465552b48..90fbc9890 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -1,7 +1,6 @@ package commands import ( - "bufio" "fmt" "io/ioutil" "os" @@ -272,32 +271,6 @@ func (c *GitCommand) GetConfigValue(key string) string { return strings.TrimSpace(output) } -func (c *GitCommand) GetSubmoduleNames() ([]string, error) { - file, err := os.Open(".gitmodules") - if err != nil { - if err == os.ErrNotExist { - return nil, nil - } - return nil, err - } - - submoduleNames := []string{} - - scanner := bufio.NewScanner(file) - scanner.Split(bufio.ScanLines) - for scanner.Scan() { - line := scanner.Text() - re := regexp.MustCompile(`\[submodule "(.*)"\]`) - matches := re.FindStringSubmatch(line) - - if len(matches) > 0 { - submoduleNames = append(submoduleNames, matches[1]) - } - } - - return submoduleNames, nil -} - func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*File { // check if config wants us ignoring untracked files untrackedFilesSetting := c.GetConfigValue("status.showUntrackedFiles") @@ -314,11 +287,6 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*File { statusStrings := utils.SplitLines(statusOutput) files := []*File{} - submoduleNames, err := c.GetSubmoduleNames() - if err != nil { - c.Log.Error(err) - } - for _, statusString := range statusStrings { if strings.HasPrefix(statusString, "warning") { c.Log.Warningf("warning when calling git status: %s", statusString) @@ -332,7 +300,6 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*File { hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange) hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change) hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change) - isSubmodule := utils.IncludesString(submoduleNames, filename) file := &File{ Name: filename, @@ -345,7 +312,6 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*File { HasInlineMergeConflicts: hasInlineMergeConflicts, Type: c.OSCommand.FileType(filename), ShortStatus: change, - IsSubmodule: isSubmodule, } files = append(files, file) } @@ -752,11 +718,7 @@ func (c *GitCommand) DiscardAllFileChanges(file *File) error { // if the file isn't tracked, we assume you want to delete it quotedFileName := c.OSCommand.Quote(file.Name) - if file.IsSubmodule { - if err := c.OSCommand.RunCommand(fmt.Sprintf("git submodule update --checkout --force --init %s", quotedFileName)); err != nil { - return err - } - } else if file.HasStagedChanges || file.HasMergeConflicts { + if file.HasStagedChanges || file.HasMergeConflicts { if err := c.OSCommand.RunCommand("git reset -- %s", quotedFileName); err != nil { return err } @@ -816,7 +778,7 @@ func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string { if filterPath != "" { filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(filterPath)) } - return fmt.Sprintf("git show --color=%s --no-renames --stat -p %s %s", c.colorArg(), sha, filterPathArg) + return fmt.Sprintf("git show --submodule --color=%s --no-renames --stat -p %s %s", c.colorArg(), sha, filterPathArg) } func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string { diff --git a/pkg/commands/submodule_config.go b/pkg/commands/submodule_config.go new file mode 100644 index 000000000..e9e7ad22b --- /dev/null +++ b/pkg/commands/submodule_config.go @@ -0,0 +1,7 @@ +package commands + +type SubmoduleConfig struct { + Name string + Path string + Url string +} diff --git a/pkg/commands/submodules.go b/pkg/commands/submodules.go new file mode 100644 index 000000000..ad130544c --- /dev/null +++ b/pkg/commands/submodules.go @@ -0,0 +1,66 @@ +package commands + +import ( + "bufio" + "os" + "regexp" +) + +// .gitmodules looks like this: +// [submodule "mysubmodule"] +// path = blah/mysubmodule +// url = git@github.com:subbo.git + +func (c *GitCommand) GetSubmoduleConfigs() ([]*SubmoduleConfig, error) { + file, err := os.Open(".gitmodules") + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + + firstMatch := func(str string, regex string) (string, bool) { + re := regexp.MustCompile(regex) + matches := re.FindStringSubmatch(str) + + if len(matches) > 0 { + return matches[1], true + } else { + return "", false + } + } + + configs := []*SubmoduleConfig{} + for scanner.Scan() { + line := scanner.Text() + + if name, ok := firstMatch(line, `\[submodule "(.*)"\]`); ok { + configs = append(configs, &SubmoduleConfig{Name: name}) + continue + } + + if len(configs) > 0 { + lastConfig := configs[len(configs)-1] + + if path, ok := firstMatch(line, `\s*path\s*=\s*(.*)\s*`); ok { + lastConfig.Path = path + } else if url, ok := firstMatch(line, `\s*url\s*=\s*(.*)\s*`); ok { + lastConfig.Url = url + } + } + } + + return configs, nil +} + +func (c *GitCommand) SubmoduleStash(config *SubmoduleConfig) error { + return c.OSCommand.RunCommand("git -C %s stash --include-untracked", config.Path) +} + +func (c *GitCommand) SubmoduleReset(config *SubmoduleConfig) error { + return c.OSCommand.RunCommand("git submodule update --force %s", config.Name) +} diff --git a/pkg/gui/discard_changes_menu_panel.go b/pkg/gui/discard_changes_menu_panel.go index f8a0f1df2..2c4e8f759 100644 --- a/pkg/gui/discard_changes_menu_panel.go +++ b/pkg/gui/discard_changes_menu_panel.go @@ -2,42 +2,74 @@ package gui import ( "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands" ) +func (gui *Gui) submoduleFromFile(file *commands.File) *commands.SubmoduleConfig { + for _, config := range gui.State.SubmoduleConfigs { + if config.Name == file.Name { + return config + } + } + + return nil +} + func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error { file := gui.getSelectedFile() if file == nil { return nil } - if file.IsSubmodule { - // git submodule foreach '[[ "$name" == "renderers/chartify" ]] && git stash --include-untracked' - // git submodule update --force renderers/chartify - } + var menuItems []*menuItem - menuItems := []*menuItem{ - { - displayString: gui.Tr.SLocalize("discardAllChanges"), - onPress: func() error { - if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil { - return gui.surfaceError(err) - } - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}}) + submoduleConfigs := gui.State.SubmoduleConfigs + if file.IsSubmodule(submoduleConfigs) { + submoduleConfig := file.SubmoduleConfig(submoduleConfigs) + + menuItems = []*menuItem{ + { + displayString: gui.Tr.SLocalize("submoduleStashAndReset"), + onPress: func() error { + if err := gui.GitCommand.UnStageFile(file.Name, file.Tracked); err != nil { + return gui.surfaceError(err) + } + if err := gui.GitCommand.SubmoduleStash(submoduleConfig); err != nil { + return gui.surfaceError(err) + } + if err := gui.GitCommand.SubmoduleReset(submoduleConfig); err != nil { + return gui.surfaceError(err) + } + return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}}) + }, }, - }, - } - - if file.HasStagedChanges && file.HasUnstagedChanges { - menuItems = append(menuItems, &menuItem{ - displayString: gui.Tr.SLocalize("discardUnstagedChanges"), - onPress: func() error { - if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil { - return gui.surfaceError(err) - } - - return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}}) + } + } else { + menuItems = []*menuItem{ + { + displayString: gui.Tr.SLocalize("discardAllChanges"), + onPress: func() error { + if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil { + return gui.surfaceError(err) + } + return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}}) + }, }, - }) + } + + if file.HasStagedChanges && file.HasUnstagedChanges { + menuItems = append(menuItems, &menuItem{ + displayString: gui.Tr.SLocalize("discardUnstagedChanges"), + onPress: func() error { + if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil { + return gui.surfaceError(err) + } + + return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}}) + }, + }) + } + } return gui.createMenu(file.Name, menuItems, createMenuOptions{showCancel: true}) diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index aa01b4b36..c57f94082 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -93,6 +93,9 @@ func (gui *Gui) refreshFiles() error { // if the filesView hasn't been instantiated yet we just return return nil } + if err := gui.refreshStateSubmoduleConfigs(); err != nil { + return err + } if err := gui.refreshStateFiles(); err != nil { return err } @@ -426,6 +429,17 @@ func (gui *Gui) refreshStateFiles() error { return nil } +func (gui *Gui) refreshStateSubmoduleConfigs() error { + configs, err := gui.GitCommand.GetSubmoduleConfigs() + if err != nil { + return err + } + + gui.State.SubmoduleConfigs = configs + + return nil +} + func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error { if gui.popupPanelFocused() { return nil diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index db6624ec0..94634113b 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -271,11 +271,12 @@ type Modes struct { } type guiState struct { - Files []*commands.File - Branches []*commands.Branch - Commits []*commands.Commit - StashEntries []*commands.StashEntry - CommitFiles []*commands.CommitFile + Files []*commands.File + SubmoduleConfigs []*commands.SubmoduleConfig + Branches []*commands.Branch + Commits []*commands.Commit + StashEntries []*commands.StashEntry + CommitFiles []*commands.CommitFile // FilteredReflogCommits are the ones that appear in the reflog panel. // when in filtering mode we only include the ones that match the given path FilteredReflogCommits []*commands.Commit diff --git a/pkg/gui/list_context.go b/pkg/gui/list_context.go index 1ba7da80a..e2dec2a96 100644 --- a/pkg/gui/list_context.go +++ b/pkg/gui/list_context.go @@ -270,7 +270,7 @@ func (gui *Gui) filesListContext() *ListContext { ResetMainViewOriginOnFocus: false, Kind: SIDE_CONTEXT, GetDisplayStrings: func() [][]string { - return presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Modes.Diffing.Ref) + return presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Modes.Diffing.Ref, gui.State.SubmoduleConfigs) }, SelectedItem: func() (ListItem, bool) { item := gui.getSelectedFile() diff --git a/pkg/gui/presentation/files.go b/pkg/gui/presentation/files.go index 6493e1024..f02aee91f 100644 --- a/pkg/gui/presentation/files.go +++ b/pkg/gui/presentation/files.go @@ -7,19 +7,19 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) -func GetFileListDisplayStrings(files []*commands.File, diffName string) [][]string { +func GetFileListDisplayStrings(files []*commands.File, diffName string, submoduleConfigs []*commands.SubmoduleConfig) [][]string { lines := make([][]string, len(files)) for i := range files { diffed := files[i].Name == diffName - lines[i] = getFileDisplayStrings(files[i], diffed) + lines[i] = getFileDisplayStrings(files[i], diffed, submoduleConfigs) } return lines } // getFileDisplayStrings returns the display string of branch -func getFileDisplayStrings(f *commands.File, diffed bool) []string { +func getFileDisplayStrings(f *commands.File, diffed bool, submoduleConfigs []*commands.SubmoduleConfig) []string { // potentially inefficient to be instantiating these color // objects with each render red := color.New(color.FgRed) @@ -55,7 +55,7 @@ func getFileDisplayStrings(f *commands.File, diffed bool) []string { output += secondCharCl.Sprint(secondChar) output += restColor.Sprintf(" %s", f.Name) - if f.IsSubmodule { + if f.IsSubmodule(submoduleConfigs) { output += utils.ColoredString(" (submodule)", theme.DefaultTextColor) } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index f774f553d..f34e74748 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -1191,6 +1191,9 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "runningCustomCommandStatus", Other: "running custom command", + }, &i18n.Message{ + ID: "submoduleStashAndReset", + Other: "stash uncommitted submodule changes and reset", }, ) }