Implement Phase 0

This commit is contained in:
2025-10-09 18:50:51 -04:00
parent 7ef57642cc
commit f444113057
10 changed files with 1104 additions and 0 deletions

32
.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
# Editor/IDE
# .idea/
# .vscode/

16
Makefile Normal file
View File

@ -0,0 +1,16 @@
# Makefile for Onyx
.PHONY: build test clean install
build:
go build -o bin/onx ./cmd/onx
go build -o bin/onxd ./cmd/onxd
test:
go test -v ./...
install:
go install ./cmd/onx
go install ./cmd/onxd
clean:
rm -rf bin/

27
go.mod
View File

@ -1,3 +1,30 @@
module git.dws.rip/DWS/onyx module git.dws.rip/DWS/onyx
go 1.24.2 go 1.24.2
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.16.3 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/spf13/cobra v1.10.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

78
go.sum Normal file
View File

@ -0,0 +1,78 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8=
github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View 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
View 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
View 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
View 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, &timestamp); 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,
}
}

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

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