mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-06 01:05:48 +02:00
Finish up gitignore implementation
This commit is contained in:
25
tools/ignorefiles/api.go
Normal file
25
tools/ignorefiles/api.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package ignorefiles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
type IgnoreFile interface {
|
||||
LoadString(string) error
|
||||
LoadBytes([]byte) error
|
||||
LoadLines(...string) error
|
||||
LoadFile(io.Reader) error
|
||||
LoadPath(string) error
|
||||
|
||||
// relpath is the path relative to the directory containing the ignorefile.
|
||||
// When is_ignored is true, linenum_of_matching_rule will be the line
|
||||
// number of the rule causing relpath to be ignored and pattern is the
|
||||
// textual representation of the matching pattern.
|
||||
IsIgnored(relpath string, ftype fs.FileMode) (is_ignored bool, linenum_of_matching_rule int, pattern string)
|
||||
}
|
||||
|
||||
func NewGitignore() IgnoreFile { return &Gitignore{index_of_last_negated_rule: -1} }
|
||||
@@ -2,29 +2,112 @@ package ignorefiles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
type GitPattern struct {
|
||||
only_dirs bool
|
||||
negated bool
|
||||
parts []string
|
||||
matcher func(path string) bool
|
||||
line_number int
|
||||
only_dirs bool
|
||||
negated bool
|
||||
pattern string
|
||||
parts []string
|
||||
matcher func(path string) bool
|
||||
}
|
||||
|
||||
type Gitignore struct {
|
||||
patterns []GitPattern
|
||||
index_of_last_negated_rule int
|
||||
line_number_offset int
|
||||
}
|
||||
|
||||
func (g Gitignore) IsIgnored(relpath string, ftype os.FileMode) (is_ignored bool, linenum_of_matching_rule int, pattern string) {
|
||||
if os.PathSeparator != '/' {
|
||||
relpath = strings.ReplaceAll(relpath, string(os.PathSeparator), "/")
|
||||
}
|
||||
linenum_of_matching_rule = -1
|
||||
for i, pat := range g.patterns {
|
||||
if is_ignored {
|
||||
if i > g.index_of_last_negated_rule {
|
||||
break
|
||||
}
|
||||
if pat.negated && pat.Match(relpath, ftype) {
|
||||
is_ignored = false
|
||||
linenum_of_matching_rule = -1
|
||||
pattern = ""
|
||||
}
|
||||
} else {
|
||||
if !pat.negated && pat.Match(relpath, ftype) {
|
||||
is_ignored = true
|
||||
linenum_of_matching_rule = pat.line_number
|
||||
pattern = pat.pattern
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (g *Gitignore) load_line(line string, line_number int) {
|
||||
if p, skipped_line := CompileGitIgnoreLine(line); !skipped_line {
|
||||
p.line_number = g.line_number_offset + line_number
|
||||
g.patterns = append(g.patterns, p)
|
||||
if p.negated {
|
||||
g.index_of_last_negated_rule = len(g.patterns) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gitignore) LoadLines(lines ...string) error {
|
||||
for i, line := range lines {
|
||||
g.load_line(line, i)
|
||||
}
|
||||
g.line_number_offset += len(lines)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gitignore) LoadString(text string) error {
|
||||
s := utils.NewLineScanner(text)
|
||||
lnum := 0
|
||||
for s.Scan() {
|
||||
g.load_line(s.Text(), lnum)
|
||||
lnum++
|
||||
}
|
||||
g.line_number_offset += lnum
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gitignore) LoadBytes(text []byte) error {
|
||||
return g.LoadString(string(text))
|
||||
}
|
||||
|
||||
func (g *Gitignore) LoadPath(path string) error {
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
return g.LoadString(utils.UnsafeBytesToString(data))
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gitignore) LoadFile(f io.Reader) error {
|
||||
if data, err := io.ReadAll(f); err == nil {
|
||||
return g.LoadString(utils.UnsafeBytesToString(data))
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (p GitPattern) Match(path string, ftype fs.FileMode) bool {
|
||||
if p.only_dirs && ftype&fs.ModeDir == 0 {
|
||||
return false
|
||||
}
|
||||
if os.PathSeparator != '/' {
|
||||
path = strings.ReplaceAll(path, string(os.PathSeparator), "/")
|
||||
}
|
||||
return p.matcher(path)
|
||||
}
|
||||
|
||||
@@ -122,6 +205,7 @@ func CompileGitIgnoreLine(line string) (ans GitPattern, skipped_line bool) {
|
||||
skipped_line = true
|
||||
return
|
||||
}
|
||||
ans.pattern = line
|
||||
|
||||
// Handle negated (accept) patterns
|
||||
if line[0] == '!' {
|
||||
|
||||
@@ -98,4 +98,48 @@ func TestGitignore(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
for text, tests := range map[string]map[string]bool{
|
||||
``: {"foo": false},
|
||||
`
|
||||
# exclude everything except directory foo/bar
|
||||
/*
|
||||
!/foo
|
||||
/foo/*
|
||||
!/foo/bar`: {
|
||||
"a": true, "foo": false, "foo/x": true, "foo/bar": false, "foo/bar/": false,
|
||||
},
|
||||
`
|
||||
**/foo
|
||||
bar `: {
|
||||
"foo": true, "baz/foo": true, "bar": true, "baz/bar": true, "a": false,
|
||||
},
|
||||
`/*.c`: {"a.c": true, "b/a.c": false},
|
||||
`
|
||||
**/external/**/*.json
|
||||
**/external/**/.*ignore
|
||||
**/external/foobar/*.css`: {
|
||||
"external/foobar/angular.foo.css": true, "external/barfoo/.gitignore": true, "external/barfoo/.bower.json": true,
|
||||
},
|
||||
"abc/def\r\nxyz": {"abc/def": true, "a/xyz": true},
|
||||
`/**/foo`: {"foo": true, "foo/": true, "a/b/foo": true, "fooo": false, "ofoo": false},
|
||||
"/.js": {".js": true, ".js/": true, ".js/a": true, ".jsa": false},
|
||||
"*.js": {".js": true, ".js/": true, ".js/a": true, "a.js/a": true, "a.js/a.js": true, ".jsa": false, "a.jsa": false},
|
||||
"foo/**/": {"foo/": false, "foo": false, "foo/abc/": true, "foo/a/b/c/": true, "foo/a": false},
|
||||
"foo/**/*.bar": {"foo/": false, "abc.bar": false, "foo/abc.bar": true, "foo/a.bar/": true, "foo/x/y/z.bar": true},
|
||||
`\#abc`: {"abc": false, "#abc": true},
|
||||
"abc\n!abc/x": {"abc": true, "abc/x": false, "abc/y": true},
|
||||
`abc/*`: {"abc": false, "abc/": false, "abc/x": true},
|
||||
} {
|
||||
p := NewGitignore()
|
||||
if err := p.LoadString(text); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for tpath, expected := range tests {
|
||||
path := strings.TrimRight(tpath, "/")
|
||||
ftype := utils.IfElse(len(path) < len(tpath), fs.ModeDir, 0)
|
||||
if actual, _, _ := p.IsIgnored(path, ftype); actual != expected {
|
||||
t.Fatalf("ignored: %v != %v for path: %#v and ignorefile:\n%s", expected, actual, tpath, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user