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.
@@ -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