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 }