198 lines
6.8 KiB
Go
198 lines
6.8 KiB
Go
package builtin
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
fileencoding "reasonix/internal/fileutil/encoding"
|
|
)
|
|
|
|
func writeFileT(t *testing.T, path, body string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func mkRepo(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(dir, ".git"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return dir
|
|
}
|
|
|
|
func TestGrepNestedGitignore(t *testing.T) {
|
|
dir := mkRepo(t)
|
|
writeFileT(t, filepath.Join(dir, ".gitignore"), "*.log\n")
|
|
writeFileT(t, filepath.Join(dir, "pkg", ".gitignore"), "secret.txt\n")
|
|
writeFileT(t, filepath.Join(dir, "pkg", "keep.go"), "NEEDLE\n")
|
|
writeFileT(t, filepath.Join(dir, "pkg", "secret.txt"), "NEEDLE\n") // ignored by nested
|
|
writeFileT(t, filepath.Join(dir, "pkg", "app.log"), "NEEDLE\n") // ignored by root
|
|
|
|
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
|
|
if !strings.Contains(out, "keep.go") {
|
|
t.Fatalf("kept file must be found: %q", out)
|
|
}
|
|
if strings.Contains(out, "secret.txt") {
|
|
t.Fatalf("a nested .gitignore must apply: %q", out)
|
|
}
|
|
if strings.Contains(out, "app.log") {
|
|
t.Fatalf("an ancestor .gitignore must apply in subdirs: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestGrepNestedNegationReincludes(t *testing.T) {
|
|
dir := mkRepo(t)
|
|
writeFileT(t, filepath.Join(dir, ".gitignore"), "*.log\n")
|
|
writeFileT(t, filepath.Join(dir, "pkg", ".gitignore"), "!keep.log\n")
|
|
writeFileT(t, filepath.Join(dir, "pkg", "keep.log"), "NEEDLE\n") // re-included by nested negation
|
|
writeFileT(t, filepath.Join(dir, "pkg", "drop.log"), "NEEDLE\n") // still ignored by root
|
|
|
|
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
|
|
if !strings.Contains(out, "keep.log") {
|
|
t.Fatalf("a nested !negation must re-include: %q", out)
|
|
}
|
|
if strings.Contains(out, "drop.log") {
|
|
t.Fatalf("a non-negated .log must stay ignored: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestGrepSkipsHidden(t *testing.T) {
|
|
dir := mkRepo(t)
|
|
writeFileT(t, filepath.Join(dir, "visible.txt"), "NEEDLE\n")
|
|
writeFileT(t, filepath.Join(dir, ".env"), "NEEDLE\n")
|
|
writeFileT(t, filepath.Join(dir, ".github", "ci.yml"), "NEEDLE\n")
|
|
|
|
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
|
|
if !strings.Contains(out, "visible.txt") {
|
|
t.Fatalf("a visible file must be found: %q", out)
|
|
}
|
|
if strings.Contains(out, ".env") || strings.Contains(out, "ci.yml") {
|
|
t.Fatalf("hidden files/dirs must be skipped by default: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestGrepExplicitHiddenRootSearched(t *testing.T) {
|
|
dir := mkRepo(t)
|
|
writeFileT(t, filepath.Join(dir, ".github", "ci.yml"), "NEEDLE\n")
|
|
// Pointing grep straight at a hidden dir searches it in full.
|
|
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": filepath.Join(dir, ".github")})
|
|
if !strings.Contains(out, "ci.yml") {
|
|
t.Fatalf("an explicitly targeted hidden dir should be searched: %q", out)
|
|
}
|
|
}
|
|
|
|
// repo scaffolds a fake git repo: a .git marker, a .gitignore, and files both
|
|
// kept and ignored — enough for the native grep walk to exercise .gitignore.
|
|
func gitignoreRepo(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(dir, ".git"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeFileT(t, filepath.Join(dir, ".gitignore"), "ignored.txt\nbuild/\n")
|
|
writeFileT(t, filepath.Join(dir, "keep.txt"), "NEEDLE in kept file\n")
|
|
writeFileT(t, filepath.Join(dir, "ignored.txt"), "NEEDLE in ignored file\n")
|
|
writeFileT(t, filepath.Join(dir, "build", "out.txt"), "NEEDLE in ignored dir\n")
|
|
writeFileT(t, filepath.Join(dir, "src", "deep.txt"), "NEEDLE deep in tree\n")
|
|
return dir
|
|
}
|
|
|
|
func TestGrepSkipsGitignored(t *testing.T) {
|
|
dir := gitignoreRepo(t)
|
|
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
|
|
|
|
if !strings.Contains(out, "keep.txt") || !strings.Contains(out, "deep.txt") {
|
|
t.Fatalf("kept files must be searched: %q", out)
|
|
}
|
|
if strings.Contains(out, "ignored.txt") {
|
|
t.Fatalf("a .gitignore'd file must be skipped: %q", out)
|
|
}
|
|
if strings.Contains(out, "out.txt") {
|
|
t.Fatalf("files under a .gitignore'd dir must be skipped: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestGrepDecodesGB18030Gitignore(t *testing.T) {
|
|
dir := mkRepo(t)
|
|
if err := os.WriteFile(filepath.Join(dir, ".gitignore"), fileencoding.Encode("秘密.txt\n", fileencoding.GB18030), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeFileT(t, filepath.Join(dir, "秘密.txt"), "NEEDLE ignored\n")
|
|
writeFileT(t, filepath.Join(dir, "公开.txt"), "NEEDLE kept\n")
|
|
|
|
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
|
|
if !strings.Contains(out, "公开.txt") {
|
|
t.Fatalf("kept Chinese file must be searched: %q", out)
|
|
}
|
|
if strings.Contains(out, "秘密.txt") {
|
|
t.Fatalf("GB18030 .gitignore pattern should skip Chinese file: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestScanGitConfigExcludesDecodesGB18030Path(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, ".gitconfig")
|
|
want := filepath.Join(dir, "中文忽略规则.txt")
|
|
body := "[core]\n\texcludesFile = " + want + "\n"
|
|
if err := os.WriteFile(path, fileencoding.Encode(body, fileencoding.GB18030), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if got := scanGitConfigExcludes(path); got != want {
|
|
t.Fatalf("scanGitConfigExcludes = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestGrepExplicitIgnoredRootStillSearched(t *testing.T) {
|
|
dir := gitignoreRepo(t)
|
|
// Pointing grep straight at a gitignored directory still searches it — the
|
|
// walk root is never pruned.
|
|
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": filepath.Join(dir, "build")})
|
|
if !strings.Contains(out, "out.txt") {
|
|
t.Fatalf("an explicitly targeted ignored dir should still be searched: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestGrepNoRepoIgnoresNothing(t *testing.T) {
|
|
// Same layout but without a .git marker: there is no repository, so
|
|
// .gitignore is not consulted and every file is searched.
|
|
dir := t.TempDir()
|
|
writeFileT(t, filepath.Join(dir, ".gitignore"), "ignored.txt\n")
|
|
writeFileT(t, filepath.Join(dir, "ignored.txt"), "NEEDLE here\n")
|
|
|
|
out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
|
|
if !strings.Contains(out, "ignored.txt") {
|
|
t.Fatalf("outside a git repo, .gitignore must not be applied: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestFindRepoRoot(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(dir, ".git"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sub := filepath.Join(dir, "a", "b")
|
|
if err := os.MkdirAll(sub, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := filepath.EvalSymlinks(findRepoRoot(sub))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want, _ := filepath.EvalSymlinks(dir)
|
|
if got != want {
|
|
t.Fatalf("findRepoRoot(%q) = %q, want %q", sub, got, want)
|
|
}
|
|
if rr := findRepoRoot(t.TempDir()); rr != "" {
|
|
t.Fatalf("a dir with no .git ancestor should return \"\", got %q", rr)
|
|
}
|
|
}
|