Skip to content
This repository was archived by the owner on Sep 11, 2020. It is now read-only.

Worktree: Provide ability to add excludes to worktree #825

Merged
merged 8 commits into from
May 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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