gem-compare 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|