refactor(massive simplification of vcs now that we're using ndjson natively):

This commit is contained in:
Lewis Wynne 2026-02-11 00:28:56 +00:00
parent 84c55311d1
commit cb441b112c
4 changed files with 61 additions and 203 deletions

View file

@ -34,16 +34,12 @@ var gitCmd = &cobra.Command{
Short: "Run any arbitrary command. Use with caution.", Short: "Run any arbitrary command. Use with caution.",
Long: `Run any arbitrary command. Use with caution. Long: `Run any arbitrary command. Use with caution.
Be wary of how pda! version control operates before using this. The Git repository lives directly in the stores directory
Regular data is stored in "PDA_DATA/pda/stores" as a store; the ("PDA_DATA/pda/stores"). Store files (*.ndjson) are tracked
Git repository is in "PDA_DATA/pda/vcs" and contains a plaintext by Git as-is.
replica of the store data.
The regular sync command (or auto-syncing) exports pda! data into If you manually modify files without using the built-in
plaintext in the Git repository. If you manually modify the commands, you may desync your repository.
repository without using the built-in commands, or exporting your
data to the Git folder in the correct format first, you may desync
your repository.
Generally prefer "pda sync".`, Generally prefer "pda sync".`,
Args: cobra.ArbitraryArgs, Args: cobra.ArbitraryArgs,

View file

@ -40,7 +40,7 @@ var initCmd = &cobra.Command{
} }
func init() { func init() {
initCmd.Flags().Bool("clean", false, "Remove existing VCS directory before initialising") initCmd.Flags().Bool("clean", false, "Remove .git from stores directory before initialising")
rootCmd.AddCommand(initCmd) rootCmd.AddCommand(initCmd)
} }
@ -55,66 +55,74 @@ func vcsInit(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
hasRemote := len(args) == 1
if clean { if clean {
entries, err := os.ReadDir(repoDir) gitDir := filepath.Join(repoDir, ".git")
if err == nil && len(entries) > 0 { if _, err := os.Stat(gitDir); err == nil {
fmt.Printf("remove existing VCS directory '%s'? (y/n)\n", repoDir) fmt.Printf("remove .git from '%s'? (y/n)\n", repoDir)
var confirm string var confirm string
if _, err := fmt.Scanln(&confirm); err != nil { if _, err := fmt.Scanln(&confirm); err != nil {
return fmt.Errorf("cannot clean vcs dir: %w", err) return fmt.Errorf("cannot clean git dir: %w", err)
} }
if strings.ToLower(confirm) != "y" { if strings.ToLower(confirm) != "y" {
return fmt.Errorf("aborted cleaning vcs dir") return fmt.Errorf("aborted cleaning git dir")
}
if err := os.RemoveAll(gitDir); err != nil {
return fmt.Errorf("cannot clean git dir: %w", err)
} }
}
if err := os.RemoveAll(repoDir); err != nil {
return fmt.Errorf("cannot clean vcs dir: %w", err)
} }
dbs, err := store.AllStores() if hasRemote {
if err == nil && len(dbs) > 0 { dbs, err := store.AllStores()
fmt.Printf("remove all existing stores? (y/n)\n") if err == nil && len(dbs) > 0 {
var confirm string fmt.Printf("remove all existing stores and .gitignore? (required for clone) (y/n)\n")
if _, err := fmt.Scanln(&confirm); err != nil { var confirm string
return fmt.Errorf("cannot clean stores: %w", err) if _, err := fmt.Scanln(&confirm); err != nil {
} return fmt.Errorf("cannot clean stores: %w", err)
if strings.ToLower(confirm) != "y" { }
return fmt.Errorf("aborted cleaning stores") if strings.ToLower(confirm) != "y" {
} return fmt.Errorf("aborted cleaning stores")
if err := wipeAllStores(store); err != nil { }
return fmt.Errorf("cannot clean stores: %w", err) if err := wipeAllStores(store); err != nil {
return fmt.Errorf("cannot clean stores: %w", err)
}
gi := filepath.Join(repoDir, ".gitignore")
if err := os.Remove(gi); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("cannot remove .gitignore: %w", err)
}
} }
} }
} }
if err := os.MkdirAll(filepath.Join(repoDir), 0o750); err != nil {
return err
}
gitDir := filepath.Join(repoDir, ".git") gitDir := filepath.Join(repoDir, ".git")
if _, err := os.Stat(gitDir); os.IsNotExist(err) { if _, err := os.Stat(gitDir); err == nil {
if len(args) == 1 {
remote := args[0]
fmt.Printf("running: git clone %s %s\n", remote, repoDir)
if err := runGit("", "clone", remote, repoDir); err != nil {
return err
}
} else {
fmt.Printf("running: git init\n")
if err := runGit(repoDir, "init"); err != nil {
return err
}
}
} else {
fmt.Println("vcs already initialised; use --clean to reinitialise") fmt.Println("vcs already initialised; use --clean to reinitialise")
return nil return nil
} }
if err := writeGitignore(repoDir); err != nil { if hasRemote {
return err // git clone requires the target directory to be empty
entries, err := os.ReadDir(repoDir)
if err == nil && len(entries) > 0 {
return fmt.Errorf("stores directory is not empty; use --clean with a remote to wipe and clone")
}
remote := args[0]
fmt.Printf("running: git clone %s %s\n", remote, repoDir)
if err := runGit("", "clone", remote, repoDir); err != nil {
return err
}
} else {
if err := os.MkdirAll(repoDir, 0o750); err != nil {
return err
}
fmt.Printf("running: git init\n")
if err := runGit(repoDir, "init"); err != nil {
return err
}
} }
if len(args) == 0 { return writeGitignore(repoDir)
return nil
}
return restoreAllSnapshots(store, repoDir)
} }

View file

@ -44,7 +44,6 @@ func init() {
} }
func sync(manual bool) error { func sync(manual bool) error {
store := &Store{}
repoDir, err := ensureVCSInitialized() repoDir, err := ensureVCSInitialized()
if err != nil { if err != nil {
return err return err
@ -89,18 +88,12 @@ func sync(manual bool) error {
return err return err
} }
} }
if err := pullRemote(repoDir, remoteInfo); err != nil { return pullRemote(repoDir, remoteInfo)
return err
}
return restoreAllSnapshots(store, repoDir)
} }
} }
} }
if err := exportAllStores(store, repoDir); err != nil { if err := runGit(repoDir, "add", "-A"); err != nil {
return err
}
if err := runGit(repoDir, "add", storeDirName); err != nil {
return err return err
} }
changed, err := repoHasStagedChanges(repoDir) changed, err := repoHasStagedChanges(repoDir)

View file

@ -9,22 +9,10 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
gap "github.com/muesli/go-app-paths"
) )
const storeDirName = "stores"
func vcsRepoRoot() (string, error) { func vcsRepoRoot() (string, error) {
scope := gap.NewVendorScope(gap.User, "pda", "vcs") return (&Store{}).path()
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) { func ensureVCSInitialized() (string, error) {
@ -47,10 +35,8 @@ func writeGitignore(repoDir string) error {
content := strings.Join([]string{ content := strings.Join([]string{
"# generated by pda", "# generated by pda",
"*", "*",
"!/",
"!.gitignore", "!.gitignore",
"!" + storeDirName + "/", "!*.ndjson",
"!" + storeDirName + "/*",
"", "",
}, "\n") }, "\n")
if err := os.WriteFile(path, []byte(content), 0o640); err != nil { if err := os.WriteFile(path, []byte(content), 0o640); err != nil {
@ -66,71 +52,6 @@ func writeGitignore(repoDir string) error {
return nil return nil
} }
// snapshotDB copies a store's .ndjson file into the VCS directory.
func snapshotDB(store *Store, repoDir, db string) error {
targetDir := filepath.Join(repoDir, storeDirName)
if err := os.MkdirAll(targetDir, 0o750); err != nil {
return err
}
srcPath, err := store.storePath(db)
if err != nil {
return err
}
data, err := os.ReadFile(srcPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
target := filepath.Join(targetDir, db+".ndjson")
return os.WriteFile(target, data, 0o640)
}
// exportAllStores copies every store's .ndjson file to repoDir/stores
// and removes stale snapshot files for deleted stores.
func exportAllStores(store *Store, repoDir string) error {
stores, err := store.AllStores()
if err != nil {
return err
}
targetDir := filepath.Join(repoDir, storeDirName)
if err := os.MkdirAll(targetDir, 0o750); err != nil {
return err
}
current := make(map[string]struct{})
for _, db := range stores {
current[db] = struct{}{}
if err := snapshotDB(store, repoDir, db); err != nil {
return fmt.Errorf("snapshot %q: %w", db, err)
}
}
entries, err := os.ReadDir(targetDir)
if err != nil {
return err
}
for _, e := range entries {
if e.IsDir() || filepath.Ext(e.Name()) != ".ndjson" {
continue
}
dbName := strings.TrimSuffix(e.Name(), ".ndjson")
if _, ok := current[dbName]; ok {
continue
}
if err := os.Remove(filepath.Join(targetDir, e.Name())); err != nil && !os.IsNotExist(err) {
return err
}
}
return nil
}
func runGit(dir string, args ...string) error { func runGit(dir string, args ...string) error {
cmd := exec.Command("git", args...) cmd := exec.Command("git", args...)
cmd.Dir = dir cmd.Dir = dir
@ -288,66 +209,6 @@ func currentBranch(dir string) (string, error) {
return branch, nil return branch, nil
} }
// restoreAllSnapshots copies .ndjson files from VCS snapshot dir into store paths,
// and removes local stores that are not in the snapshot.
func restoreAllSnapshots(store *Store, repoDir string) error {
targetDir := filepath.Join(repoDir, storeDirName)
entries, err := os.ReadDir(targetDir)
if err != nil {
if os.IsNotExist(err) {
fmt.Printf("no existing stores found, not restoring")
return nil
}
return err
}
snapshotDBs := make(map[string]struct{})
for _, e := range entries {
if e.IsDir() {
continue
}
if filepath.Ext(e.Name()) != ".ndjson" {
continue
}
dbName := strings.TrimSuffix(e.Name(), ".ndjson")
snapshotDBs[dbName] = struct{}{}
srcPath := filepath.Join(targetDir, e.Name())
data, err := os.ReadFile(srcPath)
if err != nil {
return fmt.Errorf("restore %q: %w", dbName, err)
}
dstPath, err := store.storePath(dbName)
if err != nil {
return fmt.Errorf("restore %q: %w", dbName, err)
}
if err := os.WriteFile(dstPath, data, 0o640); err != nil {
return fmt.Errorf("restore %q: %w", dbName, err)
}
}
localDBs, err := store.AllStores()
if err != nil {
return err
}
for _, db := range localDBs {
if _, ok := snapshotDBs[db]; ok {
continue
}
p, err := store.storePath(db)
if err != nil {
return err
}
if err := os.Remove(p); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove store '%s': %w", db, err)
}
}
return nil
}
func wipeAllStores(store *Store) error { func wipeAllStores(store *Store) error {
dbs, err := store.AllStores() dbs, err := store.AllStores()
if err != nil { if err != nil {