Skip to content

Commit

Permalink
git: skip ignored files while walking worktree
Browse files Browse the repository at this point in the history
Skip ignored files when walking through the worktree.

This signigifantly improves the performance of `Status()`:
In a repository with 3M ignored files `Status` now takes 27 s instead of 491 s.
  • Loading branch information
silkeh committed Nov 21, 2024
1 parent 70dd9f8 commit 8cef206
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 8 deletions.
79 changes: 79 additions & 0 deletions plumbing/format/gitignore/noder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package gitignore

import (
"slices"

"github.com/go-git/go-git/v5/utils/merkletrie/noder"
)

var _ noder.Noder = matchNoder{}

type matchNoder struct {
ignore Matcher
noder.Noder
path []string
children []noder.Noder
}

// IgnoreNoder returns a [noder.Noder] that filters out ignored nodes.
// It cannot ignore itself.
func IgnoreNoder(m Matcher, n noder.Noder) noder.Noder {
if m == nil {
return n
}

var path []string
if name := n.Name(); name != "." {
path = []string{name}
}

return matchNoder{ignore: m, Noder: n, path: path}
}

func (n matchNoder) Children() ([]noder.Noder, error) {
if len(n.children) > 0 {
return n.children, nil
}

children, err := n.Noder.Children()
if err != nil {
return nil, err
}

n.children = n.ignoreChildren(children)

return n.children, nil
}

func (n matchNoder) ignoreChildren(children []noder.Noder) []noder.Noder {
found := make([]noder.Noder, 0, len(children))

for _, child := range children {
path := append(n.path, child.Name())

if n.ignore.Match(path, child.IsDir()) {
continue
}

if child.IsDir() {
found = append(found, matchNoder{ignore: n.ignore, Noder: child, path: slices.Clone(path)})
} else {
found = append(found, child)
}
}

return found
}

func (n matchNoder) NumChildren() (int, error) {
children, err := n.Children()
if err != nil {
return 0, err
}

return len(children), nil
}

func (n matchNoder) Skip() bool {
return n.Noder.Skip()
}
24 changes: 16 additions & 8 deletions worktree_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,37 +143,45 @@ func (w *Worktree) diffStagingWithWorktree(reverse, excludeIgnoredChanges bool)

to := filesystem.NewRootNode(w.Filesystem, submodules)

var m gitignore.Matcher
if excludeIgnoredChanges {
m = w.loadIgnorePatterns()
}

var c merkletrie.Changes
if reverse {
c, err = merkletrie.DiffTree(to, from, diffTreeIsEquals)
c, err = merkletrie.DiffTree(to, gitignore.IgnoreNoder(m, from), diffTreeIsEquals)
} else {
c, err = merkletrie.DiffTree(from, to, diffTreeIsEquals)
c, err = merkletrie.DiffTree(from, gitignore.IgnoreNoder(m, to), diffTreeIsEquals)
}

if err != nil {
return nil, err
}

if excludeIgnoredChanges {
return w.excludeIgnoredChanges(c), nil
if excludeIgnoredChanges && m != nil {
return w.excludeIgnoredChanges(m, c), nil
}

return c, nil
}

func (w *Worktree) excludeIgnoredChanges(changes merkletrie.Changes) merkletrie.Changes {
func (w *Worktree) loadIgnorePatterns() gitignore.Matcher {
patterns, err := gitignore.ReadPatterns(w.Filesystem, nil)
if err != nil {
return changes
return nil
}

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

if len(patterns) == 0 {
return changes
return nil
}

m := gitignore.NewMatcher(patterns)
return gitignore.NewMatcher(patterns)
}

func (w *Worktree) excludeIgnoredChanges(m gitignore.Matcher, changes merkletrie.Changes) merkletrie.Changes {
var res merkletrie.Changes
for _, ch := range changes {
var path []string
Expand Down
17 changes: 17 additions & 0 deletions worktree_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
"github.com/go-git/go-git/v5/storage/filesystem"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -87,3 +88,19 @@ func TestIndexEntrySizeUpdatedForNonRegularFiles(t *testing.T) {
// Check whether the index was updated with the two new line breaks.
assert.Equal(t, uint32(len(content)+2), idx.Entries[0].Size)
}

func TestWorktree_Status(t *testing.T) {
r, err := PlainOpenWithOptions("/home/silke/Development/bauwatch/tools/toolchain", &PlainOpenOptions{
DetectDotGit: true,
})
require.NoError(t, err)

worktree, err := r.Worktree()
require.NoError(t, err)

worktree.Excludes, err = gitignore.LoadGlobalPatterns(osfs.New("/"))
require.NoError(t, err)

_, err = worktree.Status()
require.NoError(t, err)
}

0 comments on commit 8cef206

Please sign in to comment.