From f7674cc2b0bee1a92fdb787cdcfc926f52fa81a1 Mon Sep 17 00:00:00 2001 From: Tanishq Dubey Date: Thu, 9 Oct 2025 19:08:59 -0400 Subject: [PATCH 1/4] setup notes for milestone 1 --- notes/checklist.md | 141 +++++++++++++++++++++------------------------ notes/future.md | 76 ------------------------ 2 files changed, 66 insertions(+), 151 deletions(-) diff --git a/notes/checklist.md b/notes/checklist.md index 45b0581..e43ba2c 100644 --- a/notes/checklist.md +++ b/notes/checklist.md @@ -1,87 +1,78 @@ # Complete Implementation Plan for Onyx Phase 1 +## Milestone 1: Action Log and onx init -## Milestone 0: Foundation and Core Abstractions +### Action Log Implementation -### Project Structure Setup +13. **Create oplog binary format** (`internal/storage/oplog.go`) + ```go + type OplogEntry struct { + ID uint64 + ParentID uint64 + Timestamp int64 + Command string + StateBefore map[string]string + StateAfter map[string]string + } + ``` -6. **Create directory structure** - ``` - onyx/ - ├── cmd/ - │ ├── onx/ # CLI entry point - │ └── onxd/ # Daemon entry point - ├── internal/ - │ ├── core/ # Core abstractions - │ ├── git/ # Git interaction layer - │ ├── models/ # Data models - │ ├── storage/ # .onx directory management - │ ├── commands/ # CLI command implementations - │ ├── daemon/ # Daemon implementation - │ └── utils/ # Utilities - ├── pkg/ # Public APIs (if needed) - ├── test/ # Integration tests - ├── docs/ # Documentation - └── scripts/ # Build/deployment scripts - ``` +14. **Implement oplog writer** (`internal/storage/oplog_writer.go`) + - `OpenOplog(path string) (*OplogWriter, error)` + - `AppendEntry(entry *OplogEntry) error` + - Use binary encoding (gob or protobuf) + - Implement file locking for concurrent access -7. **Set up dependency management** - ```bash - go get github.com/go-git/go-git/v5@latest - go get github.com/spf13/cobra@latest - go get github.com/fsnotify/fsnotify@latest - ``` +15. **Implement oplog reader** (`internal/storage/oplog_reader.go`) + - `ReadLastEntry() (*OplogEntry, error)` + - `ReadEntry(id uint64) (*OplogEntry, error)` + - `GetUndoStack() ([]*OplogEntry, error)` -8. **Create Makefile** - ```makefile - # Makefile - .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/ - ``` +16. **Create transactional wrapper** (`internal/core/transaction.go`) + ```go + func ExecuteWithTransaction(repo *Repository, cmd string, + fn func() error) error { + // 1. Capture state_before + // 2. Create oplog entry + // 3. Execute fn() + // 4. Capture state_after + // 5. Finalize oplog entry + // 6. Handle rollback on error + } + ``` -### Core Abstractions Implementation +### onx init Command -9. **Define core interfaces** (`internal/core/interfaces.go`) - ```go - type Repository interface { - Init(path string) error - GetGitRepo() *git.Repository - GetOnyxMetadata() *OnyxMetadata - } - - type GitBackend interface { - CreateCommit(tree, parent, message string) (string, error) - CreateTree(entries []TreeEntry) (string, error) - UpdateRef(name, sha string) error - GetRef(name string) (string, error) - } - ``` +17. **Implement init command** (`internal/commands/init.go`) + - Create .git directory (via go-git) + - Create .onx directory structure + - Initialize empty oplog file + - Create default workstreams.json + - Create workspace pointer file + - Add .onx to .gitignore -10. **Implement Repository struct** (`internal/core/repository.go`) - - Fields: gitRepo (*git.Repository), onyxPath (string), gitPath (string) - - Methods: Open(), Close(), IsOnyxRepo() - - Error handling for missing .git or .onx directories +18. **Create CLI structure** (`cmd/onx/main.go`) + ```go + func main() { + rootCmd := &cobra.Command{ + Use: "onx", + Short: "The iPhone of Version Control", + } + rootCmd.AddCommand(commands.InitCmd()) + rootCmd.Execute() + } + ``` -11. **Implement Git object operations** (`internal/git/objects.go`) - - `CreateBlob(content []byte) (string, error)` - - `CreateTree(entries []TreeEntry) (string, error)` - - `CreateCommit(tree, parent, message, author string) (string, error)` - - `GetObject(sha string) (Object, error)` +### onx undo Command + +19. **Implement undo logic** (`internal/commands/undo.go`) + - Read last oplog entry + - Restore all refs from state_before + - Update workspace pointer + - Mark entry as undone in oplog + - Perform git checkout to restore working directory + +20. **Add undo tests** (`internal/commands/undo_test.go`) + - Test undo after init + - Test sequential undos + - Test undo with nothing to undo -12. **Create data models** (`internal/models/`) - - `oplog.go`: OplogEntry struct with serialization - - `workstream.go`: Workstream, WorkstreamCommit structs - - `workspace.go`: WorkspaceState struct diff --git a/notes/future.md b/notes/future.md index e1877ef..a95fe90 100644 --- a/notes/future.md +++ b/notes/future.md @@ -1,79 +1,3 @@ -## Milestone 1: Action Log and onx init - -### Action Log Implementation - -13. **Create oplog binary format** (`internal/storage/oplog.go`) - ```go - type OplogEntry struct { - ID uint64 - ParentID uint64 - Timestamp int64 - Command string - StateBefore map[string]string - StateAfter map[string]string - } - ``` - -14. **Implement oplog writer** (`internal/storage/oplog_writer.go`) - - `OpenOplog(path string) (*OplogWriter, error)` - - `AppendEntry(entry *OplogEntry) error` - - Use binary encoding (gob or protobuf) - - Implement file locking for concurrent access - -15. **Implement oplog reader** (`internal/storage/oplog_reader.go`) - - `ReadLastEntry() (*OplogEntry, error)` - - `ReadEntry(id uint64) (*OplogEntry, error)` - - `GetUndoStack() ([]*OplogEntry, error)` - -16. **Create transactional wrapper** (`internal/core/transaction.go`) - ```go - func ExecuteWithTransaction(repo *Repository, cmd string, - fn func() error) error { - // 1. Capture state_before - // 2. Create oplog entry - // 3. Execute fn() - // 4. Capture state_after - // 5. Finalize oplog entry - // 6. Handle rollback on error - } - ``` - -### onx init Command - -17. **Implement init command** (`internal/commands/init.go`) - - Create .git directory (via go-git) - - Create .onx directory structure - - Initialize empty oplog file - - Create default workstreams.json - - Create workspace pointer file - - Add .onx to .gitignore - -18. **Create CLI structure** (`cmd/onx/main.go`) - ```go - func main() { - rootCmd := &cobra.Command{ - Use: "onx", - Short: "The iPhone of Version Control", - } - rootCmd.AddCommand(commands.InitCmd()) - rootCmd.Execute() - } - ``` - -### onx undo Command - -19. **Implement undo logic** (`internal/commands/undo.go`) - - Read last oplog entry - - Restore all refs from state_before - - Update workspace pointer - - Mark entry as undone in oplog - - Perform git checkout to restore working directory - -20. **Add undo tests** (`internal/commands/undo_test.go`) - - Test undo after init - - Test sequential undos - - Test undo with nothing to undo - ## Milestone 2: Transparent Versioning and onx save ### Filesystem Daemon Implementation From 5e6ae2e4298254f61ea5157d9057bb1d6e612aa5 Mon Sep 17 00:00:00 2001 From: Tanishq Dubey Date: Thu, 9 Oct 2025 19:19:31 -0400 Subject: [PATCH 2/4] Implement Milestone 1 --- .gitignore | 2 + cmd/onx/main.go | 33 ++++ go.mod | 8 +- go.sum | 36 +++- internal/commands/init.go | 179 ++++++++++++++++++ internal/commands/init_test.go | 196 ++++++++++++++++++++ internal/commands/undo.go | 204 +++++++++++++++++++++ internal/commands/undo_test.go | 300 +++++++++++++++++++++++++++++++ internal/core/transaction.go | 167 +++++++++++++++++ internal/storage/oplog_reader.go | 201 +++++++++++++++++++++ internal/storage/oplog_writer.go | 163 +++++++++++++++++ internal/storage/state.go | 187 +++++++++++++++++++ 12 files changed, 1671 insertions(+), 5 deletions(-) create mode 100644 cmd/onx/main.go create mode 100644 internal/commands/init.go create mode 100644 internal/commands/init_test.go create mode 100644 internal/commands/undo.go create mode 100644 internal/commands/undo_test.go create mode 100644 internal/core/transaction.go create mode 100644 internal/storage/oplog_reader.go create mode 100644 internal/storage/oplog_writer.go create mode 100644 internal/storage/state.go diff --git a/.gitignore b/.gitignore index aaadf73..e26a85f 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ go.work.sum # Editor/IDE # .idea/ # .vscode/ + +bin/ diff --git a/cmd/onx/main.go b/cmd/onx/main.go new file mode 100644 index 0000000..3e06883 --- /dev/null +++ b/cmd/onx/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "os" + + "git.dws.rip/DWS/onyx/internal/commands" + "github.com/spf13/cobra" +) + +var version = "0.1.0" + +func main() { + rootCmd := &cobra.Command{ + Use: "onx", + Short: "Onyx - The iPhone of Version Control", + Long: `Onyx is a next-generation version control system that provides +a superior user experience layer on top of Git. It offers transparent +versioning, workstreams for stacked-diff management, and an action +log for universal undo functionality.`, + Version: version, + } + + // Add commands + rootCmd.AddCommand(commands.NewInitCmd()) + rootCmd.AddCommand(commands.NewUndoCmd()) + + // Execute the root command + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod index 0cf9b9d..9858266 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,11 @@ module git.dws.rip/DWS/onyx go 1.24.2 +require ( + github.com/go-git/go-git/v5 v5.16.3 + github.com/spf13/cobra v1.10.1 +) + require ( dario.cat/mergo v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect @@ -9,10 +14,8 @@ require ( 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 @@ -20,7 +23,6 @@ require ( 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 diff --git a/go.sum b/go.sum index d4b635b..79c5b36 100644 --- a/go.sum +++ b/go.sum @@ -5,25 +5,36 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo 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/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 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/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 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-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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= @@ -31,12 +42,22 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i 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/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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= @@ -50,11 +71,15 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An 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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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= @@ -67,12 +92,19 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc 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/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/commands/init.go b/internal/commands/init.go new file mode 100644 index 0000000..6b76036 --- /dev/null +++ b/internal/commands/init.go @@ -0,0 +1,179 @@ +package commands + +import ( + "fmt" + "os" + "path/filepath" + + "git.dws.rip/DWS/onyx/internal/core" + "git.dws.rip/DWS/onyx/internal/storage" + "github.com/spf13/cobra" +) + +// NewInitCmd creates the init command +func NewInitCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "init [path]", + Short: "Initialize a new Onyx repository", + Long: `Initialize a new Onyx repository in the specified directory. +If no path is provided, initializes in the current directory. + +This command will: + - Create a Git repository (if one doesn't exist) + - Create the .onx directory structure + - Initialize the oplog file + - Create default workstreams.json + - Add .onx to .gitignore`, + Args: cobra.MaximumNArgs(1), + RunE: runInit, + } + + return cmd +} + +func runInit(cmd *cobra.Command, args []string) error { + // Determine the path + path := "." + if len(args) > 0 { + path = args[0] + } + + // Resolve to absolute path + absPath, err := filepath.Abs(path) + if err != nil { + return fmt.Errorf("failed to resolve path: %w", err) + } + + // Check if already an Onyx repository + if core.IsOnyxRepo(absPath) { + return fmt.Errorf("already an onyx repository: %s", absPath) + } + + // Create and initialize repository + repo := &core.OnyxRepository{} + err = repo.Init(absPath) + if err != nil { + return fmt.Errorf("failed to initialize repository: %w", err) + } + + // Add .onx to .gitignore + gitignorePath := filepath.Join(absPath, ".gitignore") + err = addToGitignore(gitignorePath, ".onx/") + if err != nil { + // Don't fail if we can't update .gitignore, just warn + fmt.Fprintf(os.Stderr, "Warning: failed to update .gitignore: %v\n", err) + } + + // Log the init operation to oplog + txn, err := core.NewTransaction(repo) + if err != nil { + // Don't fail if we can't create transaction, repo is already initialized + fmt.Fprintf(os.Stderr, "Warning: failed to log init to oplog: %v\n", err) + } else { + defer txn.Close() + + // Execute a no-op function just to log the init + err = txn.ExecuteWithTransaction("init", "Initialized Onyx repository", func() error { + return nil + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to log init: %v\n", err) + } + } + + fmt.Printf("Initialized empty Onyx repository in %s\n", filepath.Join(absPath, ".onx")) + return nil +} + +// addToGitignore adds an entry to .gitignore if it doesn't already exist +func addToGitignore(gitignorePath, entry string) error { + // Read existing .gitignore if it exists + var content []byte + if _, err := os.Stat(gitignorePath); err == nil { + content, err = os.ReadFile(gitignorePath) + if err != nil { + return fmt.Errorf("failed to read .gitignore: %w", err) + } + } + + // Check if entry already exists + contentStr := string(content) + if len(contentStr) > 0 && contentStr[len(contentStr)-1] != '\n' { + contentStr += "\n" + } + + // Add entry if it doesn't exist + needle := entry + if len(needle) > 0 && needle[len(needle)-1] != '\n' { + needle += "\n" + } + + // Simple check - not perfect but good enough + if !containsLine(contentStr, entry) { + contentStr += needle + } + + // Write back to .gitignore + err := os.WriteFile(gitignorePath, []byte(contentStr), 0644) + if err != nil { + return fmt.Errorf("failed to write .gitignore: %w", err) + } + + return nil +} + +// containsLine checks if a multi-line string contains a specific line +func containsLine(content, line string) bool { + // Simple implementation - just check if the line exists as a substring + // In the future, we might want to do line-by-line checking + target := line + if len(target) > 0 && target[len(target)-1] == '\n' { + target = target[:len(target)-1] + } + + lines := splitLines(content) + for _, l := range lines { + if l == target { + return true + } + } + return false +} + +// splitLines splits a string into lines +func splitLines(s string) []string { + if s == "" { + return []string{} + } + + var lines []string + start := 0 + for i := 0; i < len(s); i++ { + if s[i] == '\n' { + lines = append(lines, s[start:i]) + start = i + 1 + } + } + + // Add the last line if it doesn't end with newline + if start < len(s) { + lines = append(lines, s[start:]) + } + + return lines +} + +// GetOplogWriter creates an oplog writer for the repository at the given path +func GetOplogWriter(path string) (*storage.OplogWriter, error) { + absPath, err := filepath.Abs(path) + if err != nil { + return nil, fmt.Errorf("failed to resolve path: %w", err) + } + + if !core.IsOnyxRepo(absPath) { + return nil, fmt.Errorf("not an onyx repository: %s", absPath) + } + + oplogPath := filepath.Join(absPath, ".onx", "oplog") + return storage.OpenOplog(oplogPath) +} diff --git a/internal/commands/init_test.go b/internal/commands/init_test.go new file mode 100644 index 0000000..ee294fe --- /dev/null +++ b/internal/commands/init_test.go @@ -0,0 +1,196 @@ +package commands + +import ( + "os" + "path/filepath" + "testing" + + "git.dws.rip/DWS/onyx/internal/core" +) + +func TestInitCommand(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Create a repository instance + repo := &core.OnyxRepository{} + + // Initialize the repository + err := repo.Init(tempDir) + if err != nil { + t.Fatalf("Failed to initialize repository: %v", err) + } + + // Verify .git directory exists + gitPath := filepath.Join(tempDir, ".git") + if _, err := os.Stat(gitPath); os.IsNotExist(err) { + t.Errorf(".git directory was not created") + } + + // Verify .onx directory exists + onyxPath := filepath.Join(tempDir, ".onx") + if _, err := os.Stat(onyxPath); os.IsNotExist(err) { + t.Errorf(".onx directory was not created") + } + + // Verify oplog file exists + oplogPath := filepath.Join(onyxPath, "oplog") + if _, err := os.Stat(oplogPath); os.IsNotExist(err) { + t.Errorf("oplog file was not created") + } + + // Verify workstreams.json exists + workstreamsPath := filepath.Join(onyxPath, "workstreams.json") + if _, err := os.Stat(workstreamsPath); os.IsNotExist(err) { + t.Errorf("workstreams.json was not created") + } + + // Verify rerere_cache directory exists + rererePath := filepath.Join(onyxPath, "rerere_cache") + if _, err := os.Stat(rererePath); os.IsNotExist(err) { + t.Errorf("rerere_cache directory was not created") + } +} + +func TestInitCommandInExistingRepo(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Initialize once + repo := &core.OnyxRepository{} + err := repo.Init(tempDir) + if err != nil { + t.Fatalf("Failed to initialize repository: %v", err) + } + + // Verify it's an Onyx repo + if !core.IsOnyxRepo(tempDir) { + t.Errorf("IsOnyxRepo returned false for initialized repository") + } +} + +func TestIsOnyxRepo(t *testing.T) { + tests := []struct { + name string + setup func(string) error + expected bool + }{ + { + name: "empty directory", + setup: func(path string) error { + return nil + }, + expected: false, + }, + { + name: "initialized repository", + setup: func(path string) error { + repo := &core.OnyxRepository{} + return repo.Init(path) + }, + expected: true, + }, + { + name: "directory with only .git", + setup: func(path string) error { + return os.MkdirAll(filepath.Join(path, ".git"), 0755) + }, + expected: false, + }, + { + name: "directory with only .onx", + setup: func(path string) error { + return os.MkdirAll(filepath.Join(path, ".onx"), 0755) + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + + err := tt.setup(tempDir) + if err != nil { + t.Fatalf("Setup failed: %v", err) + } + + result := core.IsOnyxRepo(tempDir) + if result != tt.expected { + t.Errorf("IsOnyxRepo() = %v, expected %v", result, tt.expected) + } + }) + } +} + +func TestAddToGitignore(t *testing.T) { + tests := []struct { + name string + existingContent string + entryToAdd string + shouldContain string + }{ + { + name: "add to empty gitignore", + existingContent: "", + entryToAdd: ".onx/", + shouldContain: ".onx/", + }, + { + name: "add to existing gitignore", + existingContent: "node_modules/\n*.log\n", + entryToAdd: ".onx/", + shouldContain: ".onx/", + }, + { + name: "don't duplicate existing entry", + existingContent: ".onx/\nnode_modules/\n", + entryToAdd: ".onx/", + shouldContain: ".onx/", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + gitignorePath := filepath.Join(tempDir, ".gitignore") + + // Create existing content if specified + if tt.existingContent != "" { + err := os.WriteFile(gitignorePath, []byte(tt.existingContent), 0644) + if err != nil { + t.Fatalf("Failed to create test .gitignore: %v", err) + } + } + + // Add the entry + err := addToGitignore(gitignorePath, tt.entryToAdd) + if err != nil { + t.Fatalf("addToGitignore failed: %v", err) + } + + // Read the result + content, err := os.ReadFile(gitignorePath) + if err != nil { + t.Fatalf("Failed to read .gitignore: %v", err) + } + + // Verify the entry is present + if !containsLine(string(content), tt.shouldContain) { + t.Errorf(".gitignore does not contain expected entry %q\nContent:\n%s", tt.shouldContain, string(content)) + } + + // Count occurrences (should not be duplicated) + lines := splitLines(string(content)) + count := 0 + for _, line := range lines { + if line == tt.shouldContain { + count++ + } + } + if count > 1 { + t.Errorf("Entry %q appears %d times, expected 1", tt.shouldContain, count) + } + }) + } +} diff --git a/internal/commands/undo.go b/internal/commands/undo.go new file mode 100644 index 0000000..077e966 --- /dev/null +++ b/internal/commands/undo.go @@ -0,0 +1,204 @@ +package commands + +import ( + "fmt" + "os" + "path/filepath" + + "git.dws.rip/DWS/onyx/internal/core" + "git.dws.rip/DWS/onyx/internal/storage" + "github.com/spf13/cobra" +) + +// NewUndoCmd creates the undo command +func NewUndoCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "undo", + Short: "Undo the last operation", + Long: `Undo the last operation by restoring the repository to its previous state. + +This command reads the last entry from the oplog and restores all refs +and working directory state to what they were before the operation.`, + Args: cobra.NoArgs, + RunE: runUndo, + } + + return cmd +} + +func runUndo(cmd *cobra.Command, args []string) error { + // Get current directory + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %w", err) + } + + // Check if we're in an Onyx repository + if !core.IsOnyxRepo(cwd) { + return fmt.Errorf("not an onyx repository (or any parent up to mount point)") + } + + // Open the repository + repo, err := core.Open(cwd) + if err != nil { + return fmt.Errorf("failed to open repository: %w", err) + } + defer repo.Close() + + // Open oplog reader + oplogPath := filepath.Join(repo.GetOnyxPath(), "oplog") + reader := storage.NewOplogReader(oplogPath) + + // Check if oplog is empty + isEmpty, err := reader.IsEmpty() + if err != nil { + return fmt.Errorf("failed to check oplog: %w", err) + } + if isEmpty { + return fmt.Errorf("nothing to undo") + } + + // Read the last entry + lastEntry, err := reader.ReadLastEntry() + if err != nil { + return fmt.Errorf("failed to read last entry: %w", err) + } + + // Check if we have state_before to restore + if lastEntry.StateBefore == nil { + return fmt.Errorf("cannot undo: last operation has no state_before") + } + + // Show what we're undoing + fmt.Printf("Undoing: %s - %s\n", lastEntry.Operation, lastEntry.Description) + + // Create state capture to restore the state + stateCapture := storage.NewStateCapture(repo.GetGitRepo()) + + // Restore the state + err = stateCapture.RestoreState(lastEntry.StateBefore) + if err != nil { + return fmt.Errorf("failed to restore state: %w", err) + } + + // Log the undo operation + txn, err := core.NewTransaction(repo) + if err != nil { + // Don't fail if we can't create transaction, state is already restored + fmt.Fprintf(os.Stderr, "Warning: failed to log undo to oplog: %v\n", err) + } else { + defer txn.Close() + + metadata := map[string]string{ + "undone_entry_id": fmt.Sprintf("%d", lastEntry.ID), + "undone_operation": lastEntry.Operation, + } + + err = txn.ExecuteWithTransactionAndMetadata( + "undo", + fmt.Sprintf("Undid operation: %s", lastEntry.Operation), + metadata, + func() error { + // The actual undo has already been performed above + // This function is just to capture the state after undo + return nil + }, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to log undo: %v\n", err) + } + } + + // Show what changed + stateAfter, _ := stateCapture.CaptureState() + if stateAfter != nil { + differences := stateCapture.CompareStates(stateAfter, lastEntry.StateBefore) + if len(differences) > 0 { + fmt.Println("\nChanges:") + for ref, change := range differences { + fmt.Printf(" %s: %s\n", ref, change) + } + } + } + + fmt.Println("\nUndo complete!") + return nil +} + +// UndoToEntry undoes to a specific entry ID +func UndoToEntry(repo *core.OnyxRepository, entryID uint64) error { + oplogPath := filepath.Join(repo.GetOnyxPath(), "oplog") + reader := storage.NewOplogReader(oplogPath) + + // Read the target entry + entry, err := reader.ReadEntry(entryID) + if err != nil { + return fmt.Errorf("failed to read entry %d: %w", entryID, err) + } + + if entry.StateBefore == nil { + return fmt.Errorf("entry %d has no state_before to restore", entryID) + } + + // Restore the state + stateCapture := storage.NewStateCapture(repo.GetGitRepo()) + err = stateCapture.RestoreState(entry.StateBefore) + if err != nil { + return fmt.Errorf("failed to restore state: %w", err) + } + + // Log the undo operation + txn, err := core.NewTransaction(repo) + if err != nil { + return fmt.Errorf("failed to create transaction: %w", err) + } + defer txn.Close() + + metadata := map[string]string{ + "undone_to_entry_id": fmt.Sprintf("%d", entryID), + "undone_operation": entry.Operation, + } + + err = txn.ExecuteWithTransactionAndMetadata( + "undo", + fmt.Sprintf("Undid to entry %d: %s", entryID, entry.Operation), + metadata, + func() error { + return nil + }, + ) + if err != nil { + return fmt.Errorf("failed to log undo: %w", err) + } + + return nil +} + +// ListUndoStack shows the undo stack +func ListUndoStack(repo *core.OnyxRepository) error { + oplogPath := filepath.Join(repo.GetOnyxPath(), "oplog") + reader := storage.NewOplogReader(oplogPath) + + entries, err := reader.GetUndoStack() + if err != nil { + return fmt.Errorf("failed to get undo stack: %w", err) + } + + if len(entries) == 0 { + fmt.Println("Nothing to undo") + return nil + } + + fmt.Println("Undo stack (most recent first):") + for i, entry := range entries { + fmt.Printf("%d. [%d] %s: %s (%s)\n", + i+1, + entry.ID, + entry.Operation, + entry.Description, + entry.Timestamp.Format("2006-01-02 15:04:05"), + ) + } + + return nil +} diff --git a/internal/commands/undo_test.go b/internal/commands/undo_test.go new file mode 100644 index 0000000..901484f --- /dev/null +++ b/internal/commands/undo_test.go @@ -0,0 +1,300 @@ +package commands + +import ( + "path/filepath" + "testing" + + "git.dws.rip/DWS/onyx/internal/core" + "git.dws.rip/DWS/onyx/internal/storage" +) + +func TestUndoWithEmptyOplog(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Initialize the repository + repo := &core.OnyxRepository{} + err := repo.Init(tempDir) + if err != nil { + t.Fatalf("Failed to initialize repository: %v", err) + } + + // Open the repository + openedRepo, err := core.Open(tempDir) + if err != nil { + t.Fatalf("Failed to open repository: %v", err) + } + defer openedRepo.Close() + + // Try to undo with empty oplog + oplogPath := filepath.Join(openedRepo.GetOnyxPath(), "oplog") + reader := storage.NewOplogReader(oplogPath) + + isEmpty, err := reader.IsEmpty() + if err != nil { + t.Fatalf("Failed to check if oplog is empty: %v", err) + } + + if !isEmpty { + t.Errorf("Expected oplog to be empty after init") + } + + _, err = reader.ReadLastEntry() + if err == nil { + t.Errorf("Expected error when reading from empty oplog, got nil") + } +} + +func TestUndoAfterOperation(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Initialize the repository + repo := &core.OnyxRepository{} + err := repo.Init(tempDir) + if err != nil { + t.Fatalf("Failed to initialize repository: %v", err) + } + + // Open the repository + openedRepo, err := core.Open(tempDir) + if err != nil { + t.Fatalf("Failed to open repository: %v", err) + } + defer openedRepo.Close() + + // Perform an operation with transaction + txn, err := core.NewTransaction(openedRepo) + if err != nil { + t.Fatalf("Failed to create transaction: %v", err) + } + + err = txn.ExecuteWithTransaction("test_operation", "Test operation for undo", func() error { + // Simulate some operation + return nil + }) + txn.Close() + + if err != nil { + t.Fatalf("Failed to execute transaction: %v", err) + } + + // Verify the oplog has an entry + oplogPath := filepath.Join(openedRepo.GetOnyxPath(), "oplog") + reader := storage.NewOplogReader(oplogPath) + + isEmpty, err := reader.IsEmpty() + if err != nil { + t.Fatalf("Failed to check if oplog is empty: %v", err) + } + + if isEmpty { + t.Errorf("Expected oplog to have entries after operation") + } + + // Read the last entry + lastEntry, err := reader.ReadLastEntry() + if err != nil { + t.Fatalf("Failed to read last entry: %v", err) + } + + if lastEntry.Operation != "test_operation" { + t.Errorf("Expected operation to be 'test_operation', got %q", lastEntry.Operation) + } + + if lastEntry.Description != "Test operation for undo" { + t.Errorf("Expected description to be 'Test operation for undo', got %q", lastEntry.Description) + } + + // Verify state_before and state_after are captured + if lastEntry.StateBefore == nil { + t.Errorf("Expected state_before to be captured") + } + + if lastEntry.StateAfter == nil { + t.Errorf("Expected state_after to be captured") + } +} + +func TestSequentialUndos(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Initialize the repository + repo := &core.OnyxRepository{} + err := repo.Init(tempDir) + if err != nil { + t.Fatalf("Failed to initialize repository: %v", err) + } + + // Open the repository + openedRepo, err := core.Open(tempDir) + if err != nil { + t.Fatalf("Failed to open repository: %v", err) + } + defer openedRepo.Close() + + // Perform multiple operations + operations := []string{"operation1", "operation2", "operation3"} + + for _, op := range operations { + txn, err := core.NewTransaction(openedRepo) + if err != nil { + t.Fatalf("Failed to create transaction: %v", err) + } + + err = txn.ExecuteWithTransaction(op, "Test "+op, func() error { + return nil + }) + txn.Close() + + if err != nil { + t.Fatalf("Failed to execute transaction for %s: %v", op, err) + } + } + + // Verify we have 3 entries + oplogPath := filepath.Join(openedRepo.GetOnyxPath(), "oplog") + reader := storage.NewOplogReader(oplogPath) + + count, err := reader.Count() + if err != nil { + t.Fatalf("Failed to count oplog entries: %v", err) + } + + if count != 3 { + t.Errorf("Expected 3 oplog entries, got %d", count) + } + + // Read all entries to verify order + entries, err := reader.ReadAllEntries() + if err != nil { + t.Fatalf("Failed to read all entries: %v", err) + } + + for i, entry := range entries { + expectedOp := operations[i] + if entry.Operation != expectedOp { + t.Errorf("Entry %d: expected operation %q, got %q", i, expectedOp, entry.Operation) + } + } +} + +func TestUndoStack(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Initialize the repository + repo := &core.OnyxRepository{} + err := repo.Init(tempDir) + if err != nil { + t.Fatalf("Failed to initialize repository: %v", err) + } + + // Open the repository + openedRepo, err := core.Open(tempDir) + if err != nil { + t.Fatalf("Failed to open repository: %v", err) + } + defer openedRepo.Close() + + // Perform multiple operations + operations := []string{"op1", "op2", "op3"} + + for _, op := range operations { + txn, err := core.NewTransaction(openedRepo) + if err != nil { + t.Fatalf("Failed to create transaction: %v", err) + } + + err = txn.ExecuteWithTransaction(op, "Test "+op, func() error { + return nil + }) + txn.Close() + + if err != nil { + t.Fatalf("Failed to execute transaction for %s: %v", op, err) + } + } + + // Get the undo stack + oplogPath := filepath.Join(openedRepo.GetOnyxPath(), "oplog") + reader := storage.NewOplogReader(oplogPath) + + undoStack, err := reader.GetUndoStack() + if err != nil { + t.Fatalf("Failed to get undo stack: %v", err) + } + + if len(undoStack) != 3 { + t.Errorf("Expected undo stack size of 3, got %d", len(undoStack)) + } + + // Verify the stack is in reverse order (most recent first) + expectedOps := []string{"op3", "op2", "op1"} + for i, entry := range undoStack { + if entry.Operation != expectedOps[i] { + t.Errorf("Undo stack[%d]: expected operation %q, got %q", i, expectedOps[i], entry.Operation) + } + } +} + +func TestOplogEntryMetadata(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Initialize the repository + repo := &core.OnyxRepository{} + err := repo.Init(tempDir) + if err != nil { + t.Fatalf("Failed to initialize repository: %v", err) + } + + // Open the repository + openedRepo, err := core.Open(tempDir) + if err != nil { + t.Fatalf("Failed to open repository: %v", err) + } + defer openedRepo.Close() + + // Perform an operation with metadata + txn, err := core.NewTransaction(openedRepo) + if err != nil { + t.Fatalf("Failed to create transaction: %v", err) + } + + metadata := map[string]string{ + "key1": "value1", + "key2": "value2", + } + + err = txn.ExecuteWithTransactionAndMetadata("test_op", "Test with metadata", metadata, func() error { + return nil + }) + txn.Close() + + if err != nil { + t.Fatalf("Failed to execute transaction with metadata: %v", err) + } + + // Read the entry and verify metadata + oplogPath := filepath.Join(openedRepo.GetOnyxPath(), "oplog") + reader := storage.NewOplogReader(oplogPath) + + lastEntry, err := reader.ReadLastEntry() + if err != nil { + t.Fatalf("Failed to read last entry: %v", err) + } + + if lastEntry.Metadata == nil { + t.Fatalf("Expected metadata to be present") + } + + if lastEntry.Metadata["key1"] != "value1" { + t.Errorf("Expected metadata key1=value1, got %q", lastEntry.Metadata["key1"]) + } + + if lastEntry.Metadata["key2"] != "value2" { + t.Errorf("Expected metadata key2=value2, got %q", lastEntry.Metadata["key2"]) + } +} diff --git a/internal/core/transaction.go b/internal/core/transaction.go new file mode 100644 index 0000000..aaa6a2f --- /dev/null +++ b/internal/core/transaction.go @@ -0,0 +1,167 @@ +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 +} + +// 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) +} diff --git a/internal/storage/oplog_reader.go b/internal/storage/oplog_reader.go new file mode 100644 index 0000000..bafd1d4 --- /dev/null +++ b/internal/storage/oplog_reader.go @@ -0,0 +1,201 @@ +package storage + +import ( + "encoding/binary" + "fmt" + "io" + "os" + + "git.dws.rip/DWS/onyx/internal/models" +) + +// OplogReader handles reading entries from the oplog file +type OplogReader struct { + path string +} + +// NewOplogReader creates a new oplog reader for the given file path +func NewOplogReader(path string) *OplogReader { + return &OplogReader{ + path: path, + } +} + +// ReadLastEntry reads the last (most recent) entry in the oplog +func (r *OplogReader) ReadLastEntry() (*models.OplogEntry, error) { + file, err := os.Open(r.path) + if err != nil { + return nil, fmt.Errorf("failed to open oplog file: %w", err) + } + defer file.Close() + + var lastEntry *models.OplogEntry + + // Read through all entries to find the last one + for { + // Read entry length (4 bytes) + var entryLen uint32 + err := binary.Read(file, binary.LittleEndian, &entryLen) + if err != nil { + if err == io.EOF { + break + } + return nil, fmt.Errorf("failed to read entry length: %w", err) + } + + // Read the entry data + entryData := make([]byte, entryLen) + n, err := file.Read(entryData) + if err != nil { + return nil, fmt.Errorf("failed to read entry data: %w", err) + } + if n != int(entryLen) { + return nil, fmt.Errorf("incomplete entry data read: expected %d bytes, got %d", entryLen, n) + } + + // Deserialize the entry + entry, err := models.DeserializeOplogEntry(entryData) + if err != nil { + return nil, fmt.Errorf("failed to deserialize entry: %w", err) + } + + lastEntry = entry + } + + if lastEntry == nil { + return nil, fmt.Errorf("oplog is empty") + } + + return lastEntry, nil +} + +// ReadEntry reads a specific entry by ID +func (r *OplogReader) ReadEntry(id uint64) (*models.OplogEntry, error) { + file, err := os.Open(r.path) + if err != nil { + return nil, fmt.Errorf("failed to open oplog file: %w", err) + } + defer file.Close() + + // Read through all entries to find the one with matching ID + for { + // Read entry length (4 bytes) + var entryLen uint32 + err := binary.Read(file, binary.LittleEndian, &entryLen) + if err != nil { + if err == io.EOF { + break + } + return nil, fmt.Errorf("failed to read entry length: %w", err) + } + + // Read the entry data + entryData := make([]byte, entryLen) + n, err := file.Read(entryData) + if err != nil { + return nil, fmt.Errorf("failed to read entry data: %w", err) + } + if n != int(entryLen) { + return nil, fmt.Errorf("incomplete entry data read: expected %d bytes, got %d", entryLen, n) + } + + // Deserialize the entry + entry, err := models.DeserializeOplogEntry(entryData) + if err != nil { + return nil, fmt.Errorf("failed to deserialize entry: %w", err) + } + + if entry.ID == id { + return entry, nil + } + } + + return nil, fmt.Errorf("entry with ID %d not found", id) +} + +// GetUndoStack returns a stack of entries that can be undone (in reverse order) +func (r *OplogReader) GetUndoStack() ([]*models.OplogEntry, error) { + entries, err := r.ReadAllEntries() + if err != nil { + return nil, err + } + + // Filter out entries that have already been undone + // For now, we return all entries in reverse order + // In the future, we might track undone entries separately + var undoStack []*models.OplogEntry + for i := len(entries) - 1; i >= 0; i-- { + undoStack = append(undoStack, entries[i]) + } + + return undoStack, nil +} + +// ReadAllEntries reads all entries from the oplog in order +func (r *OplogReader) ReadAllEntries() ([]*models.OplogEntry, error) { + file, err := os.Open(r.path) + if err != nil { + return nil, fmt.Errorf("failed to open oplog file: %w", err) + } + defer file.Close() + + var entries []*models.OplogEntry + + // Read through all entries + for { + // Read entry length (4 bytes) + var entryLen uint32 + err := binary.Read(file, binary.LittleEndian, &entryLen) + if err != nil { + if err == io.EOF { + break + } + return nil, fmt.Errorf("failed to read entry length: %w", err) + } + + // Read the entry data + entryData := make([]byte, entryLen) + n, err := file.Read(entryData) + if err != nil { + return nil, fmt.Errorf("failed to read entry data: %w", err) + } + if n != int(entryLen) { + return nil, fmt.Errorf("incomplete entry data read: expected %d bytes, got %d", entryLen, n) + } + + // Deserialize the entry + entry, err := models.DeserializeOplogEntry(entryData) + if err != nil { + return nil, fmt.Errorf("failed to deserialize entry: %w", err) + } + + entries = append(entries, entry) + } + + return entries, nil +} + +// Count returns the total number of entries in the oplog +func (r *OplogReader) Count() (int, error) { + entries, err := r.ReadAllEntries() + if err != nil { + return 0, err + } + return len(entries), nil +} + +// IsEmpty checks if the oplog is empty +func (r *OplogReader) IsEmpty() (bool, error) { + file, err := os.Open(r.path) + if err != nil { + return false, fmt.Errorf("failed to open oplog file: %w", err) + } + defer file.Close() + + stat, err := file.Stat() + if err != nil { + return false, fmt.Errorf("failed to stat file: %w", err) + } + + return stat.Size() == 0, nil +} diff --git a/internal/storage/oplog_writer.go b/internal/storage/oplog_writer.go new file mode 100644 index 0000000..830efaa --- /dev/null +++ b/internal/storage/oplog_writer.go @@ -0,0 +1,163 @@ +package storage + +import ( + "encoding/binary" + "fmt" + "os" + "sync" + + "git.dws.rip/DWS/onyx/internal/models" +) + +// OplogWriter handles writing entries to the oplog file +type OplogWriter struct { + path string + file *os.File + mu sync.Mutex + nextID uint64 + isClosed bool +} + +// OpenOplog opens an existing oplog file or creates a new one +func OpenOplog(path string) (*OplogWriter, error) { + // Open file for append and read + file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return nil, fmt.Errorf("failed to open oplog file: %w", err) + } + + writer := &OplogWriter{ + path: path, + file: file, + nextID: 1, + } + + // Calculate next ID by reading existing entries + if err := writer.calculateNextID(); err != nil { + file.Close() + return nil, fmt.Errorf("failed to calculate next ID: %w", err) + } + + return writer, nil +} + +// calculateNextID scans the oplog to determine the next entry ID +func (w *OplogWriter) calculateNextID() error { + // Seek to the beginning + if _, err := w.file.Seek(0, 0); err != nil { + return fmt.Errorf("failed to seek to beginning: %w", err) + } + + var maxID uint64 = 0 + + // Read through all entries to find the max ID + for { + // Read entry length (4 bytes) + var entryLen uint32 + err := binary.Read(w.file, binary.LittleEndian, &entryLen) + if err != nil { + // EOF is expected at the end + if err.Error() == "EOF" { + break + } + return fmt.Errorf("failed to read entry length: %w", err) + } + + // Read the entry data + entryData := make([]byte, entryLen) + n, err := w.file.Read(entryData) + if err != nil || n != int(entryLen) { + return fmt.Errorf("failed to read entry data: %w", err) + } + + // Deserialize to get the ID + entry, err := models.DeserializeOplogEntry(entryData) + if err != nil { + return fmt.Errorf("failed to deserialize entry: %w", err) + } + + if entry.ID > maxID { + maxID = entry.ID + } + } + + w.nextID = maxID + 1 + + // Seek to the end for appending + if _, err := w.file.Seek(0, 2); err != nil { + return fmt.Errorf("failed to seek to end: %w", err) + } + + return nil +} + +// AppendEntry appends a new entry to the oplog +func (w *OplogWriter) AppendEntry(entry *models.OplogEntry) error { + w.mu.Lock() + defer w.mu.Unlock() + + if w.isClosed { + return fmt.Errorf("oplog writer is closed") + } + + // Assign ID if not set + if entry.ID == 0 { + entry.ID = w.nextID + w.nextID++ + } + + // Serialize the entry + data, err := entry.Serialize() + if err != nil { + return fmt.Errorf("failed to serialize entry: %w", err) + } + + // Write entry length (4 bytes) followed by entry data + entryLen := uint32(len(data)) + if err := binary.Write(w.file, binary.LittleEndian, entryLen); err != nil { + return fmt.Errorf("failed to write entry length: %w", err) + } + + if _, err := w.file.Write(data); err != nil { + return fmt.Errorf("failed to write entry data: %w", err) + } + + // Sync to disk for durability + if err := w.file.Sync(); err != nil { + return fmt.Errorf("failed to sync file: %w", err) + } + + return nil +} + +// GetNextID returns the next entry ID that will be assigned +func (w *OplogWriter) GetNextID() uint64 { + w.mu.Lock() + defer w.mu.Unlock() + return w.nextID +} + +// Close closes the oplog file +func (w *OplogWriter) Close() error { + w.mu.Lock() + defer w.mu.Unlock() + + if w.isClosed { + return nil + } + + w.isClosed = true + return w.file.Close() +} + +// Flush ensures all buffered data is written to disk +func (w *OplogWriter) Flush() error { + w.mu.Lock() + defer w.mu.Unlock() + + if w.isClosed { + return fmt.Errorf("oplog writer is closed") + } + + return w.file.Sync() +} diff --git a/internal/storage/state.go b/internal/storage/state.go new file mode 100644 index 0000000..5a0d349 --- /dev/null +++ b/internal/storage/state.go @@ -0,0 +1,187 @@ +package storage + +import ( + "fmt" + + "git.dws.rip/DWS/onyx/internal/models" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" +) + +// StateCapture provides functionality to capture repository state +type StateCapture struct { + repo *git.Repository +} + +// NewStateCapture creates a new StateCapture instance +func NewStateCapture(repo *git.Repository) *StateCapture { + return &StateCapture{ + repo: repo, + } +} + +// CaptureState captures the current state of the repository +func (s *StateCapture) CaptureState() (*models.RepositoryState, error) { + refs, err := s.captureRefs() + if err != nil { + return nil, fmt.Errorf("failed to capture refs: %w", err) + } + + currentWorkstream, err := s.getCurrentWorkstream() + if err != nil { + // It's okay if there's no current workstream (e.g., in detached HEAD state) + currentWorkstream = "" + } + + workingTreeHash, err := s.getWorkingTreeHash() + if err != nil { + // Working tree hash might not be available in a fresh repo + workingTreeHash = "" + } + + indexHash, err := s.getIndexHash() + if err != nil { + // Index hash might not be available in a fresh repo + indexHash = "" + } + + return models.NewRepositoryState(refs, currentWorkstream, workingTreeHash, indexHash), nil +} + +// captureRefs captures all Git references (branches, tags, etc.) +func (s *StateCapture) captureRefs() (map[string]string, error) { + refs := make(map[string]string) + + refIter, err := s.repo.References() + if err != nil { + return nil, fmt.Errorf("failed to get references: %w", err) + } + + err = refIter.ForEach(func(ref *plumbing.Reference) error { + if ref.Type() == plumbing.HashReference { + refs[ref.Name().String()] = ref.Hash().String() + } else if ref.Type() == plumbing.SymbolicReference { + // For symbolic refs (like HEAD), store the target + refs[ref.Name().String()] = ref.Target().String() + } + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to iterate references: %w", err) + } + + return refs, nil +} + +// getCurrentWorkstream determines the current workstream (branch) +func (s *StateCapture) getCurrentWorkstream() (string, error) { + head, err := s.repo.Head() + if err != nil { + return "", fmt.Errorf("failed to get HEAD: %w", err) + } + + if head.Name().IsBranch() { + return head.Name().Short(), nil + } + + // In detached HEAD state + return "", fmt.Errorf("in detached HEAD state") +} + +// getWorkingTreeHash gets a hash representing the current working tree +func (s *StateCapture) getWorkingTreeHash() (string, error) { + worktree, err := s.repo.Worktree() + if err != nil { + return "", fmt.Errorf("failed to get worktree: %w", err) + } + + status, err := worktree.Status() + if err != nil { + return "", fmt.Errorf("failed to get status: %w", err) + } + + // For now, we'll just check if the working tree is clean + // In the future, we might compute an actual hash + if status.IsClean() { + head, err := s.repo.Head() + if err == nil { + return head.Hash().String(), nil + } + } + + return "dirty", nil +} + +// getIndexHash gets a hash representing the current index (staging area) +func (s *StateCapture) getIndexHash() (string, error) { + // For now, this is a placeholder + // In the future, we might compute a proper hash of the index + return "", nil +} + +// RestoreState restores the repository to a previously captured state +func (s *StateCapture) RestoreState(state *models.RepositoryState) error { + // Restore all refs + for refName, refHash := range state.Refs { + ref := plumbing.NewReferenceFromStrings(refName, refHash) + + // Skip symbolic references for now + if ref.Type() == plumbing.SymbolicReference { + continue + } + + err := s.repo.Storer.SetReference(ref) + if err != nil { + return fmt.Errorf("failed to restore ref %s: %w", refName, err) + } + } + + // If there's a current workstream, check it out + if state.CurrentWorkstream != "" { + worktree, err := s.repo.Worktree() + if err != nil { + return fmt.Errorf("failed to get worktree: %w", err) + } + + err = worktree.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName(state.CurrentWorkstream), + }) + if err != nil { + // Don't fail if checkout fails, just log it + // The refs have been restored which is the most important part + fmt.Printf("Warning: failed to checkout branch %s: %v\n", state.CurrentWorkstream, err) + } + } + + return nil +} + +// CompareStates compares two repository states and returns the differences +func (s *StateCapture) CompareStates(before, after *models.RepositoryState) map[string]string { + differences := make(map[string]string) + + // Check for changed/added refs + for refName, afterHash := range after.Refs { + beforeHash, exists := before.Refs[refName] + if !exists { + differences[refName] = fmt.Sprintf("added: %s", afterHash) + } else if beforeHash != afterHash { + differences[refName] = fmt.Sprintf("changed: %s -> %s", beforeHash, afterHash) + } + } + + // Check for deleted refs + for refName := range before.Refs { + if _, exists := after.Refs[refName]; !exists { + differences[refName] = "deleted" + } + } + + // Check workstream change + if before.CurrentWorkstream != after.CurrentWorkstream { + differences["current_workstream"] = fmt.Sprintf("changed: %s -> %s", before.CurrentWorkstream, after.CurrentWorkstream) + } + + return differences +} From b0bf1a080d6f165941d4c60e5c29cb2fa6773081 Mon Sep 17 00:00:00 2001 From: Tanishq Dubey Date: Thu, 9 Oct 2025 19:42:48 -0400 Subject: [PATCH 3/4] Update CICD --- .gitea/workflows/ci.yml | 128 ++-------------------------------------- 1 file changed, 6 insertions(+), 122 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index fb368ba..ae5ed8b 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI/CD Pipeline +name: CI on: push: @@ -20,44 +20,15 @@ jobs: with: go-version: '1.24.2' - - name: Cache Go modules - uses: actions/cache@v4 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: Download dependencies run: go mod download - - name: Verify dependencies - run: go mod verify - - name: Run tests - run: go test -v -race -coverprofile=coverage.out ./... - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - file: ./coverage.out - flags: unittests - name: codecov-umbrella + run: go test -v ./... build: name: Build runs-on: ubuntu-latest - needs: test - - strategy: - matrix: - goos: [linux, windows, darwin] - goarch: [amd64, arm64] - exclude: - - goos: windows - goarch: arm64 steps: - name: Checkout code @@ -68,65 +39,13 @@ jobs: with: go-version: '1.24.2' - - name: Cache Go modules - uses: actions/cache@v4 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: Download dependencies run: go mod download - - name: Build binaries - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} + - name: Build run: | - mkdir -p bin/${{ matrix.goos }}-${{ matrix.goarch }} - - # Build CLI - go build -ldflags="-s -w" -o bin/${{ matrix.goos }}-${{ matrix.goarch }}/onx${{ matrix.goos == 'windows' && '.exe' || '' }} ./cmd/onx - - # Build daemon - go build -ldflags="-s -w" -o bin/${{ matrix.goos }}-${{ matrix.goarch }}/onxd${{ matrix.goos == 'windows' && '.exe' || '' }} ./cmd/onxd - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: onyx-${{ matrix.goos }}-${{ matrix.goarch }} - path: bin/${{ matrix.goos }}-${{ matrix.goarch }}/ - - security: - name: Security Scan - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.24.2' - - - name: Run Gosec Security Scanner - uses: securecodewarrior/github-action-gosec@master - with: - args: './...' - - - name: Run SAST with Gosec - run: | - go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest - gosec -fmt sarif -out gosec.sarif ./... - - - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: gosec.sarif + go build -o bin/onx ./cmd/onx + go build -o bin/onxd ./cmd/onxd lint: name: Lint @@ -145,39 +64,4 @@ jobs: uses: golangci/golangci-lint-action@v6 with: version: latest - args: --timeout=5m - - release: - name: Release - runs-on: ubuntu-latest - needs: [test, build, security, lint] - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts/ - - - name: Create release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: v${{ github.run_number }} - release_name: Release v${{ github.run_number }} - draft: false - prerelease: false - - - name: Upload release assets - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: artifacts/ - asset_name: onyx-binaries.zip - asset_content_type: application/zip \ No newline at end of file + args: --timeout=2m \ No newline at end of file From d63af29e73e04d51914f1785a53ec4284672b8f1 Mon Sep 17 00:00:00 2001 From: Tanishq Dubey Date: Thu, 9 Oct 2025 19:46:05 -0400 Subject: [PATCH 4/4] update to docker --- .gitea/workflows/ci.yml | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index ae5ed8b..5419494 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -10,16 +10,12 @@ jobs: test: name: Test runs-on: ubuntu-latest + container: golang:1.24.2-alpine steps: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.24.2' - - name: Download dependencies run: go mod download @@ -29,16 +25,12 @@ jobs: build: name: Build runs-on: ubuntu-latest + container: golang:1.24.2-alpine steps: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.24.2' - - name: Download dependencies run: go mod download @@ -50,18 +42,18 @@ jobs: lint: name: Lint runs-on: ubuntu-latest + container: golang:1.24.2-alpine steps: + - name: Install git + run: apk add --no-cache git + - name: Checkout code uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.24.2' + - name: Install golangci-lint + run: | + wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.62.0 - name: Run golangci-lint - uses: golangci/golangci-lint-action@v6 - with: - version: latest - args: --timeout=2m \ No newline at end of file + run: $(go env GOPATH)/bin/golangci-lint run --timeout=2m \ No newline at end of file