summaryrefslogtreecommitdiff
path: root/internal/ignore/checker.go
blob: 034e3dbd394da897bbe507038c6404f1fafd8b73 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// Package ignore provides a thread-safe gitignore-aware file checker that combines .gitignore
// patterns with user-configured extra patterns. Used by the LSP server to
// skip completions and code actions for ignored files.
package ignore

import (
	"path/filepath"
	"strings"
	"sync"

	gitignore "github.com/sabhiram/go-gitignore"
)

// Checker evaluates whether an absolute file path should be ignored based on
// .gitignore patterns and/or user-configured extra patterns. It is safe for
// concurrent use.
type Checker struct {
	mu        sync.RWMutex
	gitRoot   string
	giMatcher *gitignore.GitIgnore // compiled .gitignore (nil when disabled or missing)
	exMatcher *gitignore.GitIgnore // compiled extra patterns (nil when empty)
}

// New creates a Checker. If useGitignore is true and gitRoot is non-empty, it
// loads .gitignore from gitRoot. extraPatterns are always compiled (gitignore
// syntax).
func New(gitRoot string, useGitignore bool, extraPatterns []string) *Checker {
	c := &Checker{gitRoot: gitRoot}
	c.compile(useGitignore, extraPatterns)
	return c
}

// IsIgnored returns whether absPath should be ignored and a human-readable
// reason string. When the checker is nil, nothing is ignored.
func (c *Checker) IsIgnored(absPath string) (ignored bool, reason string) {
	if c == nil {
		return false, ""
	}
	c.mu.RLock()
	defer c.mu.RUnlock()

	rel, inside := c.relPath(absPath)

	// Only check gitignore when the path is inside the git root
	if inside && c.giMatcher != nil && c.giMatcher.MatchesPath(rel) {
		return true, "matched .gitignore pattern"
	}
	if c.exMatcher != nil && c.exMatcher.MatchesPath(rel) {
		return true, "matched extra ignore pattern"
	}
	return false, ""
}

// Update recompiles matchers for hot-reload. Thread-safe.
func (c *Checker) Update(useGitignore bool, extraPatterns []string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.compile(useGitignore, extraPatterns)
}

// compile builds the gitignore and extra-pattern matchers. Must be called
// under c.mu write lock (or during construction).
func (c *Checker) compile(useGitignore bool, extraPatterns []string) {
	c.giMatcher = nil
	c.exMatcher = nil

	if useGitignore && c.gitRoot != "" {
		giPath := filepath.Join(c.gitRoot, ".gitignore")
		if gi, err := gitignore.CompileIgnoreFile(giPath); err == nil {
			c.giMatcher = gi
		}
	}
	if len(extraPatterns) > 0 {
		c.exMatcher = gitignore.CompileIgnoreLines(extraPatterns...)
	}
}

// relPath converts absPath to a path relative to gitRoot. Returns the
// relative path and true if the path is inside the git root; otherwise
// returns the original path and false.
func (c *Checker) relPath(absPath string) (string, bool) {
	if c.gitRoot == "" {
		return absPath, false
	}
	rel, err := filepath.Rel(c.gitRoot, absPath)
	if err != nil || strings.HasPrefix(rel, "..") {
		return absPath, false
	}
	return rel, true
}