Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions pkg/commands/git_commands/working_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,19 +367,29 @@ func escapeFilename(filename string) string {
return "/" + re.ReplaceAllString(filename, `\${0}`)
}

// Ignore adds a file to the gitignore for the repo
func (self *WorkingTreeCommands) Ignore(filename string) error {
return self.os.AppendLineToFile(".gitignore", escapeFilename(filename))
// Ignore adds the given files to the gitignore for the repo
func (self *WorkingTreeCommands) Ignore(filenames []string) error {
for _, filename := range filenames {
if err := self.os.AppendLineToFile(".gitignore", escapeFilename(filename)); err != nil {
return err
}
}
return nil
}

// Exclude adds a file to the .git/info/exclude for the repo
func (self *WorkingTreeCommands) Exclude(filename string) error {
// Exclude adds the given files to the .git/info/exclude for the repo
func (self *WorkingTreeCommands) Exclude(filenames []string) error {
infoDir := filepath.Join(self.repoPaths.repoGitDirPath, "info")
if err := os.MkdirAll(infoDir, 0o755); err != nil {
return err
}
excludeFile := filepath.Join(infoDir, "exclude")
return self.os.AppendLineToFile(excludeFile, escapeFilename(filename))
for _, filename := range filenames {
if err := self.os.AppendLineToFile(excludeFile, escapeFilename(filename)); err != nil {
return err
}
}
return nil
}

// WorktreeFileDiff returns the diff of a file
Expand Down
83 changes: 56 additions & 27 deletions pkg/gui/controllers/files_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
},
{
Keys: opts.GetKeys(opts.Config.Files.IgnoreFile),
Handler: self.withItem(self.ignoreOrExcludeMenu),
GetDisabledReason: self.require(self.singleItemSelected()),
Handler: self.withItems(self.ignoreOrExcludeMenu),
GetDisabledReason: self.require(self.itemsSelected()),
Description: self.c.Tr.Actions.IgnoreExcludeFile,
OpensMenu: true,
},
Expand Down Expand Up @@ -971,74 +971,103 @@ func (self *FilesController) unstageFiles(node *filetree.FileNode) error {
})
}

func (self *FilesController) ignoreOrExcludeTracked(node *filetree.FileNode, trAction string, f func(string) error) error {
func (self *FilesController) ignoreOrExcludeTracked(nodes []*filetree.FileNode, trAction string, f func([]string) error) error {
self.c.LogAction(trAction)
// not 100% sure if this is necessary but I'll assume it is
if err := self.unstageFiles(node); err != nil {
return err
}

if err := self.c.Git().WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil {
return err
nodes = normalisedSelectedNodes(nodes)

paths := make([]string, 0, len(nodes))
for _, node := range nodes {
if node.GetIsTracked() {
// not 100% sure if this is necessary but I'll assume it is
if err := self.unstageFiles(node); err != nil {
return err
}

if err := self.c.Git().WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil {
return err
}
}

paths = append(paths, node.GetPath())
}

if err := f(node.GetPath()); err != nil {
if err := f(paths); err != nil {
return err
}

self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
return nil
}

func (self *FilesController) ignoreOrExcludeUntracked(node *filetree.FileNode, trAction string, f func(string) error) error {
func (self *FilesController) ignoreOrExcludeUntracked(nodes []*filetree.FileNode, trAction string, f func([]string) error) error {
self.c.LogAction(trAction)

if err := f(node.GetPath()); err != nil {
nodes = normalisedSelectedNodes(nodes)

paths := make([]string, 0, len(nodes))
for _, node := range nodes {
paths = append(paths, node.GetPath())
}

if err := f(paths); err != nil {
return err
}

self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
return nil
}

func (self *FilesController) ignoreOrExcludeFile(node *filetree.FileNode, trText string, trPrompt string, trAction string, f func(string) error) error {
if node.GetIsTracked() {
func (self *FilesController) ignoreOrExcludeFiles(nodes []*filetree.FileNode, trText string, trPrompt string, trAction string, f func([]string) error) error {
hasTracked := false
for _, node := range nodes {
if node.GetIsTracked() {
hasTracked = true
break
}
}

if hasTracked {
self.c.Confirm(types.ConfirmOpts{
Title: trText,
Prompt: trPrompt,
HandleConfirm: func() error {
return self.ignoreOrExcludeTracked(node, trAction, f)
return self.ignoreOrExcludeTracked(nodes, trAction, f)
},
})

return nil
}
return self.ignoreOrExcludeUntracked(node, trAction, f)
return self.ignoreOrExcludeUntracked(nodes, trAction, f)
}

func (self *FilesController) ignore(node *filetree.FileNode) error {
if node.GetPath() == ".gitignore" {
return errors.New(self.c.Tr.Actions.IgnoreFileErr)
func (self *FilesController) ignore(nodes []*filetree.FileNode) error {
for _, node := range nodes {
if node.GetPath() == ".gitignore" {
return errors.New(self.c.Tr.Actions.IgnoreFileErr)
}
}
return self.ignoreOrExcludeFile(node, self.c.Tr.IgnoreTracked, self.c.Tr.IgnoreTrackedPrompt, self.c.Tr.Actions.IgnoreExcludeFile, self.c.Git().WorkingTree.Ignore)
return self.ignoreOrExcludeFiles(nodes, self.c.Tr.IgnoreTracked, self.c.Tr.IgnoreTrackedPrompt, self.c.Tr.Actions.IgnoreExcludeFile, self.c.Git().WorkingTree.Ignore)
}

func (self *FilesController) exclude(node *filetree.FileNode) error {
if node.GetPath() == ".gitignore" {
return errors.New(self.c.Tr.Actions.ExcludeGitIgnoreErr)
func (self *FilesController) exclude(nodes []*filetree.FileNode) error {
for _, node := range nodes {
if node.GetPath() == ".gitignore" {
return errors.New(self.c.Tr.Actions.ExcludeGitIgnoreErr)
}
}

return self.ignoreOrExcludeFile(node, self.c.Tr.ExcludeTracked, self.c.Tr.ExcludeTrackedPrompt, self.c.Tr.Actions.ExcludeFile, self.c.Git().WorkingTree.Exclude)
return self.ignoreOrExcludeFiles(nodes, self.c.Tr.ExcludeTracked, self.c.Tr.ExcludeTrackedPrompt, self.c.Tr.Actions.ExcludeFile, self.c.Git().WorkingTree.Exclude)
}

func (self *FilesController) ignoreOrExcludeMenu(node *filetree.FileNode) error {
func (self *FilesController) ignoreOrExcludeMenu(nodes []*filetree.FileNode) error {
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.Actions.IgnoreExcludeFile,
Items: []*types.MenuItem{
{
LabelColumns: []string{self.c.Tr.IgnoreFile},
OnPress: func() error {
if err := self.ignore(node); err != nil {
if err := self.ignore(nodes); err != nil {
return err
}
return nil
Expand All @@ -1048,7 +1077,7 @@ func (self *FilesController) ignoreOrExcludeMenu(node *filetree.FileNode) error
{
LabelColumns: []string{self.c.Tr.ExcludeFile},
OnPress: func() error {
if err := self.exclude(node); err != nil {
if err := self.exclude(nodes); err != nil {
return err
}
return nil
Expand Down
65 changes: 65 additions & 0 deletions pkg/integration/tests/file/gitignore_range_select.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package file

import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)

var GitignoreRangeSelect = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Ignore and exclude multiple files at once via range select",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
},
SetupRepo: func(shell *Shell) {
shell.CreateFile(".gitignore", "")
shell.CreateFile("toIgnore1", "")
shell.CreateFile("toIgnore2", "")
shell.CreateFile("toIgnore3", "")
shell.CreateFile("toExclude1", "")
shell.CreateFile("toExclude2", "")
shell.CreateFile("toExclude3", "")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Lines(
Equals("▼ /").IsSelected(),
Equals(" ?? .gitignore"),
Equals(" ?? toExclude1"),
Equals(" ?? toExclude2"),
Equals(" ?? toExclude3"),
Equals(" ?? toIgnore1"),
Equals(" ?? toIgnore2"),
Equals(" ?? toIgnore3"),
).
// Select range from toIgnore1 to toIgnore3
NavigateToLine(Contains("toIgnore1")).
Press(keys.Universal.ToggleRangeSelect).
NavigateToLine(Contains("toIgnore3")).
Press(keys.Files.IgnoreFile).
// Ignore all selected files
Tap(func() {
t.ExpectPopup().Menu().Title(Equals("Ignore or exclude file")).
Select(Contains("Add to .gitignore")).
Confirm()

t.FileSystem().FileContent(".gitignore", Equals("/toIgnore1\n/toIgnore2\n/toIgnore3\n"))
}).
// Dismiss the range select mode for the next set of steps
Press(keys.Universal.ToggleRangeSelect).
// Select range from toExclude1 to toExclude3
NavigateToLine(Contains("toExclude1")).
Press(keys.Universal.ToggleRangeSelect).
NavigateToLine(Contains("toExclude3")).
Press(keys.Files.IgnoreFile).
// Exclude all selected files
Tap(func() {
t.ExpectPopup().Menu().Title(Equals("Ignore or exclude file")).
Select(Contains("Add to .git/info/exclude")).
Confirm()

t.FileSystem().FileContent(".git/info/exclude", Contains("/toExclude1\n/toExclude2\n/toExclude3\n"))
})
},
})
50 changes: 50 additions & 0 deletions pkg/integration/tests/file/gitignore_tracked_range_select.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package file

import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)

var GitignoreTrackedRangeSelect = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Range-select across a tracked directory and its children (parent+child case)",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("dir-tracked/file-a", "x")
shell.CreateFileAndAdd("dir-tracked/file-b", "x")
shell.CreateFileAndAdd("tracked1", "x")
shell.Commit("initial")
shell.UpdateFile("dir-tracked/file-a", "y")
shell.UpdateFile("dir-tracked/file-b", "y")
shell.UpdateFile("tracked1", "y")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Lines(
Equals("▼ /").IsSelected(),
Equals(" ▼ dir-tracked"),
Equals(" M file-a"),
Equals(" M file-b"),
Equals(" M tracked1"),
).
NavigateToLine(Contains("dir-tracked")).
Press(keys.Universal.ToggleRangeSelect).
NavigateToLine(Contains("tracked1")).
Press(keys.Files.IgnoreFile).
Tap(func() {
t.ExpectPopup().Menu().Title(Equals("Ignore or exclude file")).
Select(Contains("Add to .gitignore")).
Confirm()

t.ExpectPopup().Confirmation().
Title(Equals("Ignore tracked file")).
Content(Contains("tracked file")).
Confirm()

t.FileSystem().FileContent(".gitignore", Equals("/dir-tracked\n/tracked1\n"))
})
},
})
2 changes: 2 additions & 0 deletions pkg/integration/tests/test_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,9 @@ var tests = []*components.IntegrationTest{
file.DiscardVariousChangesRangeSelect,
file.ExcludeWithoutInfoDir,
file.Gitignore,
file.GitignoreRangeSelect,
file.GitignoreSpecialCharacters,
file.GitignoreTrackedRangeSelect,
file.RememberCommitMessageAfterFail,
file.RenameSimilarityThresholdChange,
file.RenamedFiles,
Expand Down
Loading