From 7fa707723eb44e7c34bd26869c16119baa3917ab Mon Sep 17 00:00:00 2001 From: RerankerGuo <121015044+RerankerGuo@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:25:42 +0800 Subject: [PATCH] fix(tool): preserve colon paths in code_search Parse NUL-delimited git grep output so filenames containing colons are not mistaken for line-number separators. Cover workspace and commit modes with real repository fixtures. Test: make check && make test && make build && make coverage --- internal/tool/code_search.go | 37 ++++++++++++------------ internal/tool/code_search_test.go | 48 +++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/internal/tool/code_search.go b/internal/tool/code_search.go index bc6422ca..49901af8 100644 --- a/internal/tool/code_search.go +++ b/internal/tool/code_search.go @@ -75,7 +75,7 @@ func (p *CodeSearchProvider) buildGrepArgs(searchText string, caseSensitive bool cmdArgs = append(cmdArgs, "-F") } - cmdArgs = append(cmdArgs, "-n", "--no-color") + cmdArgs = append(cmdArgs, "-z", "-n", "--no-color") cmdArgs = append(cmdArgs, "--max-count", fmt.Sprintf("%d", gitGrepMaxCount)) cmdArgs = append(cmdArgs, "-e", searchText) @@ -176,35 +176,36 @@ func (p *CodeSearchProvider) gitGrep(ctx context.Context, searchText string, cas var fileOrder []string seen := make(map[string]bool) - hasRef := p.FileReader.Ref != "" - splitN := 3 - offset := 0 - if hasRef { - splitN = 4 - offset = 1 - } - var sb strings.Builder if truncated { sb.WriteString(fmt.Sprintf("Note: The results have been truncated. Only showing first %d results.\n", gitGrepMaxCount)) } - for _, line := range lines { - if line == "" { - continue + remaining := outStr + for remaining != "" { + fname, rest, ok := strings.Cut(remaining, "\x00") + if !ok { + break } - parts := strings.SplitN(line, ":", splitN) - if len(parts) < splitN { - continue + lineNum, rest, ok := strings.Cut(rest, "\x00") + if !ok { + break + } + var content string + content, remaining, _ = strings.Cut(rest, "\n") + if ref := p.FileReader.Ref; ref != "" { + fname, ok = strings.CutPrefix(fname, ref+":") + if !ok { + continue + } } - fname := parts[offset] m := match{} - ln, parseErr := strconv.Atoi(parts[offset+1]) + ln, parseErr := strconv.Atoi(lineNum) if parseErr != nil { continue } m.lineNum = ln - m.content = parts[offset+2] + m.content = content if !seen[fname] { seen[fname] = true fileOrder = append(fileOrder, fname) diff --git a/internal/tool/code_search_test.go b/internal/tool/code_search_test.go index 0afc23f4..61cce9a5 100644 --- a/internal/tool/code_search_test.go +++ b/internal/tool/code_search_test.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "slices" "strings" "testing" @@ -22,6 +23,7 @@ func TestBuildGrepArgs_WorkspaceMode(t *testing.T) { assertContainsInOrder(t, args, "-e", "myFunc", "--") assertContains(t, args, "-i") + assertContains(t, args, "-z") assertContains(t, args, "--untracked") if idx := slices.Index(args, "--"); idx >= 0 { for i := 0; i < idx; i++ { @@ -152,6 +154,52 @@ func TestGitGrep_WorkspaceMode_Found(t *testing.T) { } } +func TestGitGrep_PathContainingColon(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Windows filenames cannot contain colons") + } + + dir := setupTestRepo(t) + for _, name := range []string{"foo:bar.go", "plain.go"} { + if err := os.WriteFile(filepath.Join(dir, name), []byte("package main\n// needle\n"), 0o644); err != nil { + t.Fatal(err) + } + } + cmd := exec.Command("git", "add", ".") + cmd.Dir = dir + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("git add: %v\n%s", err, out) + } + cmd = exec.Command("git", "commit", "-m", "add search fixtures") + cmd.Dir = dir + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("git commit: %v\n%s", err, out) + } + + want := "File: foo:bar.go\nMatch lines: 1\n2|// needle\n\n" + + "File: plain.go\nMatch lines: 1\n2|// needle\n\n" + tests := []struct { + name string + ref string + mode ReviewMode + }{ + {name: "workspace", mode: ModeWorkspace}, + {name: "commit", ref: getHeadCommit(t, dir), mode: ModeCommit}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p := NewCodeSearch(&FileReader{RepoDir: dir, Ref: test.ref, Mode: test.mode}) + got, err := p.gitGrep(context.Background(), "needle", false, false, nil) + if err != nil { + t.Fatal(err) + } + if got != want { + t.Errorf("gitGrep() = %q, want %q", got, want) + } + }) + } +} + func TestGitGrep_WorkspaceMode_NoMatch(t *testing.T) { dir := setupTestRepo(t) p := NewCodeSearch(&FileReader{RepoDir: dir, Ref: "", Mode: ModeWorkspace})