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", ".", ".."} 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 (only if we have a valid base commit) if baseCommitSHA != "" { 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 (only if we have a valid commit) if checkoutSHA != "" { 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 { // Empty repository with no commits - return empty string // This is a valid state for a brand new repository return "", nil } return head.Hash().String(), nil } // getCurrentBranchName gets the name of the current Git branch func (wm *WorkstreamManager) getCurrentBranchName() (string, error) { // Try to get HEAD reference head, err := wm.repo.GetGitRepo().Head() if err != nil { // No HEAD yet (empty repo) - check the symbolic ref manually ref, err := wm.repo.GetGitRepo().Reference(plumbing.HEAD, false) if err != nil { // Can't determine - default to "main" return "main", nil } // Extract branch name from refs/heads/branch-name if ref.Target().IsBranch() { return ref.Target().Short(), nil } return "main", nil } // Check if we're on a branch (not detached HEAD) if !head.Name().IsBranch() { // Detached HEAD - default to "main" return "main", nil } // Extract branch name from refs/heads/branch-name branchName := head.Name().Short() return branchName, nil } // CreateDefaultWorkstream creates a workstream matching the current Git branch func (wm *WorkstreamManager) CreateDefaultWorkstream() error { // Get the current Git branch name branchName, err := wm.getCurrentBranchName() if err != nil { return fmt.Errorf("failed to get current branch: %w", err) } // Load existing workstreams to check if one already exists collection, err := storage.LoadWorkstreams(wm.workstreamsPath) if err != nil { return fmt.Errorf("failed to load workstreams: %w", err) } // If a workstream already exists, don't create another one if len(collection.Workstreams) > 0 { return nil } // Create workstream with the same name as the branch // This workstream tracks the branch it's named after return wm.CreateWorkstream(branchName, branchName) }