From 41f89d86f0d662b16639a3b31ec2ebc4f09f7d44 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 9 Apr 2025 13:21:26 +0200 Subject: [PATCH 1/2] Add test that shows problems with git-ignoring files with special characters For #, !, [, and ], the problem is that it doesn't ignore the file because the special characters need to be quoted. For *, the problem is that it ignores too much (it also hides the abc_def file because the * is treated as a glob). --- .../file/gitignore_special_characters.go | 87 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 2 files changed, 88 insertions(+) create mode 100644 pkg/integration/tests/file/gitignore_special_characters.go 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..81849fc0ce0 --- /dev/null +++ b/pkg/integration/tests/file/gitignore_special_characters.go @@ -0,0 +1,87 @@ +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(). + /* EXPECTED: + Lines( + Equals("▼ /"), + Equals(" ?? .gitignore"), + Equals(" ?? abc_def"), + ) + ACTUAL: + As you can see, it did ignore the 'file!abc' and 'file#abc' files + correctly. Those don't need to be quoted because # and ! are only + special at the beginning. + + Most of the other files are not ignored properly because their + special characters need to be escaped. For * it's the other way + round: while it does hide 'abc*def', it also hides 'abc_def', + which we don't want. + */ + Lines( + Equals("▼ /"), + Equals(" ?? !file"), + Equals(" ?? #file"), + Equals(" ?? .gitignore"), + Equals(" ?? file[x]"), + ) + + /* EXPECTED: + t.FileSystem().FileContent(".gitignore", Equals("\\#file\nfile#abc\n\\!file\nfile!abc\nabc\\*def\nfile\\[x\\]\n")) + ACTUAL: */ + 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, From b0ab6529c15e7b12c5faff386318060b3bce8fb8 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 9 Apr 2025 13:27:44 +0200 Subject: [PATCH 2/2] Escape special characters when git-ignoring files --- pkg/commands/git_commands/working_tree.go | 11 ++++++++-- .../file/gitignore_special_characters.go | 21 ------------------- 2 files changed, 9 insertions(+), 23 deletions(-) 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 index 81849fc0ce0..84aa57ec33b 100644 --- a/pkg/integration/tests/file/gitignore_special_characters.go +++ b/pkg/integration/tests/file/gitignore_special_characters.go @@ -55,33 +55,12 @@ var GitignoreSpecialCharacters = NewIntegrationTest(NewIntegrationTestArgs{ excludeFile("file[x]") t.Views().Files(). - /* EXPECTED: Lines( Equals("▼ /"), Equals(" ?? .gitignore"), Equals(" ?? abc_def"), ) - ACTUAL: - As you can see, it did ignore the 'file!abc' and 'file#abc' files - correctly. Those don't need to be quoted because # and ! are only - special at the beginning. - Most of the other files are not ignored properly because their - special characters need to be escaped. For * it's the other way - round: while it does hide 'abc*def', it also hides 'abc_def', - which we don't want. - */ - Lines( - Equals("▼ /"), - Equals(" ?? !file"), - Equals(" ?? #file"), - Equals(" ?? .gitignore"), - Equals(" ?? file[x]"), - ) - - /* EXPECTED: t.FileSystem().FileContent(".gitignore", Equals("\\#file\nfile#abc\n\\!file\nfile!abc\nabc\\*def\nfile\\[x\\]\n")) - ACTUAL: */ - t.FileSystem().FileContent(".gitignore", Equals("#file\nfile#abc\n!file\nfile!abc\nabc*def\nfile[x]\n")) }, })