feat: support .gitignore files #19

Merged
brianmcgee merged 3 commits from feat/support-gigignore into main 2024-01-11 20:52:23 +00:00
6 changed files with 144 additions and 86 deletions
Showing only changes of commit bcc4902ed8 - Show all commits

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."`
brianmcgee marked this conversation as resolved Outdated

The phrasing is a bit ambiguous. Maybe "Don't respect .gitignore files"?

The phrasing is a bit ambiguous. Maybe "Don't respect .gitignore files"?

That's a fair point.

That's a fair point.
TreeRoot string `type:"existingdir" default:"."`
brianmcgee marked this conversation as resolved Outdated

The default could be added to the config file, or detected.

The default could be added to the config file, or detected.

Made some relevant changes in bcc4902ed8

Made some relevant changes in https://git.numtide.com/numtide/treefmt/commit/bcc4902ed849d0742e9be3f49c2ae327a709b375
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")
brianmcgee marked this conversation as resolved
Review

You didn't want to use the go-git package for this?

You didn't want to use the go-git package for this?
Review

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.

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.
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)
}