From 2c184cf23406f70359afce57eebbe43e758ba09f Mon Sep 17 00:00:00 2001 From: Taylor Dickson Date: Mon, 16 Jun 2025 11:35:21 -0400 Subject: [PATCH 1/3] feat(Pathing): :sparkles: Add Windows Pathing Compatibility - Normalize paths using filepath.ToSlash() in processor for cross-platform gitignore matching - Add defensive path normalization to filetree package using strings.FieldsFunc - Add comprehensive tests for mixed path separators and edge cases --- internal/filetree/filetree.go | 14 ++++++-- internal/filetree/filetree_test.go | 52 ++++++++++++++++++++++++++++++ internal/processor/processor.go | 16 +++++---- 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/internal/filetree/filetree.go b/internal/filetree/filetree.go index 30d74da..8ca08e6 100644 --- a/internal/filetree/filetree.go +++ b/internal/filetree/filetree.go @@ -5,6 +5,7 @@ package filetree import ( + "path/filepath" "sort" "strings" ) @@ -28,9 +29,18 @@ func New(paths []string) *FileTree { tree := &FileTree{ root: make(Node), } - for _, path := range paths { - tree.addPath(strings.Split(path, "/")) + // Normalize path separators for cross-platform compatibility + normalizedPath := filepath.ToSlash(path) + + // Split path into components, automatically filtering empty parts + parts := strings.FieldsFunc(normalizedPath, func(r rune) bool { + return r == '/' + }) + + if len(parts) > 0 { + tree.addPath(parts) + } } return tree diff --git a/internal/filetree/filetree_test.go b/internal/filetree/filetree_test.go index 74e33cf..899f1af 100644 --- a/internal/filetree/filetree_test.go +++ b/internal/filetree/filetree_test.go @@ -68,4 +68,56 @@ func TestFileTree(t *testing.T) { t.Errorf("Expected:\n%s\n\nGot:\n%s", expected, result) } }) + + t.Run("mixed path separators", func(t *testing.T) { + // Test that FileTree can handle mixed Windows and Unix paths + paths := []string{ + "file1.txt", + "dir\\subdir\\file2.txt", // Windows-style + "dir/file3.txt", // Unix-style + } + result := Build(paths, "") + expected := strings.Join([]string{ + "/", + "├── dir/", + "│ ├── subdir/", + "│ │ └── file2.txt", + "│ └── file3.txt", + "└── file1.txt", + }, "\n") + + if result != expected { + t.Errorf("Expected:\n%s\n\nGot:\n%s", expected, result) + } + }) + + t.Run("edge cases with slashes", func(t *testing.T) { + // Test edge cases: double slashes, leading/trailing slashes + paths := []string{ + "normal/path.txt", + "double//slash.txt", // double slash + "/leading/slash.txt", // leading slash + "trailing/slash/.txt", // trailing slash + "multiple///slashes.txt", // multiple slashes + } + result := Build(paths, "") + expected := strings.Join([]string{ + "/", + "├── double/", + "│ └── slash.txt", + "├── leading/", + "│ └── slash.txt", + "├── multiple/", + "│ └── slashes.txt", + "├── normal/", + "│ └── path.txt", + "└── trailing/", + " └── slash/", + " └── .txt", + }, "\n") + + if result != expected { + t.Errorf("Expected:\n%s\n\nGot:\n%s", expected, result) + } + }) } diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 7142dde..2d52280 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -212,19 +212,22 @@ func (p *Processor) collectFiles() ([]string, error) { if info.IsDir() { return nil } - - // Get relative path for ignore checking + // Get relative path and normalize separators for cross-platform consistency relPath, err := filepath.Rel(p.rootDir, path) if err != nil { return fmt.Errorf("failed to get relative path: %w", err) } - - // Skip if file matches ignore pattern - if p.matcher != nil && p.matcher.Match(strings.Split(relPath, "/"), false) { + + // Normalize to forward slashes for consistent processing + // This ensures gitignore patterns work and output is uniform across platforms + normalizedPath := filepath.ToSlash(relPath) + // Check gitignore patterns using normalized path + if p.matcher != nil && p.matcher.Match(strings.Split(normalizedPath, "/"), false) { return nil } - files = append(files, relPath) + // Store normalized path for consistent cross-platform output + files = append(files, normalizedPath) return nil }) @@ -274,6 +277,5 @@ func (p *Processor) writeContents(w *bufio.Writer, files []string) error { return err } } - return nil } From cd450542bd6805672b96e37f4664ad7347b3779d Mon Sep 17 00:00:00 2001 From: Taylor Dickson Date: Thu, 19 Jun 2025 12:51:20 -0400 Subject: [PATCH 2/3] style(Pathing): :art: Remove whitespace --- internal/filetree/filetree.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/filetree/filetree.go b/internal/filetree/filetree.go index 8ca08e6..1677910 100644 --- a/internal/filetree/filetree.go +++ b/internal/filetree/filetree.go @@ -32,12 +32,12 @@ func New(paths []string) *FileTree { for _, path := range paths { // Normalize path separators for cross-platform compatibility normalizedPath := filepath.ToSlash(path) - + // Split path into components, automatically filtering empty parts parts := strings.FieldsFunc(normalizedPath, func(r rune) bool { return r == '/' }) - + if len(parts) > 0 { tree.addPath(parts) } From 57ab99f47885ff1ee0297dde20e453b080fb3a77 Mon Sep 17 00:00:00 2001 From: Taylor Dickson Date: Thu, 19 Jun 2025 13:00:42 -0400 Subject: [PATCH 3/3] fix(Pathing): :bug: Remove filepath.ToSlash in favour of splitting with string.FieldsFunc --- internal/filetree/filetree.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/internal/filetree/filetree.go b/internal/filetree/filetree.go index 1677910..ce0c838 100644 --- a/internal/filetree/filetree.go +++ b/internal/filetree/filetree.go @@ -5,7 +5,6 @@ package filetree import ( - "path/filepath" "sort" "strings" ) @@ -30,12 +29,9 @@ func New(paths []string) *FileTree { root: make(Node), } for _, path := range paths { - // Normalize path separators for cross-platform compatibility - normalizedPath := filepath.ToSlash(path) - // Split path into components, automatically filtering empty parts - parts := strings.FieldsFunc(normalizedPath, func(r rune) bool { - return r == '/' + parts := strings.FieldsFunc(path, func(r rune) bool { + return r == '/' || r == '\\' }) if len(parts) > 0 {