Repository navigation
Keep the most recently updated note in each set of duplicates - #121
Conversation
Deleting strictly by the duplicate_of marker removed the copy, which is the newer note. Anyone who duplicated a note and then carried on editing the copy would have lost that work. Group each note with the copies made from it, following duplicate_of, so a copy of a copy lands in the same set. Keep the most recently updated note in the set and delete the rest. On identical timestamps prefer the original, then the lower UUID, so the choice is stable. The listing now shows the last-updated date of every note, and which one is being kept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 23 |
| Duplication | 4 |
🟢 Coverage 70.10% diff coverage · +0.51% coverage variation
Metric Results Coverage variation ✅ +0.51% coverage variation (-1.00%) Diff coverage ✅ 70.10% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (837d1d0) 8978 921 10.26% Head commit (24ea6bb) 9037 (+59) 973 (+52) 10.77% (+0.51%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#121) 97 68 70.10% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The “kept because original is missing” list can include notes that also belong to duplicate groups (and may be deleted), and the final success message wording is now misleading given originals may be deleted.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (2)
What changed in this PR
This PR updates sn delete duplicates so it keeps the most recently updated note within each duplicate chain (following duplicate_of links), rather than always deleting the note that carries the duplicate_of marker—preventing loss of newer edits made on duplicated copies.
Changes:
- Build duplicate “sets” via union-find across
duplicate_ofrelationships and choose a stable “keep” note per set (newest updated; then prefer original; then UUID). - Update CLI output to show keep/delete decisions per set and include formatted last-updated timestamps.
- Expand test coverage for grouping/selection behavior and timestamp formatting.
| File | Description |
|---|---|
| README.md | Updates usage text to reflect deleting all but the newest note in each duplicate set. |
| internal/sncli/duplicates.go | Implements duplicate grouping + deterministic keep/delete selection; updates delete flow to delete per-group members. |
| internal/sncli/duplicates_test.go | Adds tests for newest-wins behavior, tie-breaking, grouping chains, and timestamp formatting. |
| cmd/sncli/duplicates.go | Updates CLI listing to show groups with keep/delete rows and uses new output model. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| sort.Slice(groups, func(i, j int) bool { return groups[i].Keep.UUID < groups[j].Keep.UUID }) | ||
| sort.Slice(keptNoOriginal, func(i, j int) bool { return keptNoOriginal[i].UUID < keptNoOriginal[j].UUID }) | ||
|
|
||
| return groups, keptNoOriginal |
| } | ||
|
|
||
| _, _ = fmt.Fprintln(c.App.Writer, color.Green.Sprintf("%d duplicate notes deleted", len(out.Deleted))) | ||
| _, _ = fmt.Fprintln(c.App.Writer, color.Green.Sprintf("%d duplicate notes deleted", len(out.Deleted()))) |


Follow-up to #120.
Why
sn delete duplicatesdeleted strictly by theduplicate_ofmarker. Standard Notes puts that marker on the new copy, so the command deleted the newer note and kept the older original.That loses work in a common case: duplicate a note, carry on editing the copy, and the copy is the one that gets deleted.
What changed
Each note is now grouped with the copies made from it, following
duplicate_oflinks, so a copy of a copy lands in the same set. The most recently updated note in the set is kept, and the rest are deleted.On identical timestamps the original is preferred over a copy, then the lower UUID, so the outcome is stable rather than depending on map ordering.
The listing now shows each note's last-updated date and which one is being kept:
Unchanged: a copy whose original is no longer in the account is still kept and reported, since it holds the only remaining copy of that content.
Tests
Eleven cases, including the copy edited after duplication, the untouched copy, a tie on timestamps, a missing original, a deleted original, a copy of a copy, and two unrelated sets staying separate.
make lintreports 0 issues andmake testpasses.Worth knowing
Deleting the older original is a bigger change than deleting a copy: anything pointing at the original, such as tags, now points at a deleted note. I haven't verified how Standard Notes tidies those references, so it's worth watching on the first real run.
Still not tested against a real account — no credentials here, so only the selection logic is covered.
🤖 Generated with Claude Code