207 lines
3.9 KiB
Go
207 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"github.com/d2fn/sumi/internal/ids"
|
|
"github.com/go-git/go-git/v6"
|
|
//"github.com/go-git/go-git/v5/plumbing"
|
|
"github.com/go-git/go-git/v6/plumbing/object"
|
|
"github.com/gen2brain/raylib-go/raylib"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
"database/sql"
|
|
_ "modernc.org/sqlite" // pure Go, Nix-friendly
|
|
)
|
|
|
|
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,
|
|
}
|
|
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(img *rl.Image) (string, error) {
|
|
id, _ := s.gen.Next()
|
|
kid := ids.Base62Encode(id)
|
|
path := filepath.Join(s.snapshotsDir, kid)
|
|
os.MkdirAll(path, 0755)
|
|
|
|
snapshotPng := filepath.Join(path, fmt.Sprintf("%s.png", kid))
|
|
|
|
rl.ExportImage(*img, snapshotPng)
|
|
|
|
hash, branch, committed, err := CommitAllIfDirty(s.repoRoot, "automated snapshot")
|
|
|
|
if err != nil {
|
|
s.log.Printf("Error getting working tree in a known clean state: %v", err)
|
|
} else {
|
|
s.log.Printf("Created 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) (commitHash string, branch string, committed bool, err error) {
|
|
r, err := git.PlainOpen(repoPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Determine branch (may be empty if detached)
|
|
ref, err := r.Head()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if ref.Name().IsBranch() {
|
|
branch = ref.Name().Short()
|
|
}
|
|
|
|
wt, err := r.Worktree()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
status, err := wt.Status()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if status.IsClean() {
|
|
return "", branch, false, nil
|
|
}
|
|
|
|
// Stage everything (git add -A)
|
|
if err = wt.AddWithOptions(&git.AddOptions{All: true}); err != nil {
|
|
return
|
|
}
|
|
|
|
hash, err := wt.Commit(message, &git.CommitOptions{
|
|
Author: &object.Signature{
|
|
Name: "sumi",
|
|
Email: "sumi@local",
|
|
When: time.Now(),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return hash.String(), branch, true, nil
|
|
}
|