Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions internal/restore/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ func Run(s *session.Session, dir Direction, opts Options) (*Result, error) {
done = false
break
}
if exists(field(1)) {
skip("backup still holds the deleted file (this entry was never undone)")
continue
}
if !act() {
continue
}
Expand Down Expand Up @@ -256,6 +260,10 @@ func Run(s *session.Session, dir Direction, opts Options) (*Result, error) {
skip("target occupied, use --force to overwrite")
continue
}
if exists(new_) && hasBak && exists(bak) {
skip("backup still holds the clobbered target (this entry was never undone)")
continue
}
if !act() {
continue
}
Expand Down
57 changes: 57 additions & 0 deletions internal/restore/restore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,34 @@ func TestUndoUnlinkConflictSkipsWithoutForce(t *testing.T) {
}
}

func TestRedoUnlinkKeepsOccupiedBackup(t *testing.T) {
work := t.TempDir()
victim := filepath.Join(work, "back.txt")
write(t, victim, "recreated since") // path exists again
s := newSession(t, nil)
backup := filepath.Join(s.Dir, "data", "b1")
write(t, backup, "the only copy")
s.Entries = []journal.Entry{{Op: journal.OpUnlink, Fields: []string{victim, backup}}}

// the undo skips, so the backup still holds the deleted file
res, _ := Run(s, Undo, Options{})
if res.Done != 0 || len(res.Skipped) != 1 {
t.Fatalf("want 0 done / 1 skipped, got %d done / %d skipped", res.Done, len(res.Skipped))
}

// redoing must not park the live file on top of it
res, _ = Run(s, Redo, Options{})
if res.Done != 0 || len(res.Skipped) != 1 {
t.Fatalf("want 0 done / 1 skipped, got %d done / %d skipped", res.Done, len(res.Skipped))
}
if got := read(t, backup); got != "the only copy" {
t.Errorf("redo overwrote the backup: %q", got)
}
if got := read(t, victim); got != "recreated since" {
t.Errorf("redo moved the live file away: %q", got)
}
}

func TestModSwapsBothDirections(t *testing.T) {
work := t.TempDir()
file := filepath.Join(work, "config.yaml")
Expand Down Expand Up @@ -153,6 +181,35 @@ func TestRenameUndoRestoresBothSides(t *testing.T) {
}
}

func TestRedoRenameKeepsOccupiedBackup(t *testing.T) {
work := t.TempDir()
oldp := filepath.Join(work, "a.txt")
newp := filepath.Join(work, "b.txt")
write(t, oldp, "recreated since") // the source path exists again
write(t, newp, "moved content")
s := newSession(t, nil)
backup := filepath.Join(s.Dir, "data", "b1")
write(t, backup, "the only copy of b")
s.Entries = []journal.Entry{{Op: journal.OpRename, Fields: []string{oldp, newp, backup}}}

// the undo skips, so the backup still holds what the mv clobbered
res, _ := Run(s, Undo, Options{})
if res.Done != 0 || len(res.Skipped) != 1 {
t.Fatalf("want 0 done / 1 skipped, got %d done / %d skipped", res.Done, len(res.Skipped))
}

res, _ = Run(s, Redo, Options{})
if res.Done != 0 || len(res.Skipped) != 1 {
t.Fatalf("want 0 done / 1 skipped, got %d done / %d skipped", res.Done, len(res.Skipped))
}
if got := read(t, backup); got != "the only copy of b" {
t.Errorf("redo overwrote the backup: %q", got)
}
if got := read(t, newp); got != "moved content" {
t.Errorf("redo moved b.txt away: %q", got)
}
}

func TestRmdirUndoRecreatesDirectory(t *testing.T) {
work := t.TempDir()
gone := filepath.Join(work, "sub")
Expand Down