Files
onyx-prebootstrap/internal/commands/save.go
Tanishq Dubey a0f80c5c7d
Some checks failed
CI / Test (pull_request) Failing after 6s
CI / Build (pull_request) Failing after 7s
CI / Lint (pull_request) Failing after 13s
milestone 2 complete
2025-10-10 19:03:31 -04:00

231 lines
6.3 KiB
Go

package commands
import (
"fmt"
"os"
"path/filepath"
"strings"
"git.dws.rip/DWS/onyx/internal/core"
"git.dws.rip/DWS/onyx/internal/git"
"git.dws.rip/DWS/onyx/internal/models"
"github.com/spf13/cobra"
)
const (
// OnyxWorkspaceRef is the ref where ephemeral commits are stored
OnyxWorkspaceRef = "refs/onyx/workspaces/current"
// MaxCommitTitleLength is the maximum length for a commit title
MaxCommitTitleLength = 72
)
// NewSaveCmd creates the save command
func NewSaveCmd() *cobra.Command {
var message string
cmd := &cobra.Command{
Use: "save",
Short: "Save the current work as a permanent commit",
Long: `Converts the current ephemeral snapshot into a permanent commit
in the active workstream. This is similar to 'git commit' but works
with Onyx's workstream model.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runSave(message)
},
}
cmd.Flags().StringVarP(&message, "message", "m", "", "Commit message (required)")
cmd.MarkFlagRequired("message")
return cmd
}
// runSave executes the save command
func runSave(message string) error {
// Validate the commit message
if err := validateCommitMessage(message); err != nil {
return err
}
// Get current directory
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}
// Check if this is an Onyx repository
if !core.IsOnyxRepo(cwd) {
return fmt.Errorf("not an Onyx repository")
}
// Open the repository
repo, err := core.Open(cwd)
if err != nil {
return fmt.Errorf("failed to open repository: %w", err)
}
defer repo.Close()
// Use ExecuteWithTransaction to capture state_before and state_after
err = core.ExecuteWithTransaction(repo, "save", message, func() error {
return executeSave(repo, message)
})
if err != nil {
return err
}
fmt.Printf("Successfully saved commit: %s\n", message)
return nil
}
// executeSave performs the actual save operation
func executeSave(repo *core.OnyxRepository, message string) error {
gitBackend := git.NewGitBackend(repo.GetGitRepo())
// 1. Read current ephemeral commit from workspace ref
ephemeralCommitSHA, err := getEphemeralCommit(gitBackend)
if err != nil {
return fmt.Errorf("failed to get ephemeral commit: %w", err)
}
// 2. Get the commit object to extract the tree
ephemeralCommit, err := gitBackend.GetCommit(ephemeralCommitSHA)
if err != nil {
return fmt.Errorf("failed to get ephemeral commit object: %w", err)
}
// 3. Load workstream collection
workstreams, err := loadWorkstreams(repo)
if err != nil {
return fmt.Errorf("failed to load workstreams: %w", err)
}
// 4. Get the current workstream
currentWorkstream, err := workstreams.GetCurrentWorkstream()
if err != nil {
return fmt.Errorf("no active workstream. Use 'onx new' to create one: %w", err)
}
// 5. Determine the parent commit
var parentSHA string
if !currentWorkstream.IsEmpty() {
latestCommit, err := currentWorkstream.GetLatestCommit()
if err != nil {
return err
}
parentSHA = latestCommit.SHA
} else {
// For the first commit in the workstream, use the base branch HEAD
baseBranch := currentWorkstream.BaseBranch
if baseBranch == "" {
baseBranch = "main"
}
// Try to get the base branch reference
branchRef := fmt.Sprintf("refs/heads/%s", baseBranch)
sha, err := gitBackend.GetRef(branchRef)
if err == nil {
parentSHA = sha
}
}
// 6. Create new commit with the user's message
treeHash := ephemeralCommit.TreeHash.String()
commitSHA, err := gitBackend.CreateCommit(treeHash, parentSHA, message, "User")
if err != nil {
return fmt.Errorf("failed to create commit: %w", err)
}
// 7. Determine next branch number
nextNumber := currentWorkstream.GetCommitCount() + 1
// 8. Create branch ref (e.g., refs/onyx/workstreams/feature-name/commit-1)
branchRef := fmt.Sprintf("refs/onyx/workstreams/%s/commit-%d", currentWorkstream.Name, nextNumber)
if err := gitBackend.UpdateRef(branchRef, commitSHA); err != nil {
return fmt.Errorf("failed to create branch ref: %w", err)
}
// 9. Add commit to workstream
workstreamCommit := models.NewWorkstreamCommit(
commitSHA,
message,
"User",
parentSHA,
currentWorkstream.BaseBranch,
branchRef,
)
currentWorkstream.AddCommit(workstreamCommit)
// 10. Save updated workstreams
if err := saveWorkstreams(repo, workstreams); err != nil {
return fmt.Errorf("failed to save workstreams: %w", err)
}
return nil
}
// getEphemeralCommit retrieves the current ephemeral commit SHA
func getEphemeralCommit(gitBackend *git.GitBackend) (string, error) {
sha, err := gitBackend.GetRef(OnyxWorkspaceRef)
if err != nil {
return "", fmt.Errorf("no ephemeral commit found. The daemon may not be running")
}
return sha, nil
}
// loadWorkstreams loads the workstream collection from .onx/workstreams.json
func loadWorkstreams(repo *core.OnyxRepository) (*models.WorkstreamCollection, error) {
workstreamsPath := filepath.Join(repo.GetOnyxPath(), "workstreams.json")
data, err := os.ReadFile(workstreamsPath)
if err != nil {
return nil, fmt.Errorf("failed to read workstreams file: %w", err)
}
workstreams, err := models.DeserializeWorkstreamCollection(data)
if err != nil {
return nil, fmt.Errorf("failed to deserialize workstreams: %w", err)
}
return workstreams, nil
}
// saveWorkstreams saves the workstream collection to .onx/workstreams.json
func saveWorkstreams(repo *core.OnyxRepository, workstreams *models.WorkstreamCollection) error {
workstreamsPath := filepath.Join(repo.GetOnyxPath(), "workstreams.json")
data, err := workstreams.Serialize()
if err != nil {
return fmt.Errorf("failed to serialize workstreams: %w", err)
}
if err := os.WriteFile(workstreamsPath, data, 0644); err != nil {
return fmt.Errorf("failed to write workstreams file: %w", err)
}
return nil
}
// validateCommitMessage validates the commit message
func validateCommitMessage(message string) error {
// Check if message is empty
if strings.TrimSpace(message) == "" {
return fmt.Errorf("commit message cannot be empty")
}
// Split into lines
lines := strings.Split(message, "\n")
// Validate title (first line)
title := strings.TrimSpace(lines[0])
if title == "" {
return fmt.Errorf("commit message title cannot be empty")
}
if len(title) > MaxCommitTitleLength {
return fmt.Errorf("commit message title is too long (%d characters). Maximum is %d characters", len(title), MaxCommitTitleLength)
}
return nil
}