feat: support .gitignore files #19
11
internal/cache/cache.go
vendored
11
internal/cache/cache.go
vendored
@ -9,6 +9,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.numtide.com/numtide/treefmt/internal/walk"
|
||||||
|
|
||||||
"git.numtide.com/numtide/treefmt/internal/format"
|
"git.numtide.com/numtide/treefmt/internal/format"
|
||||||
"github.com/charmbracelet/log"
|
"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.
|
// 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.
|
// 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 tx *bolt.Tx
|
||||||
var bucket *bolt.Bucket
|
var bucket *bolt.Bucket
|
||||||
var processed int
|
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 {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
|
83
internal/cache/walk.go
vendored
83
internal/cache/walk.go
vendored
@ -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()
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.numtide.com/numtide/treefmt/internal/walk"
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
"github.com/charmbracelet/log"
|
"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."`
|
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."`
|
Formatters []string `help:"Specify formatters to apply. Defaults to all formatters."`
|
||||||
TreeRoot string `type:"existingdir" default:"."`
|
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."`
|
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:"."`
|
Format Format `cmd:"" default:"."`
|
||||||
|
22
internal/walk/filesystem.go
Normal file
22
internal/walk/filesystem.go
Normal 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
69
internal/walk/git.go
Normal 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")
|
||||||
brianmcgee marked this conversation as resolved
|
|||||||
|
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
42
internal/walk/walker.go
Normal 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)
|
||||||
|
}
|
Reference in New Issue
Block a user
You didn't want to use the go-git package for this?
I tried with
go-git
but couldn't find an efficient way or traversing the worktree. It worked fine on a small repo, but on nixpkgs it broke down badly. Will revisit this again in the future.