mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-17 01:42:45 +02:00
support discarding submodule changes
This commit is contained in:
@ -19,7 +19,6 @@ type File struct {
|
|||||||
DisplayString string
|
DisplayString string
|
||||||
Type string // one of 'file', 'directory', and 'other'
|
Type string // one of 'file', 'directory', and 'other'
|
||||||
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
|
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
|
||||||
IsSubmodule bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RENAME_SEPARATOR = " -> "
|
const RENAME_SEPARATOR = " -> "
|
||||||
@ -45,3 +44,17 @@ func (f *File) ID() string {
|
|||||||
func (f *File) Description() string {
|
func (f *File) Description() string {
|
||||||
return f.Name
|
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
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
@ -272,32 +271,6 @@ func (c *GitCommand) GetConfigValue(key string) string {
|
|||||||
return strings.TrimSpace(output)
|
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 {
|
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*File {
|
||||||
// check if config wants us ignoring untracked files
|
// check if config wants us ignoring untracked files
|
||||||
untrackedFilesSetting := c.GetConfigValue("status.showUntrackedFiles")
|
untrackedFilesSetting := c.GetConfigValue("status.showUntrackedFiles")
|
||||||
@ -314,11 +287,6 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*File {
|
|||||||
statusStrings := utils.SplitLines(statusOutput)
|
statusStrings := utils.SplitLines(statusOutput)
|
||||||
files := []*File{}
|
files := []*File{}
|
||||||
|
|
||||||
submoduleNames, err := c.GetSubmoduleNames()
|
|
||||||
if err != nil {
|
|
||||||
c.Log.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, statusString := range statusStrings {
|
for _, statusString := range statusStrings {
|
||||||
if strings.HasPrefix(statusString, "warning") {
|
if strings.HasPrefix(statusString, "warning") {
|
||||||
c.Log.Warningf("warning when calling git status: %s", statusString)
|
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)
|
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
|
||||||
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
|
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
|
||||||
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
|
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
|
||||||
isSubmodule := utils.IncludesString(submoduleNames, filename)
|
|
||||||
|
|
||||||
file := &File{
|
file := &File{
|
||||||
Name: filename,
|
Name: filename,
|
||||||
@ -345,7 +312,6 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*File {
|
|||||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||||
Type: c.OSCommand.FileType(filename),
|
Type: c.OSCommand.FileType(filename),
|
||||||
ShortStatus: change,
|
ShortStatus: change,
|
||||||
IsSubmodule: isSubmodule,
|
|
||||||
}
|
}
|
||||||
files = append(files, file)
|
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
|
// if the file isn't tracked, we assume you want to delete it
|
||||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||||
if file.IsSubmodule {
|
if file.HasStagedChanges || file.HasMergeConflicts {
|
||||||
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 err := c.OSCommand.RunCommand("git reset -- %s", quotedFileName); err != nil {
|
if err := c.OSCommand.RunCommand("git reset -- %s", quotedFileName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -816,7 +778,7 @@ func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
|
|||||||
if filterPath != "" {
|
if filterPath != "" {
|
||||||
filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(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 {
|
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
|
||||||
|
7
pkg/commands/submodule_config.go
Normal file
7
pkg/commands/submodule_config.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
type SubmoduleConfig struct {
|
||||||
|
Name string
|
||||||
|
Path string
|
||||||
|
Url string
|
||||||
|
}
|
66
pkg/commands/submodules.go
Normal file
66
pkg/commands/submodules.go
Normal file
@ -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)
|
||||||
|
}
|
@ -2,42 +2,74 @@ package gui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jesseduffield/gocui"
|
"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 {
|
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
|
||||||
file := gui.getSelectedFile()
|
file := gui.getSelectedFile()
|
||||||
if file == nil {
|
if file == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if file.IsSubmodule {
|
var menuItems []*menuItem
|
||||||
// git submodule foreach '[[ "$name" == "renderers/chartify" ]] && git stash --include-untracked'
|
|
||||||
// git submodule update --force renderers/chartify
|
|
||||||
}
|
|
||||||
|
|
||||||
menuItems := []*menuItem{
|
submoduleConfigs := gui.State.SubmoduleConfigs
|
||||||
{
|
if file.IsSubmodule(submoduleConfigs) {
|
||||||
displayString: gui.Tr.SLocalize("discardAllChanges"),
|
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
|
||||||
onPress: func() error {
|
|
||||||
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
|
menuItems = []*menuItem{
|
||||||
return gui.surfaceError(err)
|
{
|
||||||
}
|
displayString: gui.Tr.SLocalize("submoduleStashAndReset"),
|
||||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
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}})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
} else {
|
||||||
|
menuItems = []*menuItem{
|
||||||
if file.HasStagedChanges && file.HasUnstagedChanges {
|
{
|
||||||
menuItems = append(menuItems, &menuItem{
|
displayString: gui.Tr.SLocalize("discardAllChanges"),
|
||||||
displayString: gui.Tr.SLocalize("discardUnstagedChanges"),
|
onPress: func() error {
|
||||||
onPress: func() error {
|
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
|
||||||
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
|
return gui.surfaceError(err)
|
||||||
return gui.surfaceError(err)
|
}
|
||||||
}
|
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
||||||
|
},
|
||||||
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})
|
return gui.createMenu(file.Name, menuItems, createMenuOptions{showCancel: true})
|
||||||
|
@ -93,6 +93,9 @@ func (gui *Gui) refreshFiles() error {
|
|||||||
// if the filesView hasn't been instantiated yet we just return
|
// if the filesView hasn't been instantiated yet we just return
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if err := gui.refreshStateSubmoduleConfigs(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := gui.refreshStateFiles(); err != nil {
|
if err := gui.refreshStateFiles(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -426,6 +429,17 @@ func (gui *Gui) refreshStateFiles() error {
|
|||||||
return nil
|
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 {
|
func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||||
if gui.popupPanelFocused() {
|
if gui.popupPanelFocused() {
|
||||||
return nil
|
return nil
|
||||||
|
@ -271,11 +271,12 @@ type Modes struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type guiState struct {
|
type guiState struct {
|
||||||
Files []*commands.File
|
Files []*commands.File
|
||||||
Branches []*commands.Branch
|
SubmoduleConfigs []*commands.SubmoduleConfig
|
||||||
Commits []*commands.Commit
|
Branches []*commands.Branch
|
||||||
StashEntries []*commands.StashEntry
|
Commits []*commands.Commit
|
||||||
CommitFiles []*commands.CommitFile
|
StashEntries []*commands.StashEntry
|
||||||
|
CommitFiles []*commands.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 []*commands.Commit
|
FilteredReflogCommits []*commands.Commit
|
||||||
|
@ -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)
|
return presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Modes.Diffing.Ref, gui.State.SubmoduleConfigs)
|
||||||
},
|
},
|
||||||
SelectedItem: func() (ListItem, bool) {
|
SelectedItem: func() (ListItem, bool) {
|
||||||
item := gui.getSelectedFile()
|
item := gui.getSelectedFile()
|
||||||
|
@ -7,19 +7,19 @@ import (
|
|||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"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))
|
lines := make([][]string, len(files))
|
||||||
|
|
||||||
for i := range files {
|
for i := range files {
|
||||||
diffed := files[i].Name == diffName
|
diffed := files[i].Name == diffName
|
||||||
lines[i] = getFileDisplayStrings(files[i], diffed)
|
lines[i] = getFileDisplayStrings(files[i], diffed, submoduleConfigs)
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFileDisplayStrings returns the display string of branch
|
// 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
|
// potentially inefficient to be instantiating these color
|
||||||
// objects with each render
|
// objects with each render
|
||||||
red := color.New(color.FgRed)
|
red := color.New(color.FgRed)
|
||||||
@ -55,7 +55,7 @@ func getFileDisplayStrings(f *commands.File, diffed bool) []string {
|
|||||||
output += secondCharCl.Sprint(secondChar)
|
output += secondCharCl.Sprint(secondChar)
|
||||||
output += restColor.Sprintf(" %s", f.Name)
|
output += restColor.Sprintf(" %s", f.Name)
|
||||||
|
|
||||||
if f.IsSubmodule {
|
if f.IsSubmodule(submoduleConfigs) {
|
||||||
output += utils.ColoredString(" (submodule)", theme.DefaultTextColor)
|
output += utils.ColoredString(" (submodule)", theme.DefaultTextColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1191,6 +1191,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
|||||||
}, &i18n.Message{
|
}, &i18n.Message{
|
||||||
ID: "runningCustomCommandStatus",
|
ID: "runningCustomCommandStatus",
|
||||||
Other: "running custom command",
|
Other: "running custom command",
|
||||||
|
}, &i18n.Message{
|
||||||
|
ID: "submoduleStashAndReset",
|
||||||
|
Other: "stash uncommitted submodule changes and reset",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user