204 lines
4.6 KiB
Go
204 lines
4.6 KiB
Go
package ctxasm
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"io/fs"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
const repoMapCommitPrefix = "commit: "
|
|
|
|
// GenerateRepoMap baut Verzeichnisbaum + exportierte Symbole (5.4). Für
|
|
// .go-Dateien über go/ast, sonst der sprachagnostische Fallback: die erste
|
|
// nicht-leere Zeile der Datei. Der Header trägt den Git-Commit, gegen den
|
|
// gebaut wurde — Assemble() warnt später, wenn HEAD davon abweicht.
|
|
func GenerateRepoMap(repoRoot string) (string, error) {
|
|
commit, _ := gitHead(repoRoot) // best effort; leer, wenn kein Git-Repo
|
|
|
|
var files []string
|
|
err := filepath.WalkDir(repoRoot, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
rel, relErr := filepath.Rel(repoRoot, path)
|
|
if relErr != nil || rel == "." {
|
|
return nil
|
|
}
|
|
base := filepath.Base(path)
|
|
if d.IsDir() {
|
|
if base == ".git" || base == ".nub" || base == "node_modules" || base == "vendor" {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
files = append(files, rel)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sort.Strings(files)
|
|
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "# REPOMAP.md\n%s%s\ngenerated_by: nub map\n\n", repoMapCommitPrefix, commit)
|
|
|
|
b.WriteString("## Tree\n\n")
|
|
for _, f := range files {
|
|
fmt.Fprintf(&b, "%s\n", f)
|
|
}
|
|
|
|
b.WriteString("\n## Symbols\n\n")
|
|
for _, f := range files {
|
|
abs := filepath.Join(repoRoot, f)
|
|
if strings.HasSuffix(f, ".go") {
|
|
syms, err := goSymbols(abs)
|
|
if err != nil || len(syms) == 0 {
|
|
continue
|
|
}
|
|
fmt.Fprintf(&b, "### %s\n", f)
|
|
for _, s := range syms {
|
|
fmt.Fprintf(&b, "- %s\n", s)
|
|
}
|
|
b.WriteString("\n")
|
|
continue
|
|
}
|
|
if header := fileHeader(abs); header != "" {
|
|
fmt.Fprintf(&b, "### %s\n%s\n\n", f, header)
|
|
}
|
|
}
|
|
|
|
return b.String(), nil
|
|
}
|
|
|
|
func goSymbols(path string) ([]string, error) {
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var syms []string
|
|
for _, decl := range f.Decls {
|
|
switch d := decl.(type) {
|
|
case *ast.FuncDecl:
|
|
if d.Name.IsExported() {
|
|
recv := ""
|
|
if d.Recv != nil && len(d.Recv.List) == 1 {
|
|
recv = "(" + exprString(d.Recv.List[0].Type) + ") "
|
|
}
|
|
syms = append(syms, "func "+recv+d.Name.Name)
|
|
}
|
|
case *ast.GenDecl:
|
|
for _, spec := range d.Specs {
|
|
switch s := spec.(type) {
|
|
case *ast.TypeSpec:
|
|
if s.Name.IsExported() {
|
|
syms = append(syms, "type "+s.Name.Name)
|
|
}
|
|
case *ast.ValueSpec:
|
|
kind := "var"
|
|
if d.Tok == token.CONST {
|
|
kind = "const"
|
|
}
|
|
for _, name := range s.Names {
|
|
if name.IsExported() {
|
|
syms = append(syms, kind+" "+name.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return syms, nil
|
|
}
|
|
|
|
func exprString(e ast.Expr) string {
|
|
switch t := e.(type) {
|
|
case *ast.Ident:
|
|
return t.Name
|
|
case *ast.StarExpr:
|
|
return "*" + exprString(t.X)
|
|
default:
|
|
return "?"
|
|
}
|
|
}
|
|
|
|
// fileHeader ist der sprachagnostische Fallback: die erste nicht-leere Zeile.
|
|
func fileHeader(path string) string {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" {
|
|
continue
|
|
}
|
|
if len(line) > 120 {
|
|
line = line[:120]
|
|
}
|
|
return line
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func gitHead(repoRoot string) (string, error) {
|
|
out, err := exec.Command("git", "-C", repoRoot, "rev-parse", "HEAD").Output()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(string(out)), nil
|
|
}
|
|
|
|
// warnStaleRepoMap vergleicht den im REPOMAP.md-Header vermerkten Commit mit
|
|
// dem aktuellen HEAD und hängt bei Abweichung eine Warnung an (nicht fatal —
|
|
// eine falsche Repomap ist schlechter als keine, aber sie soll den Start
|
|
// nicht verhindern).
|
|
func warnStaleRepoMap(repoRoot string, files []FileOrigin, warnings *[]string) {
|
|
for _, f := range files {
|
|
if filepath.Base(f.Path) != "REPOMAP.md" {
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(f.Source)
|
|
if err != nil {
|
|
return
|
|
}
|
|
recorded := ""
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
if rest, ok := strings.CutPrefix(line, repoMapCommitPrefix); ok {
|
|
recorded = strings.TrimSpace(rest)
|
|
break
|
|
}
|
|
}
|
|
if recorded == "" {
|
|
return
|
|
}
|
|
current, err := gitHead(repoRoot)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if current != recorded {
|
|
*warnings = append(*warnings, fmt.Sprintf(
|
|
"REPOMAP.md wurde gegen Commit %s erzeugt, HEAD ist jetzt %s — `nub map` erneut ausführen",
|
|
shortSHA(recorded), shortSHA(current)))
|
|
}
|
|
}
|
|
}
|
|
|
|
func shortSHA(sha string) string {
|
|
if len(sha) > 12 {
|
|
return sha[:12]
|
|
}
|
|
return sha
|
|
}
|