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