Make add idempotent: skip already-tracked files instead of erroring.

Enables glob workflows like `sgard add ~/.config/mcp/services/*` to
pick up new files without failing on ones already tracked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 09:52:37 -07:00
parent de5759ac77
commit 7713d071c2
4 changed files with 20 additions and 8 deletions

View File

@@ -7,9 +7,9 @@ ARCHITECTURE.md for design details.
## Current Status ## Current Status
**Phase:** Phase 5 complete. File exclusion feature added. **Phase:** Phase 5 complete. File exclusion feature added. Add is now idempotent.
**Last updated:** 2026-03-27 **Last updated:** 2026-03-30
## Completed Steps ## Completed Steps
@@ -114,3 +114,4 @@ Phase 6: Manifest Signing (to be planned).
| 2026-03-26 | — | `sgard list` remote support: uses `resolveRemoteConfig()` to list server manifest via `PullManifest` RPC. Client `List()` method added. | | 2026-03-26 | — | `sgard list` remote support: uses `resolveRemoteConfig()` to list server manifest via `PullManifest` RPC. Client `List()` method added. |
| 2026-03-26 | — | Version derived from git tags via `VERSION` file. flake.nix reads `VERSION`; Makefile `version` target syncs from latest tag, `build` injects via ldflags. | | 2026-03-26 | — | Version derived from git tags via `VERSION` file. flake.nix reads `VERSION`; Makefile `version` target syncs from latest tag, `build` injects via ldflags. |
| 2026-03-27 | — | File exclusion: `sgard exclude`/`include` commands, `Manifest.Exclude` field, Add/MirrorUp/MirrorDown respect exclusions, directory exclusion support. 8 tests. | | 2026-03-27 | — | File exclusion: `sgard exclude`/`include` commands, `Manifest.Exclude` field, Add/MirrorUp/MirrorDown respect exclusions, directory exclusion support. 8 tests. |
| 2026-03-30 | — | Idempotent add: `sgard add` silently skips already-tracked files/directories instead of erroring, enabling glob-based workflows. |

View File

@@ -1 +1 @@
3.2.0 3.2.1

View File

@@ -239,7 +239,7 @@ func (g *Garden) Add(paths []string, opts ...AddOptions) error {
// Track the directory itself as a structural entry. // Track the directory itself as a structural entry.
tilded := toTildePath(abs) tilded := toTildePath(abs)
if g.findEntry(tilded) != nil { if g.findEntry(tilded) != nil {
return fmt.Errorf("already tracking %s", tilded) continue
} }
entry := manifest.Entry{ entry := manifest.Entry{
Path: tilded, Path: tilded,
@@ -277,7 +277,7 @@ func (g *Garden) Add(paths []string, opts ...AddOptions) error {
} }
} }
} else { } else {
if err := g.addEntry(abs, info, now, false, o); err != nil { if err := g.addEntry(abs, info, now, true, o); err != nil {
return err return err
} }
} }

View File

@@ -200,7 +200,7 @@ func TestAddSymlink(t *testing.T) {
} }
} }
func TestAddDuplicateRejected(t *testing.T) { func TestAddDuplicateIsIdempotent(t *testing.T) {
root := t.TempDir() root := t.TempDir()
repoDir := filepath.Join(root, "repo") repoDir := filepath.Join(root, "repo")
@@ -218,8 +218,19 @@ func TestAddDuplicateRejected(t *testing.T) {
t.Fatalf("first Add: %v", err) t.Fatalf("first Add: %v", err)
} }
if err := g.Add([]string{testFile}); err == nil { if err := g.Add([]string{testFile}); err != nil {
t.Fatal("second Add of same path should fail") t.Fatalf("second Add of same path should be idempotent: %v", err)
}
entries := g.List()
count := 0
for _, e := range entries {
if e.Path == toTildePath(testFile) {
count++
}
}
if count != 1 {
t.Fatalf("expected 1 entry, got %d", count)
} }
} }