Implement Phase 0
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"
|
||||
|
||||
"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() *git.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"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
// OnyxRepository implements the Repository interface
|
||||
type OnyxRepository struct {
|
||||
gitRepo *git.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 := git.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 := git.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 := git.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() *git.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
|
||||
}
|
205
internal/git/objects.go
Normal file
205
internal/git/objects.go
Normal file
@ -0,0 +1,205 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
// GitBackend implements low-level Git object operations
|
||||
type GitBackend struct {
|
||||
repo *git.Repository
|
||||
}
|
||||
|
||||
// NewGitBackend creates a new GitBackend instance
|
||||
func NewGitBackend(repo *git.Repository) *GitBackend {
|
||||
return &GitBackend{repo: repo}
|
||||
}
|
||||
|
||||
// CreateBlob creates a new blob object from the given content
|
||||
func (gb *GitBackend) CreateBlob(content []byte) (string, error) {
|
||||
store := gb.repo.Storer
|
||||
|
||||
// Create a blob object
|
||||
blob := store.NewEncodedObject()
|
||||
blob.SetType(plumbing.BlobObject)
|
||||
blob.SetSize(int64(len(content)))
|
||||
|
||||
writer, err := blob.Writer()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get blob writer: %w", err)
|
||||
}
|
||||
|
||||
_, err = writer.Write(content)
|
||||
if err != nil {
|
||||
writer.Close()
|
||||
return "", fmt.Errorf("failed to write blob content: %w", err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return "", fmt.Errorf("failed to close blob writer: %w", err)
|
||||
}
|
||||
|
||||
// Store the blob
|
||||
hash, err := store.SetEncodedObject(blob)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to store blob: %w", err)
|
||||
}
|
||||
|
||||
return hash.String(), nil
|
||||
}
|
||||
|
||||
// TreeEntry represents an entry in a Git tree
|
||||
type TreeEntry struct {
|
||||
Mode filemode.FileMode
|
||||
Name string
|
||||
Hash plumbing.Hash
|
||||
}
|
||||
|
||||
// CreateTree creates a new tree object from the given entries
|
||||
func (gb *GitBackend) CreateTree(entries []TreeEntry) (string, error) {
|
||||
store := gb.repo.Storer
|
||||
|
||||
// Create a new tree object
|
||||
tree := &object.Tree{}
|
||||
treeEntries := make([]object.TreeEntry, len(entries))
|
||||
|
||||
for i, entry := range entries {
|
||||
treeEntries[i] = object.TreeEntry{
|
||||
Name: entry.Name,
|
||||
Mode: entry.Mode,
|
||||
Hash: entry.Hash,
|
||||
}
|
||||
}
|
||||
|
||||
tree.Entries = treeEntries
|
||||
|
||||
// Encode and store the tree
|
||||
obj := store.NewEncodedObject()
|
||||
if err := tree.Encode(obj); err != nil {
|
||||
return "", fmt.Errorf("failed to encode tree: %w", err)
|
||||
}
|
||||
|
||||
hash, err := store.SetEncodedObject(obj)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to store tree: %w", err)
|
||||
}
|
||||
|
||||
return hash.String(), nil
|
||||
}
|
||||
|
||||
// CreateCommit creates a new commit object
|
||||
func (gb *GitBackend) CreateCommit(treeHash, parentHash, message, author string) (string, error) {
|
||||
store := gb.repo.Storer
|
||||
|
||||
// Parse hashes
|
||||
tree := plumbing.NewHash(treeHash)
|
||||
|
||||
var parents []plumbing.Hash
|
||||
if parentHash != "" {
|
||||
parents = []plumbing.Hash{plumbing.NewHash(parentHash)}
|
||||
}
|
||||
|
||||
// Create commit object
|
||||
commit := &object.Commit{
|
||||
Author: object.Signature{
|
||||
Name: author,
|
||||
Email: "onyx@local",
|
||||
When: time.Now(),
|
||||
},
|
||||
Committer: object.Signature{
|
||||
Name: author,
|
||||
Email: "onyx@local",
|
||||
When: time.Now(),
|
||||
},
|
||||
Message: message,
|
||||
TreeHash: tree,
|
||||
}
|
||||
|
||||
if len(parents) > 0 {
|
||||
commit.ParentHashes = parents
|
||||
}
|
||||
|
||||
// Encode and store the commit
|
||||
obj := store.NewEncodedObject()
|
||||
if err := commit.Encode(obj); err != nil {
|
||||
return "", fmt.Errorf("failed to encode commit: %w", err)
|
||||
}
|
||||
|
||||
hash, err := store.SetEncodedObject(obj)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to store commit: %w", err)
|
||||
}
|
||||
|
||||
return hash.String(), nil
|
||||
}
|
||||
|
||||
// UpdateRef updates a Git reference to point to a new SHA
|
||||
func (gb *GitBackend) UpdateRef(refName, sha string) error {
|
||||
hash := plumbing.NewHash(sha)
|
||||
ref := plumbing.NewHashReference(plumbing.ReferenceName(refName), hash)
|
||||
|
||||
if err := gb.repo.Storer.SetReference(ref); err != nil {
|
||||
return fmt.Errorf("failed to update reference %s: %w", refName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRef retrieves the SHA that a reference points to
|
||||
func (gb *GitBackend) GetRef(refName string) (string, error) {
|
||||
ref, err := gb.repo.Reference(plumbing.ReferenceName(refName), true)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get reference %s: %w", refName, err)
|
||||
}
|
||||
|
||||
return ref.Hash().String(), nil
|
||||
}
|
||||
|
||||
// GetObject retrieves a Git object by its SHA
|
||||
func (gb *GitBackend) GetObject(sha string) (object.Object, error) {
|
||||
hash := plumbing.NewHash(sha)
|
||||
obj, err := gb.repo.Object(plumbing.AnyObject, hash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get object %s: %w", sha, err)
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// GetBlob retrieves a blob object by its SHA
|
||||
func (gb *GitBackend) GetBlob(sha string) (*object.Blob, error) {
|
||||
hash := plumbing.NewHash(sha)
|
||||
blob, err := gb.repo.BlobObject(hash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get blob %s: %w", sha, err)
|
||||
}
|
||||
|
||||
return blob, nil
|
||||
}
|
||||
|
||||
// GetTree retrieves a tree object by its SHA
|
||||
func (gb *GitBackend) GetTree(sha string) (*object.Tree, error) {
|
||||
hash := plumbing.NewHash(sha)
|
||||
tree, err := gb.repo.TreeObject(hash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tree %s: %w", sha, err)
|
||||
}
|
||||
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
// GetCommit retrieves a commit object by its SHA
|
||||
func (gb *GitBackend) GetCommit(sha string) (*object.Commit, error) {
|
||||
hash := plumbing.NewHash(sha)
|
||||
commit, err := gb.repo.CommitObject(hash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get commit %s: %w", sha, err)
|
||||
}
|
||||
|
||||
return commit, nil
|
||||
}
|
173
internal/models/oplog.go
Normal file
173
internal/models/oplog.go
Normal file
@ -0,0 +1,173 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OplogEntry represents a single entry in the action log
|
||||
type OplogEntry struct {
|
||||
// ID is a monotonically increasing entry ID
|
||||
ID uint64
|
||||
|
||||
// Timestamp when the operation was performed
|
||||
Timestamp time.Time
|
||||
|
||||
// Operation type (e.g., "save", "switch", "new", "sync")
|
||||
Operation string
|
||||
|
||||
// Description of the operation
|
||||
Description string
|
||||
|
||||
// StateBefore captures the state before the operation
|
||||
StateBefore *RepositoryState
|
||||
|
||||
// StateAfter captures the state after the operation
|
||||
StateAfter *RepositoryState
|
||||
|
||||
// Metadata contains operation-specific data
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
// RepositoryState captures the state of the repository at a point in time
|
||||
type RepositoryState struct {
|
||||
// Refs maps reference names to their SHA-1 hashes
|
||||
Refs map[string]string
|
||||
|
||||
// CurrentWorkstream is the active workstream name
|
||||
CurrentWorkstream string
|
||||
|
||||
// WorkingTreeHash is the hash of the current working tree snapshot
|
||||
WorkingTreeHash string
|
||||
|
||||
// IndexHash is the hash of the staging area
|
||||
IndexHash string
|
||||
}
|
||||
|
||||
// Serialize converts an OplogEntry to binary format
|
||||
func (e *OplogEntry) Serialize() ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
// Write entry ID (8 bytes)
|
||||
if err := binary.Write(buf, binary.LittleEndian, e.ID); err != nil {
|
||||
return nil, fmt.Errorf("failed to write ID: %w", err)
|
||||
}
|
||||
|
||||
// Write timestamp (8 bytes, Unix nano)
|
||||
timestamp := e.Timestamp.UnixNano()
|
||||
if err := binary.Write(buf, binary.LittleEndian, timestamp); err != nil {
|
||||
return nil, fmt.Errorf("failed to write timestamp: %w", err)
|
||||
}
|
||||
|
||||
// Serialize the rest as JSON for flexibility
|
||||
payload := struct {
|
||||
Operation string `json:"operation"`
|
||||
Description string `json:"description"`
|
||||
StateBefore *RepositoryState `json:"state_before"`
|
||||
StateAfter *RepositoryState `json:"state_after"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}{
|
||||
Operation: e.Operation,
|
||||
Description: e.Description,
|
||||
StateBefore: e.StateBefore,
|
||||
StateAfter: e.StateAfter,
|
||||
Metadata: e.Metadata,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal JSON: %w", err)
|
||||
}
|
||||
|
||||
// Write JSON length (4 bytes)
|
||||
jsonLen := uint32(len(jsonData))
|
||||
if err := binary.Write(buf, binary.LittleEndian, jsonLen); err != nil {
|
||||
return nil, fmt.Errorf("failed to write JSON length: %w", err)
|
||||
}
|
||||
|
||||
// Write JSON data
|
||||
if _, err := buf.Write(jsonData); err != nil {
|
||||
return nil, fmt.Errorf("failed to write JSON data: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Deserialize converts binary data back to an OplogEntry
|
||||
func DeserializeOplogEntry(data []byte) (*OplogEntry, error) {
|
||||
buf := bytes.NewReader(data)
|
||||
|
||||
entry := &OplogEntry{}
|
||||
|
||||
// Read entry ID (8 bytes)
|
||||
if err := binary.Read(buf, binary.LittleEndian, &entry.ID); err != nil {
|
||||
return nil, fmt.Errorf("failed to read ID: %w", err)
|
||||
}
|
||||
|
||||
// Read timestamp (8 bytes)
|
||||
var timestamp int64
|
||||
if err := binary.Read(buf, binary.LittleEndian, ×tamp); err != nil {
|
||||
return nil, fmt.Errorf("failed to read timestamp: %w", err)
|
||||
}
|
||||
entry.Timestamp = time.Unix(0, timestamp)
|
||||
|
||||
// Read JSON length (4 bytes)
|
||||
var jsonLen uint32
|
||||
if err := binary.Read(buf, binary.LittleEndian, &jsonLen); err != nil {
|
||||
return nil, fmt.Errorf("failed to read JSON length: %w", err)
|
||||
}
|
||||
|
||||
// Read JSON data
|
||||
jsonData := make([]byte, jsonLen)
|
||||
if _, err := io.ReadFull(buf, jsonData); err != nil {
|
||||
return nil, fmt.Errorf("failed to read JSON data: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal JSON
|
||||
payload := struct {
|
||||
Operation string `json:"operation"`
|
||||
Description string `json:"description"`
|
||||
StateBefore *RepositoryState `json:"state_before"`
|
||||
StateAfter *RepositoryState `json:"state_after"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
|
||||
}
|
||||
|
||||
entry.Operation = payload.Operation
|
||||
entry.Description = payload.Description
|
||||
entry.StateBefore = payload.StateBefore
|
||||
entry.StateAfter = payload.StateAfter
|
||||
entry.Metadata = payload.Metadata
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// NewOplogEntry creates a new oplog entry
|
||||
func NewOplogEntry(id uint64, operation, description string, before, after *RepositoryState) *OplogEntry {
|
||||
return &OplogEntry{
|
||||
ID: id,
|
||||
Timestamp: time.Now(),
|
||||
Operation: operation,
|
||||
Description: description,
|
||||
StateBefore: before,
|
||||
StateAfter: after,
|
||||
Metadata: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// NewRepositoryState creates a new repository state snapshot
|
||||
func NewRepositoryState(refs map[string]string, currentWorkstream, workingTreeHash, indexHash string) *RepositoryState {
|
||||
return &RepositoryState{
|
||||
Refs: refs,
|
||||
CurrentWorkstream: currentWorkstream,
|
||||
WorkingTreeHash: workingTreeHash,
|
||||
IndexHash: indexHash,
|
||||
}
|
||||
}
|
107
internal/models/workspace.go
Normal file
107
internal/models/workspace.go
Normal file
@ -0,0 +1,107 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WorkspaceState represents the current state of the workspace
|
||||
type WorkspaceState struct {
|
||||
// CurrentCommitSHA is the SHA of the current ephemeral commit
|
||||
CurrentCommitSHA string `json:"current_commit_sha"`
|
||||
|
||||
// WorkstreamName is the name of the active workstream
|
||||
WorkstreamName string `json:"workstream_name"`
|
||||
|
||||
// LastSnapshot is when the last automatic snapshot was created
|
||||
LastSnapshot time.Time `json:"last_snapshot"`
|
||||
|
||||
// IsDirty indicates if there are uncommitted changes
|
||||
IsDirty bool `json:"is_dirty"`
|
||||
|
||||
// TreeHash is the hash of the current working tree
|
||||
TreeHash string `json:"tree_hash,omitempty"`
|
||||
|
||||
// IndexHash is the hash of the staging area
|
||||
IndexHash string `json:"index_hash,omitempty"`
|
||||
|
||||
// Metadata contains additional workspace-specific data
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// NewWorkspaceState creates a new workspace state
|
||||
func NewWorkspaceState(commitSHA, workstreamName string) *WorkspaceState {
|
||||
return &WorkspaceState{
|
||||
CurrentCommitSHA: commitSHA,
|
||||
WorkstreamName: workstreamName,
|
||||
LastSnapshot: time.Now(),
|
||||
IsDirty: false,
|
||||
Metadata: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateSnapshot updates the workspace state with a new snapshot
|
||||
func (ws *WorkspaceState) UpdateSnapshot(commitSHA, treeHash, indexHash string, isDirty bool) {
|
||||
ws.CurrentCommitSHA = commitSHA
|
||||
ws.TreeHash = treeHash
|
||||
ws.IndexHash = indexHash
|
||||
ws.IsDirty = isDirty
|
||||
ws.LastSnapshot = time.Now()
|
||||
}
|
||||
|
||||
// SetWorkstream changes the active workstream
|
||||
func (ws *WorkspaceState) SetWorkstream(workstreamName string) {
|
||||
ws.WorkstreamName = workstreamName
|
||||
}
|
||||
|
||||
// MarkDirty marks the workspace as having uncommitted changes
|
||||
func (ws *WorkspaceState) MarkDirty() {
|
||||
ws.IsDirty = true
|
||||
}
|
||||
|
||||
// MarkClean marks the workspace as clean (no uncommitted changes)
|
||||
func (ws *WorkspaceState) MarkClean() {
|
||||
ws.IsDirty = false
|
||||
}
|
||||
|
||||
// Serialize converts the workspace state to JSON
|
||||
func (ws *WorkspaceState) Serialize() ([]byte, error) {
|
||||
data, err := json.MarshalIndent(ws, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal workspace state: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// DeserializeWorkspaceState converts JSON data to a workspace state
|
||||
func DeserializeWorkspaceState(data []byte) (*WorkspaceState, error) {
|
||||
ws := &WorkspaceState{}
|
||||
if err := json.Unmarshal(data, ws); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal workspace state: %w", err)
|
||||
}
|
||||
return ws, nil
|
||||
}
|
||||
|
||||
// GetTimeSinceLastSnapshot returns the duration since the last snapshot
|
||||
func (ws *WorkspaceState) GetTimeSinceLastSnapshot() time.Duration {
|
||||
return time.Since(ws.LastSnapshot)
|
||||
}
|
||||
|
||||
// Clone creates a deep copy of the workspace state
|
||||
func (ws *WorkspaceState) Clone() *WorkspaceState {
|
||||
metadata := make(map[string]string, len(ws.Metadata))
|
||||
for k, v := range ws.Metadata {
|
||||
metadata[k] = v
|
||||
}
|
||||
|
||||
return &WorkspaceState{
|
||||
CurrentCommitSHA: ws.CurrentCommitSHA,
|
||||
WorkstreamName: ws.WorkstreamName,
|
||||
LastSnapshot: ws.LastSnapshot,
|
||||
IsDirty: ws.IsDirty,
|
||||
TreeHash: ws.TreeHash,
|
||||
IndexHash: ws.IndexHash,
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
214
internal/models/workstream.go
Normal file
214
internal/models/workstream.go
Normal file
@ -0,0 +1,214 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Workstream represents a stacked-diff workflow
|
||||
type Workstream struct {
|
||||
// Name is the unique identifier for the workstream
|
||||
Name string `json:"name"`
|
||||
|
||||
// Description provides context about the workstream
|
||||
Description string `json:"description"`
|
||||
|
||||
// BaseBranch is the Git branch this workstream is based on
|
||||
BaseBranch string `json:"base_branch"`
|
||||
|
||||
// Commits is an ordered list of commits in this workstream
|
||||
Commits []WorkstreamCommit `json:"commits"`
|
||||
|
||||
// Created is when the workstream was created
|
||||
Created time.Time `json:"created"`
|
||||
|
||||
// Updated is when the workstream was last modified
|
||||
Updated time.Time `json:"updated"`
|
||||
|
||||
// Status indicates the current state (active, merged, abandoned)
|
||||
Status WorkstreamStatus `json:"status"`
|
||||
|
||||
// Metadata contains additional workstream-specific data
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// WorkstreamCommit represents a single commit in a workstream
|
||||
type WorkstreamCommit struct {
|
||||
// SHA is the Git commit hash
|
||||
SHA string `json:"sha"`
|
||||
|
||||
// Message is the commit message
|
||||
Message string `json:"message"`
|
||||
|
||||
// Author is the commit author
|
||||
Author string `json:"author"`
|
||||
|
||||
// Timestamp is when the commit was created
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
|
||||
// ParentSHA is the parent commit in the workstream (empty for first commit)
|
||||
ParentSHA string `json:"parent_sha,omitempty"`
|
||||
|
||||
// BaseSHA is the base commit from the base branch
|
||||
BaseSHA string `json:"base_sha"`
|
||||
|
||||
// BranchRef is the Git reference for this commit (e.g., refs/onyx/workstreams/name/commit-1)
|
||||
BranchRef string `json:"branch_ref"`
|
||||
}
|
||||
|
||||
// WorkstreamStatus represents the state of a workstream
|
||||
type WorkstreamStatus string
|
||||
|
||||
const (
|
||||
// WorkstreamStatusActive indicates the workstream is being actively developed
|
||||
WorkstreamStatusActive WorkstreamStatus = "active"
|
||||
|
||||
// WorkstreamStatusMerged indicates the workstream has been merged
|
||||
WorkstreamStatusMerged WorkstreamStatus = "merged"
|
||||
|
||||
// WorkstreamStatusAbandoned indicates the workstream has been abandoned
|
||||
WorkstreamStatusAbandoned WorkstreamStatus = "abandoned"
|
||||
|
||||
// WorkstreamStatusArchived indicates the workstream has been archived
|
||||
WorkstreamStatusArchived WorkstreamStatus = "archived"
|
||||
)
|
||||
|
||||
// WorkstreamCollection represents the collection of all workstreams
|
||||
type WorkstreamCollection struct {
|
||||
// Workstreams is a map of workstream name to Workstream
|
||||
Workstreams map[string]*Workstream `json:"workstreams"`
|
||||
|
||||
// CurrentWorkstream is the name of the active workstream
|
||||
CurrentWorkstream string `json:"current_workstream,omitempty"`
|
||||
}
|
||||
|
||||
// NewWorkstream creates a new workstream
|
||||
func NewWorkstream(name, description, baseBranch string) *Workstream {
|
||||
now := time.Now()
|
||||
return &Workstream{
|
||||
Name: name,
|
||||
Description: description,
|
||||
BaseBranch: baseBranch,
|
||||
Commits: []WorkstreamCommit{},
|
||||
Created: now,
|
||||
Updated: now,
|
||||
Status: WorkstreamStatusActive,
|
||||
Metadata: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// AddCommit adds a commit to the workstream
|
||||
func (w *Workstream) AddCommit(commit WorkstreamCommit) {
|
||||
w.Commits = append(w.Commits, commit)
|
||||
w.Updated = time.Now()
|
||||
}
|
||||
|
||||
// GetLatestCommit returns the latest commit in the workstream
|
||||
func (w *Workstream) GetLatestCommit() (*WorkstreamCommit, error) {
|
||||
if len(w.Commits) == 0 {
|
||||
return nil, fmt.Errorf("workstream has no commits")
|
||||
}
|
||||
return &w.Commits[len(w.Commits)-1], nil
|
||||
}
|
||||
|
||||
// GetCommitCount returns the number of commits in the workstream
|
||||
func (w *Workstream) GetCommitCount() int {
|
||||
return len(w.Commits)
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the workstream has no commits
|
||||
func (w *Workstream) IsEmpty() bool {
|
||||
return len(w.Commits) == 0
|
||||
}
|
||||
|
||||
// NewWorkstreamCommit creates a new workstream commit
|
||||
func NewWorkstreamCommit(sha, message, author, parentSHA, baseSHA, branchRef string) WorkstreamCommit {
|
||||
return WorkstreamCommit{
|
||||
SHA: sha,
|
||||
Message: message,
|
||||
Author: author,
|
||||
Timestamp: time.Now(),
|
||||
ParentSHA: parentSHA,
|
||||
BaseSHA: baseSHA,
|
||||
BranchRef: branchRef,
|
||||
}
|
||||
}
|
||||
|
||||
// NewWorkstreamCollection creates a new workstream collection
|
||||
func NewWorkstreamCollection() *WorkstreamCollection {
|
||||
return &WorkstreamCollection{
|
||||
Workstreams: make(map[string]*Workstream),
|
||||
}
|
||||
}
|
||||
|
||||
// AddWorkstream adds a workstream to the collection
|
||||
func (wc *WorkstreamCollection) AddWorkstream(workstream *Workstream) error {
|
||||
if _, exists := wc.Workstreams[workstream.Name]; exists {
|
||||
return fmt.Errorf("workstream '%s' already exists", workstream.Name)
|
||||
}
|
||||
wc.Workstreams[workstream.Name] = workstream
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetWorkstream retrieves a workstream by name
|
||||
func (wc *WorkstreamCollection) GetWorkstream(name string) (*Workstream, error) {
|
||||
workstream, exists := wc.Workstreams[name]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("workstream '%s' not found", name)
|
||||
}
|
||||
return workstream, nil
|
||||
}
|
||||
|
||||
// RemoveWorkstream removes a workstream from the collection
|
||||
func (wc *WorkstreamCollection) RemoveWorkstream(name string) error {
|
||||
if _, exists := wc.Workstreams[name]; !exists {
|
||||
return fmt.Errorf("workstream '%s' not found", name)
|
||||
}
|
||||
delete(wc.Workstreams, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListWorkstreams returns all workstreams
|
||||
func (wc *WorkstreamCollection) ListWorkstreams() []*Workstream {
|
||||
workstreams := make([]*Workstream, 0, len(wc.Workstreams))
|
||||
for _, ws := range wc.Workstreams {
|
||||
workstreams = append(workstreams, ws)
|
||||
}
|
||||
return workstreams
|
||||
}
|
||||
|
||||
// SetCurrentWorkstream sets the active workstream
|
||||
func (wc *WorkstreamCollection) SetCurrentWorkstream(name string) error {
|
||||
if _, exists := wc.Workstreams[name]; !exists {
|
||||
return fmt.Errorf("workstream '%s' not found", name)
|
||||
}
|
||||
wc.CurrentWorkstream = name
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCurrentWorkstream returns the current workstream
|
||||
func (wc *WorkstreamCollection) GetCurrentWorkstream() (*Workstream, error) {
|
||||
if wc.CurrentWorkstream == "" {
|
||||
return nil, fmt.Errorf("no current workstream set")
|
||||
}
|
||||
return wc.GetWorkstream(wc.CurrentWorkstream)
|
||||
}
|
||||
|
||||
// Serialize converts the workstream collection to JSON
|
||||
func (wc *WorkstreamCollection) Serialize() ([]byte, error) {
|
||||
data, err := json.MarshalIndent(wc, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal workstream collection: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// DeserializeWorkstreamCollection converts JSON data to a workstream collection
|
||||
func DeserializeWorkstreamCollection(data []byte) (*WorkstreamCollection, error) {
|
||||
wc := &WorkstreamCollection{}
|
||||
if err := json.Unmarshal(data, wc); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal workstream collection: %w", err)
|
||||
}
|
||||
return wc, nil
|
||||
}
|
Reference in New Issue
Block a user