Skip to content

Commit

Permalink
Worktree: Provide ability to add excludes (src-d#825)
Browse files Browse the repository at this point in the history
Worktree: Provide ability to add excludes
  • Loading branch information
maguro authored and mcuadros committed May 11, 2018
1 parent 47417ae commit e63b032
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 7 deletions.
87 changes: 83 additions & 4 deletions plumbing/format/gitignore/dir.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
package gitignore

import (
"bytes"
"io/ioutil"
"os"
"os/user"
"strings"

"gopkg.in/src-d/go-billy.v4"
"gopkg.in/src-d/go-git.v4/plumbing/format/config"
gioutil "gopkg.in/src-d/go-git.v4/utils/ioutil"
)

const (
commentPrefix = "#"
coreSection = "core"
eol = "\n"
excludesfile = "excludesfile"
gitDir = ".git"
gitignoreFile = ".gitignore"
gitconfigFile = ".gitconfig"
systemFile = "/etc/gitconfig"
)

// ReadPatterns reads gitignore patterns recursively traversing through the directory
// structure. The result is in the ascending order of priority (last higher).
func ReadPatterns(fs billy.Filesystem, path []string) (ps []Pattern, err error) {
f, err := fs.Open(fs.Join(append(path, gitignoreFile)...))
// readIgnoreFile reads a specific git ignore file.
func readIgnoreFile(fs billy.Filesystem, path []string, ignoreFile string) (ps []Pattern, err error) {
f, err := fs.Open(fs.Join(append(path, ignoreFile)...))
if err == nil {
defer f.Close()

Expand All @@ -33,6 +40,14 @@ func ReadPatterns(fs billy.Filesystem, path []string) (ps []Pattern, err error)
return nil, err
}

return
}

// ReadPatterns reads gitignore patterns recursively traversing through the directory
// structure. The result is in the ascending order of priority (last higher).
func ReadPatterns(fs billy.Filesystem, path []string) (ps []Pattern, err error) {
ps, _ = readIgnoreFile(fs, path, gitignoreFile)

var fis []os.FileInfo
fis, err = fs.ReadDir(fs.Join(path...))
if err != nil {
Expand All @@ -55,3 +70,67 @@ func ReadPatterns(fs billy.Filesystem, path []string) (ps []Pattern, err error)

return
}

func loadPatterns(fs billy.Filesystem, path string) (ps []Pattern, err error) {
f, err := fs.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}

defer gioutil.CheckClose(f, &err)

b, err := ioutil.ReadAll(f)
if err != nil {
return
}

d := config.NewDecoder(bytes.NewBuffer(b))

raw := config.New()
if err = d.Decode(raw); err != nil {
return
}

s := raw.Section(coreSection)
efo := s.Options.Get(excludesfile)
if efo == "" {
return nil, nil
}

ps, err = readIgnoreFile(fs, nil, efo)
if os.IsNotExist(err) {
return nil, nil
}

return
}

// LoadGlobalPatterns loads gitignore patterns from from the gitignore file
// declared in a user's ~/.gitconfig file. If the ~/.gitconfig file does not
// exist the function will return nil. If the core.excludesfile property
// is not declared, the function will return nil. If the file pointed to by
// the core.excludesfile property does not exist, the function will return nil.
//
// The function assumes fs is rooted at the root filesystem.
func LoadGlobalPatterns(fs billy.Filesystem) (ps []Pattern, err error) {
usr, err := user.Current()
if err != nil {
return
}

return loadPatterns(fs, fs.Join(usr.HomeDir, gitconfigFile))
}

// LoadSystemPatterns loads gitignore patterns from from the gitignore file
// declared in a system's /etc/gitconfig file. If the ~/.gitconfig file does
// not exist the function will return nil. If the core.excludesfile property
// is not declared, the function will return nil. If the file pointed to by
// the core.excludesfile property does not exist, the function will return nil.
//
// The function assumes fs is rooted at the root filesystem.
func LoadSystemPatterns(fs billy.Filesystem) (ps []Pattern, err error) {
return loadPatterns(fs, systemFile)
}
169 changes: 166 additions & 3 deletions plumbing/format/gitignore/dir_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,28 @@ package gitignore

import (
"os"
"os/user"
"strconv"

. "gopkg.in/check.v1"
"gopkg.in/src-d/go-billy.v4"
"gopkg.in/src-d/go-billy.v4/memfs"
)

type MatcherSuite struct {
FS billy.Filesystem
GFS billy.Filesystem // git repository root
RFS billy.Filesystem // root that contains user home
MCFS billy.Filesystem // root that contains user home, but missing ~/.gitconfig
MEFS billy.Filesystem // root that contains user home, but missing excludesfile entry
MIFS billy.Filesystem // root that contains user home, but missing .gitnignore

SFS billy.Filesystem // root that contains /etc/gitconfig
}

var _ = Suite(&MatcherSuite{})

func (s *MatcherSuite) SetUpTest(c *C) {
// setup generic git repository root
fs := memfs.New()
f, err := fs.Create(".gitignore")
c.Assert(err, IsNil)
Expand All @@ -36,15 +45,169 @@ func (s *MatcherSuite) SetUpTest(c *C) {
fs.MkdirAll("vendor/github.com", os.ModePerm)
fs.MkdirAll("vendor/gopkg.in", os.ModePerm)

s.FS = fs
s.GFS = fs

// setup root that contains user home
usr, err := user.Current()
c.Assert(err, IsNil)

fs = memfs.New()
err = fs.MkdirAll(usr.HomeDir, os.ModePerm)
c.Assert(err, IsNil)

f, err = fs.Create(fs.Join(usr.HomeDir, gitconfigFile))
c.Assert(err, IsNil)
_, err = f.Write([]byte("[core]\n"))
c.Assert(err, IsNil)
_, err = f.Write([]byte(" excludesfile = " + strconv.Quote(fs.Join(usr.HomeDir, ".gitignore_global")) + "\n"))
c.Assert(err, IsNil)
err = f.Close()
c.Assert(err, IsNil)

f, err = fs.Create(fs.Join(usr.HomeDir, ".gitignore_global"))
c.Assert(err, IsNil)
_, err = f.Write([]byte("# IntelliJ\n"))
c.Assert(err, IsNil)
_, err = f.Write([]byte(".idea/\n"))
c.Assert(err, IsNil)
_, err = f.Write([]byte("*.iml\n"))
c.Assert(err, IsNil)
err = f.Close()
c.Assert(err, IsNil)

s.RFS = fs

// root that contains user home, but missing ~/.gitconfig
fs = memfs.New()
err = fs.MkdirAll(usr.HomeDir, os.ModePerm)
c.Assert(err, IsNil)

f, err = fs.Create(fs.Join(usr.HomeDir, ".gitignore_global"))
c.Assert(err, IsNil)
_, err = f.Write([]byte("# IntelliJ\n"))
c.Assert(err, IsNil)
_, err = f.Write([]byte(".idea/\n"))
c.Assert(err, IsNil)
_, err = f.Write([]byte("*.iml\n"))
c.Assert(err, IsNil)
err = f.Close()
c.Assert(err, IsNil)

s.MCFS = fs

// setup root that contains user home, but missing excludesfile entry
fs = memfs.New()
err = fs.MkdirAll(usr.HomeDir, os.ModePerm)
c.Assert(err, IsNil)

f, err = fs.Create(fs.Join(usr.HomeDir, gitconfigFile))
c.Assert(err, IsNil)
_, err = f.Write([]byte("[core]\n"))
c.Assert(err, IsNil)
err = f.Close()
c.Assert(err, IsNil)

f, err = fs.Create(fs.Join(usr.HomeDir, ".gitignore_global"))
c.Assert(err, IsNil)
_, err = f.Write([]byte("# IntelliJ\n"))
c.Assert(err, IsNil)
_, err = f.Write([]byte(".idea/\n"))
c.Assert(err, IsNil)
_, err = f.Write([]byte("*.iml\n"))
c.Assert(err, IsNil)
err = f.Close()
c.Assert(err, IsNil)

s.MEFS = fs

// setup root that contains user home, but missing .gitnignore
fs = memfs.New()
err = fs.MkdirAll(usr.HomeDir, os.ModePerm)
c.Assert(err, IsNil)

f, err = fs.Create(fs.Join(usr.HomeDir, gitconfigFile))
c.Assert(err, IsNil)
_, err = f.Write([]byte("[core]\n"))
c.Assert(err, IsNil)
_, err = f.Write([]byte(" excludesfile = " + strconv.Quote(fs.Join(usr.HomeDir, ".gitignore_global")) + "\n"))
c.Assert(err, IsNil)
err = f.Close()
c.Assert(err, IsNil)

s.MIFS = fs

// setup root that contains user home
fs = memfs.New()
err = fs.MkdirAll("etc", os.ModePerm)
c.Assert(err, IsNil)

f, err = fs.Create(systemFile)
c.Assert(err, IsNil)
_, err = f.Write([]byte("[core]\n"))
c.Assert(err, IsNil)
_, err = f.Write([]byte(" excludesfile = /etc/gitignore_global\n"))
c.Assert(err, IsNil)
err = f.Close()
c.Assert(err, IsNil)

f, err = fs.Create("/etc/gitignore_global")
c.Assert(err, IsNil)
_, err = f.Write([]byte("# IntelliJ\n"))
c.Assert(err, IsNil)
_, err = f.Write([]byte(".idea/\n"))
c.Assert(err, IsNil)
_, err = f.Write([]byte("*.iml\n"))
c.Assert(err, IsNil)
err = f.Close()
c.Assert(err, IsNil)

s.SFS = fs
}

func (s *MatcherSuite) TestDir_ReadPatterns(c *C) {
ps, err := ReadPatterns(s.FS, nil)
ps, err := ReadPatterns(s.GFS, nil)
c.Assert(err, IsNil)
c.Assert(ps, HasLen, 2)

m := NewMatcher(ps)
c.Assert(m.Match([]string{"vendor", "gopkg.in"}, true), Equals, true)
c.Assert(m.Match([]string{"vendor", "github.com"}, true), Equals, false)
}

func (s *MatcherSuite) TestDir_LoadGlobalPatterns(c *C) {
ps, err := LoadGlobalPatterns(s.RFS)
c.Assert(err, IsNil)
c.Assert(ps, HasLen, 2)

m := NewMatcher(ps)
c.Assert(m.Match([]string{"go-git.v4.iml"}, true), Equals, true)
c.Assert(m.Match([]string{".idea"}, true), Equals, true)
}

func (s *MatcherSuite) TestDir_LoadGlobalPatternsMissingGitconfig(c *C) {
ps, err := LoadGlobalPatterns(s.MCFS)
c.Assert(err, IsNil)
c.Assert(ps, HasLen, 0)
}

func (s *MatcherSuite) TestDir_LoadGlobalPatternsMissingExcludesfile(c *C) {
ps, err := LoadGlobalPatterns(s.MEFS)
c.Assert(err, IsNil)
c.Assert(ps, HasLen, 0)
}

func (s *MatcherSuite) TestDir_LoadGlobalPatternsMissingGitignore(c *C) {
ps, err := LoadGlobalPatterns(s.MIFS)
c.Assert(err, IsNil)
c.Assert(ps, HasLen, 0)
}

func (s *MatcherSuite) TestDir_LoadSystemPatterns(c *C) {
ps, err := LoadSystemPatterns(s.SFS)
c.Assert(err, IsNil)
c.Assert(ps, HasLen, 2)

m := NewMatcher(ps)
c.Assert(m.Match([]string{"go-git.v4.iml"}, true), Equals, true)
c.Assert(m.Match([]string{".idea"}, true), Equals, true)
}
3 changes: 3 additions & 0 deletions worktree.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"gopkg.in/src-d/go-git.v4/config"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
"gopkg.in/src-d/go-git.v4/plumbing/format/gitignore"
"gopkg.in/src-d/go-git.v4/plumbing/format/index"
"gopkg.in/src-d/go-git.v4/plumbing/object"
"gopkg.in/src-d/go-git.v4/plumbing/storer"
Expand All @@ -33,6 +34,8 @@ var (
type Worktree struct {
// Filesystem underlying filesystem.
Filesystem billy.Filesystem
// External excludes not found in the repository .gitignore
Excludes []gitignore.Pattern

r *Repository
}
Expand Down
3 changes: 3 additions & 0 deletions worktree_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ func (w *Worktree) excludeIgnoredChanges(changes merkletrie.Changes) merkletrie.
if err != nil || len(patterns) == 0 {
return changes
}

patterns = append(patterns, w.Excludes...)

m := gitignore.NewMatcher(patterns)

var res merkletrie.Changes
Expand Down
30 changes: 30 additions & 0 deletions worktree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"gopkg.in/src-d/go-git.v4/config"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
"gopkg.in/src-d/go-git.v4/plumbing/format/gitignore"
"gopkg.in/src-d/go-git.v4/plumbing/format/index"
"gopkg.in/src-d/go-git.v4/plumbing/object"
"gopkg.in/src-d/go-git.v4/storage/memory"
Expand Down Expand Up @@ -1072,6 +1073,35 @@ func (s *WorktreeSuite) TestAddUntracked(c *C) {
c.Assert(obj.Size(), Equals, int64(3))
}

func (s *WorktreeSuite) TestIgnored(c *C) {
fs := memfs.New()
w := &Worktree{
r: s.Repository,
Filesystem: fs,
}

w.Excludes = make([]gitignore.Pattern, 0)
w.Excludes = append(w.Excludes, gitignore.ParsePattern("foo", nil))

err := w.Checkout(&CheckoutOptions{Force: true})
c.Assert(err, IsNil)

idx, err := w.r.Storer.Index()
c.Assert(err, IsNil)
c.Assert(idx.Entries, HasLen, 9)

err = util.WriteFile(w.Filesystem, "foo", []byte("FOO"), 0755)
c.Assert(err, IsNil)

status, err := w.Status()
c.Assert(err, IsNil)
c.Assert(status, HasLen, 0)

file := status.File("foo")
c.Assert(file.Staging, Equals, Untracked)
c.Assert(file.Worktree, Equals, Untracked)
}

func (s *WorktreeSuite) TestAddModified(c *C) {
fs := memfs.New()
w := &Worktree{
Expand Down

0 comments on commit e63b032

Please sign in to comment.