Skip to content

Keep the most recently updated note in each set of duplicates - #121

Merged
jonhadfield merged 1 commit into
mainfrom
feat/duplicates-keep-newest
Sep 21, 2026
Merged

jonhadfield merged 1 commit into
mainfrom
feat/duplicates-keep-newest

Conversation

@jonhadfield

Copy link
Copy Markdown
Owner

Follow-up to #120.

Why

sn delete duplicates deleted strictly by the duplicate_of marker. 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_of links, 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:

2 notes would be deleted, in 1 sets of duplicates:
  keep    2026-09-21 14:30  9f2a...  Shopping copy
  delete  2026-09-02 09:12  1b77...  Shopping

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 lint reports 0 issues and make test passes.

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

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>
Copilot AI lite review requested due to automatic review settings September 21, 2026 17:52
@codacy-production

codacy-production Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 23 complexity · 4 duplication

Metric Results
Complexity 23
Duplication 4

View in Codacy

🟢 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

View coverage diff in Codacy

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.

@jonhadfield
jonhadfield merged commit b3a9b86 into main Sep 21, 2026
11 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 Medium severity · 1 Low severity

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_of relationships 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.

Comment on lines +147 to +150
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
Comment thread cmd/sncli/duplicates.go
}

_, _ = 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())))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants