// SiYuan - Build Your Eternal Digital Garden // Copyright (c) 2020-present, b3log.org // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package util // 该文件代码来自 https://github.com/go-git/go-git 项目,Apache-2.0 license import ( "path/filepath" "strings" ) // https://github.com/go-git/go-git/blob/master/plumbing/format/gitignore/matcher.go // Matcher defines a global multi-pattern matcher for gitignore patterns type Matcher interface { // Match matches patterns in the order of priorities. As soon as an inclusion or // exclusion is found, not further matching is performed. Match(path []string, isDir bool) bool } // NewMatcher constructs a new global matcher. Patterns must be given in the order of // increasing priority. That is most generic settings files first, then the content of // the repo .gitignore, then content of .gitignore down the path or the repo and then // the content command line arguments. func NewMatcher(ps []Pattern) Matcher { return &matcher{ps} } type matcher struct { patterns []Pattern } func (m *matcher) Match(path []string, isDir bool) bool { n := len(m.patterns) for i := n - 1; i >= 0; i-- { if match := m.patterns[i].Match(path, isDir); match > NoMatch { return match == Exclude } } return false } // https://github.com/go-git/go-git/blob/master/plumbing/format/gitignore/pattern.go // 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 { 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 }