Skip to content

Commit 4ee628c

Browse files
authored
Add cache management commands to swap-deps (#39) (#44)
* Add cache management commands to swap-deps utility This adds cache management functionality to help manage disk space used by GitHub repositories cloned by the swap-deps tool. New features: - --show-cache: Display cache location, size, and list of cached repositories - --clean-cache: Remove all cached repositories - --clean-cache <gem>: Remove cache for a specific gem (e.g., shakapacker) The cache directory (~/.cache/swap-deps/) can accumulate significant disk space when testing multiple branches with --github option. These commands help users monitor and manage the cache size. Implementation details: - Added show_cache_info method to display cache statistics - Added clean_cache method with optional gem filtering - Helper methods for directory size calculation and human-readable formatting - Respects --dry-run flag for safe preview of cleanup operations Fixes #39
1 parent 27f8f03 commit 4ee628c

File tree

3 files changed

+542
-4
lines changed

3 files changed

+542
-4
lines changed

lib/demo_scripts/gem_swapper.rb

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require 'pathname'
66
require 'fileutils'
77
require 'open3'
8+
require 'find'
89

910
module DemoScripts
1011
# Manages swapping dependencies between production and local/GitHub versions
@@ -132,6 +133,52 @@ def kill_watch_processes
132133
end
133134
# rubocop:enable Metrics/MethodLength
134135

136+
# CLI entry point: Display cache information including location, size, and cached repositories
137+
def show_cache_info
138+
unless File.directory?(CACHE_DIR)
139+
puts 'ℹ️ Cache directory does not exist'
140+
puts " Location: #{CACHE_DIR}"
141+
return
142+
end
143+
144+
# Get all repo directories once to avoid race conditions
145+
repo_dirs = cache_repo_dirs
146+
147+
# Calculate cache size and count repos
148+
repo_info = repo_dirs.map do |path|
149+
{ path: path, basename: File.basename(path), size: directory_size(path) }
150+
end
151+
152+
total_size = repo_info.sum { |info| info[:size] }
153+
154+
puts '📊 Cache information:'
155+
puts " Location: #{CACHE_DIR}"
156+
puts " Repositories: #{repo_info.count}"
157+
puts " Total size: #{human_readable_size(total_size)}"
158+
159+
return unless repo_info.any?
160+
161+
puts "\n Cached repositories:"
162+
repo_info.each do |info|
163+
puts " - #{info[:basename]} (#{human_readable_size(info[:size])})"
164+
end
165+
end
166+
167+
# CLI entry point: Remove cached GitHub repositories
168+
# @param gem_name [String, nil] Optional gem name to clean specific gem cache, or nil to clean all
169+
def clean_cache(gem_name: nil)
170+
unless File.directory?(CACHE_DIR)
171+
puts 'ℹ️ Cache directory does not exist - nothing to clean'
172+
return
173+
end
174+
175+
if gem_name
176+
clean_gem_cache(gem_name)
177+
else
178+
clean_all_cache
179+
end
180+
end
181+
135182
def load_config(config_file)
136183
return unless File.exist?(config_file)
137184

@@ -156,6 +203,126 @@ def load_config(config_file)
156203

157204
private
158205

206+
def cache_repo_dirs
207+
return [] unless File.directory?(CACHE_DIR)
208+
209+
Dir.glob(File.join(CACHE_DIR, '*')).select do |path|
210+
File.directory?(path) && File.basename(path) != 'watch_logs'
211+
end
212+
end
213+
214+
# Match gem name in cache directory pattern: {org}-{gem}-{branch}
215+
# This ensures we match the repository component, not the org or branch
216+
def matches_gem_cache_pattern?(basename, gem_name)
217+
# Normalize gem name for both underscore and hyphen variants
218+
normalized_gem = gem_name.tr('_', '-')
219+
220+
# Match the middle component after the first hyphen
221+
# Pattern: ^{org}-{gem}-{branch}$
222+
# This prevents false positives like matching "test" in "test-user-repo-branch"
223+
basename.match?(/\A[^-]+-#{Regexp.escape(normalized_gem)}-/) ||
224+
basename.match?(/\A[^-]+-#{Regexp.escape(gem_name)}-/)
225+
end
226+
227+
def directory_size(path)
228+
size = 0
229+
Find.find(path) do |file_path|
230+
# Skip symlinks to avoid circular references and incorrect sizes
231+
if File.symlink?(file_path)
232+
Find.prune
233+
next
234+
end
235+
236+
size += File.size(file_path) if File.file?(file_path)
237+
end
238+
size
239+
rescue Errno::EACCES => e
240+
warn " ⚠️ Warning: Permission denied accessing #{path}: #{e.message}" if verbose
241+
0
242+
rescue Errno::ENOENT => e
243+
warn " ⚠️ Warning: Path not found #{path}: #{e.message}" if verbose
244+
0
245+
rescue StandardError => e
246+
warn " ⚠️ Warning: Error calculating size for #{path}: #{e.message}" if verbose
247+
0
248+
end
249+
250+
def human_readable_size(bytes)
251+
units = %w[B KB MB GB TB]
252+
return "0 #{units[0]}" if bytes.zero?
253+
254+
exp = (Math.log(bytes) / Math.log(1024)).to_i
255+
exp = [exp, units.length - 1].min
256+
size = bytes.to_f / (1024**exp)
257+
format('%<size>.2f %<unit>s', size: size, unit: units[exp])
258+
end
259+
260+
# rubocop:disable Metrics/MethodLength
261+
def clean_gem_cache(gem_name)
262+
# Validate gem name to prevent path traversal
263+
unless gem_name.match?(/\A[\w.-]+\z/)
264+
raise Error,
265+
"Invalid gem name: #{gem_name}. Only alphanumeric characters, hyphens, underscores, and dots allowed."
266+
end
267+
268+
# Find all cached repos for this gem
269+
# Expected format: {org}-{repo}-{branch} (e.g., shakacode-shakapacker-main)
270+
matching_dirs = cache_repo_dirs.select do |path|
271+
matches_gem_cache_pattern?(File.basename(path), gem_name)
272+
end
273+
274+
if matching_dirs.empty?
275+
puts "ℹ️ No cached repositories found for: #{gem_name}"
276+
return
277+
end
278+
279+
puts "🗑️ Cleaning cache for #{gem_name}..."
280+
matching_dirs.each do |dir|
281+
size = directory_size(dir)
282+
basename = File.basename(dir)
283+
if dry_run
284+
puts " [DRY-RUN] Would remove #{basename} (#{human_readable_size(size)})"
285+
else
286+
FileUtils.rm_rf(dir)
287+
puts " ✓ Removed #{basename} (#{human_readable_size(size)})"
288+
end
289+
end
290+
end
291+
# rubocop:enable Metrics/MethodLength
292+
293+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
294+
def clean_all_cache
295+
# Get all repo directories (exclude watch_logs)
296+
repo_dirs = cache_repo_dirs
297+
298+
if repo_dirs.empty?
299+
puts 'ℹ️ Cache is empty - nothing to clean'
300+
return
301+
end
302+
303+
# Calculate sizes once to avoid redundant directory traversal
304+
repo_info = repo_dirs.map do |dir|
305+
{ path: dir, basename: File.basename(dir), size: directory_size(dir) }
306+
end
307+
308+
total_size = repo_info.sum { |info| info[:size] }
309+
puts "🗑️ Cleaning entire cache (#{repo_info.count} repositories, #{human_readable_size(total_size)})..."
310+
311+
if dry_run
312+
puts ' [DRY-RUN] Would remove:'
313+
repo_info.each do |info|
314+
puts " - #{info[:basename]} (#{human_readable_size(info[:size])})"
315+
end
316+
else
317+
repo_info.each do |info|
318+
FileUtils.rm_rf(info[:path])
319+
puts " ✓ Removed #{info[:basename]} (#{human_readable_size(info[:size])})"
320+
end
321+
puts "✅ Cleaned cache - freed #{human_readable_size(total_size)}"
322+
end
323+
end
324+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
325+
159326
def load_watch_pids
160327
return {} unless File.exist?(WATCH_PIDS_FILE)
161328

lib/demo_scripts/swap_deps_cli.rb

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ class SwapDepsCLI
88
CONFIG_FILE = '.swap-deps.yml'
99

1010
attr_reader :gem_paths, :github_repos, :dry_run, :verbose, :restore, :apply_config,
11-
:skip_build, :watch_mode, :demo_filter, :demos_dir, :list_watch, :kill_watch
11+
:skip_build, :watch_mode, :demo_filter, :demos_dir, :list_watch, :kill_watch,
12+
:show_cache, :clean_cache, :clean_cache_gem
1213

1314
def initialize
1415
@gem_paths = {}
@@ -26,17 +27,24 @@ def initialize
2627
@auto_demos_dir = nil
2728
@list_watch = false
2829
@kill_watch = false
30+
@show_cache = false
31+
@clean_cache = false
32+
@clean_cache_gem = nil
2933
end
3034

31-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
35+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
3236
def run!
3337
detect_context!
3438
parse_options!
3539

3640
# Require bundler/setup only when actually running commands (not for --help)
3741
require 'bundler/setup'
3842

39-
if @list_watch
43+
if @show_cache
44+
show_cache_info
45+
elsif @clean_cache || @clean_cache_gem
46+
clean_cache_handler
47+
elsif @list_watch
4048
list_watch_processes
4149
elsif @kill_watch
4250
kill_watch_processes
@@ -60,7 +68,7 @@ def run!
6068
warn e.backtrace.join("\n") if verbose
6169
exit 1
6270
end
63-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
71+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
6472

6573
private
6674

@@ -175,6 +183,21 @@ def parse_options!
175183
@kill_watch = true
176184
end
177185

186+
opts.separator ''
187+
opts.separator 'Cache management:'
188+
189+
opts.on('--show-cache', 'Show cache location, size, and cached repositories') do
190+
@show_cache = true
191+
end
192+
193+
opts.on('--clean-cache [GEM]', 'Remove cached repositories (all or specific gem, excludes watch_logs)') do |gem|
194+
if gem
195+
@clean_cache_gem = gem
196+
else
197+
@clean_cache = true
198+
end
199+
end
200+
178201
opts.separator ''
179202
opts.separator 'General options:'
180203

@@ -235,6 +258,15 @@ def parse_options!
235258
puts ' # Stop all watch processes'
236259
puts ' bin/swap-deps --kill-watch'
237260
puts ''
261+
puts ' # Show cache information'
262+
puts ' bin/swap-deps --show-cache'
263+
puts ''
264+
puts ' # Clean all cached repositories'
265+
puts ' bin/swap-deps --clean-cache'
266+
puts ''
267+
puts ' # Clean cache for specific gem'
268+
puts ' bin/swap-deps --clean-cache shakapacker'
269+
puts ''
238270
puts 'Configuration file:'
239271
puts " Create #{CONFIG_FILE} (see #{CONFIG_FILE}.example) with your dependency paths."
240272
puts ' This file is git-ignored for local development.'
@@ -266,6 +298,16 @@ def kill_watch_processes
266298
swapper.kill_watch_processes
267299
end
268300

301+
def show_cache_info
302+
swapper = create_swapper
303+
swapper.show_cache_info
304+
end
305+
306+
def clean_cache_handler
307+
swapper = create_swapper
308+
swapper.clean_cache(gem_name: @clean_cache_gem)
309+
end
310+
269311
def apply_from_config
270312
# Use root config if in demo directory, otherwise look for local config
271313
config_file = @root_config_file || CONFIG_FILE

0 commit comments

Comments
 (0)