temp for tree extraction
This commit is contained in:
74
internal/core/interfaces.go
Normal file
74
internal/core/interfaces.go
Normal file
@ -0,0 +1,74 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
// Repository represents an Onyx repository with both Git and Onyx-specific metadata
|
||||
type Repository interface {
|
||||
// Init initializes a new Onyx repository at the given path
|
||||
Init(path string) error
|
||||
|
||||
// GetGitRepo returns the underlying Git repository
|
||||
GetGitRepo() *gogit.Repository
|
||||
|
||||
// GetOnyxMetadata returns Onyx-specific metadata
|
||||
GetOnyxMetadata() *OnyxMetadata
|
||||
|
||||
// Close releases any resources held by the repository
|
||||
Close() error
|
||||
}
|
||||
|
||||
// GitBackend provides low-level Git object operations
|
||||
type GitBackend interface {
|
||||
// CreateCommit creates a new commit object
|
||||
CreateCommit(tree, parent, message string) (string, error)
|
||||
|
||||
// CreateTree creates a new tree object from the given entries
|
||||
CreateTree(entries []TreeEntry) (string, error)
|
||||
|
||||
// UpdateRef updates a Git reference to point to a new SHA
|
||||
UpdateRef(name, sha string) error
|
||||
|
||||
// GetRef retrieves the SHA that a reference points to
|
||||
GetRef(name string) (string, error)
|
||||
|
||||
// CreateBlob creates a new blob object from content
|
||||
CreateBlob(content []byte) (string, error)
|
||||
|
||||
// GetObject retrieves a Git object by its SHA
|
||||
GetObject(sha string) (Object, error)
|
||||
}
|
||||
|
||||
// TreeEntry represents an entry in a Git tree object
|
||||
type TreeEntry struct {
|
||||
Mode int // File mode (e.g., 0100644 for regular file, 040000 for directory)
|
||||
Name string // Entry name
|
||||
SHA string // Object SHA-1 hash
|
||||
}
|
||||
|
||||
// Object represents a Git object (blob, tree, commit, or tag)
|
||||
type Object interface {
|
||||
// Type returns the type of the object (blob, tree, commit, tag)
|
||||
Type() string
|
||||
|
||||
// SHA returns the SHA-1 hash of the object
|
||||
SHA() string
|
||||
|
||||
// Size returns the size of the object in bytes
|
||||
Size() int64
|
||||
}
|
||||
|
||||
// OnyxMetadata holds Onyx-specific repository metadata
|
||||
type OnyxMetadata struct {
|
||||
// Version of the Onyx repository format
|
||||
Version string
|
||||
|
||||
// Created timestamp when the repository was initialized
|
||||
Created time.Time
|
||||
|
||||
// Path to the .onx directory
|
||||
OnyxPath string
|
||||
}
|
178
internal/core/repository.go
Normal file
178
internal/core/repository.go
Normal file
@ -0,0 +1,178 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
// OnyxRepository implements the Repository interface
|
||||
type OnyxRepository struct {
|
||||
gitRepo *gogit.Repository
|
||||
onyxPath string
|
||||
gitPath string
|
||||
metadata *OnyxMetadata
|
||||
}
|
||||
|
||||
// Open opens an existing Onyx repository at the given path
|
||||
func Open(path string) (*OnyxRepository, error) {
|
||||
// Resolve to absolute path
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve path: %w", err)
|
||||
}
|
||||
|
||||
// Check if .git directory exists
|
||||
gitPath := filepath.Join(absPath, ".git")
|
||||
if _, err := os.Stat(gitPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("not a git repository (no .git directory found)")
|
||||
}
|
||||
|
||||
// Check if .onx directory exists
|
||||
onyxPath := filepath.Join(absPath, ".onx")
|
||||
if _, err := os.Stat(onyxPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("not an onyx repository (no .onx directory found)")
|
||||
}
|
||||
|
||||
// Open the Git repository
|
||||
gitRepo, err := gogit.PlainOpen(absPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open git repository: %w", err)
|
||||
}
|
||||
|
||||
// Load Onyx metadata
|
||||
metadata := &OnyxMetadata{
|
||||
Version: "1.0.0",
|
||||
Created: time.Now(), // TODO: Load from .onx/metadata file
|
||||
OnyxPath: onyxPath,
|
||||
}
|
||||
|
||||
return &OnyxRepository{
|
||||
gitRepo: gitRepo,
|
||||
onyxPath: onyxPath,
|
||||
gitPath: gitPath,
|
||||
metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Init initializes a new Onyx repository at the given path
|
||||
func (r *OnyxRepository) Init(path string) error {
|
||||
// Resolve to absolute path
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve path: %w", err)
|
||||
}
|
||||
|
||||
// Check if directory exists, create if it doesn't
|
||||
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(absPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Git repository if it doesn't exist
|
||||
gitPath := filepath.Join(absPath, ".git")
|
||||
if _, err := os.Stat(gitPath); os.IsNotExist(err) {
|
||||
_, err := gogit.PlainInit(absPath, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize git repository: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create .onx directory structure
|
||||
onyxPath := filepath.Join(absPath, ".onx")
|
||||
if err := os.MkdirAll(onyxPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create .onx directory: %w", err)
|
||||
}
|
||||
|
||||
// Create subdirectories
|
||||
subdirs := []string{"rerere_cache"}
|
||||
for _, subdir := range subdirs {
|
||||
subdirPath := filepath.Join(onyxPath, subdir)
|
||||
if err := os.MkdirAll(subdirPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create %s directory: %w", subdir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize oplog file
|
||||
oplogPath := filepath.Join(onyxPath, "oplog")
|
||||
if _, err := os.Stat(oplogPath); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(oplogPath, []byte{}, 0644); err != nil {
|
||||
return fmt.Errorf("failed to create oplog file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize workstreams.json
|
||||
workstreamsPath := filepath.Join(onyxPath, "workstreams.json")
|
||||
if _, err := os.Stat(workstreamsPath); os.IsNotExist(err) {
|
||||
initialContent := []byte("{\"workstreams\":{}}\n")
|
||||
if err := os.WriteFile(workstreamsPath, initialContent, 0644); err != nil {
|
||||
return fmt.Errorf("failed to create workstreams.json: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Open the repository
|
||||
gitRepo, err := gogit.PlainOpen(absPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open git repository: %w", err)
|
||||
}
|
||||
|
||||
// Set up the repository instance
|
||||
r.gitRepo = gitRepo
|
||||
r.onyxPath = onyxPath
|
||||
r.gitPath = gitPath
|
||||
r.metadata = &OnyxMetadata{
|
||||
Version: "1.0.0",
|
||||
Created: time.Now(),
|
||||
OnyxPath: onyxPath,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGitRepo returns the underlying Git repository
|
||||
func (r *OnyxRepository) GetGitRepo() *gogit.Repository {
|
||||
return r.gitRepo
|
||||
}
|
||||
|
||||
// GetOnyxMetadata returns Onyx-specific metadata
|
||||
func (r *OnyxRepository) GetOnyxMetadata() *OnyxMetadata {
|
||||
return r.metadata
|
||||
}
|
||||
|
||||
// Close releases any resources held by the repository
|
||||
func (r *OnyxRepository) Close() error {
|
||||
// Currently, go-git doesn't require explicit closing
|
||||
// This method is here for future-proofing
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsOnyxRepo checks if the given path is an Onyx repository
|
||||
func IsOnyxRepo(path string) bool {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for both .git and .onx directories
|
||||
gitPath := filepath.Join(absPath, ".git")
|
||||
onyxPath := filepath.Join(absPath, ".onx")
|
||||
|
||||
_, gitErr := os.Stat(gitPath)
|
||||
_, onyxErr := os.Stat(onyxPath)
|
||||
|
||||
return gitErr == nil && onyxErr == nil
|
||||
}
|
||||
|
||||
// GetOnyxPath returns the path to the .onx directory
|
||||
func (r *OnyxRepository) GetOnyxPath() string {
|
||||
return r.onyxPath
|
||||
}
|
||||
|
||||
// GetGitPath returns the path to the .git directory
|
||||
func (r *OnyxRepository) GetGitPath() string {
|
||||
return r.gitPath
|
||||
}
|
186
internal/core/transaction.go
Normal file
186
internal/core/transaction.go
Normal file
@ -0,0 +1,186 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"git.dws.rip/DWS/onyx/internal/models"
|
||||
"git.dws.rip/DWS/onyx/internal/storage"
|
||||
)
|
||||
|
||||
// Transaction represents a transactional operation with oplog support
|
||||
type Transaction struct {
|
||||
repo *OnyxRepository
|
||||
oplogWriter *storage.OplogWriter
|
||||
stateCapture *storage.StateCapture
|
||||
}
|
||||
|
||||
// NewTransaction creates a new transaction for the given repository
|
||||
func NewTransaction(repo *OnyxRepository) (*Transaction, error) {
|
||||
oplogPath := filepath.Join(repo.GetOnyxPath(), "oplog")
|
||||
oplogWriter, err := storage.OpenOplog(oplogPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open oplog: %w", err)
|
||||
}
|
||||
|
||||
stateCapture := storage.NewStateCapture(repo.GetGitRepo())
|
||||
|
||||
return &Transaction{
|
||||
repo: repo,
|
||||
oplogWriter: oplogWriter,
|
||||
stateCapture: stateCapture,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ExecuteWithTransaction executes a function within a transaction context
|
||||
// It captures the state before and after the operation and logs it to the oplog
|
||||
func (t *Transaction) ExecuteWithTransaction(operation, description string, fn func() error) error {
|
||||
// 1. Capture state_before
|
||||
stateBefore, err := t.stateCapture.CaptureState()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to capture state before: %w", err)
|
||||
}
|
||||
|
||||
// 2. Execute the function
|
||||
err = fn()
|
||||
if err != nil {
|
||||
// On error, we don't log to oplog since the operation failed
|
||||
return fmt.Errorf("operation failed: %w", err)
|
||||
}
|
||||
|
||||
// 3. Capture state_after
|
||||
stateAfter, err := t.stateCapture.CaptureState()
|
||||
if err != nil {
|
||||
// Even if we can't capture the after state, we should try to log what we can
|
||||
// This is a warning situation rather than a failure
|
||||
fmt.Printf("Warning: failed to capture state after: %v\n", err)
|
||||
stateAfter = stateBefore // Use the before state as a fallback
|
||||
}
|
||||
|
||||
// 4. Create oplog entry
|
||||
entry := models.NewOplogEntry(0, operation, description, stateBefore, stateAfter)
|
||||
|
||||
// 5. Write to oplog
|
||||
err = t.oplogWriter.AppendEntry(entry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to oplog: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the transaction and releases resources
|
||||
func (t *Transaction) Close() error {
|
||||
return t.oplogWriter.Close()
|
||||
}
|
||||
|
||||
// ExecuteWithTransactionAndMetadata executes a function with custom metadata
|
||||
func (t *Transaction) ExecuteWithTransactionAndMetadata(
|
||||
operation, description string,
|
||||
metadata map[string]string,
|
||||
fn func() error,
|
||||
) error {
|
||||
// Capture state_before
|
||||
stateBefore, err := t.stateCapture.CaptureState()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to capture state before: %w", err)
|
||||
}
|
||||
|
||||
// Execute the function
|
||||
err = fn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("operation failed: %w", err)
|
||||
}
|
||||
|
||||
// Capture state_after
|
||||
stateAfter, err := t.stateCapture.CaptureState()
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: failed to capture state after: %v\n", err)
|
||||
stateAfter = stateBefore
|
||||
}
|
||||
|
||||
// Create oplog entry with metadata
|
||||
entry := models.NewOplogEntry(0, operation, description, stateBefore, stateAfter)
|
||||
entry.Metadata = metadata
|
||||
|
||||
// Write to oplog
|
||||
err = t.oplogWriter.AppendEntry(entry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to oplog: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback attempts to rollback to a previous state
|
||||
func (t *Transaction) Rollback(entryID uint64) error {
|
||||
// Read the oplog entry
|
||||
oplogPath := filepath.Join(t.repo.GetOnyxPath(), "oplog")
|
||||
reader := storage.NewOplogReader(oplogPath)
|
||||
|
||||
entry, err := reader.ReadEntry(entryID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read entry %d: %w", entryID, err)
|
||||
}
|
||||
|
||||
// Restore the state_before from that entry
|
||||
if entry.StateBefore == nil {
|
||||
return fmt.Errorf("entry %d has no state_before to restore", entryID)
|
||||
}
|
||||
|
||||
err = t.stateCapture.RestoreState(entry.StateBefore)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restore state: %w", err)
|
||||
}
|
||||
|
||||
// Log the rollback operation
|
||||
stateAfter, _ := t.stateCapture.CaptureState()
|
||||
rollbackEntry := models.NewOplogEntry(
|
||||
0,
|
||||
"rollback",
|
||||
fmt.Sprintf("Rolled back to entry %d", entryID),
|
||||
stateAfter, // The current state becomes the "before"
|
||||
entry.StateBefore, // The restored state becomes the "after"
|
||||
)
|
||||
rollbackEntry.Metadata = map[string]string{
|
||||
"rollback_to_entry_id": fmt.Sprintf("%d", entryID),
|
||||
}
|
||||
|
||||
err = t.oplogWriter.AppendEntry(rollbackEntry)
|
||||
if err != nil {
|
||||
// Don't fail the rollback if we can't log it
|
||||
fmt.Printf("Warning: failed to log rollback: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Commit captures the final state and writes to oplog
|
||||
func (t *Transaction) Commit(operation, description string) error {
|
||||
// Capture state_after
|
||||
stateAfter, err := t.stateCapture.CaptureState()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to capture state: %w", err)
|
||||
}
|
||||
|
||||
// Create oplog entry
|
||||
entry := models.NewOplogEntry(0, operation, description, nil, stateAfter)
|
||||
|
||||
// Write to oplog
|
||||
if err := t.oplogWriter.AppendEntry(entry); err != nil {
|
||||
return fmt.Errorf("failed to write to oplog: %w", err)
|
||||
}
|
||||
|
||||
return t.Close()
|
||||
}
|
||||
|
||||
// Helper function to execute a transaction on a repository
|
||||
func ExecuteWithTransaction(repo *OnyxRepository, operation, description string, fn func() error) error {
|
||||
txn, err := NewTransaction(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer txn.Close()
|
||||
|
||||
return txn.ExecuteWithTransaction(operation, description, fn)
|
||||
}
|
389
internal/core/workstream_manager.go
Normal file
389
internal/core/workstream_manager.go
Normal file
@ -0,0 +1,389 @@
|
||||
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)
|
||||
}
|
Reference in New Issue
Block a user