feat(VCS): initial toying
This commit is contained in:
parent
ebabae41b6
commit
d5fb28b711
1 changed files with 198 additions and 0 deletions
198
cmd/vcs.go
Normal file
198
cmd/vcs.go
Normal 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()
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue