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
27 changes: 21 additions & 6 deletions file_handling.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,33 @@ func (f *File) Write(content string) error {
return err
}

tempName := filepath.Join(f.Dir(), RandomString(20))
if err := os.WriteFile(tempName, []byte(content), mode); err != nil {
tempFile, err := os.CreateTemp(f.Dir(), ".find-replace-*")
if err != nil {
return fmt.Errorf("create tempfile in %v: %w", f.Dir(), err)
}
// Make sure the temp file is removed if the rename below fails. On
// success, the rename has already moved the file to f.Path so this is
// a no-op (we deliberately ignore the not-exist error).
defer os.Remove(tempName)
tempName := tempFile.Name()
removeTemp := true
defer func() {
if removeTemp {
_ = os.Remove(tempName)
}
}()
if err := tempFile.Chmod(mode); err != nil {
_ = tempFile.Close()
return fmt.Errorf("chmod temp file %v: %w", tempName, err)
}
if _, err := tempFile.WriteString(content); err != nil {
_ = tempFile.Close()
return fmt.Errorf("write temp file %v: %w", tempName, err)
}
if err := tempFile.Close(); err != nil {
return fmt.Errorf("close temp file %v: %w", tempName, err)
}

log.Printf("Rewriting %v", f.Path)
if err := os.Rename(tempName, f.Path); err != nil {
return fmt.Errorf("atomically move temp file %v to %v: %w", tempName, f.Path, err)
}
removeTemp = false
return nil
}
26 changes: 26 additions & 0 deletions file_handling_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"os"
"path/filepath"
"testing"
)
Expand Down Expand Up @@ -77,3 +78,28 @@ func TestNewFile(t *testing.T) {
})
}
}

func TestWriteUsesCreateTempPrefix(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "target.txt")
if err := os.WriteFile(path, []byte("old"), 0o644); err != nil {
t.Fatal(err)
}
f, err := NewFile(path)
if err != nil {
t.Fatal(err)
}
if err := f.Write("new"); err != nil {
t.Fatal(err)
}
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
for _, e := range entries {
if e.Name() != "target.txt" {
t.Fatalf("unexpected leftover file %q", e.Name())
}
}
}

7 changes: 7 additions & 0 deletions find_replace.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ func (fr *findReplace) WalkDir(f *File) {
}

for _, file := range files {
if strings.HasPrefix(file.Name(), ".find-replace-") {
continue
Comment on lines +127 to +128
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve traversal of matching user directories

This skips every entry whose name starts with .find-replace- before checking whether it is a stale temp file or a directory. The temp files introduced here are created with os.CreateTemp, so orphaned entries from this tool are regular files; a real directory such as .find-replace-config/ will now be omitted entirely and none of its contents or child names will be processed, even though the CLI only documents skipping .git/ and binary files.

Useful? React with 👍 / 👎.

}
childPath := filepath.Join(f.Path, file.Name())
childFile, err := NewFile(childPath)
if err != nil {
Expand Down Expand Up @@ -156,6 +159,10 @@ func (fr *findReplace) HandleFile(f *File) error {
return err
}

if strings.HasPrefix(f.Base(), ".find-replace-") {
return nil
}

// If file is a directory, recurse immediately (depth-first).
if info.IsDir() {
// Ignore certain directories
Expand Down
39 changes: 39 additions & 0 deletions find_replace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -684,3 +684,42 @@ func BenchmarkNova(b *testing.B) {
fr.WalkDir(d)
}
}

func TestSkipStaleTempFiles(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "keep.txt"), []byte("alpha"), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, ".find-replace-stale"), []byte("alpha"), 0644); err != nil {
t.Fatal(err)
}

orig, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(orig) })

code := run([]string{"find-replace", "alpha", "beta"}, ioDiscard{t})
if code != 0 {
t.Fatalf("run exit = %d; want 0", code)
}

if _, err := os.Stat(filepath.Join(dir, ".find-replace-stale")); err != nil {
t.Fatalf("stale temp file should remain untouched: %v", err)
}
got, err := os.ReadFile(filepath.Join(dir, "keep.txt"))
if err != nil {
t.Fatal(err)
}
if string(got) != "beta" {
t.Fatalf("keep.txt = %q; want beta", got)
}
}

type ioDiscard struct{ t *testing.T }

func (d ioDiscard) Write(p []byte) (int, error) { return len(p), nil }