Skip to content

Commit

Permalink
limited gitignore support
Browse files Browse the repository at this point in the history
  • Loading branch information
richardjennings committed Nov 4, 2024
1 parent cc3608e commit e279335
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 33 deletions.
58 changes: 36 additions & 22 deletions config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package g

import (
"bufio"
"fmt"
"os"
"path/filepath"
Expand All @@ -18,9 +19,23 @@ const (
DefaultEditor = "vim"
DefaultPackedRefsFile = "info/refs"
DefaultPackfileDirectory = "pack"
DefaultGitIgnoreFileName = ".gitignore"
)

var config Cnf
var config = &Cnf{
GitDirectory: DefaultGitDirectory,
Path: DefaultPath,
HeadFile: DefaultHeadFile,
IndexFile: DefaultIndexFile,
ObjectsDirectory: DefaultObjectsDirectory,
RefsDirectory: DefaultRefsDirectory,
RefsHeadsDirectory: DefaultRefsHeadsDirectory,
PackedRefsFile: DefaultPackedRefsFile,
PackfileDirectory: DefaultPackfileDirectory,
DefaultBranch: DefaultBranchName,
Editor: DefaultEditor,
GitIgnoreFileName: DefaultGitIgnoreFileName,
}

type (
Cnf struct {
Expand All @@ -39,9 +54,10 @@ type (
PackedRefsFile string
PackfileDirectory string
DefaultBranch string
GitIgnore []string
GitIgnore [][]byte
Editor string
EditorArgs []string
GitIgnoreFileName string
}
Opt func(m *Cnf) error
)
Expand All @@ -65,35 +81,33 @@ func WithGitDirectory(name string) Opt {
}

func Configure(opts ...Opt) error {
c := &Cnf{
GitDirectory: DefaultGitDirectory,
Path: DefaultPath,
HeadFile: DefaultHeadFile,
IndexFile: DefaultIndexFile,
ObjectsDirectory: DefaultObjectsDirectory,
RefsDirectory: DefaultRefsDirectory,
RefsHeadsDirectory: DefaultRefsHeadsDirectory,
PackedRefsFile: DefaultPackedRefsFile,
PackfileDirectory: DefaultPackfileDirectory,
DefaultBranch: DefaultBranchName,
Editor: DefaultEditor,
GitIgnore: []string{ //@todo read from .gitignore
".idea/",
},
}

for _, opt := range opts {
if err := opt(c); err != nil {
if err := opt(config); err != nil {
return err
}
}
if c.Path == "" {
if config.Path == DefaultPath {
p, err := filepath.Abs(DefaultPath)
if err != nil {
return err
}
c.Path = p
config.Path = p
}

// read .gitignore
// @todo there can be multiple, and some of the rules are relative to those
// files ...
config.GitIgnore = make([][]byte, 0)
file, err := os.Open(config.GitIgnoreFileName)
if err != nil {
return nil
}
defer func() { _ = file.Close() }()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
config.GitIgnore = append(config.GitIgnore, scanner.Bytes())
}
config = *c
return nil
}

Expand Down
2 changes: 1 addition & 1 deletion fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func Ls(path string) ([]*FileStatus, error) {
return nil
}
// do not add ignored files
if !IsIgnored(path) {
if !IsIgnored(path, config.GitIgnore) {
files = append(files, &FileStatus{
path: strings.TrimPrefix(path, WorkingDirectory()),
wd: &fileInfo{
Expand Down
73 changes: 63 additions & 10 deletions ignore.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,75 @@
package g

import (
"path/filepath"
"bytes"
"fmt"
"strings"
)

func IsIgnored(path string) bool {
// remove absolute portion of Path
func IsIgnored(path string, rules [][]byte) bool {

// make the path relative
path = strings.TrimPrefix(path, Path())
path = strings.TrimPrefix(path, string(filepath.Separator))
if path == "" {

// ignore the git directory regardless
if strings.HasPrefix(path, fmt.Sprintf("/%s/", config.GitDirectory)) {
return true
}
// @todo fix literal string prefix matching and iteration
for _, v := range config.GitIgnore {
if strings.HasPrefix(path, v) {
return true

for _, v := range rules {
prefixMatch := false
dirMatch := false

if v == nil || len(v) == 0 {

Check failure on line 23 in ignore.go

View workflow job for this annotation

GitHub Actions / Run CI (ubuntu-latest, 1.23.x)

should omit nil check; len() for []byte is defined as zero (S1009)

Check failure on line 23 in ignore.go

View workflow job for this annotation

GitHub Actions / Run CI (macOS-latest, 1.23.x)

should omit nil check; len() for []byte is defined as zero (S1009)
// blank line separator
continue
}
if v[0] == '#' {
// comment
continue
}
if v[0] == '\\' && v[1] == '#' {
// escaped #
v = v[1:]
}
for i, vv := range v {
// allow escaped spaces
if vv == '\\' && v[i+1] == ' ' && i < len(v)-1 {
v = append(v[:i], v[:i+1]...)
}
}
if v[0] == '/' {
// starts with /
prefixMatch = true
} else if l := bytes.LastIndex(v, []byte{'/'}); l > -1 && (l < len(v)-1 || l == 0) {
// of has / in it but not the end
prefixMatch = true
// easier to add explicit '/'
v = append([]byte{'/'}, v...)
}
if v[len(v)-1] == '/' {
dirMatch = true
}

// check for suffix match
if !prefixMatch && !dirMatch {
// other things to check before using suffix ...
if bytes.HasSuffix([]byte(path), v) {
return true
}
}

if dirMatch {
if path[len(path)-1] == '/' {
return true
}
}

if prefixMatch {
if bytes.HasPrefix([]byte(path), v) {
return true
}
}
}
return strings.HasPrefix(path, config.GitDirectory+string(filepath.Separator))
return false
}
94 changes: 94 additions & 0 deletions ignore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package g

import (
"fmt"
"testing"
)

func TestIsIgnored(t *testing.T) {
type tc struct {
Pattern string
Path string
Expect bool
}
for _, tt := range []tc{
// A blank line matches no files, so it can serve as a separator for
// readability.
{Pattern: "", Path: "/test/hello", Expect: false},
{Pattern: "", Path: "/.git/HEAD", Expect: true},

// A line starting with # serves as a comment. Put a backslash ("\") in
// front of the first hash for patterns that begin with a hash.
{Pattern: "#test", Path: "/test/#test", Expect: false},
{Pattern: `\#test`, Path: "/test/#test", Expect: true},
// Trailing spaces are ignored unless they are quoted with backslash
// ("\").
// @todo

// An optional prefix "!" which negates the pattern; any matching file excluded by a previous pattern will
// become included again. It is not possible to re-include a file if a parent directory of that file is
// excluded. Git doesn’t list excluded directories for performance reasons, so any patterns on contained files
// have no effect, no matter where they are defined. Put a backslash ("\") in front of the first "!" for
// patterns that begin with a literal "!", for example, "\!important!.txt".
// @todo

// The slash "/" is used as the directory separator. Separators may
// occur at the beginning, middle or end of the .gitignore search
// pattern.

// If there is a separator at the beginning or middle (or both) of the pattern, then the pattern is relative to
// the directory level of the particular .gitignore file itself. Otherwise the pattern may also match at any
// level below the .gitignore level.
{Pattern: "/a", Path: "/a", Expect: true},
{Pattern: "a", Path: "/a", Expect: true},
{Pattern: "a", Path: "/b/a", Expect: true},
{Pattern: "a", Path: "/c/b/a", Expect: true},
{Pattern: "/a/b", Path: "/a/b", Expect: true},
{Pattern: "a/b", Path: "/a/b", Expect: true},
{Pattern: "a/b", Path: "/d/a/b", Expect: false},

// If there is a separator at the end of the pattern then the pattern will only match directories, otherwise the
// pattern can match both files and directories.
//{Pattern: "/a/b/", Path: "/a/b", Expect: true},

// For example, a pattern doc/frotz/ matches doc/frotz directory, but not a/doc/frotz directory; however frotz/
// matches frotz and a/frotz that is a directory (all paths are relative from the .gitignore file).
{Pattern: "doc/frotz/", Path: "/doc/frotz/", Expect: true},
{Pattern: "doc/frotz/", Path: "/a/doc/frotz", Expect: false},
{Pattern: "frotz", Path: "/a/frotz", Expect: true},

// An asterisk "*" matches anything except a slash. The character "?" matches any one character except "/". The
// range notation, e.g. [a-zA-Z], can be used to match one of the characters in a range. See fnmatch(3) and the
// FNM_PATHNAME flag for a more detailed description.
// @todo

// Two consecutive asterisks ("**") in patterns matched against full pathname may have special meaning:
// @todo

// A leading "**" followed by a slash means match in all directories. For example, "**/foo" matches file or
// directory "foo" anywhere, the same as pattern "foo". "**/foo/bar" matches file or directory "bar" anywhere
// that is directly under directory "foo".
// @todo

// A trailing "/**" matches everything inside. For example, "abc/**" matches all files inside directory "abc",
// relative to the location of the .gitignore file, with infinite depth.
// @todo

// A slash followed by two consecutive asterisks then a slash matches zero or more directories. For example,
// "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on.
// @todo

// Other consecutive asterisks are considered regular asterisks and will match according to the previous rules.

// some ad-hoc test cases ...
{Pattern: "hello", Path: "/test/hello", Expect: true},
{Pattern: "hello", Path: "/test/path/hello", Expect: true},
} {
t.Run(fmt.Sprintf("%s with %s", tt.Pattern, tt.Path), func(t *testing.T) {
actual := IsIgnored(tt.Path, [][]byte{[]byte(tt.Pattern)})
if actual != tt.Expect {
t.Errorf("got %v, want %v", actual, tt.Expect)
}
})
}
}

0 comments on commit e279335

Please sign in to comment.