gem-compare 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ require 'rainbow'
2
+
3
+ class Gem::Comparator
4
+ module Base
5
+ include Gem::UserInteraction
6
+
7
+ class DiffCommandMissing < StandardError; end
8
+
9
+ SPACE = ' '
10
+ DEFAULT_INDENT = SPACE*7
11
+ OPERATORS = ['=', '!=', '>', '<', '>=', '<=', '~>']
12
+ VERSION_REGEX = /\A(\d+\.){0,}\d+(\.[a-zA-Z]+\d{0,1}){0,1}\z/
13
+ SHEBANG_REGEX = /\A#!.*/
14
+ SPEC_PARAMS = %w[ author authors name platform require_paths rubygems_version summary
15
+ license licenses bindir cert_chain description email executables
16
+ extensions homepage metadata post_install_message rdoc_options
17
+ required_ruby_version required_rubygems_version requirements
18
+ signing_key has_rdoc date version ].sort
19
+ SPEC_FILES_PARAMS = %w[ files test_files extra_rdoc_files ]
20
+ DEPENDENCY_PARAMS = %w[ runtime_dependency development_dependency ]
21
+ GEMFILE_PARAMS = %w[ gemfiles ]
22
+
23
+ private
24
+
25
+ def param_exists?(param)
26
+ (SPEC_PARAMS.include? param) ||
27
+ (SPEC_FILES_PARAMS.include? param) ||
28
+ (DEPENDENCY_PARAMS.include? param) ||
29
+ (GEMFILE_PARAMS.include? param)
30
+ end
31
+
32
+ def filter_params(params, param)
33
+ if param
34
+ if params.include? param
35
+ return [param]
36
+ else
37
+ return []
38
+ end
39
+ end
40
+
41
+ params
42
+ end
43
+
44
+ def same
45
+ Rainbow('SAME').green.bright
46
+ end
47
+
48
+ def different
49
+ Rainbow('DIFFERENT').red.bright
50
+ end
51
+
52
+ def info(msg)
53
+ say msg if Gem.configuration.really_verbose
54
+ end
55
+
56
+ def warn(msg)
57
+ say Rainbow("WARNING: #{msg}").red
58
+ end
59
+
60
+ def error(msg)
61
+ say Rainbow("ERROR: #{msg}").red
62
+ exit 1
63
+ end
64
+
65
+ def extract_gem(package, target_dir)
66
+ gem_file = File.basename(package.spec.full_name, '.gem')
67
+ gem_dir = File.join(target_dir, gem_file)
68
+
69
+ if Dir.exist? gem_dir
70
+ info "Unpacked gem version exists, using #{gem_dir}."
71
+ return gem_dir
72
+ end
73
+
74
+ info "Unpacking gem '#{package.spec.full_name}' in " + gem_dir
75
+ package.extract_files gem_dir
76
+ gem_dir
77
+ end
78
+
79
+ def check_diff_command_is_installed
80
+ begin
81
+ IO.popen('diff --version')
82
+ rescue Exception
83
+ raise DiffCommandMissing, \
84
+ 'Calling `diff` command failed. Do you have it installed?'
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,111 @@
1
+ require 'rubygems/comparator/base'
2
+
3
+ class Gem::Comparator
4
+
5
+ ##
6
+ # Gem::Comparator::DependencyComparator can
7
+ # compare dependencies between gem's versions
8
+ # based on the given Gem::Specification objects
9
+
10
+ class DependencyComparator
11
+ include Gem::Comparator::Base
12
+
13
+ COMPARES = :specs
14
+
15
+ ##
16
+ # Compare dependencies in given +specs+ and
17
+ # write the changes to the +report+
18
+ #
19
+ # If +options[:param]+ is set, it compares only
20
+ # those dependencies
21
+
22
+ def compare(specs, report, options = {})
23
+ info 'Checking dependencies...'
24
+
25
+ filter_params(DEPENDENCY_PARAMS, options[:param]).each do |param|
26
+ all_same = true
27
+ type = param.gsub('_dependency', '').to_sym
28
+
29
+ specs.each_with_index do |s, index|
30
+ next if index == 0
31
+
32
+ prev_deps = specs[index-1].dependencies.keep_if { |d| d.type == type }
33
+ curr_deps = specs[index].dependencies.keep_if { |d| d.type == type }
34
+ added, deleted, updated = resolve_dependencies(prev_deps, curr_deps)
35
+
36
+ if (!deleted.empty? || !added.empty? || !updated.empty?)
37
+ all_same = false
38
+ end
39
+
40
+ ver = "#{specs[index-1].version}->#{specs[index].version}"
41
+
42
+ report[param][ver].section do
43
+ set_header "#{Rainbow(specs[index-1].version).blue}->#{Rainbow(s.version).blue}:"
44
+
45
+ nest('deleted').section do
46
+ set_header '* Deleted:'
47
+ puts deleted.map { |x| "#{x.name} #{x.requirements_list} (#{x.type})" } unless deleted.empty?
48
+ end
49
+
50
+ nest('added').section do
51
+ set_header '* Added:'
52
+ puts added.map { |x| "#{x.name} #{x.requirements_list} (#{x.type})" } unless added.empty?
53
+ end
54
+
55
+ nest('updated').section do
56
+ set_header '* Updated:'
57
+ puts updated unless updated.empty?
58
+ end
59
+ end
60
+ end
61
+ if all_same && options[:log_all]
62
+ report[param].set_header "#{same} #{type} dependencies:" if options[:log_all]
63
+ deps = specs[0].dependencies.keep_if{ |d| d.type == type }.map{ |d| "#{d.name}: #{d.requirements_list}" }
64
+ deps = '[]' if deps.empty?
65
+ report[param] << deps
66
+ elsif !all_same
67
+ report[param].set_header "#{different} #{type} dependencies:"
68
+ end
69
+ end
70
+ report
71
+ end
72
+
73
+ private
74
+
75
+ ##
76
+ # Find dependencies between +prev_deps+ and +curr_deps+
77
+ #
78
+ # Return [added, deleted, updated] deps
79
+
80
+ def resolve_dependencies(prev_deps, curr_deps)
81
+ added = curr_deps - prev_deps
82
+ deleted = prev_deps - curr_deps
83
+ split_dependencies(added, deleted)
84
+ end
85
+
86
+ ##
87
+ # Find updated dependencies between +added+ and
88
+ # +deleted+ deps and move them out to +updated+.
89
+ #
90
+ # Return [added, deleted, updated] deps
91
+
92
+ def split_dependencies(added, deleted)
93
+ updated = []
94
+ added.dup.each do |ad|
95
+ deleted.dup.each do |dd|
96
+ if ad.name == dd.name
97
+ unless ad.requirements_list == dd.requirements_list
98
+ updated << "#{ad.name} " +
99
+ "from: #{dd.requirements_list} " +
100
+ "to: #{ad.requirements_list}"
101
+ end
102
+ added.delete ad
103
+ deleted.delete dd
104
+ end
105
+ end
106
+ end
107
+ [added, deleted, updated]
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,262 @@
1
+ require 'diffy'
2
+ require 'rubygems/comparator/base'
3
+
4
+ class Gem::Comparator
5
+
6
+ ##
7
+ # Gem::Comparator::FileListComparator can
8
+ # compare file lists from gem's specs
9
+ # based on the given Gem::Package objects
10
+ #
11
+ # To compare the files it needs to extract
12
+ # gem packages to +options[:output]+
13
+
14
+ class FileListComparator
15
+ include Gem::Comparator::Base
16
+
17
+ COMPARES = :packages
18
+
19
+ ##
20
+ # Compare file lists for gem's Gem::Package objects
21
+ # in +packages+ and writes the changes to the +report+
22
+ #
23
+ # If +options[:param]+ is set, it compares only
24
+ # that file list
25
+
26
+ def compare(packages, report, options = {})
27
+ info 'Checking file lists...'
28
+ check_diff_command_is_installed
29
+
30
+ @packages = packages
31
+
32
+ # Check file lists from older versions to newer
33
+ filter_params(SPEC_FILES_PARAMS, options[:param]).each do |param|
34
+ all_same = true
35
+
36
+ packages.each_with_index do |pkg, index|
37
+ unpacked_gem_dirs[packages[index].spec.version] = extract_gem(pkg, options[:output])
38
+ next if index == 0
39
+
40
+ # File lists as arrays
41
+ previous = value_from_spec(param, packages[index-1].spec)
42
+ current = value_from_spec(param, pkg.spec)
43
+ next unless (previous && current)
44
+
45
+ vers = "#{packages[index-1].spec.version}->#{packages[index].spec.version}"
46
+
47
+ if previous == current && !all_same
48
+ report[param][vers] << "#{Rainbow(packages[index-1].spec.version).blue}->" + \
49
+ "#{Rainbow(packages[index].spec.version).blue}: No change"
50
+ end
51
+
52
+ unless previous == current
53
+ deleted = previous - current
54
+ added = current - previous
55
+ same = current - added
56
+
57
+ if !added.empty? || !deleted.empty?
58
+ report[param].set_header "#{different} #{param}:"
59
+ all_same = false
60
+ end
61
+
62
+ report[param][vers].section do
63
+ set_header "#{Rainbow(packages[index-1].spec.version).blue}->" +
64
+ "#{Rainbow(packages[index].spec.version).blue}:"
65
+ nest('deleted').section do
66
+ set_header '* Deleted:'
67
+ puts deleted unless deleted.empty?
68
+ end
69
+
70
+ nest('added').section do
71
+ set_header '* Added:'
72
+ puts added unless added.empty?
73
+ end
74
+ end
75
+
76
+ report[param][vers]['changed'].set_header '* Changed:'
77
+ report = check_same_files(param, vers, index, same, report)
78
+ end
79
+ end
80
+
81
+ if all_same && options[:log_all]
82
+ report[param].set_header "#{same} #{param}:"
83
+ value = value_from_spec(param, @packages[0].spec)
84
+ value = '[]' if value.empty?
85
+ report[param] << value
86
+ end
87
+ end
88
+ report
89
+ end
90
+
91
+ private
92
+
93
+ ##
94
+ # Access @unpacked_gem_dirs hash that stores
95
+ # paths to the unpacked gem dirs
96
+ #
97
+ # Keys of the hash are gem's versions
98
+
99
+ def unpacked_gem_dirs
100
+ @unpacked_gem_dirs ||= {}
101
+ end
102
+
103
+ def check_same_files(param, vers, index, files, report)
104
+ files.each do |file|
105
+ prev_file = File.join(unpacked_gem_dirs[@packages[index-1].spec.version], file)
106
+ curr_file = File.join(unpacked_gem_dirs[@packages[index].spec.version], file)
107
+
108
+ next unless check_files([prev_file, curr_file])
109
+
110
+ line_changes = lines_changed(prev_file, curr_file)
111
+
112
+ changes = permission_changed(prev_file, curr_file),
113
+ executables_changed(prev_file, curr_file),
114
+ shebangs_changed(prev_file, curr_file)
115
+
116
+ unless (line_changes.empty? && changes.join.empty?)
117
+ report[param][vers]['changed'] << \
118
+ "#{file} #{line_changes}"
119
+ end
120
+
121
+ changes.each do |change|
122
+ report[param][vers]['changed'] << change unless change.empty?
123
+ end
124
+ end
125
+ report
126
+ end
127
+
128
+ ##
129
+ # Check that files exist
130
+
131
+ def check_files(files)
132
+ files.each do |file|
133
+ unless File.exist? file
134
+ warn "#{file} mentioned in spec does not exist " +
135
+ "in the gem package, skipping check"
136
+ return false
137
+ end
138
+ end
139
+ true
140
+ end
141
+
142
+
143
+ ##
144
+ # Return how many lines differ between +prev_file+
145
+ # and +curr_file+ in format +ADDED/-DELETED
146
+
147
+ def lines_changed(prev_file, curr_file)
148
+ line = compact_files_diff(prev_file, curr_file)
149
+ return '' if line.empty?
150
+ "#{Rainbow(line.count('+')).green}/#{Rainbow(line.count('-')).red}"
151
+ end
152
+
153
+ ##
154
+ # Return +value+ in the given +spec+
155
+
156
+ def value_from_spec(param, spec)
157
+ if spec.respond_to? :"#{param}"
158
+ spec.send(:"#{param}")
159
+ else
160
+ warn "#{spec.full_name} does not respond to " +
161
+ "#{param}, skipping check"
162
+ nil
163
+ end
164
+ end
165
+
166
+ ##
167
+ # Return changes between files:
168
+ # + for line added
169
+ # - for line deleted
170
+
171
+ def compact_files_diff(prev_file, curr_file)
172
+ changes = ''
173
+ Diffy::Diff.new(
174
+ prev_file, curr_file, :source => 'files', :context => 0
175
+ ).each do |line|
176
+ case line
177
+ when /^\+/ then changes << Rainbow('+').green
178
+ when /^-/ then changes << Rainbow('-').red
179
+ end
180
+ end
181
+ changes
182
+ end
183
+
184
+ ##
185
+ # Get file's permission
186
+
187
+ def file_permissions(file)
188
+ sprintf("%o", File.stat(file).mode)
189
+ end
190
+
191
+ ##
192
+ # Find and return permission changes between files
193
+
194
+ def permission_changed(prev_file, curr_file)
195
+ prev_permissions = file_permissions(prev_file)
196
+ curr_permissions = file_permissions(curr_file)
197
+
198
+ if prev_permissions != curr_permissions
199
+ " (!) New permissions: " +
200
+ "#{prev_permissions} -> #{curr_permissions}"
201
+ else
202
+ ''
203
+ end
204
+ end
205
+
206
+ ##
207
+ # Find if the file is now/or was executable
208
+
209
+ def executables_changed(prev_file, curr_file)
210
+ prev_executable = File.stat(prev_file).executable?
211
+ curr_executable = File.stat(curr_file).executable?
212
+
213
+ if !prev_executable && curr_executable
214
+ " (!) File is now executable!"
215
+ elsif prev_executable && !curr_executable
216
+ " (!) File is no longer executable!"
217
+ else
218
+ ''
219
+ end
220
+ end
221
+
222
+ ##
223
+ # Return the first line of the +file+
224
+
225
+ def first_line(file)
226
+ begin
227
+ File.open(file) { |f| f.readline }.gsub(/(.*)\n/, '\1')
228
+ rescue
229
+ info "#{file} is binary, skipping shebang check"
230
+ ''
231
+ end
232
+ end
233
+
234
+ ##
235
+ # Find if the shabang of the file has been changed
236
+
237
+ def shebangs_changed(prev_file, curr_file)
238
+ first_lines = {}
239
+
240
+ [prev_file, curr_file].each do |file|
241
+ first_lines[file] = first_line(file)
242
+ end
243
+
244
+ return '' if first_lines[prev_file] == first_lines[curr_file]
245
+
246
+ prev_has_shebang = (first_lines[prev_file] =~ SHEBANG_REGEX)
247
+ curr_has_shebang = (first_lines[curr_file] =~ SHEBANG_REGEX)
248
+
249
+ if prev_has_shebang && !curr_has_shebang
250
+ " (!) Shebang probably lost: #{first_lines[prev_file]}"
251
+ elsif !prev_has_shebang && curr_has_shebang
252
+ " (!) Shebang probably added: #{first_lines[curr_file]}"
253
+ elsif prev_has_shebang && curr_has_shebang
254
+ " (!) Shebang probably changed: " +
255
+ "#{first_lines[prev_file]} -> #{first_lines[curr_file]}"
256
+ else
257
+ ''
258
+ end
259
+ end
260
+
261
+ end
262
+ end