331 lines
9.8 KiB
Go
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
|
|
}
|