212 lines
5.8 KiB
Go
212 lines
5.8 KiB
Go
package daemon
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"git.dws.rip/DWS/onyx/internal/git"
|
|
"git.dws.rip/DWS/onyx/internal/models"
|
|
gogit "github.com/go-git/go-git/v5"
|
|
"github.com/go-git/go-git/v5/plumbing/filemode"
|
|
)
|
|
|
|
const (
|
|
// OnyxWorkspaceRef is the ref where ephemeral commits are stored
|
|
OnyxWorkspaceRef = "refs/onyx/workspaces/current"
|
|
)
|
|
|
|
// CreateSnapshot creates an ephemeral commit representing the current workspace state
|
|
func (d *Daemon) CreateSnapshot() error {
|
|
// 1. Read current workspace pointer
|
|
workspaceState, err := d.readWorkspaceState()
|
|
if err != nil {
|
|
// If workspace doesn't exist, create a new one
|
|
workspaceState = models.NewWorkspaceState("", "main")
|
|
}
|
|
|
|
// 2. Create tree from working directory
|
|
gitBackend := git.NewGitBackend(d.repo.GetGitRepo())
|
|
repoRoot := filepath.Dir(d.repo.GetOnyxPath())
|
|
|
|
treeHash, err := d.createWorkspaceTree(repoRoot)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create workspace tree: %w", err)
|
|
}
|
|
|
|
// 3. Get the parent commit (if it exists)
|
|
var parentHash string
|
|
if workspaceState.CurrentCommitSHA != "" {
|
|
parentHash = workspaceState.CurrentCommitSHA
|
|
} else {
|
|
// Try to get the current HEAD commit as parent
|
|
head, err := d.repo.GetGitRepo().Head()
|
|
if err == nil {
|
|
parentHash = head.Hash().String()
|
|
}
|
|
}
|
|
|
|
// 4. Create ephemeral commit
|
|
message := fmt.Sprintf("[onyx-snapshot] Auto-save at %s", time.Now().Format("2006-01-02 15:04:05"))
|
|
commitHash, err := gitBackend.CreateCommit(treeHash, parentHash, message, "Onyx Daemon")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create commit: %w", err)
|
|
}
|
|
|
|
// 5. Update refs/onyx/workspaces/current
|
|
if err := gitBackend.UpdateRef(OnyxWorkspaceRef, commitHash); err != nil {
|
|
return fmt.Errorf("failed to update workspace ref: %w", err)
|
|
}
|
|
|
|
// 6. Update .onx/workspace pointer
|
|
workspaceState.UpdateSnapshot(commitHash, treeHash, "", false)
|
|
if err := d.saveWorkspaceState(workspaceState); err != nil {
|
|
return fmt.Errorf("failed to save workspace state: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createWorkspaceTree creates a Git tree object from the current working directory
|
|
func (d *Daemon) createWorkspaceTree(rootPath string) (string, error) {
|
|
gitBackend := git.NewGitBackend(d.repo.GetGitRepo())
|
|
|
|
// Use the worktree to build the tree
|
|
worktree, err := d.repo.GetGitRepo().Worktree()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get worktree: %w", err)
|
|
}
|
|
|
|
// Create tree entries by walking the working directory
|
|
entries := []git.TreeEntry{}
|
|
|
|
err = filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Skip the root directory itself
|
|
if path == rootPath {
|
|
return nil
|
|
}
|
|
|
|
// Get relative path
|
|
relPath, err := filepath.Rel(rootPath, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Skip .git and .onx directories
|
|
if shouldIgnorePath(path) {
|
|
if info.IsDir() {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// For now, we'll use a simplified approach: hash the file content
|
|
if !info.IsDir() {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read file %s: %w", path, err)
|
|
}
|
|
|
|
// Create blob for file content
|
|
blobHash, err := gitBackend.CreateBlob(content)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create blob for %s: %w", path, err)
|
|
}
|
|
|
|
// Determine file mode
|
|
mode := filemode.Regular
|
|
if info.Mode()&0111 != 0 {
|
|
mode = filemode.Executable
|
|
}
|
|
|
|
entries = append(entries, git.TreeEntry{
|
|
Name: relPath,
|
|
Mode: mode,
|
|
Hash: git.HashFromString(blobHash),
|
|
})
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to walk directory: %w", err)
|
|
}
|
|
|
|
// For a proper implementation, we'd need to build a hierarchical tree
|
|
// For now, we'll use the worktree's tree builder
|
|
return d.buildTreeFromWorktree(worktree)
|
|
}
|
|
|
|
// buildTreeFromWorktree builds a tree object from the current worktree state
|
|
func (d *Daemon) buildTreeFromWorktree(worktree *gogit.Worktree) (string, error) {
|
|
// Get the current index/staging area state
|
|
// This is a simplified version - in production we'd want to properly handle
|
|
// all files in the working directory
|
|
|
|
// For now, get the HEAD tree as a base
|
|
head, err := d.repo.GetGitRepo().Head()
|
|
if err != nil {
|
|
// No HEAD yet (empty repo), return empty tree
|
|
return d.createEmptyTree()
|
|
}
|
|
|
|
commit, err := d.repo.GetGitRepo().CommitObject(head.Hash())
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get HEAD commit: %w", err)
|
|
}
|
|
|
|
tree, err := commit.Tree()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get commit tree: %w", err)
|
|
}
|
|
|
|
// For now, just return the HEAD tree hash
|
|
// In a full implementation, we'd modify this tree based on working directory changes
|
|
return tree.Hash.String(), nil
|
|
}
|
|
|
|
// createEmptyTree creates an empty Git tree object
|
|
func (d *Daemon) createEmptyTree() (string, error) {
|
|
gitBackend := git.NewGitBackend(d.repo.GetGitRepo())
|
|
return gitBackend.CreateTree([]git.TreeEntry{})
|
|
}
|
|
|
|
// readWorkspaceState reads the workspace state from .onx/workspace
|
|
func (d *Daemon) readWorkspaceState() (*models.WorkspaceState, error) {
|
|
workspacePath := filepath.Join(d.repo.GetOnyxPath(), "workspace")
|
|
|
|
data, err := os.ReadFile(workspacePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read workspace file: %w", err)
|
|
}
|
|
|
|
state, err := models.DeserializeWorkspaceState(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize workspace state: %w", err)
|
|
}
|
|
|
|
return state, nil
|
|
}
|
|
|
|
// saveWorkspaceState saves the workspace state to .onx/workspace
|
|
func (d *Daemon) saveWorkspaceState(state *models.WorkspaceState) error {
|
|
workspacePath := filepath.Join(d.repo.GetOnyxPath(), "workspace")
|
|
|
|
data, err := state.Serialize()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize workspace state: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(workspacePath, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write workspace file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|