mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-01-10 04:07:18 +02:00
ca7252ef8e
async fetching of suggestions remove limit cache the trie for future use more more
314 lines
6.6 KiB
Go
314 lines
6.6 KiB
Go
// Copyright 2018 iriri. All rights reserved. Use of this source code is
|
|
// governed by a BSD-style license which can be found in the LICENSE file.
|
|
|
|
// Package gitignore can be used to parse .gitignore-style files into lists of
|
|
// globs that can be used to test against paths or selectively walk a file
|
|
// tree. Gobwas's glob package is used for matching because it is faster than
|
|
// using regexp, which is overkill, and supports globstars (**), unlike
|
|
// filepath.Match.
|
|
package gitignore
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gobwas/glob"
|
|
)
|
|
|
|
type ignoreFile struct {
|
|
globs []glob.Glob
|
|
abspath []string
|
|
}
|
|
|
|
type IgnoreList struct {
|
|
files []ignoreFile
|
|
cwd []string
|
|
}
|
|
|
|
func toSplit(path string) []string {
|
|
return strings.Split(filepath.ToSlash(path), "/")
|
|
}
|
|
|
|
func fromSplit(path []string) string {
|
|
return filepath.FromSlash(strings.Join(path, "/"))
|
|
}
|
|
|
|
// New creates a new ignore list.
|
|
func New() (IgnoreList, error) {
|
|
cwd, err := filepath.Abs(".")
|
|
if err != nil {
|
|
return IgnoreList{}, err
|
|
}
|
|
files := make([]ignoreFile, 1, 4)
|
|
files[0].globs = make([]glob.Glob, 0, 16)
|
|
return IgnoreList{
|
|
files,
|
|
toSplit(cwd),
|
|
}, nil
|
|
}
|
|
|
|
// From creates a new ignore list and populates the first entry with the
|
|
// contents of the specified file.
|
|
func From(path string) (IgnoreList, error) {
|
|
ign, err := New()
|
|
if err == nil {
|
|
err = ign.append(path, nil)
|
|
}
|
|
return ign, err
|
|
}
|
|
|
|
// FromGit finds the root directory of the current git repository and creates a
|
|
// new ignore list with the contents of all .gitignore files in that git
|
|
// repository.
|
|
func FromGit() (IgnoreList, error) {
|
|
ign, err := New()
|
|
if err == nil {
|
|
err = ign.AppendGit()
|
|
}
|
|
return ign, err
|
|
}
|
|
|
|
func clean(s string) string {
|
|
i := len(s) - 1
|
|
for ; i >= 0; i-- {
|
|
if s[i] != ' ' || i > 0 && s[i-1] == '\\' {
|
|
return s[:i+1]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// AppendGlob appends a single glob as a new entry in the ignore list. The root
|
|
// (relevant for matching patterns that begin with "/") is assumed to be the
|
|
// current working directory.
|
|
func (ign *IgnoreList) AppendGlob(s string) error {
|
|
g, err := glob.Compile(clean(s), '/')
|
|
if err == nil {
|
|
ign.files[0].globs = append(ign.files[0].globs, g)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func toRelpath(s string, dir, cwd []string) string {
|
|
if s != "" {
|
|
if s[0] != '/' {
|
|
return s
|
|
}
|
|
if dir == nil || cwd == nil {
|
|
return s[1:]
|
|
}
|
|
dir = append(dir, toSplit(s[1:])...)
|
|
}
|
|
|
|
i := 0
|
|
min := len(cwd)
|
|
if len(dir) < min {
|
|
min = len(dir)
|
|
}
|
|
for ; i < min; i++ {
|
|
if dir[i] != cwd[i] {
|
|
break
|
|
}
|
|
}
|
|
if i == min && len(cwd) == len(dir) {
|
|
return "."
|
|
}
|
|
|
|
ss := make([]string, (len(cwd)-i)+(len(dir)-i))
|
|
j := 0
|
|
for ; j < len(cwd)-i; j++ {
|
|
ss[j] = ".."
|
|
}
|
|
for k := 0; j < len(ss); j, k = j+1, k+1 {
|
|
ss[j] = dir[i+k]
|
|
}
|
|
return fromSplit(ss)
|
|
}
|
|
|
|
func (ign *IgnoreList) append(path string, dir []string) error {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
var ignf *ignoreFile
|
|
if dir != nil {
|
|
ignf = &ign.files[0]
|
|
} else {
|
|
d, err := filepath.Abs(filepath.Dir(path))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d != fromSplit(ign.cwd) {
|
|
dir = toSplit(d)
|
|
ignf = &ignoreFile{
|
|
make([]glob.Glob, 0, 16),
|
|
dir,
|
|
}
|
|
} else {
|
|
ignf = &ign.files[0]
|
|
}
|
|
}
|
|
scn := bufio.NewScanner(bufio.NewReader(f))
|
|
for scn.Scan() {
|
|
s := scn.Text()
|
|
if s == "" || s[0] == '#' {
|
|
continue
|
|
}
|
|
g, err := glob.Compile(toRelpath(clean(s), dir, ign.cwd), '/')
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ignf.globs = append(ignf.globs, g)
|
|
}
|
|
ign.files = append(ign.files, *ignf)
|
|
return nil
|
|
}
|
|
|
|
// Append appends the globs in the specified file to the ignore list. Files are
|
|
// expected to have the same format as .gitignore files.
|
|
func (ign *IgnoreList) Append(path string) error {
|
|
return ign.append(path, nil)
|
|
}
|
|
|
|
func exists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return !os.IsNotExist(err)
|
|
}
|
|
|
|
func findGitRoot(cwd []string) (string, error) {
|
|
p := fromSplit(cwd)
|
|
for !exists(p + "/.git") {
|
|
if len(cwd) == 1 {
|
|
return "", errors.New("not in a git repository")
|
|
}
|
|
cwd = cwd[:len(cwd)-1]
|
|
p = fromSplit(cwd)
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (ign *IgnoreList) appendAll(fname, root string) error {
|
|
return filepath.Walk(
|
|
root,
|
|
func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if filepath.Base(path) == fname {
|
|
ign.append(path, nil)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// AppendGit finds the root directory of the current git repository and appends
|
|
// the contents of all .gitignore files in that git repository to the ignore
|
|
// list.
|
|
func (ign *IgnoreList) AppendGit() error {
|
|
gitRoot, err := findGitRoot(ign.cwd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = ign.AppendGlob(".git"); err != nil {
|
|
return err
|
|
}
|
|
usr, err := user.Current()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if gg := filepath.Join(usr.HomeDir, ".gitignore_global"); exists(gg) {
|
|
if err = ign.append(gg, toSplit(gitRoot)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return ign.appendAll(".gitignore", gitRoot)
|
|
}
|
|
|
|
func isPrefix(abspath, dir []string) bool {
|
|
if len(abspath) > len(dir) {
|
|
return false
|
|
}
|
|
for i := range abspath {
|
|
if abspath[i] != dir[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (ign *IgnoreList) match(path string, info os.FileInfo) bool {
|
|
if path == "." {
|
|
return false
|
|
}
|
|
ss := make([]string, 0, 4)
|
|
base := filepath.Base(path)
|
|
ss = append(ss, path)
|
|
if base != path {
|
|
ss = append(ss, base)
|
|
} else {
|
|
ss = append(ss, "./"+path)
|
|
}
|
|
if info != nil && info.IsDir() {
|
|
ss = append(ss, path+"/")
|
|
if base != path {
|
|
ss = append(ss, base+"/")
|
|
} else {
|
|
ss = append(ss, "./"+path+"/")
|
|
}
|
|
}
|
|
|
|
d, err := filepath.Abs(filepath.Dir(path))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
dir := toSplit(d)
|
|
for _, f := range ign.files {
|
|
if isPrefix(f.abspath, dir) || len(f.abspath) == 0 {
|
|
for _, g := range f.globs {
|
|
for _, s := range ss {
|
|
if g.Match(s) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Match returns whether any of the globs in the ignore list match the
|
|
// specified path. Uses the same matching rules as .gitignore files.
|
|
func (ign *IgnoreList) Match(path string) bool {
|
|
return ign.match(path, nil)
|
|
}
|
|
|
|
// Walk walks the file tree with the specified root and calls fn on each file
|
|
// or directory. Files and directories that match any of the globs in the
|
|
// ignore list are skipped.
|
|
func (ign *IgnoreList) Walk(root string, fn filepath.WalkFunc) error {
|
|
abs, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return filepath.Walk(
|
|
toRelpath("", toSplit(abs), ign.cwd),
|
|
func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ign.match(path, info) {
|
|
if info.IsDir() {
|
|
return filepath.SkipDir
|
|
}
|
|
return err
|
|
}
|
|
return fn(path, info, err)
|
|
})
|
|
}
|