From 7713d071c2146de586b9f008a15fff5a554ac21f Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Mon, 30 Mar 2026 09:52:37 -0700 Subject: [PATCH] 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) --- PROGRESS.md | 5 +++-- VERSION | 2 +- garden/garden.go | 4 ++-- garden/garden_test.go | 17 ++++++++++++++--- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 21cf484..3363290 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -7,9 +7,9 @@ ARCHITECTURE.md for design details. ## 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 @@ -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 | — | 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-30 | — | Idempotent add: `sgard add` silently skips already-tracked files/directories instead of erroring, enabling glob-based workflows. | diff --git a/VERSION b/VERSION index 944880f..e4604e3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.0 +3.2.1 diff --git a/garden/garden.go b/garden/garden.go index 835d9a6..2962ec9 100644 --- a/garden/garden.go +++ b/garden/garden.go @@ -239,7 +239,7 @@ func (g *Garden) Add(paths []string, opts ...AddOptions) error { // Track the directory itself as a structural entry. tilded := toTildePath(abs) if g.findEntry(tilded) != nil { - return fmt.Errorf("already tracking %s", tilded) + continue } entry := manifest.Entry{ Path: tilded, @@ -277,7 +277,7 @@ func (g *Garden) Add(paths []string, opts ...AddOptions) error { } } } 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 } } diff --git a/garden/garden_test.go b/garden/garden_test.go index ccfa5ea..f1478ea 100644 --- a/garden/garden_test.go +++ b/garden/garden_test.go @@ -200,7 +200,7 @@ func TestAddSymlink(t *testing.T) { } } -func TestAddDuplicateRejected(t *testing.T) { +func TestAddDuplicateIsIdempotent(t *testing.T) { root := t.TempDir() repoDir := filepath.Join(root, "repo") @@ -218,8 +218,19 @@ func TestAddDuplicateRejected(t *testing.T) { t.Fatalf("first Add: %v", err) } - if err := g.Add([]string{testFile}); err == nil { - t.Fatal("second Add of same path should fail") + if err := g.Add([]string{testFile}); err != nil { + 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) } }