temp for tree extraction
This commit is contained in:
225
internal/commands/save.go
Normal file
225
internal/commands/save.go
Normal file
@ -0,0 +1,225 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.dws.rip/DWS/onyx/internal/core"
|
||||
"git.dws.rip/DWS/onyx/internal/git"
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
"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())
|
||||
gitRepo := repo.GetGitRepo()
|
||||
|
||||
// 1. Try to read current ephemeral commit from workspace ref
|
||||
ephemeralCommitSHA, err := getEphemeralCommit(gitBackend)
|
||||
|
||||
var treeHash string
|
||||
if err != nil {
|
||||
// No ephemeral commit yet - this is the first commit or daemon isn't running
|
||||
// Use worktree to stage and get tree from working directory
|
||||
worktree, err := gitRepo.Worktree()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get worktree: %w", err)
|
||||
}
|
||||
|
||||
// Get all files including untracked ones
|
||||
status, err := worktree.Status()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get status: %w", err)
|
||||
}
|
||||
|
||||
// Add all files (including untracked) to the index
|
||||
for file := range status {
|
||||
_, err = worktree.Add(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add file %s: %w", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(status) == 0 {
|
||||
return fmt.Errorf("no changes to commit")
|
||||
}
|
||||
|
||||
// Create a temporary commit to get the tree hash
|
||||
// This is a workaround since we need the tree object
|
||||
tempCommitHash, err := worktree.Commit("temp for tree extraction", &gogit.CommitOptions{
|
||||
All: true,
|
||||
AllowEmptyCommits: false,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temporary commit: %w", err)
|
||||
}
|
||||
|
||||
// Get the tree hash from the temporary commit
|
||||
tempCommit, err := gitRepo.CommitObject(tempCommitHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get temporary commit: %w", err)
|
||||
}
|
||||
|
||||
treeHash = tempCommit.TreeHash.String()
|
||||
|
||||
// Note: We leave the temp commit in place - it will be orphaned
|
||||
// when we create the real commit, and can be cleaned up by git gc
|
||||
} else {
|
||||
// 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)
|
||||
}
|
||||
treeHash = ephemeralCommit.TreeHash.String()
|
||||
}
|
||||
|
||||
// 3. Create workstream manager
|
||||
wsManager := core.NewWorkstreamManager(repo)
|
||||
|
||||
// 4. Get the current workstream
|
||||
currentWorkstream, err := wsManager.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 commit
|
||||
baseCommitSHA := currentWorkstream.Metadata["base_commit"]
|
||||
if baseCommitSHA == "" {
|
||||
// Fallback to getting the base branch HEAD
|
||||
baseBranch := currentWorkstream.BaseBranch
|
||||
if baseBranch == "" {
|
||||
baseBranch = "main"
|
||||
}
|
||||
branchRef := fmt.Sprintf("refs/heads/%s", baseBranch)
|
||||
sha, err := gitBackend.GetRef(branchRef)
|
||||
if err == nil {
|
||||
parentSHA = sha
|
||||
}
|
||||
} else {
|
||||
parentSHA = baseCommitSHA
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Create new commit with the user's message
|
||||
commitSHA, err := gitBackend.CreateCommit(treeHash, parentSHA, message, "User")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create commit: %w", err)
|
||||
}
|
||||
|
||||
// 7. Add commit to workstream using the manager
|
||||
if err := wsManager.AddCommitToWorkstream(commitSHA, message); err != nil {
|
||||
return fmt.Errorf("failed to add commit to workstream: %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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
Reference in New Issue
Block a user