Files
git-velocity/internal/generator/site/generator.go
T

175 lines
4.1 KiB
Go

package site
import (
"embed"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
json "github.com/goccy/go-json"
"github.com/lukaszraczylo/git-velocity/internal/config"
"github.com/lukaszraczylo/git-velocity/internal/domain/models"
)
//go:embed dist/*
var spaFS embed.FS
// Generator handles static site generation
type Generator struct {
outputDir string
config *config.Config
}
// NewGenerator creates a new site generator
func NewGenerator(outputDir string, cfg *config.Config) (*Generator, error) {
return &Generator{
outputDir: outputDir,
config: cfg,
}, nil
}
// Generate creates the static site from metrics
func (g *Generator) Generate(metrics *models.GlobalMetrics) error {
// Create output directory
if err := os.MkdirAll(g.outputDir, 0750); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
// Generate data files
if err := g.generateDataFiles(metrics); err != nil {
return fmt.Errorf("failed to generate data files: %w", err)
}
// Copy Vue SPA files
if err := g.copySPAFiles(); err != nil {
return fmt.Errorf("failed to copy SPA files: %w", err)
}
return nil
}
func (g *Generator) generateDataFiles(metrics *models.GlobalMetrics) error {
dataDir := filepath.Join(g.outputDir, "data")
// Clean old data directory to ensure fresh state
if err := os.RemoveAll(dataDir); err != nil {
return fmt.Errorf("failed to clean data directory: %w", err)
}
if err := os.MkdirAll(dataDir, 0750); err != nil {
return err
}
// Prepare global data with timestamp
globalData := struct {
*models.GlobalMetrics
GeneratedAt time.Time `json:"generated_at"`
}{
GlobalMetrics: metrics,
GeneratedAt: time.Now(),
}
// Global metrics
if err := writeJSON(filepath.Join(dataDir, "global.json"), globalData); err != nil {
return err
}
// Leaderboard
if err := writeJSON(filepath.Join(dataDir, "leaderboard.json"), metrics.Leaderboard); err != nil {
return err
}
// Per-repository data
for _, repo := range metrics.Repositories {
repoDir := filepath.Join(dataDir, "repos", repo.Owner, repo.Name)
if err := os.MkdirAll(repoDir, 0750); err != nil {
return err
}
if err := writeJSON(filepath.Join(repoDir, "metrics.json"), repo); err != nil {
return err
}
}
// Per-team data
if len(metrics.Teams) > 0 {
teamDir := filepath.Join(dataDir, "teams")
if err := os.MkdirAll(teamDir, 0750); err != nil {
return err
}
for _, team := range metrics.Teams {
if err := writeJSON(filepath.Join(teamDir, slugify(team.Name)+".json"), team); err != nil {
return err
}
}
}
// Per-contributor data (use aggregated global contributors, not per-repo)
contributorDir := filepath.Join(dataDir, "contributors")
if err := os.MkdirAll(contributorDir, 0750); err != nil {
return err
}
for _, contributor := range metrics.Contributors {
if err := writeJSON(filepath.Join(contributorDir, contributor.Login+".json"), contributor); err != nil {
return err
}
}
return nil
}
func (g *Generator) copySPAFiles() error {
return fs.WalkDir(spaFS, "dist", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Skip the root dist directory itself
if path == "dist" {
return nil
}
// Calculate the relative path from "dist/"
relPath := strings.TrimPrefix(path, "dist/")
destPath := filepath.Join(g.outputDir, relPath)
if d.IsDir() {
return os.MkdirAll(destPath, 0750)
}
// Read file from embedded FS
content, err := spaFS.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read embedded file %s: %w", path, err)
}
// Write to destination
return os.WriteFile(destPath, content, 0600)
})
}
// Helper functions
func writeJSON(path string, data interface{}) error {
cleanPath := filepath.Clean(path)
file, err := os.OpenFile(cleanPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) // #nosec G304 -- path is constructed internally
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(data)
}
func slugify(s string) string {
s = strings.ToLower(s)
s = strings.ReplaceAll(s, " ", "-")
s = strings.ReplaceAll(s, "_", "-")
return s
}