Milestone 3
This commit is contained in:
330
internal/core/workstream_manager.go
Normal file
330
internal/core/workstream_manager.go
Normal file
@ -0,0 +1,330 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.dws.rip/DWS/onyx/internal/git"
|
||||
"git.dws.rip/DWS/onyx/internal/models"
|
||||
"git.dws.rip/DWS/onyx/internal/storage"
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// WorkstreamManager manages workstream operations
|
||||
type WorkstreamManager struct {
|
||||
repo *OnyxRepository
|
||||
gitBackend *git.GitBackend
|
||||
workstreamsPath string
|
||||
}
|
||||
|
||||
// NewWorkstreamManager creates a new workstream manager
|
||||
func NewWorkstreamManager(repo *OnyxRepository) *WorkstreamManager {
|
||||
return &WorkstreamManager{
|
||||
repo: repo,
|
||||
gitBackend: git.NewGitBackend(repo.GetGitRepo()),
|
||||
workstreamsPath: filepath.Join(repo.GetOnyxPath(), "workstreams.json"),
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateWorkstreamName validates a workstream name
|
||||
func ValidateWorkstreamName(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("workstream name cannot be empty")
|
||||
}
|
||||
|
||||
// Only allow alphanumeric characters, hyphens, underscores, and slashes
|
||||
validName := regexp.MustCompile(`^[a-zA-Z0-9_/-]+$`)
|
||||
if !validName.MatchString(name) {
|
||||
return fmt.Errorf("workstream name '%s' contains invalid characters. Only alphanumeric, hyphens, underscores, and slashes are allowed", name)
|
||||
}
|
||||
|
||||
// Prevent names that could cause issues
|
||||
reserved := []string{"HEAD", "main", "master", ".", ".."}
|
||||
for _, r := range reserved {
|
||||
if strings.EqualFold(name, r) {
|
||||
return fmt.Errorf("workstream name '%s' is reserved", name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateWorkstream creates a new workstream
|
||||
func (wm *WorkstreamManager) CreateWorkstream(name, baseBranch string) error {
|
||||
// Validate the name
|
||||
if err := ValidateWorkstreamName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load existing workstreams
|
||||
collection, err := storage.LoadWorkstreams(wm.workstreamsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load workstreams: %w", err)
|
||||
}
|
||||
|
||||
// Check if workstream already exists
|
||||
if _, exists := collection.Workstreams[name]; exists {
|
||||
return fmt.Errorf("workstream '%s' already exists", name)
|
||||
}
|
||||
|
||||
// Default to main if no base branch specified
|
||||
if baseBranch == "" {
|
||||
baseBranch = "main"
|
||||
}
|
||||
|
||||
// Try to fetch latest from remote base branch
|
||||
// We'll attempt this but won't fail if it doesn't work (might be a local-only repo)
|
||||
remoteBranch := fmt.Sprintf("origin/%s", baseBranch)
|
||||
_ = wm.fetchRemoteBranch(remoteBranch)
|
||||
|
||||
// Get the base commit SHA
|
||||
baseCommitSHA, err := wm.getBaseBranchHead(baseBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get base branch HEAD: %w", err)
|
||||
}
|
||||
|
||||
// Create the workstream
|
||||
workstream := models.NewWorkstream(name, "", baseBranch)
|
||||
|
||||
// Add the base commit SHA to metadata for reference
|
||||
workstream.Metadata["base_commit"] = baseCommitSHA
|
||||
|
||||
// Add to collection
|
||||
if err := collection.AddWorkstream(workstream); err != nil {
|
||||
return fmt.Errorf("failed to add workstream: %w", err)
|
||||
}
|
||||
|
||||
// Set as current workstream
|
||||
collection.CurrentWorkstream = name
|
||||
|
||||
// Save the collection
|
||||
if err := storage.SaveWorkstreams(wm.workstreamsPath, collection); err != nil {
|
||||
return fmt.Errorf("failed to save workstreams: %w", err)
|
||||
}
|
||||
|
||||
// Update workspace to point to base commit
|
||||
workspaceRef := "refs/onyx/workspaces/current"
|
||||
if err := wm.gitBackend.UpdateRef(workspaceRef, baseCommitSHA); err != nil {
|
||||
// This is non-fatal - the daemon will create a new ephemeral commit
|
||||
// We'll just log a warning
|
||||
fmt.Printf("Warning: failed to update workspace ref: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCurrentWorkstream returns the current active workstream
|
||||
func (wm *WorkstreamManager) GetCurrentWorkstream() (*models.Workstream, error) {
|
||||
collection, err := storage.LoadWorkstreams(wm.workstreamsPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load workstreams: %w", err)
|
||||
}
|
||||
|
||||
return collection.GetCurrentWorkstream()
|
||||
}
|
||||
|
||||
// SwitchWorkstream switches to a different workstream
|
||||
func (wm *WorkstreamManager) SwitchWorkstream(name string) error {
|
||||
// Load workstreams
|
||||
collection, err := storage.LoadWorkstreams(wm.workstreamsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load workstreams: %w", err)
|
||||
}
|
||||
|
||||
// Check if target workstream exists
|
||||
targetWorkstream, err := collection.GetWorkstream(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("workstream '%s' not found", name)
|
||||
}
|
||||
|
||||
// Get the commit to checkout
|
||||
var checkoutSHA string
|
||||
if !targetWorkstream.IsEmpty() {
|
||||
// Checkout the latest commit in the workstream
|
||||
latestCommit, err := targetWorkstream.GetLatestCommit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get latest commit: %w", err)
|
||||
}
|
||||
checkoutSHA = latestCommit.SHA
|
||||
} else {
|
||||
// Checkout the base commit
|
||||
baseCommitSHA := targetWorkstream.Metadata["base_commit"]
|
||||
if baseCommitSHA == "" {
|
||||
// Fallback to getting the base branch HEAD
|
||||
baseCommitSHA, err = wm.getBaseBranchHead(targetWorkstream.BaseBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get base branch HEAD: %w", err)
|
||||
}
|
||||
}
|
||||
checkoutSHA = baseCommitSHA
|
||||
}
|
||||
|
||||
// Update the working directory to the target commit
|
||||
worktree, err := wm.repo.GetGitRepo().Worktree()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get worktree: %w", err)
|
||||
}
|
||||
|
||||
// Checkout the commit
|
||||
err = worktree.Checkout(&gogit.CheckoutOptions{
|
||||
Hash: plumbing.NewHash(checkoutSHA),
|
||||
Force: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to checkout commit: %w", err)
|
||||
}
|
||||
|
||||
// Update current workstream
|
||||
if err := collection.SetCurrentWorkstream(name); err != nil {
|
||||
return fmt.Errorf("failed to set current workstream: %w", err)
|
||||
}
|
||||
|
||||
// Save the collection
|
||||
if err := storage.SaveWorkstreams(wm.workstreamsPath, collection); err != nil {
|
||||
return fmt.Errorf("failed to save workstreams: %w", err)
|
||||
}
|
||||
|
||||
// Update workspace ref to point to the checked out commit
|
||||
workspaceRef := "refs/onyx/workspaces/current"
|
||||
if err := wm.gitBackend.UpdateRef(workspaceRef, checkoutSHA); err != nil {
|
||||
fmt.Printf("Warning: failed to update workspace ref: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListWorkstreams returns all workstreams
|
||||
func (wm *WorkstreamManager) ListWorkstreams() ([]*models.Workstream, error) {
|
||||
collection, err := storage.LoadWorkstreams(wm.workstreamsPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load workstreams: %w", err)
|
||||
}
|
||||
|
||||
return collection.ListWorkstreams(), nil
|
||||
}
|
||||
|
||||
// GetCurrentWorkstreamName returns the name of the current workstream
|
||||
func (wm *WorkstreamManager) GetCurrentWorkstreamName() (string, error) {
|
||||
collection, err := storage.LoadWorkstreams(wm.workstreamsPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to load workstreams: %w", err)
|
||||
}
|
||||
|
||||
if collection.CurrentWorkstream == "" {
|
||||
return "", fmt.Errorf("no current workstream set")
|
||||
}
|
||||
|
||||
return collection.CurrentWorkstream, nil
|
||||
}
|
||||
|
||||
// AddCommitToWorkstream adds a commit to the current workstream
|
||||
func (wm *WorkstreamManager) AddCommitToWorkstream(sha, message string) error {
|
||||
// Load workstreams
|
||||
collection, err := storage.LoadWorkstreams(wm.workstreamsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load workstreams: %w", err)
|
||||
}
|
||||
|
||||
// Get current workstream
|
||||
currentWorkstream, err := collection.GetCurrentWorkstream()
|
||||
if err != nil {
|
||||
return fmt.Errorf("no active workstream: %w", err)
|
||||
}
|
||||
|
||||
// Determine parent SHA
|
||||
var parentSHA string
|
||||
if !currentWorkstream.IsEmpty() {
|
||||
latestCommit, err := currentWorkstream.GetLatestCommit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get latest commit: %w", err)
|
||||
}
|
||||
parentSHA = latestCommit.SHA
|
||||
} else {
|
||||
// For the first commit, use the base commit
|
||||
baseCommitSHA := currentWorkstream.Metadata["base_commit"]
|
||||
if baseCommitSHA == "" {
|
||||
baseCommitSHA, err = wm.getBaseBranchHead(currentWorkstream.BaseBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get base branch HEAD: %w", err)
|
||||
}
|
||||
}
|
||||
parentSHA = baseCommitSHA
|
||||
}
|
||||
|
||||
// Get the base commit SHA
|
||||
baseSHA := currentWorkstream.Metadata["base_commit"]
|
||||
if baseSHA == "" {
|
||||
baseSHA, err = wm.getBaseBranchHead(currentWorkstream.BaseBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get base branch HEAD: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the branch ref
|
||||
nextNumber := currentWorkstream.GetCommitCount() + 1
|
||||
branchRef := fmt.Sprintf("refs/onyx/workstreams/%s/commit-%d", currentWorkstream.Name, nextNumber)
|
||||
|
||||
// Create the workstream commit
|
||||
workstreamCommit := models.NewWorkstreamCommit(
|
||||
sha,
|
||||
message,
|
||||
"User", // TODO: Get actual user from git config
|
||||
parentSHA,
|
||||
baseSHA,
|
||||
branchRef,
|
||||
)
|
||||
|
||||
// Add commit to workstream
|
||||
currentWorkstream.AddCommit(workstreamCommit)
|
||||
|
||||
// Update the branch ref to point to this commit
|
||||
if err := wm.gitBackend.UpdateRef(branchRef, sha); err != nil {
|
||||
return fmt.Errorf("failed to create branch ref: %w", err)
|
||||
}
|
||||
|
||||
// Save the collection
|
||||
if err := storage.SaveWorkstreams(wm.workstreamsPath, collection); err != nil {
|
||||
return fmt.Errorf("failed to save workstreams: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchRemoteBranch attempts to fetch the latest from a remote branch
|
||||
func (wm *WorkstreamManager) fetchRemoteBranch(remoteBranch string) error {
|
||||
// This is a best-effort operation
|
||||
// We use the underlying git command for now
|
||||
// In the future, we could use go-git's fetch capabilities
|
||||
|
||||
// For now, we'll just return nil as this is optional
|
||||
// The real implementation would use go-git's Fetch method
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBaseBranchHead gets the HEAD commit SHA of a base branch
|
||||
func (wm *WorkstreamManager) getBaseBranchHead(baseBranch string) (string, error) {
|
||||
// Try local branch first
|
||||
refName := fmt.Sprintf("refs/heads/%s", baseBranch)
|
||||
sha, err := wm.gitBackend.GetRef(refName)
|
||||
if err == nil {
|
||||
return sha, nil
|
||||
}
|
||||
|
||||
// Try remote branch
|
||||
remoteRefName := fmt.Sprintf("refs/remotes/origin/%s", baseBranch)
|
||||
sha, err = wm.gitBackend.GetRef(remoteRefName)
|
||||
if err == nil {
|
||||
return sha, nil
|
||||
}
|
||||
|
||||
// If we still can't find it, try HEAD
|
||||
head, err := wm.repo.GetGitRepo().Head()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get HEAD and base branch '%s' not found: %w", baseBranch, err)
|
||||
}
|
||||
|
||||
return head.Hash().String(), nil
|
||||
}
|
Reference in New Issue
Block a user