mirror of
https://github.com/go-task/task.git
synced 2026-06-23 12:45:52 +00:00
The sabhiram/go-gitignore dependency is unmaintained (since 2021) and has known gitignore spec gaps (* traverses /, ? broken, weak ** handling). Vendor the pattern/matcher core of go-git (plumbing/format/gitignore, v5.19.1) into internal/gitignore instead. It is ~185 lines of pure-stdlib, spec-correct code (proper **, anchoring, dir-only and negation handling). The file-walking helpers (dir.go) are intentionally omitted as they pull in go-billy and other go-git internals; importing the upstream package directly would re-add that transitive weight, which is why go-git was dropped before. This gains gitignore spec compliance while removing a direct dependency rather than adding one. The vendored code keeps its Apache-2.0 license (internal/gitignore/LICENSE) with attribution headers; upstream test cases are ported to table-driven stdlib tests to avoid an extra test dependency.
166 lines
3.6 KiB
Go
166 lines
3.6 KiB
Go
// Package gitignore implements gitignore pattern matching.
|
|
//
|
|
// This package is vendored from go-git:
|
|
//
|
|
// github.com/go-git/go-git/v5 v5.19.1, plumbing/format/gitignore
|
|
//
|
|
// Only the pattern parsing and matching logic (pattern.go and matcher.go) is
|
|
// copied; the file-walking helpers (dir.go) are omitted as they pull in
|
|
// go-billy and other go-git internals. The original code is licensed under the
|
|
// Apache License 2.0; see the LICENSE file in this directory.
|
|
package gitignore
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// MatchResult defines outcomes of a match, no match, exclusion or inclusion.
|
|
type MatchResult int
|
|
|
|
const (
|
|
// NoMatch defines the no match outcome of a match check
|
|
NoMatch MatchResult = iota
|
|
// Exclude defines an exclusion of a file as a result of a match check
|
|
Exclude
|
|
// Include defines an explicit inclusion of a file as a result of a match check
|
|
Include
|
|
)
|
|
|
|
const (
|
|
inclusionPrefix = "!"
|
|
zeroToManyDirs = "**"
|
|
patternDirSep = "/"
|
|
)
|
|
|
|
// Pattern defines a single gitignore pattern.
|
|
type Pattern interface {
|
|
// Match matches the given path to the pattern.
|
|
Match(path []string, isDir bool) MatchResult
|
|
}
|
|
|
|
type pattern struct {
|
|
domain []string
|
|
pattern []string
|
|
inclusion bool
|
|
dirOnly bool
|
|
isGlob bool
|
|
}
|
|
|
|
// ParsePattern parses a gitignore pattern string into the Pattern structure.
|
|
func ParsePattern(p string, domain []string) Pattern {
|
|
// storing domain, copy it to ensure it isn't changed externally
|
|
domain = append([]string(nil), domain...)
|
|
res := pattern{domain: domain}
|
|
|
|
if strings.HasPrefix(p, inclusionPrefix) {
|
|
res.inclusion = true
|
|
p = p[1:]
|
|
}
|
|
|
|
if !strings.HasSuffix(p, "\\ ") {
|
|
p = strings.TrimRight(p, " ")
|
|
}
|
|
|
|
if strings.HasSuffix(p, patternDirSep) {
|
|
res.dirOnly = true
|
|
p = p[:len(p)-1]
|
|
}
|
|
|
|
if strings.Contains(p, patternDirSep) {
|
|
res.isGlob = true
|
|
}
|
|
|
|
res.pattern = strings.Split(p, patternDirSep)
|
|
return &res
|
|
}
|
|
|
|
func (p *pattern) Match(path []string, isDir bool) MatchResult {
|
|
if len(path) <= len(p.domain) {
|
|
return NoMatch
|
|
}
|
|
for i, e := range p.domain {
|
|
if path[i] != e {
|
|
return NoMatch
|
|
}
|
|
}
|
|
|
|
path = path[len(p.domain):]
|
|
if p.isGlob && !p.globMatch(path, isDir) {
|
|
return NoMatch
|
|
} else if !p.isGlob && !p.simpleNameMatch(path, isDir) {
|
|
return NoMatch
|
|
}
|
|
|
|
if p.inclusion {
|
|
return Include
|
|
} else {
|
|
return Exclude
|
|
}
|
|
}
|
|
|
|
func (p *pattern) simpleNameMatch(path []string, isDir bool) bool {
|
|
for i, name := range path {
|
|
if match, err := filepath.Match(p.pattern[0], name); err != nil {
|
|
return false
|
|
} else if !match {
|
|
continue
|
|
}
|
|
if p.dirOnly && !isDir && i == len(path)-1 {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *pattern) globMatch(path []string, isDir bool) bool {
|
|
matched := false
|
|
canTraverse := false
|
|
for i, pattern := range p.pattern {
|
|
if pattern == "" {
|
|
canTraverse = false
|
|
continue
|
|
}
|
|
if pattern == zeroToManyDirs {
|
|
if i == len(p.pattern)-1 {
|
|
break
|
|
}
|
|
canTraverse = true
|
|
continue
|
|
}
|
|
if strings.Contains(pattern, zeroToManyDirs) {
|
|
return false
|
|
}
|
|
if len(path) == 0 {
|
|
return false
|
|
}
|
|
if canTraverse {
|
|
canTraverse = false
|
|
for len(path) > 0 {
|
|
e := path[0]
|
|
path = path[1:]
|
|
if match, err := filepath.Match(pattern, e); err != nil {
|
|
return false
|
|
} else if match {
|
|
matched = true
|
|
break
|
|
} else if len(path) == 0 {
|
|
// if nothing left then fail
|
|
matched = false
|
|
}
|
|
}
|
|
} else {
|
|
if match, err := filepath.Match(pattern, path[0]); err != nil || !match {
|
|
return false
|
|
}
|
|
matched = true
|
|
path = path[1:]
|
|
}
|
|
}
|
|
if matched && p.dirOnly && !isDir && len(path) == 0 {
|
|
matched = false
|
|
}
|
|
return matched
|
|
}
|