Git Repository
Stackyard
Stackyard scannt lokale Entwicklungsordner und bündelt Projekte, Git-Status, TODOs, Toolchains, Docker-Compose-Setups, Datei- und Launcher-Funktionen sowie Operations-Informationen in einer lokalen WebUI.
HTTPS
https://zanvex.de/git/stackyard.gitinternal/scanner/scanner.go
Zum Verzeichnispackage scanner
import (
"io/fs"
"os"
"path/filepath"
"slices"
"strings"
"time"
"stackyard/internal/config"
)
type Marker struct {
File string
Type string
}
var Markers = []Marker{
{".git", "git"},
{"go.mod", "go"},
{"package.json", "node"},
{"pyproject.toml", "python"},
{"requirements.txt", "python"},
{"Cargo.toml", "rust"},
{"composer.json", "php"},
{"docker-compose.yml", "docker"},
{"compose.yml", "docker"},
{".mise.toml", "mise"},
{"README.md", "docs"},
}
type Candidate struct {
RootName string
Path string
Name string
PrimaryType string
Markers []Marker
LastModified time.Time
}
func NameFromPath(path string) string {
return filepath.Base(path)
}
type Scanner struct {
maxDepth int
ignoreDirs map[string]struct{}
}
func New(cfg config.ScannerConfig) Scanner {
ignoreDirs := make(map[string]struct{}, len(cfg.IgnoreDirs))
for _, dir := range cfg.IgnoreDirs {
ignoreDirs[dir] = struct{}{}
}
return Scanner{
maxDepth: cfg.MaxDepth,
ignoreDirs: ignoreDirs,
}
}
func (s Scanner) Scan(roots []config.RootConfig) ([]Candidate, error) {
results := make([]Candidate, 0)
for _, root := range roots {
candidates, err := s.scanRoot(root)
if err != nil {
return nil, err
}
results = append(results, candidates...)
}
slices.SortFunc(results, func(a, b Candidate) int {
return strings.Compare(a.Path, b.Path)
})
return results, nil
}
func (s Scanner) scanRoot(root config.RootConfig) ([]Candidate, error) {
entries, err := os.ReadDir(root.Path)
if err != nil {
return nil, err
}
results := make([]Candidate, 0)
for _, entry := range entries {
if !entry.IsDir() || isSymlink(entry) || s.isIgnored(entry.Name()) {
continue
}
startPath := filepath.Join(root.Path, entry.Name())
candidates, err := s.walk(root.Name, startPath, 1)
if err != nil {
return nil, err
}
results = append(results, candidates...)
}
return results, nil
}
func (s Scanner) walk(rootName, dir string, depth int) ([]Candidate, error) {
if depth > s.maxDepth {
return nil, nil
}
entryInfo, err := os.Stat(dir)
if err != nil {
return nil, err
}
candidate, hasMarkers, err := inspectDirectory(rootName, dir, entryInfo.ModTime())
if err != nil {
return nil, err
}
results := make([]Candidate, 0, 1)
if hasMarkers {
results = append(results, candidate)
if !shouldDescend(candidate.Markers) {
return results, nil
}
}
entries, err := os.ReadDir(dir)
if err != nil {
return results, nil
}
for _, entry := range entries {
if !entry.IsDir() || isSymlink(entry) || s.isIgnored(entry.Name()) {
continue
}
childPath := filepath.Join(dir, entry.Name())
childResults, err := s.walk(rootName, childPath, depth+1)
if err != nil {
return nil, err
}
results = append(results, childResults...)
}
return results, nil
}
func inspectDirectory(rootName, dir string, modTime time.Time) (Candidate, bool, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return Candidate{}, false, err
}
var markers []Marker
for _, marker := range Markers {
if containsEntry(entries, marker.File) {
markers = append(markers, marker)
}
}
candidate := Candidate{
RootName: rootName,
Path: dir,
Name: NameFromPath(dir),
PrimaryType: detectPrimaryType(markers),
Markers: markers,
LastModified: modTime,
}
return candidate, len(markers) > 0, nil
}
func detectPrimaryType(markers []Marker) string {
for _, marker := range markers {
switch marker.Type {
case "go", "node", "python", "rust", "php", "docker", "mise":
return marker.Type
}
}
if len(markers) == 0 {
return "unknown"
}
return markers[0].Type
}
func containsEntry(entries []fs.DirEntry, name string) bool {
for _, entry := range entries {
if entry.Name() == name {
return true
}
}
return false
}
func isSymlink(entry fs.DirEntry) bool {
return entry.Type()&fs.ModeSymlink != 0
}
func (s Scanner) isIgnored(name string) bool {
if strings.HasPrefix(name, ".") {
return true
}
_, ok := s.ignoreDirs[name]
return ok
}
func shouldDescend(markers []Marker) bool {
if len(markers) == 0 {
return true
}
for _, marker := range markers {
switch marker.Type {
case "git", "go", "node", "python", "rust", "php", "docker", "mise":
return false
}
}
return true
}