mirror of
https://github.com/lukaszraczylo/git-velocity.git
synced 2026-07-09 06:43:26 +00:00
Initial commit.
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user