databricks-cli/git/fileset.go

155 lines
3.7 KiB
Go

package git
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
ignore "github.com/sabhiram/go-gitignore"
"golang.org/x/exp/slices"
)
type File struct {
fs.DirEntry
Absolute, Relative string
}
func (f File) Modified() (ts time.Time) {
info, err := f.Info()
if err != nil {
// return default time, beginning of epoch
return ts
}
return info.ModTime()
}
// FileSet facilitates fast recursive tracked file listing
// with respect to patterns defined in `.gitignore` file
//
// root: Root of the git repository
// ignore: List of patterns defined in `.gitignore`.
//
// We do not sync files that match this pattern
type FileSet struct {
root string
ignore *ignore.GitIgnore
}
// Retuns FileSet for the git repo located at `root`
func NewFileSet(root string) *FileSet {
syncIgnoreLines := append(getGitIgnoreLines(root), getSyncIgnoreLines()...)
return &FileSet{
root: root,
ignore: ignore.CompileIgnoreLines(syncIgnoreLines...),
}
}
func getGitIgnoreLines(root string) []string {
gitIgnoreLines := []string{}
gitIgnorePath := fmt.Sprintf("%s/.gitignore", root)
rawIgnore, err := os.ReadFile(gitIgnorePath)
if err == nil {
// add entries from .gitignore if the file exists (did read correctly)
for _, line := range strings.Split(string(rawIgnore), "\n") {
// underlying library doesn't behave well with Rule 5 of .gitignore,
// hence this workaround
gitIgnoreLines = append(gitIgnoreLines, strings.Trim(line, "/"))
}
}
return gitIgnoreLines
}
// This contains additional files/dirs to be ignored while syncing that are
// not mentioned in .gitignore
func getSyncIgnoreLines() []string {
return []string{".git"}
}
// Only call this function for a bricks project root
// since it will create a .gitignore file if missing
func (w *FileSet) EnsureValidGitIgnoreExists() error {
gitIgnoreLines := getGitIgnoreLines(w.root)
gitIgnorePath := fmt.Sprintf("%s/.gitignore", w.root)
if slices.Contains(gitIgnoreLines, ".databricks") {
return nil
}
f, err := os.OpenFile(gitIgnorePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString("\n.databricks\n")
if err != nil {
return err
}
gitIgnoreLines = append(gitIgnoreLines, ".databricks")
w.ignore = ignore.CompileIgnoreLines(append(gitIgnoreLines, getSyncIgnoreLines()...)...)
return nil
}
// Return root for fileset.
func (w *FileSet) Root() string {
return w.root
}
// Return all tracked files for Repo
func (w *FileSet) All() ([]File, error) {
return w.RecursiveListFiles(w.root)
}
func (w *FileSet) IsGitIgnored(pattern string) bool {
return w.ignore.MatchesPath(pattern)
}
// Recursively traverses dir in a depth first manner and returns a list of all files
// that are being tracked in the FileSet (ie not being ignored for matching one of the
// patterns in w.ignore)
func (w *FileSet) RecursiveListFiles(dir string) (fileList []File, err error) {
queue, err := readDir(dir, w.root)
if err != nil {
return nil, err
}
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
if w.ignore.MatchesPath(current.Relative) {
continue
}
if !current.IsDir() {
fileList = append(fileList, current)
continue
}
children, err := readDir(current.Absolute, w.root)
if err != nil {
return nil, err
}
queue = append(queue, children...)
}
return fileList, nil
}
func readDir(dir, root string) (queue []File, err error) {
f, err := os.Open(dir)
if err != nil {
return
}
defer f.Close()
dirs, err := f.ReadDir(-1)
if err != nil {
return
}
for _, v := range dirs {
absolute := filepath.Join(dir, v.Name())
relative, err := filepath.Rel(root, absolute)
if err != nil {
return nil, err
}
queue = append(queue, File{v, absolute, relative})
}
return
}