diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-22 16:52:37 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-22 16:52:37 +0200 |
| commit | bcb07f5587c310063b74d280f7e82aa47a132c39 (patch) | |
| tree | 15ec499cf9acdde3b3876f3fd1cf9316a602cf6d /internal/store/store.go | |
| parent | bb5ce162f82417191b80c04f69193d1a8af6b3d8 (diff) | |
Address store package review findings (task 352/store)
Fix CommitIndex to respect force=false by checking os.Stat before writing,
mirroring the Data.Commit behaviour and keeping index/data pairs consistent.
Skip .git directory in WalkIndexes via filepath.SkipDir to avoid spurious
errors or false matches inside the git metadata tree.
Make ShredAllExported continue past individual shred errors and return the
last error, matching Ruby's best-effort shredding behaviour.
Accept io.Reader in Store.Remove instead of hardwiring os.Stdin, enabling
deterministic testing via strings.NewReader injection.
Fix runFzf comment to state that any non-zero fzf exit is treated as no
selection (not only exit 130).
Document ImportRecursive divergence from Ruby basename-flattening behaviour.
Add 14 new tests: Search, SearchActionCat, SearchActionCatBinarySkip,
ShredAllExported, SearchActionExport, ImportRecursive, ReimportAfterExport,
RemoveInteractive, RemoveInteractiveDecline, CommitIndexSkipsExisting,
LoadIndexMissingFile, LoadIndexCorrupted, LoadDataMissingFile,
LoadDataCorrupted, DataExportUnwritable, HashPathEdgeCases,
ImportMissingSourceFile, WalkIndexesInvalidRegex.
Improve TestIndexSort to call sort.Sort and assert final order.
Remove orphaned section-header comment from store_test.go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/store/store.go')
| -rw-r--r-- | internal/store/store.go | 36 |
1 files changed, 28 insertions, 8 deletions
diff --git a/internal/store/store.go b/internal/store/store.go index 114b597..fe9f132 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -89,7 +89,16 @@ func (s *Store) WalkIndexes(ctx context.Context, searchTerm string, fn func(*Ind if err != nil { return err } - if d.IsDir() || !strings.HasSuffix(path, ".index") { + // Skip the .git directory entirely — the data directory is a git repo + // but no secrets live inside .git, so descending into it is wasteful + // and may surface spurious errors if any path happens to end in ".index". + if d.IsDir() { + if d.Name() == ".git" { + return filepath.SkipDir + } + return nil + } + if !strings.HasSuffix(path, ".index") { return nil } return s.processIndexFile(ctx, path, searchTerm, regex, fn) @@ -241,7 +250,8 @@ func runFzf(ctx context.Context, entries []string) (string, error) { cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - // fzf exits 130 when the user presses Escape — treat as no selection. + // Any non-zero exit from fzf (e.g., 130 for Escape, 1 for no match) + // is treated as no selection — the caller receives ("", nil). return "", nil } @@ -297,6 +307,10 @@ func (s *Store) Import(ctx context.Context, srcPath, destPath string, force bool // ImportRecursive walks directory and imports every regular file under destDir. // The description for each file is its path relative to the source directory. +// Note: the Ruby import_recursive flattens subdirectories to basename in the +// hash/storage path while preserving the full relative path only in the +// description. Go preserves the full subpath in both description and hash path. +// The compatibility verification task (355) will surface any impact on live data. func (s *Store) ImportRecursive(ctx context.Context, directory, destDir string) error { return filepath.WalkDir(directory, func(path string, d os.DirEntry, err error) error { if err != nil { @@ -316,7 +330,8 @@ func (s *Store) ImportRecursive(ctx context.Context, directory, destDir string) // Remove finds all indexes matching searchTerm, prints each one, and prompts // the user interactively before deleting the index+data pair. Mirrors Ruby's rm. -func (s *Store) Remove(ctx context.Context, searchTerm string) error { +// Pass os.Stdin as the reader for interactive use; a strings.Reader in tests. +func (s *Store) Remove(ctx context.Context, searchTerm string, input io.Reader) error { var indexes IndexSlice if err := s.WalkIndexes(ctx, searchTerm, func(idx *Index) error { indexes = append(indexes, idx) @@ -327,7 +342,7 @@ func (s *Store) Remove(ctx context.Context, searchTerm string) error { sort.Sort(indexes) - scanner := bufio.NewScanner(os.Stdin) + scanner := bufio.NewScanner(input) for _, idx := range indexes { if err := s.confirmAndRemove(ctx, idx, scanner); err != nil { return err @@ -366,22 +381,27 @@ func (s *Store) confirmAndRemove(ctx context.Context, idx *Index, scanner *bufio // ShredAllExported removes (shreds) every regular file in cfg.ExportDir. // Uses GNU shred when available; falls back to "rm -Pfv" otherwise. +// Mirrors Ruby's shred_all_exported: iterates all files and returns the last +// non-nil error so that as many files as possible are shredded even on failure. func (s *Store) ShredAllExported(ctx context.Context) error { entries, err := filepath.Glob(filepath.Join(s.cfg.ExportDir, "*")) if err != nil { return fmt.Errorf("listing export dir: %w", err) } + var lastErr error for _, entry := range entries { info, err := os.Stat(entry) if err != nil || !info.Mode().IsRegular() { continue } if err := shredFile(ctx, entry); err != nil { - return err + // Record the error but keep shredding — security demands best-effort + // destruction of all exported secrets even if one fails. + lastErr = err } } - return nil + return lastErr } // shredFile destroys a single file using shred(1) if available, or rm -Pfv. @@ -404,8 +424,8 @@ func shredFile(ctx context.Context, filePath string) error { func (s *Store) buildPair(description, hash string) (*Index, *Data) { indexPath := filepath.Join(s.cfg.DataDir, hash+".index") dataPath := filepath.Join(s.cfg.DataDir, hash+".data") - hashBase := filepath.Base(hash + ".index") - hashBase = strings.TrimSuffix(hashBase, ".index") + // filepath.Base of the hash gives the final path component (the filename stem). + hashBase := filepath.Base(hash) idx := &Index{ Description: description, |
