Files
onyx-prebootstrap/internal/core/workstream_manager.go
Tanishq Dubey 8b1339d0cf
Some checks failed
CI / Test (pull_request) Failing after 6s
CI / Build (pull_request) Failing after 6s
CI / Lint (pull_request) Failing after 12s
Milestone 3
2025-10-14 22:10:45 -04:00

331 lines
9.8 KiB
Go

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
}