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.gitcmd/stackyard/main.go
Zum Verzeichnispackage main
import (
"context"
"errors"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"time"
"stackyard/internal/config"
"stackyard/internal/git"
"stackyard/internal/project"
"stackyard/internal/scanner"
"stackyard/internal/storage"
"stackyard/internal/todo"
"stackyard/internal/web"
)
func main() {
serve := flag.NewFlagSet("serve", flag.ExitOnError)
serveConfigPath := serve.String("config", "", "Path to config file")
scan := flag.NewFlagSet("scan", flag.ExitOnError)
scanConfigPath := scan.String("config", "", "Path to config file")
if len(os.Args) < 2 {
fmt.Println("usage: stackyard <serve|scan> [options]")
os.Exit(1)
}
switch os.Args[1] {
case "serve":
_ = serve.Parse(os.Args[2:])
if err := runServe(*serveConfigPath); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
case "scan":
_ = scan.Parse(os.Args[2:])
if err := runScan(*scanConfigPath); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
default:
fmt.Printf("unknown command: %s\n", os.Args[1])
os.Exit(1)
}
}
func runServe(configPath string) error {
cfg, err := config.Load(configPath)
if err != nil {
return err
}
store, err := storage.Open(cfg.Database.Path)
if err != nil {
return err
}
defer store.Close()
webServer := web.NewServer(cfg, store)
defer webServer.Close()
server := &http.Server{
Addr: cfg.Server.Listen,
Handler: webServer.Handler(),
ReadHeaderTimeout: 5 * time.Second,
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = server.Shutdown(shutdownCtx)
}()
fmt.Printf("Stackyard listening on http://%s\n", cfg.Server.Listen)
err = server.ListenAndServe()
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
}
func runScan(configPath string) error {
cfg, err := config.Load(configPath)
if err != nil {
return err
}
store, err := storage.Open(cfg.Database.Path)
if err != nil {
return err
}
defer store.Close()
sc := scanner.New(cfg.Scanner)
candidates, err := sc.Scan(cfg.Roots)
if err != nil {
return err
}
gitClient := git.NewClient()
projects := make([]project.Project, 0, len(candidates))
for _, candidate := range candidates {
p := project.Project{
ID: project.IDFromPath(candidate.Path),
Name: candidate.Name,
RootName: candidate.RootName,
Path: candidate.Path,
PrimaryType: candidate.PrimaryType,
State: project.StateActive,
LastModified: candidate.LastModified,
LastScannedAt: time.Now(),
}
status, commit, err := gitClient.Inspect(context.Background(), candidate.Path)
if err != nil {
fmt.Fprintf(os.Stderr, "stackyard: git inspect skipped for %s: %v\n", candidate.Path, err)
} else {
p.Git.IsRepo = status.IsRepo
p.Git.Branch = status.Branch
p.Git.Dirty = status.Dirty
p.Git.Ahead = status.Ahead
p.Git.Behind = status.Behind
if commit != nil {
p.Git.LastCommitHash = commit.Hash
p.Git.LastCommitSubject = commit.Subject
p.Git.LastCommitAt = commit.Committed
}
}
todoPath, exists, err := todo.Resolve(candidate.Path)
if err != nil {
fmt.Fprintf(os.Stderr, "stackyard: todo resolve skipped for %s: %v\n", candidate.Path, err)
} else if exists {
p.TodoPath = todoPath
}
projects = append(projects, p)
fmt.Printf("%s [%s]\n", candidate.Name, candidate.PrimaryType)
fmt.Printf(" path: %s\n", candidate.Path)
if status.IsRepo {
fmt.Printf(" git: branch=%s dirty=%t ahead=%d behind=%d\n", status.Branch, status.Dirty, status.Ahead, status.Behind)
} else {
fmt.Println(" git: no repository")
}
if commit != nil {
fmt.Printf(" last commit: %s (%s)\n", commit.Subject, commit.Committed.Format(time.RFC3339))
}
if exists {
fmt.Printf(" todo: %s\n", todoPath)
} else {
fmt.Printf(" todo: missing (%s)\n", todoPath)
}
}
return store.UpsertProjects(context.Background(), projects)
}