mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-25 17:52:02 +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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kovidgoyal/kitty/tools/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = fmt.Print
|
var _ = fmt.Print
|
||||||
|
|
||||||
type GitPattern struct {
|
type GitPattern struct {
|
||||||
only_dirs bool
|
line_number int
|
||||||
negated bool
|
only_dirs bool
|
||||||
parts []string
|
negated bool
|
||||||
matcher func(path string) 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 {
|
func (p GitPattern) Match(path string, ftype fs.FileMode) bool {
|
||||||
if p.only_dirs && ftype&fs.ModeDir == 0 {
|
if p.only_dirs && ftype&fs.ModeDir == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if os.PathSeparator != '/' {
|
|
||||||
path = strings.ReplaceAll(path, string(os.PathSeparator), "/")
|
|
||||||
}
|
|
||||||
return p.matcher(path)
|
return p.matcher(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +205,7 @@ func CompileGitIgnoreLine(line string) (ans GitPattern, skipped_line bool) {
|
|||||||
skipped_line = true
|
skipped_line = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ans.pattern = line
|
||||||
|
|
||||||
// Handle negated (accept) patterns
|
// Handle negated (accept) patterns
|
||||||
if line[0] == '!' {
|
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