mirror of
https://github.com/lukaszraczylo/git-velocity.git
synced 2026-06-05 22:43:56 +00:00
183 lines
4.3 KiB
Go
183 lines
4.3 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
|
|
contributorsSeen := make(map[string]bool)
|
|
contributorDir := filepath.Join(dataDir, "contributors")
|
|
if err := os.MkdirAll(contributorDir, 0750); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, repo := range metrics.Repositories {
|
|
for _, contributor := range repo.Contributors {
|
|
if contributorsSeen[contributor.Login] {
|
|
continue
|
|
}
|
|
contributorsSeen[contributor.Login] = true
|
|
|
|
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
|
|
}
|