1require 'rubygems/command'
2require 'rubygems/dependency_list'
3require 'rubygems/uninstaller'
4
5class Gem::Commands::CleanupCommand < Gem::Command
6
7  def initialize
8    super 'cleanup',
9          'Clean up old versions of installed gems in the local repository',
10          :force => false, :install_dir => Gem.dir
11
12    add_option('-d', '--dryrun', "") do |value, options|
13      options[:dryrun] = true
14    end
15
16    @candidate_gems  = nil
17    @default_gems    = []
18    @full            = nil
19    @gems_to_cleanup = nil
20    @original_home   = nil
21    @original_path   = nil
22    @primary_gems    = nil
23  end
24
25  def arguments # :nodoc:
26    "GEMNAME       name of gem to cleanup"
27  end
28
29  def defaults_str # :nodoc:
30    "--no-dryrun"
31  end
32
33  def description # :nodoc:
34    <<-EOF
35The cleanup command removes old gems from GEM_HOME.  If an older version is
36installed elsewhere in GEM_PATH the cleanup command won't touch it.
37
38Older gems that are required to satisify the dependencies of gems
39are not removed.
40    EOF
41  end
42
43  def usage # :nodoc:
44    "#{program_name} [GEMNAME ...]"
45  end
46
47  def execute
48    say "Cleaning up installed gems..."
49
50    if options[:args].empty? then
51      done     = false
52      last_set = nil
53
54      until done do
55        clean_gems
56
57        this_set = @gems_to_cleanup.map { |spec| spec.full_name }.sort
58
59        done = this_set.empty? || last_set == this_set
60
61        last_set = this_set
62      end
63    else
64      clean_gems
65    end
66
67    say "Clean Up Complete"
68
69    if Gem.configuration.really_verbose then
70      skipped = @default_gems.map { |spec| spec.full_name }
71
72      say "Skipped default gems: #{skipped.join ', '}"
73    end
74  end
75
76  def clean_gems
77    get_primary_gems
78    get_candidate_gems
79    get_gems_to_cleanup
80
81    @full = Gem::DependencyList.from_specs
82
83    deplist = Gem::DependencyList.new
84    @gems_to_cleanup.each do |spec| deplist.add spec end
85
86    deps = deplist.strongly_connected_components.flatten
87
88    @original_home = Gem.dir
89    @original_path = Gem.path
90
91    deps.reverse_each do |spec|
92      uninstall_dep spec
93    end
94
95    Gem::Specification.reset
96  end
97
98  def get_candidate_gems
99    @candidate_gems = unless options[:args].empty? then
100                        options[:args].map do |gem_name|
101                          Gem::Specification.find_all_by_name gem_name
102                        end.flatten
103                      else
104                        Gem::Specification.to_a
105                      end
106  end
107
108  def get_gems_to_cleanup
109    gems_to_cleanup = @candidate_gems.select { |spec|
110      @primary_gems[spec.name].version != spec.version
111    }
112
113    default_gems, gems_to_cleanup = gems_to_cleanup.partition { |spec|
114      spec.default_gem?
115    }
116
117    @default_gems += default_gems
118    @default_gems.uniq!
119    @gems_to_cleanup = gems_to_cleanup.uniq
120  end
121
122  def get_primary_gems
123    @primary_gems = {}
124
125    Gem::Specification.each do |spec|
126      if @primary_gems[spec.name].nil? or
127         @primary_gems[spec.name].version < spec.version then
128        @primary_gems[spec.name] = spec
129      end
130    end
131  end
132
133  def uninstall_dep spec
134    return unless @full.ok_to_remove?(spec.full_name)
135
136    if options[:dryrun] then
137      say "Dry Run Mode: Would uninstall #{spec.full_name}"
138      return
139    end
140
141    say "Attempting to uninstall #{spec.full_name}"
142
143    uninstall_options = {
144      :executables => false,
145      :version => "= #{spec.version}",
146    }
147
148    uninstall_options[:user_install] = Gem.user_dir == spec.base_dir
149
150    uninstaller = Gem::Uninstaller.new spec.name, uninstall_options
151
152    begin
153      uninstaller.uninstall
154    rescue Gem::DependencyRemovalException, Gem::InstallError,
155           Gem::GemNotInHomeException, Gem::FilePermissionError => e
156      say "Unable to uninstall #{spec.full_name}:"
157      say "\t#{e.class}: #{e.message}"
158    end
159  ensure
160    # Restore path Gem::Uninstaller may have changed
161    Gem.use_paths @original_home, *@original_path
162  end
163
164end
165
166