unfiltered walker tree
This commit is contained in:
+23
-63
@@ -1,76 +1,36 @@
|
||||
package tree
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.flo.fo/FLO/dirmd/internal/walker"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
Name string
|
||||
IsDir bool
|
||||
Children map[string]*Node
|
||||
Name string
|
||||
IsDir bool
|
||||
Children map[string]*Node
|
||||
Annotation string
|
||||
Ref string
|
||||
}
|
||||
|
||||
func Build(entries []walker.Entry) *Node {
|
||||
root := &Node{IsDir: true, Children: make(map[string]*Node)}
|
||||
|
||||
for _, e := range entries {
|
||||
parts := strings.Split(e.RelPath, "/")
|
||||
cur := root
|
||||
|
||||
for i, part := range parts {
|
||||
isLeaf := i == len(parts)-1
|
||||
if _, ok := cur.Children[part]; !ok {
|
||||
cur.Children[part] = &Node{Name: part, IsDir: !isLeaf, Children: make(map[string]*Node)}
|
||||
func splitPath(relPath string) []string {
|
||||
var parts []string
|
||||
start := 0
|
||||
for i := 0; i <= len(relPath); i++ {
|
||||
if i == len(relPath) || relPath[i] == '/' || relPath[i] == '\\' {
|
||||
if i > start {
|
||||
parts = append(parts, relPath[start:i])
|
||||
}
|
||||
cur = cur.Children[part]
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
return root
|
||||
return parts
|
||||
}
|
||||
|
||||
func Render(root *Node, rootName string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(rootName)
|
||||
sb.WriteString("/\n")
|
||||
renderNode(&sb, root, "")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func renderNode(sb *strings.Builder, node *Node, prefix string) {
|
||||
keys := make([]string, 0, len(node.Children))
|
||||
for k := range node.Children {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for i, key := range keys {
|
||||
child := node.Children[key]
|
||||
isLast := i == len(keys)-1
|
||||
|
||||
if isLast {
|
||||
sb.WriteString(prefix + "└── ")
|
||||
} else {
|
||||
sb.WriteString(prefix + "├── ")
|
||||
func addPath(root *Node, rel string, isDir bool) *Node {
|
||||
parts := splitPath(rel)
|
||||
cur := root
|
||||
for i, part := range parts {
|
||||
if _, ok := cur.Children[part]; !ok {
|
||||
cur.Children[part] = &Node{Name: part, IsDir: i < len(parts)-1, Children: make(map[string]*Node)}
|
||||
}
|
||||
|
||||
sb.WriteString(child.Name)
|
||||
if child.IsDir {
|
||||
sb.WriteString("/")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
if child.IsDir {
|
||||
newPrefix := prefix
|
||||
if isLast {
|
||||
newPrefix += " "
|
||||
} else {
|
||||
newPrefix += "│ "
|
||||
}
|
||||
renderNode(sb, child, newPrefix)
|
||||
}
|
||||
cur = cur.Children[part]
|
||||
}
|
||||
cur.IsDir = isDir
|
||||
return cur
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package tree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.flo.fo/FLO/dirmd/internal/config"
|
||||
"git.flo.fo/FLO/dirmd/internal/filter"
|
||||
)
|
||||
|
||||
func BuildFullTree(absRoot string, cfg *config.Config) (*Node, error) {
|
||||
root := &Node{IsDir: true, Children: make(map[string]*Node)}
|
||||
|
||||
err := filepath.WalkDir(absRoot, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rel, rerr := filepath.Rel(absRoot, path)
|
||||
if rerr != nil || rel == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
if filter.IsSymlink(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
depth := strings.Count(rel, string(filepath.Separator)) + 1
|
||||
if depth > cfg.MaxDepth {
|
||||
node := addPath(root, rel, d.IsDir())
|
||||
node.Annotation = "exceeded --max-depth"
|
||||
if d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
name := filepath.Base(path)
|
||||
|
||||
if d.IsDir() {
|
||||
if isIgnoredName(name, cfg.Ignores) {
|
||||
node := addPath(root, rel, true)
|
||||
node.Annotation = "ignored (--ignore)"
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if cfg.OmitTest && isTestDir(rel) {
|
||||
node := addPath(root, rel, true)
|
||||
node.Annotation = "tests omitted (--omit-test)"
|
||||
return filepath.SkipDir
|
||||
}
|
||||
addPath(root, rel, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
if annotation, ok := fileAnnotation(rel, name, cfg); ok {
|
||||
node := addPath(root, rel, false)
|
||||
node.Annotation = annotation
|
||||
return nil
|
||||
}
|
||||
|
||||
addPath(root, rel, false)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot walk %s: %w", absRoot, err)
|
||||
}
|
||||
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func isIgnoredName(name string, ignores []string) bool {
|
||||
if strings.HasPrefix(name, ".") {
|
||||
return true
|
||||
}
|
||||
for _, pattern := range ignores {
|
||||
if matched, _ := filepath.Match(pattern, name); matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isTestDir(rel string) bool {
|
||||
slashPath := filepath.ToSlash(rel)
|
||||
return strings.Contains(slashPath, "/tests/") || strings.HasPrefix(slashPath, "tests/")
|
||||
}
|
||||
|
||||
func fileAnnotation(rel, name string, cfg *config.Config) (string, bool) {
|
||||
if strings.HasPrefix(name, ".") {
|
||||
return "", false
|
||||
}
|
||||
for _, pattern := range cfg.Ignores {
|
||||
if matched, _ := filepath.Match(pattern, name); matched {
|
||||
return "ignored (--ignore)", true
|
||||
}
|
||||
}
|
||||
if cfg.OmitTest && isTestFileName(rel, name) {
|
||||
return "tests omitted (--omit-test)", true
|
||||
}
|
||||
if cfg.OmitMd && isNonReadmeMarkdown(name) {
|
||||
return "omitted (--omit-md)", true
|
||||
}
|
||||
if cfg.FrontendOnly && !isFrontendOrReadme(rel, name) {
|
||||
return "omitted (--frontend-only)", true
|
||||
}
|
||||
if cfg.SkipFrontend && isFrontendExt(rel) {
|
||||
return "omitted (--skip-frontend)", true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func isTestFileName(rel, name string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
switch ext {
|
||||
case ".go":
|
||||
return strings.HasSuffix(strings.ToLower(name), "_test.go")
|
||||
case ".java":
|
||||
base := strings.TrimSuffix(name, filepath.Ext(name))
|
||||
if strings.HasSuffix(base, "Test") || strings.HasSuffix(base, "Tests") {
|
||||
return true
|
||||
}
|
||||
slashPath := filepath.ToSlash(rel)
|
||||
return strings.Contains(slashPath, "/src/test/")
|
||||
case ".py":
|
||||
lower := strings.ToLower(name)
|
||||
if strings.HasPrefix(lower, "test_") || strings.HasSuffix(lower, "_test.py") {
|
||||
return true
|
||||
}
|
||||
if strings.EqualFold(name, "conftest.py") {
|
||||
return true
|
||||
}
|
||||
return isTestDir(rel)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isNonReadmeMarkdown(name string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
if ext != ".md" && ext != ".markdown" {
|
||||
return false
|
||||
}
|
||||
return !strings.EqualFold(name, "README.md")
|
||||
}
|
||||
|
||||
func isFrontendOrReadme(rel, name string) bool {
|
||||
if isFrontendExt(rel) {
|
||||
return true
|
||||
}
|
||||
return strings.EqualFold(name, "README.md")
|
||||
}
|
||||
|
||||
func isFrontendExt(rel string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(rel))
|
||||
for _, fe := range frontendExts {
|
||||
if strings.EqualFold(ext, fe) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var frontendExts = []string{
|
||||
".html", ".htm", ".gohtml", ".tmpl",
|
||||
".css", ".scss", ".sass", ".less",
|
||||
".js", ".jsx", ".mjs", ".cjs",
|
||||
".ts", ".tsx",
|
||||
".vue", ".svelte",
|
||||
}
|
||||
Reference in New Issue
Block a user