Skip to content

Add sn delete duplicates - #120

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

jonhadfield merged 1 commit into
mainfrom
feat/delete-duplicates

Conversation

@jonhadfield

Copy link
Copy Markdown
Owner

Why

Standard Notes sets duplicate_of on a note when the app duplicates it, and sn stats already lists those copies. There was no way to remove them.

What it does

sn delete duplicates deletes notes carrying that marker. It lists what it will remove and asks for confirmation first.

sn delete duplicates --dry-run   # list them and stop
sn delete duplicates             # list, confirm, delete
sn delete duplicates --yes       # skip the prompt

Only the marker counts as a duplicate. Notes that merely share a title or text are left alone, since the account doesn't say they're copies.

Safety

  • The list is shown before anything is deleted, up to 20 entries plus a count of the rest, and confirmation is required unless --yes is given. This follows sn wipe.
  • A duplicate whose original is no longer in the account is kept. If the original was deleted, the copy is the only remaining copy of that content, so deleting it would lose the note. Those are reported separately with the reason. The same applies when the original is present but flagged deleted.

Tests

internal/sncli/duplicates_test.go covers the selection logic: a marked copy with its original present, a missing original, a deleted original, an already-deleted duplicate, an account with no duplicates, and a copy of a copy.

make lint reports 0 issues and make test passes.

Worth knowing

The command syncs twice: once to show the list, and again to delete after you confirm. So it acts on the account as it is at the moment you confirm, rather than on a list that may have gone stale. If the two differ, the count reported at the end is what was actually deleted.

Not tested against a real account — no credentials were available, so only the selection logic is covered by tests. Worth a --dry-run against a real account before merging.

🤖 Generated with Claude Code

Standard Notes sets duplicate_of when the app duplicates a note, and
'sn stats' already lists those copies, but there was no way to remove
them.

'sn delete duplicates' deletes them, after listing what it will remove
and asking for confirmation. --dry-run lists them and stops; --yes skips
the prompt.

A duplicate whose original is no longer in the account is the only copy
of that content left, so it is kept and reported rather than deleted.

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:45
@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 54 complexity · 0 duplication

Metric Results
Complexity 54
Duplication 0

View in Codacy

🟢 Coverage 14.29% diff coverage · +0.07% coverage variation

Metric Results
Coverage variation ✅ +0.07% coverage variation (-1.00%)
Diff coverage ✅ 14.29% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (55e311e) 8825 899 10.19%
Head commit (1d76372) 8978 (+153) 921 (+22) 10.26% (+0.07%)

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 (#120) 154 22 14.29%

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 837d1d0 into main Sep 21, 2026
11 checks passed
@jonhadfield
jonhadfield deleted the feat/delete-duplicates branch September 21, 2026 17:48

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

There are confirmed functional and UX issues in the new duplicates delete flow (DB handle reuse across the two-run command, misleading “no duplicates found” output, and potentially incorrect deleted counts) that should be corrected before merge.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 Medium severity · 1 Low severity

Open (3)
What changed in this PR

This PR adds a new sn delete duplicates subcommand to safely delete Standard Notes items marked with the duplicate_of marker, including a dry-run mode and confirmation prompt, and documents the new behavior.

Changes:

  • Add duplicate detection and deletion logic (FindDuplicateNotes, DeleteDuplicateNotesConfig.Run) with safeguards to keep duplicates whose originals are missing/deleted.
  • Wire a new sn delete duplicates CLI subcommand with --dry-run and --yes, and user-facing output of deletions/kept items.
  • Update README to document the new usage.
File Description
README.md Documents sn delete duplicates usage and updates command list description.
internal/​sncli/​duplicates.go Implements duplicate discovery + deletion workflow based on duplicate_of and original presence.
internal/​sncli/​duplicates_test.go Adds unit tests for duplicate selection logic and safety rules.
cmd/​sncli/​duplicates.go Adds CLI plumbing/output/prompting for sn delete duplicates.
cmd/​sncli/​delete.go Registers the new delete duplicates subcommand and bash completion options.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cmd/sncli/duplicates.go
Comment on lines +40 to +44
if len(found.Deleted) == 0 {
_, _ = fmt.Fprintln(c.App.Writer, "no duplicate notes found")

return nil
}
Comment on lines +122 to +137
var notesToDelete items.Notes

for _, dup := range out.Deleted {
note := byUUID[dup.UUID]
if note == nil || note.GetContent() == nil {
continue
}

note.Content.SetText("")
note.SetDeleted(true)
notesToDelete = append(notesToDelete, *note)
}

if err = cache.SaveNotes(i.Session, so.DB, notesToDelete, true); err != nil {
return out, err
}
Comment on lines +72 to +74
// KeptOriginalMissing holds duplicates left alone because the note they
// were copied from is no longer in the account.
KeptOriginalMissing []DuplicateNote
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