From 1caa2ec9d47d2aa1aebed9d4ea721a124b9509fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:01:20 +0000 Subject: [PATCH 1/4] Initial plan From 3adc4075638e99a737ef73ec35828ce258cc003b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:06:01 +0000 Subject: [PATCH 2/4] Initial analysis - identify .gptinclude directory pattern matching issue Co-authored-by: chand1012 <3521582+chand1012@users.noreply.github.com> --- .gptinclude | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gptinclude diff --git a/.gptinclude b/.gptinclude deleted file mode 100644 index 8431972..0000000 --- a/.gptinclude +++ /dev/null @@ -1 +0,0 @@ -prompt/ \ No newline at end of file From 99082c3b7c97843fdaf4d1692e765c25afe2e497 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:13:28 +0000 Subject: [PATCH 3/4] Identify critical glob pattern matching bug in ignore/include logic Co-authored-by: chand1012 <3521582+chand1012@users.noreply.github.com> --- .gptinclude | 1 + debug.go | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++++ glob.go | 18 ++++++ 3 files changed, 202 insertions(+) create mode 100644 .gptinclude create mode 100644 debug.go create mode 100644 glob.go diff --git a/.gptinclude b/.gptinclude new file mode 100644 index 0000000..0d5ea5a --- /dev/null +++ b/.gptinclude @@ -0,0 +1 @@ +prompt diff --git a/debug.go b/debug.go new file mode 100644 index 0000000..4bd005f --- /dev/null +++ b/debug.go @@ -0,0 +1,183 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" + "github.com/gobwas/glob" +) + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +func getIgnoreList(ignoreFilePath string) ([]string, error) { + var ignoreList []string + file, err := os.Open(ignoreFilePath) + if err != nil { + return ignoreList, err + } + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if strings.HasSuffix(line, "/") { + line = line + "**" + } + line = strings.TrimPrefix(line, "/") + ignoreList = append(ignoreList, line) + } + return ignoreList, scanner.Err() +} + +func getIncludeList(includeFilePath string) ([]string, error) { + var includeList []string + file, err := os.Open(includeFilePath) + if err != nil { + return includeList, err + } + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if strings.HasSuffix(line, "/") { + line = line + "**" + } + line = strings.TrimPrefix(line, "/") + includeList = append(includeList, line) + } + return includeList, scanner.Err() +} + +func GenerateIgnoreList(repoPath, ignoreFilePath string, useGitignore bool) []string { + if ignoreFilePath == "" { + ignoreFilePath = filepath.Join(repoPath, ".gptignore") + } + var ignoreList []string + if _, err := os.Stat(ignoreFilePath); err == nil { + ignoreList, _ = getIgnoreList(ignoreFilePath) + } + ignoreList = append(ignoreList, ".git/**", ".gitignore", ".gptignore", ".gptinclude") + if useGitignore { + gitignorePath := filepath.Join(repoPath, ".gitignore") + if _, err := os.Stat(gitignorePath); err == nil { + gitignoreList, _ := getIgnoreList(gitignorePath) + ignoreList = append(ignoreList, gitignoreList...) + } + } + var finalIgnoreList []string + for _, pattern := range ignoreList { + if !contains(finalIgnoreList, pattern) { + info, err := os.Stat(filepath.Join(repoPath, pattern)) + if err == nil && info.IsDir() { + pattern = filepath.Join(pattern, "**") + } + finalIgnoreList = append(finalIgnoreList, pattern) + } + } + return finalIgnoreList +} + +func GenerateIncludeList(repoPath, includeFilePath string) []string { + if includeFilePath == "" { + includeFilePath = filepath.Join(repoPath, ".gptinclude") + } + var includeList []string + if _, err := os.Stat(includeFilePath); err == nil { + includeList, _ = getIncludeList(includeFilePath) + } + + fmt.Printf("Raw include list from file: %v\n", includeList) + + var finalIncludeList []string + for _, pattern := range includeList { + if !contains(finalIncludeList, pattern) { + fmt.Printf("Processing pattern: %s\n", pattern) + info, err := os.Stat(filepath.Join(repoPath, pattern)) + fmt.Printf(" Stat result: err=%v, isDir=%v\n", err, err == nil && info != nil && info.IsDir()) + if err == nil && info.IsDir() { + pattern = filepath.Join(pattern, "**") + fmt.Printf(" Modified pattern to: %s\n", pattern) + } + finalIncludeList = append(finalIncludeList, pattern) + } + } + fmt.Printf("Final include list: %v\n", finalIncludeList) + return finalIncludeList +} + +func windowsToUnixPath(windowsPath string) string { + unixPath := strings.ReplaceAll(windowsPath, "\\", "/") + return unixPath +} + +func main() { + if len(os.Args) < 2 { + fmt.Println("Usage: go run debug.go ") + return + } + + repoPath := os.Args[1] + includePatterns := GenerateIncludeList(repoPath, "") + ignorePatterns := GenerateIgnoreList(repoPath, "", false) + + fmt.Printf("Include patterns: %v\n", includePatterns) + fmt.Printf("Ignore patterns: %v\n", ignorePatterns) + + // Walk through all files in the repository + filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + relativeFilePath, _ := filepath.Rel(repoPath, path) + + fmt.Printf("\nTesting file: %s\n", relativeFilePath) + + // Test include logic + included := false + if len(includePatterns) > 0 { + for _, pattern := range includePatterns { + g := glob.MustCompile(pattern, '/') + matches := g.Match(windowsToUnixPath(relativeFilePath)) + fmt.Printf(" Include pattern %s matches: %v\n", pattern, matches) + if matches { + included = true + } + } + } else { + included = true + fmt.Printf(" No include patterns, would include by default\n") + } + + // Test ignore logic + ignored := false + for _, pattern := range ignorePatterns { + g := glob.MustCompile(pattern, '/') + matches := g.Match(windowsToUnixPath(relativeFilePath)) + fmt.Printf(" Ignore pattern %s matches: %v\n", pattern, matches) + if matches { + ignored = true + } + } + + finalDecision := included && !ignored + fmt.Printf(" Final decision: %s (included=%v, ignored=%v)\n", + map[bool]string{true: "INCLUDE", false: "EXCLUDE"}[finalDecision], included, ignored) + } + return nil + }) +} \ No newline at end of file diff --git a/glob.go b/glob.go new file mode 100644 index 0000000..564e47f --- /dev/null +++ b/glob.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "github.com/gobwas/glob" +) + +func main() { + pattern := "*.go" + testFiles := []string{"main.go", "src/helper.go", "helper.go", "test.js"} + + g := glob.MustCompile(pattern, '/') + + for _, file := range testFiles { + matches := g.Match(file) + fmt.Printf("Pattern '%s' matches '%s': %v\n", pattern, file, matches) + } +} \ No newline at end of file From e56ffaad425eb97003717f16fee670e72304948b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:19:45 +0000 Subject: [PATCH 4/4] Fix glob pattern matching for extension patterns - resolves user issue Co-authored-by: chand1012 <3521582+chand1012@users.noreply.github.com> --- debug.go | 183 ------------------------------------- glob.go | 18 ---- prompt/pattern_fix_test.go | 145 +++++++++++++++++++++++++++++ prompt/prompt.go | 20 +++- 4 files changed, 163 insertions(+), 203 deletions(-) delete mode 100644 debug.go delete mode 100644 glob.go create mode 100644 prompt/pattern_fix_test.go diff --git a/debug.go b/debug.go deleted file mode 100644 index 4bd005f..0000000 --- a/debug.go +++ /dev/null @@ -1,183 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "os" - "path/filepath" - "strings" - "github.com/gobwas/glob" -) - -func contains(s []string, e string) bool { - for _, a := range s { - if a == e { - return true - } - } - return false -} - -func getIgnoreList(ignoreFilePath string) ([]string, error) { - var ignoreList []string - file, err := os.Open(ignoreFilePath) - if err != nil { - return ignoreList, err - } - defer file.Close() - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - if strings.HasSuffix(line, "/") { - line = line + "**" - } - line = strings.TrimPrefix(line, "/") - ignoreList = append(ignoreList, line) - } - return ignoreList, scanner.Err() -} - -func getIncludeList(includeFilePath string) ([]string, error) { - var includeList []string - file, err := os.Open(includeFilePath) - if err != nil { - return includeList, err - } - defer file.Close() - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - if strings.HasSuffix(line, "/") { - line = line + "**" - } - line = strings.TrimPrefix(line, "/") - includeList = append(includeList, line) - } - return includeList, scanner.Err() -} - -func GenerateIgnoreList(repoPath, ignoreFilePath string, useGitignore bool) []string { - if ignoreFilePath == "" { - ignoreFilePath = filepath.Join(repoPath, ".gptignore") - } - var ignoreList []string - if _, err := os.Stat(ignoreFilePath); err == nil { - ignoreList, _ = getIgnoreList(ignoreFilePath) - } - ignoreList = append(ignoreList, ".git/**", ".gitignore", ".gptignore", ".gptinclude") - if useGitignore { - gitignorePath := filepath.Join(repoPath, ".gitignore") - if _, err := os.Stat(gitignorePath); err == nil { - gitignoreList, _ := getIgnoreList(gitignorePath) - ignoreList = append(ignoreList, gitignoreList...) - } - } - var finalIgnoreList []string - for _, pattern := range ignoreList { - if !contains(finalIgnoreList, pattern) { - info, err := os.Stat(filepath.Join(repoPath, pattern)) - if err == nil && info.IsDir() { - pattern = filepath.Join(pattern, "**") - } - finalIgnoreList = append(finalIgnoreList, pattern) - } - } - return finalIgnoreList -} - -func GenerateIncludeList(repoPath, includeFilePath string) []string { - if includeFilePath == "" { - includeFilePath = filepath.Join(repoPath, ".gptinclude") - } - var includeList []string - if _, err := os.Stat(includeFilePath); err == nil { - includeList, _ = getIncludeList(includeFilePath) - } - - fmt.Printf("Raw include list from file: %v\n", includeList) - - var finalIncludeList []string - for _, pattern := range includeList { - if !contains(finalIncludeList, pattern) { - fmt.Printf("Processing pattern: %s\n", pattern) - info, err := os.Stat(filepath.Join(repoPath, pattern)) - fmt.Printf(" Stat result: err=%v, isDir=%v\n", err, err == nil && info != nil && info.IsDir()) - if err == nil && info.IsDir() { - pattern = filepath.Join(pattern, "**") - fmt.Printf(" Modified pattern to: %s\n", pattern) - } - finalIncludeList = append(finalIncludeList, pattern) - } - } - fmt.Printf("Final include list: %v\n", finalIncludeList) - return finalIncludeList -} - -func windowsToUnixPath(windowsPath string) string { - unixPath := strings.ReplaceAll(windowsPath, "\\", "/") - return unixPath -} - -func main() { - if len(os.Args) < 2 { - fmt.Println("Usage: go run debug.go ") - return - } - - repoPath := os.Args[1] - includePatterns := GenerateIncludeList(repoPath, "") - ignorePatterns := GenerateIgnoreList(repoPath, "", false) - - fmt.Printf("Include patterns: %v\n", includePatterns) - fmt.Printf("Ignore patterns: %v\n", ignorePatterns) - - // Walk through all files in the repository - filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() { - relativeFilePath, _ := filepath.Rel(repoPath, path) - - fmt.Printf("\nTesting file: %s\n", relativeFilePath) - - // Test include logic - included := false - if len(includePatterns) > 0 { - for _, pattern := range includePatterns { - g := glob.MustCompile(pattern, '/') - matches := g.Match(windowsToUnixPath(relativeFilePath)) - fmt.Printf(" Include pattern %s matches: %v\n", pattern, matches) - if matches { - included = true - } - } - } else { - included = true - fmt.Printf(" No include patterns, would include by default\n") - } - - // Test ignore logic - ignored := false - for _, pattern := range ignorePatterns { - g := glob.MustCompile(pattern, '/') - matches := g.Match(windowsToUnixPath(relativeFilePath)) - fmt.Printf(" Ignore pattern %s matches: %v\n", pattern, matches) - if matches { - ignored = true - } - } - - finalDecision := included && !ignored - fmt.Printf(" Final decision: %s (included=%v, ignored=%v)\n", - map[bool]string{true: "INCLUDE", false: "EXCLUDE"}[finalDecision], included, ignored) - } - return nil - }) -} \ No newline at end of file diff --git a/glob.go b/glob.go deleted file mode 100644 index 564e47f..0000000 --- a/glob.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "fmt" - "github.com/gobwas/glob" -) - -func main() { - pattern := "*.go" - testFiles := []string{"main.go", "src/helper.go", "helper.go", "test.js"} - - g := glob.MustCompile(pattern, '/') - - for _, file := range testFiles { - matches := g.Match(file) - fmt.Printf("Pattern '%s' matches '%s': %v\n", pattern, file, matches) - } -} \ No newline at end of file diff --git a/prompt/pattern_fix_test.go b/prompt/pattern_fix_test.go new file mode 100644 index 0000000..be1bd5a --- /dev/null +++ b/prompt/pattern_fix_test.go @@ -0,0 +1,145 @@ +package prompt + +import ( + "os" + "path/filepath" + "testing" +) + +func TestExtensionPatternFix(t *testing.T) { + // Create a temporary directory structure for testing + tempDir, err := os.MkdirTemp("", "git2gpt-pattern-test") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create test files + testFiles := []struct { + path string + contents string + }{ + {"file.go", "package main"}, // Root level .go file + {"config.json", `{"test": true}`}, // Root level .json file + {"README.md", "# Root readme"}, // Root level .md file + {"src/main.go", "package main\nfunc main(){}"},// Nested .go file + {"src/config.json", `{"nested": true}`}, // Nested .json file + {"docs/api.md", "# API docs"}, // Nested .md file + {"deep/nested/file.go", "package deep"}, // Deeply nested .go file + } + + for _, tf := range testFiles { + fullPath := filepath.Join(tempDir, tf.path) + dir := filepath.Dir(fullPath) + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatalf("Failed to create directory %s: %v", dir, err) + } + if err := os.WriteFile(fullPath, []byte(tf.contents), 0644); err != nil { + t.Fatalf("Failed to write file %s: %v", fullPath, err) + } + } + + // Test case: Include all .md files (both root and nested), ignore all .go files (both root and nested) + t.Run("Extension patterns should match both root and nested files", func(t *testing.T) { + // Create .gptinclude with *.md pattern + includeFilePath := filepath.Join(tempDir, ".gptinclude") + if err := os.WriteFile(includeFilePath, []byte("*.md"), 0644); err != nil { + t.Fatalf("Failed to write .gptinclude file: %v", err) + } + + // Create .gptignore with *.go pattern + ignoreFilePath := filepath.Join(tempDir, ".gptignore") + if err := os.WriteFile(ignoreFilePath, []byte("*.go"), 0644); err != nil { + t.Fatalf("Failed to write .gptignore file: %v", err) + } + + // Generate include and ignore lists + includeList := GenerateIncludeList(tempDir, "") + ignoreList := GenerateIgnoreList(tempDir, "", false) + + // Process the repository + repo, err := ProcessGitRepo(tempDir, includeList, ignoreList) + if err != nil { + t.Fatalf("Failed to process repository: %v", err) + } + + // Expected: Only .md files should be included (both root and nested) + expectedFiles := []string{"README.md", "docs/api.md"} + unexpectedFiles := []string{"file.go", "src/main.go", "deep/nested/file.go", "config.json", "src/config.json"} + + // Check expected files are included + for _, expectedFile := range expectedFiles { + found := false + for _, file := range repo.Files { + if file.Path == expectedFile { + found = true + break + } + } + if !found { + t.Errorf("Expected file %s to be included, but it wasn't", expectedFile) + } + } + + // Check unexpected files are excluded + for _, unexpectedFile := range unexpectedFiles { + for _, file := range repo.Files { + if file.Path == unexpectedFile { + t.Errorf("File %s should have been excluded, but it was included", unexpectedFile) + break + } + } + } + }) + + // Test case: Ensure existing patterns with ** still work + t.Run("Existing ** patterns should continue to work", func(t *testing.T) { + // Create .gptinclude with existing **/*.json pattern (should not be modified) + includeFilePath := filepath.Join(tempDir, ".gptinclude") + if err := os.WriteFile(includeFilePath, []byte("**/*.json"), 0644); err != nil { + t.Fatalf("Failed to write .gptinclude file: %v", err) + } + + // Remove .gptignore for this test + ignoreFilePath := filepath.Join(tempDir, ".gptignore") + os.Remove(ignoreFilePath) + + // Generate include and ignore lists + includeList := GenerateIncludeList(tempDir, "") + ignoreList := GenerateIgnoreList(tempDir, "", false) + + // Process the repository + repo, err := ProcessGitRepo(tempDir, includeList, ignoreList) + if err != nil { + t.Fatalf("Failed to process repository: %v", err) + } + + // Expected: Only nested .json files should be included (not root level) + expectedFiles := []string{"src/config.json"} + unexpectedFiles := []string{"config.json", "README.md", "file.go", "src/main.go"} + + // Check expected files are included + for _, expectedFile := range expectedFiles { + found := false + for _, file := range repo.Files { + if file.Path == expectedFile { + found = true + break + } + } + if !found { + t.Errorf("Expected file %s to be included, but it wasn't", expectedFile) + } + } + + // Check unexpected files are excluded + for _, unexpectedFile := range unexpectedFiles { + for _, file := range repo.Files { + if file.Path == unexpectedFile { + t.Errorf("File %s should have been excluded, but it was included", unexpectedFile) + break + } + } + } + }) +} \ No newline at end of file diff --git a/prompt/prompt.go b/prompt/prompt.go index 426ce30..7db51a8 100644 --- a/prompt/prompt.go +++ b/prompt/prompt.go @@ -53,7 +53,15 @@ func getIgnoreList(ignoreFilePath string) ([]string, error) { line = line + "**" } line = strings.TrimPrefix(line, "/") - ignoreList = append(ignoreList, line) + + // Convert simple extension patterns like "*.ext" to match both root and nested files + // Add both "*.ext" (for root files) and "**/*.ext" (for nested files) + if strings.HasPrefix(line, "*.") && !strings.Contains(line, "/") && !strings.Contains(line, "**") { + ignoreList = append(ignoreList, line) // Keep original for root files + ignoreList = append(ignoreList, "**/"+line) // Add recursive pattern for nested files + } else { + ignoreList = append(ignoreList, line) + } } return ignoreList, scanner.Err() } @@ -76,7 +84,15 @@ func getIncludeList(includeFilePath string) ([]string, error) { line = line + "**" } line = strings.TrimPrefix(line, "/") - includeList = append(includeList, line) + + // Convert simple extension patterns like "*.ext" to match both root and nested files + // Add both "*.ext" (for root files) and "**/*.ext" (for nested files) + if strings.HasPrefix(line, "*.") && !strings.Contains(line, "/") && !strings.Contains(line, "**") { + includeList = append(includeList, line) // Keep original for root files + includeList = append(includeList, "**/"+line) // Add recursive pattern for nested files + } else { + includeList = append(includeList, line) + } } return includeList, scanner.Err() }