treefmt/walk/git.go
Brian McGee 2e77e123d1
feat: optimize git walker
Signed-off-by: Brian McGee <brian@bmcgee.ie>
2024-05-07 18:02:50 +01:00

126 lines
2.4 KiB
Go

package walk
import (
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/charmbracelet/log"
"github.com/go-git/go-git/v5"
)
type gitWalker struct {
root string
paths chan string
repo *git.Repository
}
func (g *gitWalker) Root() string {
return g.root
}
func (g *gitWalker) Walk(ctx context.Context, fn WalkFunc) error {
// for quick relative paths
relPathOffset := len(g.root) + 1
relPathFn := func(path string) (relPath string) {
if len(path) >= relPathOffset {
relPath = path[relPathOffset:]
}
return
}
idx, err := g.repo.Storer.Index()
if err != nil {
return fmt.Errorf("failed to open git index: %w", err)
}
// cache in-memory whether a path is present in the git index
var cache map[string]bool
for path := range g.paths {
if path == g.root {
// we can just iterate the index entries
for _, entry := range idx.Entries {
select {
case <-ctx.Done():
return ctx.Err()
default:
path := filepath.Join(g.root, entry.Name)
// stat the file
info, err := os.Lstat(path)
file := File{
Path: path,
RelPath: relPathFn(path),
Info: info,
}
if err = fn(&file, err); err != nil {
return err
}
}
}
continue
}
// otherwise we ensure the git index entries are cached and then check if they are in the git index
if cache == nil {
cache = make(map[string]bool)
for _, entry := range idx.Entries {
cache[entry.Name] = true
}
}
relPath, err := filepath.Rel(g.root, path)
if err != nil {
return fmt.Errorf("failed to find relative path for %v: %w", path, err)
}
_, ok := cache[relPath]
if !(path == g.root || ok) {
log.Debugf("path %v not found in git index, skipping", path)
continue
}
return filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
if info.IsDir() {
return nil
}
relPath, err := filepath.Rel(g.root, path)
if err != nil {
return err
}
if _, ok := cache[relPath]; !ok {
log.Debugf("path %v not found in git index, skipping", path)
return nil
}
file := File{
Path: path,
RelPath: relPathFn(path),
Info: info,
}
return fn(&file, err)
})
}
return nil
}
func NewGit(root string, paths chan string) (Walker, error) {
repo, err := git.PlainOpen(root)
if err != nil {
return nil, fmt.Errorf("failed to open git repo: %w", err)
}
return &gitWalker{root, paths, repo}, nil
}