feat(VCS): initial toying

This commit is contained in:
Lewis Wynne 2025-12-18 19:16:11 +00:00
parent ebabae41b6
commit d5fb28b711

198
cmd/vcs.go Normal file
View file

@ -0,0 +1,198 @@
package cmd
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"unicode/utf8"
"github.com/dgraph-io/badger/v4"
gap "github.com/muesli/go-app-paths"
"github.com/spf13/cobra"
)
var vcsCmd = &cobra.Command{
Use: "vcs",
Short: "Version control utilities",
}
var vcsInitCmd = &cobra.Command{
Use: "init",
Short: "Initialise local version control for pda data",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
repoDir, err := vcsRepoRoot()
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(repoDir), 0o750); err != nil {
return err
}
gitDir := filepath.Join(repoDir, ".git")
if _, err := os.Stat(gitDir); os.IsNotExist(err) {
if err := runGit(repoDir, "init"); err != nil {
return err
}
}
if err := writeGitignore(repoDir); err != nil {
return err
}
return nil
},
}
var vcsSnapshotCmd = &cobra.Command{
Use: "snapshot",
Short: "commit a snapshot into the vcs",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
repoDir, err := ensureVCSInitialized()
if err != nil {
return err
}
store := &Store{}
stores, err := store.AllStores()
if err != nil {
return err
}
for _, db := range stores {
if err := snapshotDB(store, repoDir, db); err != nil {
return fmt.Errorf("snapshot %q: %w", db, err)
}
}
if err := runGit(repoDir, "add", "snapshots"); err != nil {
return err
}
message := fmt.Sprintf("snapshot: %s", time.Now().UTC().Format(time.RFC3339))
if err := runGit(repoDir, "commit", "-m", message); err != nil {
fmt.Println(err.Error())
return nil
}
return nil
},
}
func init() {
vcsCmd.AddCommand(vcsInitCmd)
vcsCmd.AddCommand(vcsSnapshotCmd)
rootCmd.AddCommand(vcsCmd)
}
func vcsRepoRoot() (string, error) {
scope := gap.NewVendorScope(gap.User, "pda", "vcs")
dir, err := scope.DataPath("")
if err != nil {
return "", err
}
if err := os.MkdirAll(dir, 0o750); err != nil {
return "", err
}
return dir, nil
}
func ensureVCSInitialized() (string, error) {
repoDir, err := vcsRepoRoot()
if err != nil {
return "", err
}
if _, err := os.Stat(filepath.Join(repoDir, ".git")); err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("vcs repository not initialised; run 'pda vcs init' first")
}
return "", err
}
return repoDir, nil
}
func writeGitignore(repoDir string) error {
path := filepath.Join(repoDir, ".gitignore")
content := strings.Join([]string{
"# generated by pda",
"*",
"!snapshots/",
"!snapshots/*.ndjson",
"",
}, "\n")
return os.WriteFile(path, []byte(content), 0o640)
}
func snapshotDB(store *Store, repoDir, db string) error {
snapDir := filepath.Join(repoDir, "snapshots")
if err := os.MkdirAll(snapDir, 0o750); err != nil {
return err
}
target := filepath.Join(snapDir, fmt.Sprintf("%s.ndjson", db))
f, err := os.Create(target)
if err != nil {
return err
}
defer f.Close()
trans := TransactionArgs{
key: "@" + db,
readonly: true,
sync: true,
transact: func(tx *badger.Txn, k []byte) error {
it := tx.NewIterator(badger.DefaultIteratorOptions)
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
item := it.Item()
key := item.KeyCopy(nil)
meta := item.UserMeta()
isSecret := meta&metaSecret != 0
expiresAt := item.ExpiresAt()
if err := item.Value(func(v []byte) error {
entry := dumpEntry{
Key: string(key),
Secret: isSecret,
}
if expiresAt > 0 {
ts := int64(expiresAt)
entry.ExpiresAt = &ts
}
if utf8.Valid(v) {
entry.Encoding = "text"
entry.Value = string(v)
} else {
encodeBase64(&entry, v)
}
payload, err := json.Marshal(entry)
if err != nil {
return err
}
_, err = fmt.Fprintln(f, string(payload))
return err
}); err != nil {
return err
}
}
return nil
},
}
if err := store.Transaction(trans); err != nil {
return err
}
return f.Sync()
}
func runGit(dir string, args ...string) error {
cmd := exec.Command("git", args...)
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}