temp for tree extraction

This commit is contained in:
2025-10-15 19:19:52 -04:00
commit ffa434630f
51 changed files with 9036 additions and 0 deletions

View 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
View 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
}

View 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)
}

View 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)
}