feat: refine walking into a separate package

Signed-off-by: Brian McGee <brian@bmcgee.ie>
This commit is contained in:
Brian McGee 2024-01-10 15:04:39 +00:00
parent fcb8aa6312
commit bcc4902ed8
Signed by: brianmcgee
GPG Key ID: D49016E76AD1E8C0
6 changed files with 144 additions and 86 deletions

View File

@ -9,6 +9,8 @@ import (
"os"
"time"
"git.numtide.com/numtide/treefmt/internal/walk"
"git.numtide.com/numtide/treefmt/internal/format"
"github.com/charmbracelet/log"
@ -171,7 +173,7 @@ func putEntry(bucket *bolt.Bucket, path string, entry *Entry) error {
// ChangeSet is used to walk a filesystem, starting at root, and outputting any new or changed paths using pathsCh.
// It determines if a path is new or has changed by comparing against cache entries.
func ChangeSet(ctx context.Context, root string, walker string, pathsCh chan<- string) error {
func ChangeSet(ctx context.Context, root string, walkerType walk.Type, pathsCh chan<- string) error {
var tx *bolt.Tx
var bucket *bolt.Bucket
var processed int
@ -183,7 +185,12 @@ func ChangeSet(ctx context.Context, root string, walker string, pathsCh chan<- s
}
}()
return walk(ctx, root, walker, func(path string, info fs.FileInfo, err error) error {
w, err := walk.New(walkerType, root)
if err != nil {
return fmt.Errorf("%w: failed to create walker", err)
}
return w.Walk(ctx, func(path string, info fs.FileInfo, err error) error {
select {
case <-ctx.Done():
return ctx.Err()

View File

@ -1,83 +0,0 @@
package cache
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"github.com/charmbracelet/log"
"golang.org/x/sync/errgroup"
)
const (
GitWalker = "git"
FilesystemWalker = "filesystem"
)
func walk(ctx context.Context, root string, walker string, fn filepath.WalkFunc) error {
l := log.WithPrefix("walk")
if walker == GitWalker {
// check if we're dealing with a git repository
cmd := exec.Command("git", "-C", root, "rev-parse", "--is-inside-work-tree")
_, err := cmd.CombinedOutput()
if err != nil {
l.Info("git repo check failed, falling back to filesystem", "err", err)
walker = "filesystem"
}
}
l.Infof("walking %s with %s", root, walker)
switch walker {
case GitWalker:
return walkGit(ctx, root, fn)
case FilesystemWalker:
return filepath.Walk(root, fn)
default:
return fmt.Errorf("unknown walker: %s", walker)
}
}
func walkGit(ctx context.Context, root string, fn filepath.WalkFunc) error {
r, w := io.Pipe()
cmd := exec.Command("git", "-C", root, "ls-files")
cmd.Stdout = w
cmd.Stderr = w
eg := errgroup.Group{}
eg.Go(func() error {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
select {
case <-ctx.Done():
return ctx.Err()
default:
line := scanner.Text()
path := filepath.Join(root, line)
// stat the file
info, err := os.Lstat(path)
if err = fn(path, info, err); err != nil {
return err
}
}
}
return nil
})
if err := w.CloseWithError(cmd.Run()); err != nil {
return err
}
return eg.Wait()
}

View File

@ -1,6 +1,7 @@
package cli
import (
"git.numtide.com/numtide/treefmt/internal/walk"
"github.com/alecthomas/kong"
"github.com/charmbracelet/log"
)
@ -15,7 +16,7 @@ type Options struct {
FailOnChange bool `help:"Exit with error if any changes were made. Useful for CI."`
Formatters []string `help:"Specify formatters to apply. Defaults to all formatters."`
TreeRoot string `type:"existingdir" default:"."`
Walk string `enum:"filesystem,git" default:"git" help:"The method used to traverse the files within --tree-root. Currently supports 'git' or 'filesystem'."`
Walk walk.Type `enum:"auto,git,filesystem" default:"auto" help:"The method used to traverse the files within --tree-root. Currently supports 'auto', 'git' or 'filesystem'."`
Verbosity int `name:"verbose" short:"v" type:"counter" default:"0" env:"LOG_LEVEL" help:"Set the verbosity of logs e.g. -vv."`
Format Format `cmd:"" default:"."`

View File

@ -0,0 +1,22 @@
package walk
import (
"context"
"path/filepath"
)
type filesystem struct {
root string
}
func (f filesystem) Root() string {
return f.root
}
func (f filesystem) Walk(_ context.Context, fn filepath.WalkFunc) error {
return filepath.Walk(f.root, fn)
}
func NewFilesystem(root string) (Walker, error) {
return filesystem{root}, nil
}

69
internal/walk/git.go Normal file
View File

@ -0,0 +1,69 @@
package walk
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"golang.org/x/sync/errgroup"
)
type git struct {
root string
}
func (g *git) Root() string {
return g.root
}
func (g *git) Walk(ctx context.Context, fn filepath.WalkFunc) error {
r, w := io.Pipe()
cmd := exec.Command("git", "-C", g.root, "ls-files")
cmd.Stdout = w
cmd.Stderr = w
eg := errgroup.Group{}
eg.Go(func() error {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
select {
case <-ctx.Done():
return ctx.Err()
default:
line := scanner.Text()
path := filepath.Join(g.root, line)
// stat the file
info, err := os.Lstat(path)
if err = fn(path, info, err); err != nil {
return err
}
}
}
return nil
})
if err := w.CloseWithError(cmd.Run()); err != nil {
return err
}
return eg.Wait()
}
func NewGit(root string) (Walker, error) {
// check if we're dealing with a git repository
cmd := exec.Command("git", "-C", root, "rev-parse", "--is-inside-work-tree")
_, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("%w: git repo check failed", err)
}
return &git{root}, nil
}

42
internal/walk/walker.go Normal file
View File

@ -0,0 +1,42 @@
package walk
import (
"context"
"fmt"
"path/filepath"
)
type Type string
const (
Git Type = "git"
Auto Type = "auto"
Filesystem Type = "filesystem"
)
type Walker interface {
Root() string
Walk(ctx context.Context, fn filepath.WalkFunc) error
}
func New(walkerType Type, root string) (Walker, error) {
switch walkerType {
case Git:
return NewGit(root)
case Auto:
return Detect(root)
case Filesystem:
return NewFilesystem(root)
default:
return nil, fmt.Errorf("unknown walker type: %v", walkerType)
}
}
func Detect(root string) (Walker, error) {
// for now, we keep it simple and try git first, filesystem second
w, err := NewGit(root)
if err == nil {
return w, err
}
return NewFilesystem(root)
}