package main import ( "fmt" "github.com/d2fn/sumi/internal/ids" "github.com/go-git/go-git/v6" "log" //"github.com/go-git/go-git/v5/plumbing" "database/sql" "github.com/gen2brain/raylib-go/raylib" "github.com/go-git/go-git/v6/plumbing/object" _ "modernc.org/sqlite" // pure Go, Nix-friendly "os" "path/filepath" "time" ) type Storage struct { repoRoot string snapshotsDir string gen *ids.Generator db *sql.DB log *log.Logger } func NewStorage(snapshotsDir string) (*Storage, error) { log := log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) gen, _ := ids.NewGenerator(82) db, err := OpenDB(filepath.Join(snapshotsDir, "snapshots.db"), log) if err != nil { return nil, err } s := Storage{ repoRoot: ".", snapshotsDir: snapshotsDir, gen: gen, db: db, log: log, } return &s, nil } func OpenDB(path string, log *log.Logger) (*sql.DB, error) { log.Printf("Opening sqlite db") first := false if _, err := os.Stat(path); os.IsNotExist(err) { log.Printf("Database not found, initializing") first = true } db, err := sql.Open("sqlite", path) if err != nil { log.Printf("Error opening database at %s", path) return nil, err } if first { log.Printf("Initializing empty db with schema", path) if err := initSchema(db); err != nil { db.Close() return nil, err } log.Printf("Error initializing schema: %v", err) } return db, nil } func initSchema(db *sql.DB) error { schema, err := os.ReadFile("db/schema.sql") if err != nil { return err } tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() if _, err := tx.Exec(string(schema)); err != nil { return err } return tx.Commit() } func (s *Storage) Save(capture *SketchCapture) (string, error) { id, _ := s.gen.Next() kid := ids.Base62Encode(id) path := filepath.Join(s.snapshotsDir, kid) os.MkdirAll(path, 0755) // wysiwyg at screen res img := rl.LoadImageFromScreen() defer rl.UnloadImage(img) snapshotPng := filepath.Join(path, fmt.Sprintf("%s.png", kid)) rl.ExportImage(*img, snapshotPng) // capture full res compsite compositePng := filepath.Join(path, fmt.Sprintf("%s-composite.png", kid)) rl.ExportImage(*capture.compositeImage, compositePng) // capture full res layer for i, layerImage := range capture.layerImages { layerPng := filepath.Join(path, fmt.Sprintf("%s-%03d.png", kid, i)) rl.ExportImage(*layerImage, layerPng) } s.log.Printf("Checking git status...\n") hash, branch, committed, err := CommitAllIfDirty(s.repoRoot, "automated snapshot", s.log) if err != nil { s.log.Printf("Error getting working tree in a known clean state: %v", err) } else { if committed { s.log.Printf("Created commit %s on %s for snapshot %s", hash, branch, kid) } else { s.log.Printf("Referencing commit %s on %s for snapshot %s", hash, branch, kid) } } _, err = s.db.Exec(` INSERT INTO snapshots (id, sid, created_at, branch, git_hash, committed, path) VALUES (?, ?, ?, ?, ?, ?, ?) `, id, kid, time.Now().UnixMilli(), branch, hash, committed, path, ) if err != nil { s.log.Printf("Error inserting snapshot row into db: %v\n", err) } s.log.Printf("Saved snapshot to %s\n", path) return path, nil } func HeadHash(repoPath string) (string, error) { r, err := git.PlainOpen(repoPath) if err != nil { return "", err } ref, err := r.Head() if err != nil { return "", err } return ref.Hash().String(), nil } func IsDirty(repoPath string) (bool, error) { r, err := git.PlainOpen(repoPath) if err != nil { return false, err } wt, err := r.Worktree() if err != nil { return false, err } status, err := wt.Status() if err != nil { return false, err } return !status.IsClean(), nil } func CommitAllIfDirty(repoPath, message string, log *log.Logger) (commitHash string, branch string, committed bool, err error) { r, err := git.PlainOpen(repoPath) if err != nil { log.Printf("Error opening git repo") return } // Determine branch (may be empty if detached) ref, err := r.Head() if err != nil { log.Printf("Error determining head commit") return } if ref.Name().IsBranch() { branch = ref.Name().Short() } wt, err := r.Worktree() if err != nil { log.Printf("Error getting worktree state") return } status, err := wt.Status() if err != nil { log.Printf("Error getting worktree status") return } if status.IsClean() { hash, err := HeadHash(repoPath) if err != nil { log.Printf("Repo was clean but there was an error checking the commit for HEAD: %v", err) } return hash, branch, false, nil } // Stage everything (git add -A) if err = wt.AddWithOptions(&git.AddOptions{All: true}); err != nil { log.Printf("Error adding git changes to index") return } hash, err := wt.Commit(message, &git.CommitOptions{ Author: &object.Signature{ Name: "sumi", Email: "sumi@local", When: time.Now(), }, }) if err != nil { log.Printf("Error creating commit") return } return hash.String(), branch, true, nil }