Skip to content

Commit 6e65e93

Browse files
dyoung522claude
andcommitted
feat: add imt sync cleanup command to remove duplicates
Scans mods and tools collections for entries with the same name+author, keeps the most recently updated entry, and deletes older duplicates. Supports --dry-run to preview deletions. Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent c2a7c9a commit 6e65e93

5 files changed

Lines changed: 120 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ All notable changes to this project will be documented in this file.
44

55
## History (reverse chronological order)
66

7-
### v2.5.3 - 2026-03-04
7+
### v2.6.0 - 2026-03-04
88

9+
- Add `imt sync cleanup` command to remove duplicate entries from mods and tools collections
10+
- Groups entries by name+author and keeps the most recently updated
11+
- Supports `--dry-run` to preview what would be deleted
912
- Fix `find_info` to match by both name AND author, not just name
1013
- Previously, mods/tools with the same name but different authors could cause incorrect deletion logic
1114
- Now consistent with `info_array` deduplication and `find_by_type` lookups

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
Icarus-Mod-Tools (2.5.2)
4+
Icarus-Mod-Tools (2.5.3)
55
google-cloud-firestore (~> 2.7)
66
octokit (~> 6.0)
77
paint (~> 2.3)

lib/icarus/mod/cli/sync.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,63 @@ def tools
5252
sync_list(:tools)
5353
end
5454

55+
desc "cleanup", "Remove duplicate entries from mods and tools collections"
56+
def cleanup
57+
cleanup_duplicates(:mods)
58+
cleanup_duplicates(:tools)
59+
end
60+
5561
no_commands do
5662
def firestore
5763
$firestore ||= Firestore.new
5864
end
5965

66+
def cleanup_duplicates(type)
67+
singular_type = type.to_s.chomp("s").to_sym
68+
collection = firestore.send(type)
69+
70+
puts "Scanning #{type} for duplicates..." if verbose?
71+
72+
# Group by [name, author]
73+
grouped = collection.group_by { |item| [item.name, item.author] }
74+
duplicates = grouped.select { |_, items| items.length > 1 }
75+
76+
if duplicates.empty?
77+
puts "No duplicate #{type} found." if verbose?
78+
return
79+
end
80+
81+
puts "Found #{duplicates.length} duplicate #{singular_type}(s) to clean up:" if verbose?
82+
83+
duplicates.each do |key, items|
84+
name, author = key
85+
# Sort by updated_at descending, keep the most recent
86+
sorted = items.sort_by { |item| item.updated_at || Time.at(0) }.reverse
87+
keeper = sorted.first
88+
to_delete = sorted[1..]
89+
90+
puts " #{author}/#{name}: #{items.length} entries" if verbose?
91+
puts " Keeping: #{keeper.id} (updated: #{keeper.updated_at})" if verbose?
92+
93+
to_delete.each do |item|
94+
if options[:dry_run]
95+
puts Paint[" Would delete: #{item.id} (updated: #{item.updated_at})", :yellow] if verbose?
96+
else
97+
puts " Deleting: #{item.id} (updated: #{item.updated_at})" if verbose?
98+
response = firestore.delete(singular_type, item)
99+
puts " #{success_or_failure(response)}" if verbose > 1
100+
end
101+
end
102+
end
103+
104+
deleted_count = duplicates.values.sum { |items| items.length - 1 }
105+
if options[:dry_run]
106+
puts Paint["Dry run; no changes made. Would have deleted #{deleted_count} duplicate #{type}.", :yellow] if verbose?
107+
else
108+
puts "Deleted #{deleted_count} duplicate #{type}." if verbose?
109+
end
110+
end
111+
60112
def success_or_failure(status)
61113
format("%<status>10s", status: status ? Paint["Success", :green] : Paint["Failure", :red])
62114
end

lib/icarus/mod/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
module Icarus
44
module Mod
5-
VERSION = "2.5.3"
5+
VERSION = "2.6.0"
66
end
77
end

spec/icarus/mod/cli/sync_spec.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,66 @@
233233
expect { sync_command.all }.to output(String).to_stdout
234234
end
235235
end
236+
237+
describe "#cleanup" do
238+
let(:options) { { verbose: [true], dry_run: false } }
239+
let(:mod1) { Icarus::Mod::Tools::Modinfo.new({ name: "DupeMod", author: "Author1", description: "Test" }, id: "mod1", updated: Time.now - 3600) }
240+
let(:mod2) { Icarus::Mod::Tools::Modinfo.new({ name: "DupeMod", author: "Author1", description: "Test" }, id: "mod2", updated: Time.now) }
241+
let(:mod3) { Icarus::Mod::Tools::Modinfo.new({ name: "UniqueMod", author: "Author2", description: "Test" }, id: "mod3", updated: Time.now) }
242+
let(:tool1) { Icarus::Mod::Tools::Toolinfo.new({ name: "DupeTool", author: "Author1", description: "Test" }, id: "tool1", updated: Time.now - 3600) }
243+
let(:tool2) { Icarus::Mod::Tools::Toolinfo.new({ name: "DupeTool", author: "Author1", description: "Test" }, id: "tool2", updated: Time.now) }
244+
245+
before do
246+
allow(firestore_double).to receive(:mods).and_return([mod1, mod2, mod3])
247+
allow(firestore_double).to receive(:tools).and_return([tool1, tool2])
248+
allow(firestore_double).to receive(:delete).and_return(true)
249+
end
250+
251+
it "identifies duplicate mods by name and author" do
252+
expect { sync_command.cleanup }.to output(/Found 1 duplicate mod/).to_stdout
253+
end
254+
255+
it "identifies duplicate tools by name and author" do
256+
expect { sync_command.cleanup }.to output(/Found 1 duplicate tool/).to_stdout
257+
end
258+
259+
it "keeps the most recently updated entry" do
260+
expect { sync_command.cleanup }.to output(/Keeping.*mod2/).to_stdout
261+
end
262+
263+
it "deletes older duplicate entries" do
264+
sync_command.cleanup
265+
expect(firestore_double).to have_received(:delete).with(:mod, mod1)
266+
expect(firestore_double).to have_received(:delete).with(:tool, tool1)
267+
end
268+
269+
it "does not delete unique entries" do
270+
sync_command.cleanup
271+
expect(firestore_double).not_to have_received(:delete).with(:mod, mod3)
272+
end
273+
274+
context "when no duplicates exist" do
275+
before do
276+
allow(firestore_double).to receive(:mods).and_return([mod3])
277+
allow(firestore_double).to receive(:tools).and_return([])
278+
end
279+
280+
it "reports no duplicates found" do
281+
expect { sync_command.cleanup }.to output(/No duplicate mods found/).to_stdout
282+
end
283+
end
284+
285+
context "with dry_run enabled" do
286+
let(:options) { { verbose: [true], dry_run: true } }
287+
288+
it "does not delete any entries" do
289+
expect { sync_command.cleanup }.to output(/Dry run/).to_stdout
290+
expect(firestore_double).not_to have_received(:delete)
291+
end
292+
293+
it "shows what would be deleted" do
294+
expect { sync_command.cleanup }.to output(/Would delete.*mod1/).to_stdout
295+
end
296+
end
297+
end
236298
end

0 commit comments

Comments
 (0)