diff --git a/cmd/git.go b/cmd/git.go index 489cd4e..4254d4d 100644 --- a/cmd/git.go +++ b/cmd/git.go @@ -34,16 +34,12 @@ var gitCmd = &cobra.Command{ Short: "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. -Regular data is stored in "PDA_DATA/pda/stores" as a store; the -Git repository is in "PDA_DATA/pda/vcs" and contains a plaintext -replica of the store data. +The Git repository lives directly in the stores directory +("PDA_DATA/pda/stores"). Store files (*.ndjson) are tracked +by Git as-is. -The regular sync command (or auto-syncing) exports pda! data into -plaintext in the Git repository. If you manually modify the -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. +If you manually modify files without using the built-in +commands, you may desync your repository. Generally prefer "pda sync".`, Args: cobra.ArbitraryArgs, diff --git a/cmd/init.go b/cmd/init.go index d0259b2..07c64a9 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -40,7 +40,7 @@ var initCmd = &cobra.Command{ } 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) } @@ -55,66 +55,74 @@ func vcsInit(cmd *cobra.Command, args []string) error { if err != nil { return err } + + hasRemote := len(args) == 1 + if clean { - entries, err := os.ReadDir(repoDir) - if err == nil && len(entries) > 0 { - fmt.Printf("remove existing VCS directory '%s'? (y/n)\n", repoDir) + gitDir := filepath.Join(repoDir, ".git") + if _, err := os.Stat(gitDir); err == nil { + fmt.Printf("remove .git from '%s'? (y/n)\n", repoDir) var confirm string 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" { - 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 err == nil && len(dbs) > 0 { - fmt.Printf("remove all existing stores? (y/n)\n") - var confirm string - 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 err := wipeAllStores(store); err != nil { - return fmt.Errorf("cannot clean stores: %w", err) + if hasRemote { + dbs, err := store.AllStores() + if err == nil && len(dbs) > 0 { + fmt.Printf("remove all existing stores and .gitignore? (required for clone) (y/n)\n") + var confirm string + 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 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") - if _, err := os.Stat(gitDir); os.IsNotExist(err) { - 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 { + if _, err := os.Stat(gitDir); err == nil { fmt.Println("vcs already initialised; use --clean to reinitialise") return nil } - if err := writeGitignore(repoDir); err != nil { - return err + if hasRemote { + // 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 nil - } - return restoreAllSnapshots(store, repoDir) + return writeGitignore(repoDir) } diff --git a/cmd/sync.go b/cmd/sync.go index 222ee3c..b5b696e 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -44,7 +44,6 @@ func init() { } func sync(manual bool) error { - store := &Store{} repoDir, err := ensureVCSInitialized() if err != nil { return err @@ -89,18 +88,12 @@ func sync(manual bool) error { return err } } - if err := pullRemote(repoDir, remoteInfo); err != nil { - return err - } - return restoreAllSnapshots(store, repoDir) + return pullRemote(repoDir, remoteInfo) } } } - if err := exportAllStores(store, repoDir); err != nil { - return err - } - if err := runGit(repoDir, "add", storeDirName); err != nil { + if err := runGit(repoDir, "add", "-A"); err != nil { return err } changed, err := repoHasStagedChanges(repoDir) diff --git a/cmd/vcs.go b/cmd/vcs.go index 757a996..9a82706 100644 --- a/cmd/vcs.go +++ b/cmd/vcs.go @@ -9,22 +9,10 @@ import ( "path/filepath" "strconv" "strings" - - gap "github.com/muesli/go-app-paths" ) -const storeDirName = "stores" - 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 + return (&Store{}).path() } func ensureVCSInitialized() (string, error) { @@ -47,10 +35,8 @@ func writeGitignore(repoDir string) error { content := strings.Join([]string{ "# generated by pda", "*", - "!/", "!.gitignore", - "!" + storeDirName + "/", - "!" + storeDirName + "/*", + "!*.ndjson", "", }, "\n") if err := os.WriteFile(path, []byte(content), 0o640); err != nil { @@ -66,71 +52,6 @@ func writeGitignore(repoDir string) error { 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 { cmd := exec.Command("git", args...) cmd.Dir = dir @@ -288,66 +209,6 @@ func currentBranch(dir string) (string, error) { 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 { dbs, err := store.AllStores() if err != nil {