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.
- checksums.yaml +4 -4
- data/{LICENSE.txt → LICENSE} +8 -10
- data/README.md +141 -16
- data/Rakefile +41 -1
- data/lib/rubygems/commands/compare_command.rb +79 -2
- data/lib/rubygems/comparator.rb +198 -0
- data/lib/rubygems/comparator/base.rb +88 -0
- data/lib/rubygems/comparator/dependency_comparator.rb +111 -0
- data/lib/rubygems/comparator/file_list_comparator.rb +262 -0
- data/lib/rubygems/comparator/gemfile_comparator.rb +150 -0
- data/lib/rubygems/comparator/report.rb +121 -0
- data/lib/rubygems/comparator/spec_comparator.rb +77 -0
- data/lib/rubygems_plugin.rb +0 -7
- data/test/rubygems/comparator/test_dependency_comparator.rb +29 -0
- data/test/rubygems/comparator/test_file_list_comparator.rb +22 -0
- data/test/rubygems/comparator/test_gemfile_comparator.rb +14 -0
- data/test/rubygems/comparator/test_spec_comparator.rb +153 -0
- data/test/rubygems/test_gem_commands_compare_command.rb +34 -0
- data/test/test_helper.rb +20 -0
- metadata +48 -25
- data/.gitignore +0 -17
- data/Gemfile +0 -4
- data/gem-compare.gemspec +0 -23
- data/lib/gem/compare.rb +0 -7
- data/lib/gem/compare/version.rb +0 -5
@@ -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
|