refactor(massive simplification of vcs now that we're using ndjson natively):
This commit is contained in:
parent
84c55311d1
commit
cb441b112c
4 changed files with 61 additions and 203 deletions
14
cmd/git.go
14
cmd/git.go
|
|
@ -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,
|
||||||
|
|
|
||||||
96
cmd/init.go
96
cmd/init.go
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
cmd/sync.go
11
cmd/sync.go
|
|
@ -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)
|
||||||
|
|
|
||||||
143
cmd/vcs.go
143
cmd/vcs.go
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue