diff --git a/pkg/commands/git_commands/working_tree.go b/pkg/commands/git_commands/working_tree.go index c967ca576f6..6a4c2210da4 100644 --- a/pkg/commands/git_commands/working_tree.go +++ b/pkg/commands/git_commands/working_tree.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/commands/models" @@ -230,15 +231,21 @@ func (self *WorkingTreeCommands) DiscardUnstagedFileChanges(file *models.File) e return self.cmd.New(cmdArgs).Run() } +// Escapes special characters in a filename for gitignore and exclude files +func escapeFilename(filename string) string { + re := regexp.MustCompile(`^[!#]|[\[\]*]`) + 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", filename) + return self.os.AppendLineToFile(".gitignore", escapeFilename(filename)) } // Exclude adds a file to the .git/info/exclude for the repo func (self *WorkingTreeCommands) Exclude(filename string) error { excludeFile := filepath.Join(self.repoPaths.repoGitDirPath, "info", "exclude") - return self.os.AppendLineToFile(excludeFile, filename) + return self.os.AppendLineToFile(excludeFile, escapeFilename(filename)) } // WorktreeFileDiff returns the diff of a file diff --git a/pkg/integration/tests/file/gitignore_special_characters.go b/pkg/integration/tests/file/gitignore_special_characters.go new file mode 100644 index 00000000000..84aa57ec33b --- /dev/null +++ b/pkg/integration/tests/file/gitignore_special_characters.go @@ -0,0 +1,66 @@ +package file + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var GitignoreSpecialCharacters = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Ignore files with special characters in their names", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + }, + SetupRepo: func(shell *Shell) { + shell.CreateFile(".gitignore", "") + shell.CreateFile("#file", "") + shell.CreateFile("file#abc", "") + shell.CreateFile("!file", "") + shell.CreateFile("file!abc", "") + shell.CreateFile("abc*def", "") + shell.CreateFile("abc_def", "") + shell.CreateFile("file[x]", "") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + excludeFile := func(fileName string) { + t.Views().Files(). + NavigateToLine(Contains(fileName)). + Press(keys.Files.IgnoreFile) + + t.ExpectPopup().Menu(). + Title(Equals("Ignore or exclude file")). + Select(Contains("Add to .gitignore")). + Confirm() + } + + t.Views().Files(). + Focus(). + Lines( + Equals("▼ /"), + Equals(" ?? !file"), + Equals(" ?? #file"), + Equals(" ?? .gitignore"), + Equals(" ?? abc*def"), + Equals(" ?? abc_def"), + Equals(" ?? file!abc"), + Equals(" ?? file#abc"), + Equals(" ?? file[x]"), + ) + + excludeFile("#file") + excludeFile("file#abc") + excludeFile("!file") + excludeFile("file!abc") + excludeFile("abc*def") + excludeFile("file[x]") + + t.Views().Files(). + Lines( + Equals("▼ /"), + Equals(" ?? .gitignore"), + Equals(" ?? abc_def"), + ) + + t.FileSystem().FileContent(".gitignore", Equals("\\#file\nfile#abc\n\\!file\nfile!abc\nabc\\*def\nfile\\[x\\]\n")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index c79dcd2f8ee..cbcf4ab5a94 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -197,6 +197,7 @@ var tests = []*components.IntegrationTest{ file.DiscardVariousChanges, file.DiscardVariousChangesRangeSelect, file.Gitignore, + file.GitignoreSpecialCharacters, file.RememberCommitMessageAfterFail, file.RenameSimilarityThresholdChange, file.RenamedFiles,