Add directory recursion for Add and mirror up/down commands.
Add now recursively walks directories instead of creating a single "directory" type entry. Extract addEntry helper for reuse. Implement MirrorUp (sync filesystem state into manifest) and MirrorDown (sync manifest state to filesystem with untracked file cleanup). Add CLI mirror command with up/down subcommands. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
102
garden/garden.go
102
garden/garden.go
@@ -98,9 +98,55 @@ func (g *Garden) SetClock(c clockwork.Clock) {
|
||||
g.clock = c
|
||||
}
|
||||
|
||||
// addEntry adds a single file or symlink to the manifest. The abs path must
|
||||
// already be resolved and info must come from os.Lstat. If skipDup is true,
|
||||
// already-tracked paths are silently skipped instead of returning an error.
|
||||
func (g *Garden) addEntry(abs string, info os.FileInfo, now time.Time, skipDup bool) error {
|
||||
tilded := toTildePath(abs)
|
||||
|
||||
if g.findEntry(tilded) != nil {
|
||||
if skipDup {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("already tracking %s", tilded)
|
||||
}
|
||||
|
||||
entry := manifest.Entry{
|
||||
Path: tilded,
|
||||
Mode: fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
Updated: now,
|
||||
}
|
||||
|
||||
switch {
|
||||
case info.Mode()&os.ModeSymlink != 0:
|
||||
target, err := os.Readlink(abs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading symlink %s: %w", abs, err)
|
||||
}
|
||||
entry.Type = "link"
|
||||
entry.Target = target
|
||||
|
||||
default:
|
||||
data, err := os.ReadFile(abs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading file %s: %w", abs, err)
|
||||
}
|
||||
hash, err := g.store.Write(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("storing blob for %s: %w", abs, err)
|
||||
}
|
||||
entry.Type = "file"
|
||||
entry.Hash = hash
|
||||
}
|
||||
|
||||
g.manifest.Files = append(g.manifest.Files, entry)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add tracks new files, directories, or symlinks. Each path is resolved
|
||||
// to an absolute path, inspected for its type, and added to the manifest.
|
||||
// Regular files are hashed and stored in the blob store.
|
||||
// Regular files are hashed and stored in the blob store. Directories are
|
||||
// recursively walked and all leaf files and symlinks are added individually.
|
||||
func (g *Garden) Add(paths []string) error {
|
||||
now := g.clock.Now().UTC()
|
||||
|
||||
@@ -115,45 +161,29 @@ func (g *Garden) Add(paths []string) error {
|
||||
return fmt.Errorf("stat %s: %w", abs, err)
|
||||
}
|
||||
|
||||
tilded := toTildePath(abs)
|
||||
|
||||
// Check if already tracked.
|
||||
if g.findEntry(tilded) != nil {
|
||||
return fmt.Errorf("already tracking %s", tilded)
|
||||
}
|
||||
|
||||
entry := manifest.Entry{
|
||||
Path: tilded,
|
||||
Mode: fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
Updated: now,
|
||||
}
|
||||
|
||||
switch {
|
||||
case info.Mode()&os.ModeSymlink != 0:
|
||||
target, err := os.Readlink(abs)
|
||||
if info.IsDir() {
|
||||
// Recursively walk the directory, adding all files and symlinks.
|
||||
err := filepath.WalkDir(abs, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil // skip directory entries themselves
|
||||
}
|
||||
fi, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stat %s: %w", path, err)
|
||||
}
|
||||
return g.addEntry(path, fi, now, true)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading symlink %s: %w", abs, err)
|
||||
return fmt.Errorf("walking directory %s: %w", abs, err)
|
||||
}
|
||||
entry.Type = "link"
|
||||
entry.Target = target
|
||||
|
||||
case info.IsDir():
|
||||
entry.Type = "directory"
|
||||
|
||||
default:
|
||||
data, err := os.ReadFile(abs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading file %s: %w", abs, err)
|
||||
} else {
|
||||
if err := g.addEntry(abs, info, now, false); err != nil {
|
||||
return err
|
||||
}
|
||||
hash, err := g.store.Write(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("storing blob for %s: %w", abs, err)
|
||||
}
|
||||
entry.Type = "file"
|
||||
entry.Hash = hash
|
||||
}
|
||||
|
||||
g.manifest.Files = append(g.manifest.Files, entry)
|
||||
}
|
||||
|
||||
g.manifest.Updated = now
|
||||
|
||||
@@ -135,17 +135,29 @@ func TestAddDirectory(t *testing.T) {
|
||||
if err := os.Mkdir(testDir, 0o755); err != nil {
|
||||
t.Fatalf("creating test dir: %v", err)
|
||||
}
|
||||
testFile := filepath.Join(testDir, "inside.txt")
|
||||
if err := os.WriteFile(testFile, []byte("inside"), 0o644); err != nil {
|
||||
t.Fatalf("writing file inside dir: %v", err)
|
||||
}
|
||||
|
||||
if err := g.Add([]string{testDir}); err != nil {
|
||||
t.Fatalf("Add: %v", err)
|
||||
}
|
||||
|
||||
entry := g.manifest.Files[0]
|
||||
if entry.Type != "directory" {
|
||||
t.Errorf("expected type directory, got %s", entry.Type)
|
||||
if len(g.manifest.Files) != 1 {
|
||||
t.Fatalf("expected 1 file, got %d", len(g.manifest.Files))
|
||||
}
|
||||
if entry.Hash != "" {
|
||||
t.Error("directories should have no hash")
|
||||
|
||||
entry := g.manifest.Files[0]
|
||||
if entry.Type != "file" {
|
||||
t.Errorf("expected type file, got %s", entry.Type)
|
||||
}
|
||||
if entry.Hash == "" {
|
||||
t.Error("expected non-empty hash")
|
||||
}
|
||||
expectedPath := toTildePath(testFile)
|
||||
if entry.Path != expectedPath {
|
||||
t.Errorf("expected path %s, got %s", expectedPath, entry.Path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
187
garden/mirror.go
Normal file
187
garden/mirror.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package garden
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MirrorUp synchronises the manifest with the current filesystem state for
|
||||
// each given directory path. New files/symlinks are added, deleted files are
|
||||
// removed from the manifest, and changed files are re-hashed.
|
||||
func (g *Garden) MirrorUp(paths []string) error {
|
||||
now := g.clock.Now().UTC()
|
||||
|
||||
for _, p := range paths {
|
||||
abs, err := filepath.Abs(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving path %s: %w", p, err)
|
||||
}
|
||||
|
||||
tildePrefix := toTildePath(abs)
|
||||
// Ensure we match entries *under* the directory, not just the dir itself.
|
||||
if !strings.HasSuffix(tildePrefix, "/") {
|
||||
tildePrefix += "/"
|
||||
}
|
||||
|
||||
// 1. Walk the directory and add any new files/symlinks.
|
||||
err = filepath.WalkDir(abs, func(path string, d os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
fi, lstatErr := os.Lstat(path)
|
||||
if lstatErr != nil {
|
||||
return fmt.Errorf("stat %s: %w", path, lstatErr)
|
||||
}
|
||||
return g.addEntry(path, fi, now, true)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("walking directory %s: %w", abs, err)
|
||||
}
|
||||
|
||||
// 2. Remove manifest entries whose files no longer exist on disk.
|
||||
kept := g.manifest.Files[:0]
|
||||
for _, e := range g.manifest.Files {
|
||||
if strings.HasPrefix(e.Path, tildePrefix) {
|
||||
expanded, err := ExpandTildePath(e.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expanding path %s: %w", e.Path, err)
|
||||
}
|
||||
if _, err := os.Lstat(expanded); err != nil {
|
||||
// File no longer exists — drop entry.
|
||||
continue
|
||||
}
|
||||
}
|
||||
kept = append(kept, e)
|
||||
}
|
||||
g.manifest.Files = kept
|
||||
|
||||
// 3. Re-hash remaining file entries under the prefix (like Checkpoint).
|
||||
for i := range g.manifest.Files {
|
||||
entry := &g.manifest.Files[i]
|
||||
if !strings.HasPrefix(entry.Path, tildePrefix) {
|
||||
continue
|
||||
}
|
||||
if entry.Type != "file" {
|
||||
continue
|
||||
}
|
||||
|
||||
expanded, err := ExpandTildePath(entry.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expanding path %s: %w", entry.Path, err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(expanded)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %s: %w", expanded, err)
|
||||
}
|
||||
hash, err := g.store.Write(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("storing blob for %s: %w", expanded, err)
|
||||
}
|
||||
if hash != entry.Hash {
|
||||
entry.Hash = hash
|
||||
entry.Updated = now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.manifest.Updated = now
|
||||
if err := g.manifest.Save(g.manifestPath); err != nil {
|
||||
return fmt.Errorf("saving manifest: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MirrorDown synchronises the filesystem with the manifest for each given
|
||||
// directory path. Tracked entries are restored and untracked files on disk
|
||||
// are deleted. If force is false, confirm is called before each deletion;
|
||||
// a false return skips that file.
|
||||
func (g *Garden) MirrorDown(paths []string, force bool, confirm func(string) bool) error {
|
||||
for _, p := range paths {
|
||||
abs, err := filepath.Abs(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving path %s: %w", p, err)
|
||||
}
|
||||
|
||||
tildePrefix := toTildePath(abs)
|
||||
if !strings.HasSuffix(tildePrefix, "/") {
|
||||
tildePrefix += "/"
|
||||
}
|
||||
|
||||
// 1. Collect manifest entries under this prefix.
|
||||
tracked := make(map[string]bool)
|
||||
for i := range g.manifest.Files {
|
||||
entry := &g.manifest.Files[i]
|
||||
if !strings.HasPrefix(entry.Path, tildePrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
expanded, err := ExpandTildePath(entry.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expanding path %s: %w", entry.Path, err)
|
||||
}
|
||||
tracked[expanded] = true
|
||||
|
||||
// Create parent directories.
|
||||
dir := filepath.Dir(expanded)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("creating directory %s: %w", dir, err)
|
||||
}
|
||||
|
||||
// Restore the entry.
|
||||
switch entry.Type {
|
||||
case "file":
|
||||
if err := g.restoreFile(expanded, entry); err != nil {
|
||||
return err
|
||||
}
|
||||
case "link":
|
||||
if err := restoreLink(expanded, entry); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Walk disk and delete files not in manifest.
|
||||
var emptyDirs []string
|
||||
err = filepath.WalkDir(abs, func(path string, d os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if d.IsDir() {
|
||||
// Collect directories for potential cleanup (post-order).
|
||||
if path != abs {
|
||||
emptyDirs = append(emptyDirs, path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if tracked[path] {
|
||||
return nil
|
||||
}
|
||||
// Untracked file/symlink on disk.
|
||||
if !force {
|
||||
if confirm == nil || !confirm(path) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
_ = os.Remove(path)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("walking directory %s: %w", abs, err)
|
||||
}
|
||||
|
||||
// 3. Clean up empty directories (reverse order so children come first).
|
||||
for i := len(emptyDirs) - 1; i >= 0; i-- {
|
||||
// os.Remove only removes empty directories.
|
||||
_ = os.Remove(emptyDirs[i])
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
297
garden/mirror_test.go
Normal file
297
garden/mirror_test.go
Normal file
@@ -0,0 +1,297 @@
|
||||
package garden
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAddRecursesDirectory(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
repoDir := filepath.Join(root, "repo")
|
||||
|
||||
g, err := Init(repoDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
// Create a directory tree with nested files.
|
||||
dir := filepath.Join(root, "dotfiles")
|
||||
if err := os.MkdirAll(filepath.Join(dir, "sub"), 0o755); err != nil {
|
||||
t.Fatalf("creating dirs: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "a.conf"), []byte("aaa"), 0o644); err != nil {
|
||||
t.Fatalf("writing a.conf: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "sub", "b.conf"), []byte("bbb"), 0o644); err != nil {
|
||||
t.Fatalf("writing b.conf: %v", err)
|
||||
}
|
||||
|
||||
if err := g.Add([]string{dir}); err != nil {
|
||||
t.Fatalf("Add: %v", err)
|
||||
}
|
||||
|
||||
if len(g.manifest.Files) != 2 {
|
||||
t.Fatalf("expected 2 files, got %d", len(g.manifest.Files))
|
||||
}
|
||||
|
||||
for _, e := range g.manifest.Files {
|
||||
if e.Type == "directory" {
|
||||
t.Errorf("should not have directory type entries, got %+v", e)
|
||||
}
|
||||
if e.Type != "file" {
|
||||
t.Errorf("expected type file, got %s for %s", e.Type, e.Path)
|
||||
}
|
||||
if e.Hash == "" {
|
||||
t.Errorf("expected non-empty hash for %s", e.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddRecursesSkipsDuplicates(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
repoDir := filepath.Join(root, "repo")
|
||||
|
||||
g, err := Init(repoDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
dir := filepath.Join(root, "dotfiles")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("creating dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "f.txt"), []byte("data"), 0o644); err != nil {
|
||||
t.Fatalf("writing file: %v", err)
|
||||
}
|
||||
|
||||
if err := g.Add([]string{dir}); err != nil {
|
||||
t.Fatalf("first Add: %v", err)
|
||||
}
|
||||
|
||||
// Second add of the same directory should not error or create duplicates.
|
||||
if err := g.Add([]string{dir}); err != nil {
|
||||
t.Fatalf("second Add should not error: %v", err)
|
||||
}
|
||||
|
||||
if len(g.manifest.Files) != 1 {
|
||||
t.Errorf("expected 1 entry, got %d", len(g.manifest.Files))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMirrorUpAddsNew(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
repoDir := filepath.Join(root, "repo")
|
||||
|
||||
g, err := Init(repoDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
dir := filepath.Join(root, "dotfiles")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("creating dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "existing.txt"), []byte("old"), 0o644); err != nil {
|
||||
t.Fatalf("writing file: %v", err)
|
||||
}
|
||||
|
||||
if err := g.Add([]string{dir}); err != nil {
|
||||
t.Fatalf("Add: %v", err)
|
||||
}
|
||||
|
||||
if len(g.manifest.Files) != 1 {
|
||||
t.Fatalf("expected 1 file after Add, got %d", len(g.manifest.Files))
|
||||
}
|
||||
|
||||
// Create a new file inside the directory.
|
||||
if err := os.WriteFile(filepath.Join(dir, "new.txt"), []byte("new"), 0o644); err != nil {
|
||||
t.Fatalf("writing new file: %v", err)
|
||||
}
|
||||
|
||||
if err := g.MirrorUp([]string{dir}); err != nil {
|
||||
t.Fatalf("MirrorUp: %v", err)
|
||||
}
|
||||
|
||||
if len(g.manifest.Files) != 2 {
|
||||
t.Fatalf("expected 2 files after MirrorUp, got %d", len(g.manifest.Files))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMirrorUpRemovesDeleted(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
repoDir := filepath.Join(root, "repo")
|
||||
|
||||
g, err := Init(repoDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
dir := filepath.Join(root, "dotfiles")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("creating dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "keep.txt"), []byte("keep"), 0o644); err != nil {
|
||||
t.Fatalf("writing keep file: %v", err)
|
||||
}
|
||||
deleteFile := filepath.Join(dir, "delete.txt")
|
||||
if err := os.WriteFile(deleteFile, []byte("delete"), 0o644); err != nil {
|
||||
t.Fatalf("writing delete file: %v", err)
|
||||
}
|
||||
|
||||
if err := g.Add([]string{dir}); err != nil {
|
||||
t.Fatalf("Add: %v", err)
|
||||
}
|
||||
|
||||
if len(g.manifest.Files) != 2 {
|
||||
t.Fatalf("expected 2 files, got %d", len(g.manifest.Files))
|
||||
}
|
||||
|
||||
// Delete one file from disk.
|
||||
_ = os.Remove(deleteFile)
|
||||
|
||||
if err := g.MirrorUp([]string{dir}); err != nil {
|
||||
t.Fatalf("MirrorUp: %v", err)
|
||||
}
|
||||
|
||||
if len(g.manifest.Files) != 1 {
|
||||
t.Fatalf("expected 1 file after MirrorUp, got %d", len(g.manifest.Files))
|
||||
}
|
||||
|
||||
if g.manifest.Files[0].Path != toTildePath(filepath.Join(dir, "keep.txt")) {
|
||||
t.Errorf("remaining entry should be keep.txt, got %s", g.manifest.Files[0].Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMirrorUpRehashesChanged(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
repoDir := filepath.Join(root, "repo")
|
||||
|
||||
g, err := Init(repoDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
dir := filepath.Join(root, "dotfiles")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("creating dir: %v", err)
|
||||
}
|
||||
f := filepath.Join(dir, "config.txt")
|
||||
if err := os.WriteFile(f, []byte("original"), 0o644); err != nil {
|
||||
t.Fatalf("writing file: %v", err)
|
||||
}
|
||||
|
||||
if err := g.Add([]string{dir}); err != nil {
|
||||
t.Fatalf("Add: %v", err)
|
||||
}
|
||||
|
||||
origHash := g.manifest.Files[0].Hash
|
||||
|
||||
// Modify the file.
|
||||
if err := os.WriteFile(f, []byte("modified"), 0o644); err != nil {
|
||||
t.Fatalf("modifying file: %v", err)
|
||||
}
|
||||
|
||||
if err := g.MirrorUp([]string{dir}); err != nil {
|
||||
t.Fatalf("MirrorUp: %v", err)
|
||||
}
|
||||
|
||||
if g.manifest.Files[0].Hash == origHash {
|
||||
t.Error("MirrorUp did not update hash for modified file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMirrorDownRestoresAndCleans(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
repoDir := filepath.Join(root, "repo")
|
||||
|
||||
g, err := Init(repoDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
dir := filepath.Join(root, "dotfiles")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("creating dir: %v", err)
|
||||
}
|
||||
tracked := filepath.Join(dir, "tracked.txt")
|
||||
if err := os.WriteFile(tracked, []byte("tracked"), 0o644); err != nil {
|
||||
t.Fatalf("writing tracked file: %v", err)
|
||||
}
|
||||
|
||||
if err := g.Add([]string{dir}); err != nil {
|
||||
t.Fatalf("Add: %v", err)
|
||||
}
|
||||
if err := g.Checkpoint(""); err != nil {
|
||||
t.Fatalf("Checkpoint: %v", err)
|
||||
}
|
||||
|
||||
// Modify the tracked file and create an untracked file.
|
||||
if err := os.WriteFile(tracked, []byte("overwritten"), 0o644); err != nil {
|
||||
t.Fatalf("modifying tracked file: %v", err)
|
||||
}
|
||||
extra := filepath.Join(dir, "extra.txt")
|
||||
if err := os.WriteFile(extra, []byte("extra"), 0o644); err != nil {
|
||||
t.Fatalf("writing extra file: %v", err)
|
||||
}
|
||||
|
||||
if err := g.MirrorDown([]string{dir}, true, nil); err != nil {
|
||||
t.Fatalf("MirrorDown: %v", err)
|
||||
}
|
||||
|
||||
// Tracked file should be restored to original content.
|
||||
got, err := os.ReadFile(tracked)
|
||||
if err != nil {
|
||||
t.Fatalf("reading tracked file: %v", err)
|
||||
}
|
||||
if string(got) != "tracked" {
|
||||
t.Errorf("tracked file content = %q, want %q", got, "tracked")
|
||||
}
|
||||
|
||||
// Extra file should be deleted.
|
||||
if _, err := os.Stat(extra); err == nil {
|
||||
t.Error("extra file should have been deleted by MirrorDown with force")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMirrorDownConfirmSkips(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
repoDir := filepath.Join(root, "repo")
|
||||
|
||||
g, err := Init(repoDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
dir := filepath.Join(root, "dotfiles")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("creating dir: %v", err)
|
||||
}
|
||||
tracked := filepath.Join(dir, "tracked.txt")
|
||||
if err := os.WriteFile(tracked, []byte("tracked"), 0o644); err != nil {
|
||||
t.Fatalf("writing tracked file: %v", err)
|
||||
}
|
||||
|
||||
if err := g.Add([]string{dir}); err != nil {
|
||||
t.Fatalf("Add: %v", err)
|
||||
}
|
||||
if err := g.Checkpoint(""); err != nil {
|
||||
t.Fatalf("Checkpoint: %v", err)
|
||||
}
|
||||
|
||||
// Create an untracked file.
|
||||
extra := filepath.Join(dir, "extra.txt")
|
||||
if err := os.WriteFile(extra, []byte("extra"), 0o644); err != nil {
|
||||
t.Fatalf("writing extra file: %v", err)
|
||||
}
|
||||
|
||||
// Confirm returns false — should NOT delete.
|
||||
alwaysNo := func(path string) bool { return false }
|
||||
if err := g.MirrorDown([]string{dir}, false, alwaysNo); err != nil {
|
||||
t.Fatalf("MirrorDown: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(extra); err != nil {
|
||||
t.Error("extra file should NOT have been deleted when confirm returns false")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user