mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Respect .gitignore, add .cbmignore support, skip symlinks, expand ignore list
- Parse full .gitignore hierarchy (nested files + .git/info/exclude) via boyter/gocodewalker go-gitignore library for git repos - Add .cbmignore as repo-root gitignore-style file that stacks on top of .gitignore for indexer-specific exclusions (e.g. generated code in git) - Skip symlinked files and directories to prevent duplicate indexing - Move bin/, build/, out/ from always-on IGNORE_PATTERNS to fast-mode-only (prevents false exclusions in Go, CMake, Maven projects) - Add 28 ecosystem-specific cache/artifact dirs to IGNORE_PATTERNS: .next, .nuxt, .svelte-kit, .angular, .turbo, .terraform, bazel-bin, zig-cache, .cargo, .stack-work, elm-stuff, .metals, .bloop, etc. - Keep .cgrignore for backward compatibility - 17 new tests: gitignore patterns, negation, globstar, nested, exclude file, non-git repos, dir-only, cbmignore stacking, symlinks, pattern changes
This commit is contained in:
1 parent
a208b34c8b
commit
d8df2be5dc
5 files changed
+516
-13
No files matched your search
@@ -3,17 +3,22 @@ module github.com/DeusData/codebase-memory-mcp
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/boyter/gocodewalker v1.5.1
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/mattn/go-sqlite3 v1.14.34
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0
|
||||
github.com/zeebo/xxh3 v1.1.0
|
||||
go.uber.org/goleak v1.3.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/sys v0.40.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
|
||||
github.com/google/jsonschema-go v0.4.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
github.com/segmentio/encoding v0.5.3 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
github.com/boyter/gocodewalker v1.5.1 h1:0YeK2QAkd+ymW5MsagMZapIXD3v9/vrZl0HkFSLpKsw=
|
||||
github.com/boyter/gocodewalker v1.5.1/go.mod h1:9k+yM6+fIx61F0xI9ChXEGE5DYoLhggw8AxSOtW+kKo=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
@@ -6,20 +15,30 @@ github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbc
|
||||
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0 h1:u0kr8lbJc1oBcawK7Df+/ajNMpIDFE41OEPxdeTLOn8=
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
|
||||
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
|
||||
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
|
||||
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
@@ -28,7 +47,8 @@ golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -15,18 +15,34 @@ import (
|
||||
|
||||
// IGNORE_PATTERNS are directory names to skip during discovery.
|
||||
var IGNORE_PATTERNS = map[string]bool{
|
||||
".cache": true, ".claude": true, ".eclipse": true, ".eggs": true,
|
||||
".env": true, ".git": true, ".gradle": true, ".hg": true,
|
||||
".idea": true, ".maven": true, ".mypy_cache": true, ".nox": true,
|
||||
".npm": true, ".nyc_output": true, ".pnpm-store": true,
|
||||
".pytest_cache": true, ".qdrant_code_embeddings": true,
|
||||
".ruff_cache": true, ".svn": true, ".tmp": true, ".tox": true,
|
||||
".venv": true, ".vs": true, ".vscode": true, ".yarn": true,
|
||||
"__pycache__": true, "bin": true, "bower_components": true,
|
||||
"build": true, "coverage": true, "dist": true, "env": true,
|
||||
"htmlcov": true, "node_modules": true, "obj": true, "out": true,
|
||||
"Pods": true, "site-packages": true, "target": true, "temp": true,
|
||||
"tmp": true, "vendor": true, "venv": true,
|
||||
// VCS / IDE
|
||||
".git": true, ".hg": true, ".svn": true,
|
||||
".idea": true, ".vs": true, ".vscode": true, ".eclipse": true, ".claude": true,
|
||||
// Python
|
||||
".cache": true, ".eggs": true, ".env": true, ".mypy_cache": true, ".nox": true,
|
||||
".pytest_cache": true, ".ruff_cache": true, ".tox": true, ".venv": true,
|
||||
"__pycache__": true, "env": true, "htmlcov": true, "site-packages": true, "venv": true,
|
||||
// JS/TS
|
||||
".npm": true, ".nyc_output": true, ".pnpm-store": true, ".yarn": true,
|
||||
"bower_components": true, "coverage": true, "node_modules": true,
|
||||
// JS/TS framework caches
|
||||
".next": true, ".nuxt": true, ".svelte-kit": true, ".angular": true,
|
||||
".turbo": true, ".parcel-cache": true, ".docusaurus": true, ".expo": true,
|
||||
// Build artifacts
|
||||
"dist": true, "obj": true, "Pods": true, "target": true, "temp": true, "tmp": true,
|
||||
// Build system caches
|
||||
".terraform": true, ".serverless": true,
|
||||
"bazel-bin": true, "bazel-out": true, "bazel-testlogs": true,
|
||||
// Language-specific caches
|
||||
".cargo": true, ".stack-work": true, ".dart_tool": true,
|
||||
"zig-cache": true, "zig-out": true,
|
||||
".metals": true, ".bloop": true, ".bsp": true,
|
||||
".ccls-cache": true, ".clangd": true,
|
||||
"elm-stuff": true, "_opam": true, ".cpcache": true, ".shadow-cljs": true,
|
||||
// Deploy/hosting
|
||||
".vercel": true, ".netlify": true,
|
||||
// Misc
|
||||
".qdrant_code_embeddings": true, ".tmp": true, "vendor": true,
|
||||
}
|
||||
|
||||
// IGNORE_SUFFIXES are file suffixes that are never source files.
|
||||
@@ -82,6 +98,8 @@ var fastIgnoreDirs = map[string]bool{
|
||||
"e2e": true, "integration": true,
|
||||
"locale": true, "locales": true, "i18n": true, "l10n": true,
|
||||
"scripts": true, "tools": true, "hack": true,
|
||||
// Generic dirs moved from IGNORE_PATTERNS (cause false exclusions in Go, CMake, Maven)
|
||||
"bin": true, "build": true, "out": true,
|
||||
}
|
||||
|
||||
// fastIgnoreSuffixes are additional file extensions skipped in fast mode.
|
||||
@@ -323,6 +341,9 @@ func Discover(ctx context.Context, repoPath string, opts *Options) ([]FileInfo,
|
||||
mode = opts.Mode
|
||||
}
|
||||
|
||||
// Load gitignore + cbmignore matchers (nil-safe for non-git repos)
|
||||
matchers := loadIgnoreMatchers(repoPath)
|
||||
|
||||
err = filepath.Walk(repoPath, func(path string, info os.FileInfo, walkErr error) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
@@ -331,12 +352,28 @@ func Discover(ctx context.Context, repoPath string, opts *Options) ([]FileInfo,
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
// Skip symlinks — prevents duplicate indexing
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rel, _ := filepath.Rel(repoPath, path)
|
||||
|
||||
if info.IsDir() {
|
||||
// Hardcoded patterns first (fast O(1) map lookup)
|
||||
if shouldSkipDir(info.Name(), rel, extraIgnore, mode) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
// Then gitignore + cbmignore (skip root — library panics on base==path)
|
||||
if rel != "." && matchers.shouldIgnore(path, true) {
|
||||
slog.Debug("discover.gitignore_skip", "dir", rel)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Gitignore + cbmignore check for files
|
||||
if matchers.shouldIgnore(path, false) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package discover
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
gitignore "github.com/boyter/gocodewalker/go-gitignore"
|
||||
)
|
||||
|
||||
// ignoreMatchers holds the layered ignore matchers for a repository.
|
||||
// Evaluation order: hardcoded patterns (fastest) → gitignore → cbmignore.
|
||||
type ignoreMatchers struct {
|
||||
gitignore gitignore.GitIgnore // .gitignore hierarchy + .git/info/exclude (nil for non-git repos)
|
||||
cbmignore gitignore.GitIgnore // .cbmignore at repo root (nil if absent)
|
||||
}
|
||||
|
||||
// loadIgnoreMatchers loads all gitignore-style matchers for the repository.
|
||||
// .gitignore is loaded only for git repos (presence of .git dir).
|
||||
// .cbmignore stacks on top — patterns there additionally exclude from indexing.
|
||||
func loadIgnoreMatchers(repoPath string) ignoreMatchers {
|
||||
var m ignoreMatchers
|
||||
|
||||
// .gitignore — only for git repos
|
||||
gitDir := filepath.Join(repoPath, ".git")
|
||||
if info, err := os.Stat(gitDir); err == nil && info.IsDir() {
|
||||
gi, err := gitignore.NewRepository(repoPath)
|
||||
if err != nil {
|
||||
slog.Warn("discover.gitignore_load_err", "err", err)
|
||||
} else {
|
||||
m.gitignore = gi
|
||||
}
|
||||
}
|
||||
|
||||
// .cbmignore — repo-root file with gitignore-style patterns
|
||||
cbmPath := filepath.Join(repoPath, ".cbmignore")
|
||||
if _, err := os.Stat(cbmPath); err == nil {
|
||||
ci, err := gitignore.NewFromFile(cbmPath)
|
||||
if err != nil {
|
||||
slog.Warn("discover.cbmignore_load_err", "err", err)
|
||||
} else {
|
||||
m.cbmignore = ci
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// shouldIgnore checks if a path should be ignored by gitignore or cbmignore.
|
||||
// absPath must be absolute; isDir indicates whether the path is a directory.
|
||||
func (m *ignoreMatchers) shouldIgnore(absPath string, isDir bool) bool {
|
||||
if m.gitignore != nil {
|
||||
if match := m.gitignore.Absolute(absPath, isDir); match != nil && match.Ignore() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if m.cbmignore != nil {
|
||||
if match := m.cbmignore.Absolute(absPath, isDir); match != nil && match.Ignore() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
package discover
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// initGitRepo creates a minimal .git directory to make the temp dir look like a git repo.
|
||||
func initGitRepo(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Join(dir, ".git", "info"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, path, content string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func discoverRelPaths(t *testing.T, dir string) map[string]bool {
|
||||
t.Helper()
|
||||
files, err := Discover(context.Background(), dir, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := make(map[string]bool, len(files))
|
||||
for _, f := range files {
|
||||
m[f.RelPath] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func TestGitignoreBasicPatterns(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
writeFile(t, filepath.Join(dir, ".gitignore"), "*.log\nbuild/\n")
|
||||
writeFile(t, filepath.Join(dir, "main.go"), "package main\n")
|
||||
writeFile(t, filepath.Join(dir, "app.log"), "log data\n")
|
||||
writeFile(t, filepath.Join(dir, "build", "out.go"), "package build\n")
|
||||
|
||||
found := discoverRelPaths(t, dir)
|
||||
if !found["main.go"] {
|
||||
t.Error("main.go should be discovered")
|
||||
}
|
||||
if found["app.log"] {
|
||||
t.Error("app.log should be ignored by *.log pattern")
|
||||
}
|
||||
if found["build/out.go"] {
|
||||
t.Error("build/out.go should be ignored by build/ pattern")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitignoreNegation(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
writeFile(t, filepath.Join(dir, ".gitignore"), "*.log\n!important.log\n")
|
||||
writeFile(t, filepath.Join(dir, "debug.log"), "debug\n")
|
||||
writeFile(t, filepath.Join(dir, "important.log"), "keep me\n")
|
||||
// .log isn't a supported language, so add a Go file to prove discovery works
|
||||
writeFile(t, filepath.Join(dir, "main.go"), "package main\n")
|
||||
|
||||
files, err := Discover(context.Background(), dir, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
found := make(map[string]bool)
|
||||
for _, f := range files {
|
||||
found[f.RelPath] = true
|
||||
}
|
||||
|
||||
if !found["main.go"] {
|
||||
t.Error("main.go should be discovered")
|
||||
}
|
||||
// Note: .log files aren't a supported language, so they won't appear in Discover
|
||||
// results regardless. The gitignore negation is still effective — we test it
|
||||
// via the matcher directly.
|
||||
m := loadIgnoreMatchers(dir)
|
||||
if !m.shouldIgnore(filepath.Join(dir, "debug.log"), false) {
|
||||
t.Error("debug.log should be ignored")
|
||||
}
|
||||
if m.shouldIgnore(filepath.Join(dir, "important.log"), false) {
|
||||
t.Error("important.log should NOT be ignored (negated)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitignoreGlobstar(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
writeFile(t, filepath.Join(dir, ".gitignore"), "**/vendor/\n")
|
||||
writeFile(t, filepath.Join(dir, "main.go"), "package main\n")
|
||||
writeFile(t, filepath.Join(dir, "vendor", "lib.go"), "package vendor\n")
|
||||
writeFile(t, filepath.Join(dir, "src", "vendor", "dep.go"), "package vendor\n")
|
||||
|
||||
found := discoverRelPaths(t, dir)
|
||||
if !found["main.go"] {
|
||||
t.Error("main.go should be discovered")
|
||||
}
|
||||
// vendor/ is also in IGNORE_PATTERNS, but the gitignore globstar should catch nested ones
|
||||
if found["src/vendor/dep.go"] {
|
||||
t.Error("src/vendor/dep.go should be ignored by **/vendor/ pattern")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitignoreNested(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
writeFile(t, filepath.Join(dir, ".gitignore"), "*.txt\n")
|
||||
writeFile(t, filepath.Join(dir, "subdir", ".gitignore"), "!readme.txt\n")
|
||||
writeFile(t, filepath.Join(dir, "top.txt"), "top\n")
|
||||
writeFile(t, filepath.Join(dir, "subdir", "readme.txt"), "readme\n")
|
||||
writeFile(t, filepath.Join(dir, "subdir", "notes.txt"), "notes\n")
|
||||
|
||||
// Test via matchers since .txt isn't a supported language
|
||||
m := loadIgnoreMatchers(dir)
|
||||
|
||||
if !m.shouldIgnore(filepath.Join(dir, "top.txt"), false) {
|
||||
t.Error("top.txt should be ignored by root *.txt")
|
||||
}
|
||||
if m.shouldIgnore(filepath.Join(dir, "subdir", "readme.txt"), false) {
|
||||
t.Error("subdir/readme.txt should NOT be ignored (negated by subdir/.gitignore)")
|
||||
}
|
||||
if !m.shouldIgnore(filepath.Join(dir, "subdir", "notes.txt"), false) {
|
||||
t.Error("subdir/notes.txt should be ignored by root *.txt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitignoreExcludeFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
// Pattern only in .git/info/exclude
|
||||
writeFile(t, filepath.Join(dir, ".git", "info", "exclude"), "*.secret\n")
|
||||
writeFile(t, filepath.Join(dir, "main.go"), "package main\n")
|
||||
|
||||
m := loadIgnoreMatchers(dir)
|
||||
if !m.shouldIgnore(filepath.Join(dir, "config.secret"), false) {
|
||||
t.Error("config.secret should be ignored by .git/info/exclude")
|
||||
}
|
||||
if m.shouldIgnore(filepath.Join(dir, "main.go"), false) {
|
||||
t.Error("main.go should not be ignored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitignoreNonGitRepo(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// No .git directory
|
||||
|
||||
writeFile(t, filepath.Join(dir, "main.go"), "package main\n")
|
||||
|
||||
m := loadIgnoreMatchers(dir)
|
||||
if m.gitignore != nil {
|
||||
t.Error("gitignore should be nil for non-git repo")
|
||||
}
|
||||
|
||||
// Discovery should still work with hardcoded patterns
|
||||
found := discoverRelPaths(t, dir)
|
||||
if !found["main.go"] {
|
||||
t.Error("main.go should be discovered in non-git repo")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitignoreDirOnly(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
writeFile(t, filepath.Join(dir, ".gitignore"), "logs/\n")
|
||||
writeFile(t, filepath.Join(dir, "logs", "app.go"), "package logs\n")
|
||||
|
||||
m := loadIgnoreMatchers(dir)
|
||||
// Directory should be ignored
|
||||
if !m.shouldIgnore(filepath.Join(dir, "logs"), true) {
|
||||
t.Error("logs/ directory should be ignored")
|
||||
}
|
||||
// File named "logs" should NOT be ignored (trailing slash = dir-only)
|
||||
if m.shouldIgnore(filepath.Join(dir, "logs"), false) {
|
||||
t.Error("file named 'logs' should NOT be ignored by 'logs/' pattern")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCBMIgnoreBasic(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
// .cbmignore with patterns that stack on top of .gitignore
|
||||
writeFile(t, filepath.Join(dir, ".cbmignore"), "generated/\n*.pb.go\n")
|
||||
writeFile(t, filepath.Join(dir, "main.go"), "package main\n")
|
||||
writeFile(t, filepath.Join(dir, "generated", "types.go"), "package generated\n")
|
||||
writeFile(t, filepath.Join(dir, "api.pb.go"), "package api\n")
|
||||
|
||||
found := discoverRelPaths(t, dir)
|
||||
if !found["main.go"] {
|
||||
t.Error("main.go should be discovered")
|
||||
}
|
||||
if found["generated/types.go"] {
|
||||
t.Error("generated/types.go should be ignored by .cbmignore")
|
||||
}
|
||||
if found["api.pb.go"] {
|
||||
t.Error("api.pb.go should be ignored by .cbmignore")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCBMIgnoreStacksOnGitignore(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
// .gitignore ignores *.log
|
||||
writeFile(t, filepath.Join(dir, ".gitignore"), "*.log\n")
|
||||
// .cbmignore additionally ignores docs/
|
||||
writeFile(t, filepath.Join(dir, ".cbmignore"), "docs/\n")
|
||||
writeFile(t, filepath.Join(dir, "main.go"), "package main\n")
|
||||
writeFile(t, filepath.Join(dir, "docs", "api.go"), "package docs\n")
|
||||
|
||||
m := loadIgnoreMatchers(dir)
|
||||
|
||||
// Both matchers should work
|
||||
if !m.shouldIgnore(filepath.Join(dir, "app.log"), false) {
|
||||
t.Error("app.log should be ignored by .gitignore")
|
||||
}
|
||||
if !m.shouldIgnore(filepath.Join(dir, "docs"), true) {
|
||||
t.Error("docs/ should be ignored by .cbmignore")
|
||||
}
|
||||
if m.shouldIgnore(filepath.Join(dir, "main.go"), false) {
|
||||
t.Error("main.go should not be ignored")
|
||||
}
|
||||
|
||||
// End-to-end via Discover
|
||||
found := discoverRelPaths(t, dir)
|
||||
if !found["main.go"] {
|
||||
t.Error("main.go should be discovered")
|
||||
}
|
||||
if found["docs/api.go"] {
|
||||
t.Error("docs/api.go should be ignored by .cbmignore")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCBMIgnoreWithoutGitRepo(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// No .git — but .cbmignore should still work
|
||||
|
||||
writeFile(t, filepath.Join(dir, ".cbmignore"), "scratch/\n")
|
||||
writeFile(t, filepath.Join(dir, "main.go"), "package main\n")
|
||||
writeFile(t, filepath.Join(dir, "scratch", "tmp.go"), "package scratch\n")
|
||||
|
||||
found := discoverRelPaths(t, dir)
|
||||
if !found["main.go"] {
|
||||
t.Error("main.go should be discovered")
|
||||
}
|
||||
if found["scratch/tmp.go"] {
|
||||
t.Error("scratch/tmp.go should be ignored by .cbmignore")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSymlinkedFilesSkipped(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("symlinks require elevated privileges on Windows")
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
writeFile(t, filepath.Join(dir, "real.go"), "package main\n")
|
||||
if err := os.Symlink(filepath.Join(dir, "real.go"), filepath.Join(dir, "link.go")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
found := discoverRelPaths(t, dir)
|
||||
if !found["real.go"] {
|
||||
t.Error("real.go should be discovered")
|
||||
}
|
||||
// filepath.Walk uses Lstat, so symlinked files show ModeSymlink
|
||||
// Our check should skip them
|
||||
if found["link.go"] {
|
||||
t.Error("link.go (symlink) should be skipped")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSymlinkedDirsSkipped(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("symlinks require elevated privileges on Windows")
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
writeFile(t, filepath.Join(dir, "src", "main.go"), "package main\n")
|
||||
if err := os.Symlink(filepath.Join(dir, "src"), filepath.Join(dir, "src-link")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
found := discoverRelPaths(t, dir)
|
||||
if !found["src/main.go"] {
|
||||
t.Error("src/main.go should be discovered")
|
||||
}
|
||||
// filepath.Walk does NOT follow symlinked dirs, so src-link/ contents won't appear.
|
||||
// The symlinked dir entry itself should be skipped by our ModeSymlink check.
|
||||
if found["src-link/main.go"] {
|
||||
t.Error("src-link/main.go should not appear")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewIgnorePatterns(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
// Create directories that should now be in IGNORE_PATTERNS
|
||||
dirs := []string{".next", ".terraform", "zig-cache", ".cargo", "elm-stuff", "bazel-out"}
|
||||
for _, d := range dirs {
|
||||
writeFile(t, filepath.Join(dir, d, "file.go"), "package x\n")
|
||||
}
|
||||
writeFile(t, filepath.Join(dir, "main.go"), "package main\n")
|
||||
|
||||
found := discoverRelPaths(t, dir)
|
||||
if !found["main.go"] {
|
||||
t.Error("main.go should be discovered")
|
||||
}
|
||||
for _, d := range dirs {
|
||||
if found[d+"/file.go"] {
|
||||
t.Errorf("%s/file.go should be ignored by IGNORE_PATTERNS", d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericDirsNotIgnoredInFullMode(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
// bin, build, out were moved to fastIgnoreDirs — should be discovered in full mode
|
||||
dirs := []string{"bin", "build", "out"}
|
||||
for _, d := range dirs {
|
||||
writeFile(t, filepath.Join(dir, d, "main.go"), "package "+d+"\n")
|
||||
}
|
||||
|
||||
found := discoverRelPaths(t, dir)
|
||||
for _, d := range dirs {
|
||||
if !found[d+"/main.go"] {
|
||||
t.Errorf("%s/main.go should be discovered in full mode", d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenericDirsIgnoredInFastMode(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
dirs := []string{"bin", "build", "out"}
|
||||
for _, d := range dirs {
|
||||
writeFile(t, filepath.Join(dir, d, "main.go"), "package "+d+"\n")
|
||||
}
|
||||
|
||||
files, err := Discover(context.Background(), dir, &Options{Mode: ModeFast})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := make(map[string]bool, len(files))
|
||||
for _, f := range files {
|
||||
found[f.RelPath] = true
|
||||
}
|
||||
|
||||
for _, d := range dirs {
|
||||
if found[d+"/main.go"] {
|
||||
t.Errorf("%s/main.go should be ignored in fast mode", d)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user