myprecious 0.0.8 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/myprecious +5 -1
- data/lib/myprecious.rb +536 -107
- data/lib/myprecious/data_caches.rb +71 -0
- data/lib/myprecious/python_packages.rb +1175 -0
- data/lib/myprecious/ruby_gems.rb +273 -0
- metadata +134 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be0d5a3297a77bea22a403968fa7c3a23f2fd7c2d4ce96b957c832788f29de91
|
4
|
+
data.tar.gz: d82b540dbb7be2ffb7601fe571cc07482d2fbe87563b8d78d937bc791bd22d3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79e7317fa010abdc6ab9d7207d7be1229d85e2ba2a53cb2225e1a8bed7c95063d51294fab4917f1449e106210e1aa3ef5c52fa743bde9e6fb85f582a6942694a
|
7
|
+
data.tar.gz: 0fb7d8ec2c159fe58867b2f5cbbfa95a9a27c7f0e6f020ea2f69fb2ecf55b49b2ed385857c3a6162670c07a39871b54724f2005c71dd6897e691438701d33316
|
data/bin/myprecious
CHANGED
data/lib/myprecious.rb
CHANGED
@@ -1,121 +1,550 @@
|
|
1
|
-
require 'gems'
|
2
1
|
require 'date'
|
3
2
|
require 'git'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'pathname'
|
5
|
+
require 'rake/toolkit_program'
|
6
|
+
require 'uri'
|
4
7
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
8
|
+
# Declare the module here so it doesn't cause problems for files in the
|
9
|
+
# "myprecious" directory (or _does_ cause problems if they try to declare
|
10
|
+
# it a class)
|
11
|
+
module MyPrecious
|
12
|
+
DATA_DIR = Pathname('~/.local/lib/myprecious').expand_path
|
13
|
+
ONE_DAY = 60 * 60 * 24
|
14
|
+
end
|
15
|
+
require 'myprecious/data_caches'
|
16
|
+
|
17
|
+
module MyPrecious
|
18
|
+
extend Rake::DSL
|
19
|
+
|
20
|
+
Program = Rake::ToolkitProgram
|
21
|
+
Program.title = "myprecious Dependecy Reporting Tool"
|
22
|
+
|
23
|
+
def self.common_program_args(parser, args)
|
24
|
+
parser.on(
|
25
|
+
'-o', '--out FILE',
|
26
|
+
"Output file to generate",
|
27
|
+
) {|fpath| args.output_file = Pathname(fpath)}
|
28
|
+
|
29
|
+
args.target = Pathname.pwd
|
30
|
+
parser.on(
|
31
|
+
'-C', '--dir PATH',
|
32
|
+
"Path to inspect",
|
33
|
+
) do |fpath|
|
34
|
+
fpath = Pathname(fpath)
|
35
|
+
parser.invalid_args!("#{fpath} does not exist.") unless fpath.exist?
|
36
|
+
args.target = fpath
|
37
|
+
end
|
38
|
+
|
39
|
+
parser.on(
|
40
|
+
'--[no-]cache',
|
41
|
+
"Control caching of gem information"
|
42
|
+
) {|v| MyPrecious.caching_disabled = v}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Declare the tasks exposed as subcommands in this block
|
46
|
+
Program.command_tasks do
|
47
|
+
desc "Generate report on Ruby gems"
|
48
|
+
task('ruby-gems').parse_args(into: OpenStruct.new) do |parser, args|
|
49
|
+
parser.expect_positional_cardinality(0)
|
50
|
+
common_program_args(parser, args)
|
51
|
+
end
|
52
|
+
task 'ruby-gems' do
|
53
|
+
require 'myprecious/ruby_gems'
|
54
|
+
args = Program.args
|
55
|
+
out_fpath = args.output_file || Reporting.default_output_fpath(args.target)
|
56
|
+
|
57
|
+
col_order = Reporting.read_column_order_from(out_fpath)
|
58
|
+
|
59
|
+
# Get all gems used via RubyGemInfo.each_gem_used, accumulating version requirements
|
60
|
+
gems = RubyGemInfo.accum_gem_lock_info(args.target)
|
61
|
+
|
62
|
+
out_fpath.open('w') do |outf|
|
63
|
+
# Header
|
64
|
+
outf.puts "Last updated: #{Time.now.rfc2822}; Use for directional purposes only, this data is not real time and might be slightly inaccurate"
|
65
|
+
outf.puts
|
66
|
+
|
67
|
+
Reporting.header_lines(
|
68
|
+
col_order,
|
69
|
+
RubyGemInfo.method(:col_title)
|
70
|
+
).each {|l| outf.puts l}
|
71
|
+
|
72
|
+
# Iterate all gems in name order, pulling column values from the RubyGemInfo objects
|
73
|
+
gems.keys.sort_by {|n| n.downcase}.map {|name| gems[name]}.each do |gem|
|
74
|
+
Reporting.on_dependency(gem.name) do
|
75
|
+
outf.puts col_order.markdown_columns(MarkdownAdapter.new(gem))
|
42
76
|
end
|
43
|
-
elsif line_number > 2
|
44
|
-
gem_name_index = word_array[gem_name_pos].strip
|
45
|
-
#extract just the name of the gem from the first column
|
46
|
-
#since that column contains a markdown-formatted hyperlink
|
47
|
-
gem_name_index = gem_name_index[/\[(.*?)\]/,1]
|
48
|
-
gem_lines[gem_name_index] = line_number
|
49
77
|
end
|
50
|
-
|
51
78
|
end
|
52
|
-
|
53
|
-
else
|
54
|
-
File.open(repo_name+'.md', 'w') { |write_file|
|
55
|
-
write_file.puts "Last updated:" + Date.today.to_s + "; Use for directional purposes only, this data is not real time and might be slightly inaccurate" + "\n\n"
|
56
|
-
write_file.puts "Gem | Our Version | Latest Version | Date available | Age (in days) | License Type | Change Log"
|
57
|
-
write_file.puts "--- | --- | --- | --- | --- | --- | ---"
|
58
|
-
}
|
79
|
+
|
59
80
|
end
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
81
|
+
|
82
|
+
desc "Generate report on Python packages"
|
83
|
+
task('python-packages').parse_args(into: OpenStruct.new) do |parser, args|
|
84
|
+
parser.expect_positional_cardinality(0)
|
85
|
+
common_program_args(parser, args)
|
86
|
+
|
87
|
+
parser.on(
|
88
|
+
'-r', '--requirements-file FILE',
|
89
|
+
"requirements.txt-style file to read"
|
90
|
+
) do |file|
|
91
|
+
if args.requirements_file
|
92
|
+
invalid_args!("Only one requirements file may be specified.")
|
93
|
+
end
|
94
|
+
args.requirements_file = file
|
95
|
+
end
|
96
|
+
end
|
97
|
+
task 'python-packages' do
|
98
|
+
require 'myprecious/python_packages'
|
99
|
+
args = Program.args
|
100
|
+
out_fpath = args.output_file || Reporting.default_output_fpath(args.target)
|
101
|
+
|
102
|
+
col_order = Reporting.read_column_order_from(out_fpath)
|
103
|
+
|
104
|
+
req_file = args.requirements_file
|
105
|
+
req_file ||= PyPackageInfo.guess_req_file(args.target)
|
106
|
+
req_file = args.target.join(req_file) unless req_file.nil?
|
107
|
+
if req_file.nil? || !req_file.exist?
|
108
|
+
invalid_args!("Unable to guess requirement file name; specify with '-r FILE' option.")
|
109
|
+
end
|
110
|
+
pkgs = PyPackageInfo::Reader.new(req_file).each_installed_package
|
111
|
+
|
112
|
+
out_fpath.open('w') do |outf|
|
113
|
+
# Header
|
114
|
+
outf.puts "Last updated: #{Time.now.rfc2822}; Use for directional purposes only, this data is not real time and might be slightly inaccurate"
|
115
|
+
outf.puts
|
116
|
+
|
117
|
+
Reporting.header_lines(
|
118
|
+
col_order,
|
119
|
+
PyPackageInfo.method(:col_title)
|
120
|
+
).each {|l| outf.puts l}
|
121
|
+
|
122
|
+
pkgs = pkgs.to_a
|
123
|
+
pkgs.sort_by! {|pkg| pkg.name.downcase}
|
124
|
+
pkgs.each do |pkg|
|
125
|
+
Reporting.on_dependency(pkg.name) do
|
126
|
+
outf.puts col_order.markdown_columns(MarkdownAdapter.new(pkg))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
desc "Clear all myprecious data caches"
|
133
|
+
task('clear-caches').parse_args(into: OpenStruct.new) do |parser, args|
|
134
|
+
args.prompt = true
|
135
|
+
parser.on('--[no-]confirm', "Control confirmation prompt") {|v| args.prompt = v}
|
136
|
+
|
137
|
+
# This option exists to force users to specify the full "--no-confirm"
|
138
|
+
parser.on('--no-confir', "Warn about incomplete --no-confirm") do
|
139
|
+
parser.invalid_args!("Incomplete --no-confirm flag")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
task 'clear-caches' do
|
143
|
+
# Load all "myprecious/*.rb" files so we know where all the caches are
|
144
|
+
Dir[File.expand_path('myprecious/*.rb', __dir__)].each {|f| require f}
|
145
|
+
|
146
|
+
next if Program.args.prompt && !yes_no('Delete all cached data (y/n)?')
|
147
|
+
MyPrecious.data_caches.each do |cache|
|
69
148
|
begin
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
149
|
+
rm_r cache
|
150
|
+
rescue Errno::ENOENT
|
151
|
+
# No problem, we wanted to delete this anyway
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# Prompt user for a yes/no answer
|
159
|
+
#
|
160
|
+
# It doesn't matter if they've redirected STDIN/STDOUT -- this grabs the TTY
|
161
|
+
# directly.
|
162
|
+
#
|
163
|
+
def self.yes_no(prompt)
|
164
|
+
Pathname('/dev/tty').open('r+') do |term|
|
165
|
+
loop do
|
166
|
+
term.write("#{prompt} ")
|
167
|
+
case term.gets[0..-2]
|
168
|
+
when 'y', 'Y', 'yes', 'Yes', 'YES'
|
169
|
+
return true
|
170
|
+
when 'n', 'N', 'no', 'No', 'NO'
|
171
|
+
return false
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
##
|
178
|
+
# Tool for getting information about the Git repository associated with a
|
179
|
+
# directory
|
180
|
+
#
|
181
|
+
class GitInfoExtractor
|
182
|
+
URL_PATTERN = /\/([^\/]+)\.git$/
|
183
|
+
|
184
|
+
def initialize(dir)
|
185
|
+
super()
|
186
|
+
@dir = dir
|
187
|
+
end
|
188
|
+
attr_reader :dir
|
189
|
+
|
190
|
+
def git_info
|
191
|
+
@git_info ||= Git.open(self.dir)
|
192
|
+
end
|
193
|
+
|
194
|
+
def origin_remote
|
195
|
+
git_info.remotes.find {|r| r.name == 'origin'}
|
196
|
+
end
|
197
|
+
|
198
|
+
def repo_name
|
199
|
+
@repo_name ||= (URL_PATTERN =~ origin_remote.url) && $1
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
##
|
204
|
+
# Common behavior in dependency report generation
|
205
|
+
#
|
206
|
+
# The methods here are not specific to any language or dependency management
|
207
|
+
# framework. They do work with ColumnOrder and an expected set of attributes
|
208
|
+
# on the dependency information objects.
|
209
|
+
#
|
210
|
+
module Reporting
|
211
|
+
##
|
212
|
+
# Compute the default output filepath for a directory
|
213
|
+
#
|
214
|
+
def default_output_fpath(dir)
|
215
|
+
dir / (GitInfoExtractor.new(dir).repo_name + "-dependency-tracking.md")
|
216
|
+
end
|
217
|
+
module_function :default_output_fpath
|
218
|
+
|
219
|
+
##
|
220
|
+
# Read the column order from the file at the given path
|
221
|
+
#
|
222
|
+
# If +fpath+ indicates a file that does not exist, return the default
|
223
|
+
# ColumnOrder.
|
224
|
+
#
|
225
|
+
def read_column_order_from(fpath)
|
226
|
+
result = ColumnOrder.new
|
227
|
+
begin
|
228
|
+
prev_line = nil
|
229
|
+
fpath.open {|inf| inf.each_line do |line|
|
230
|
+
if prev_line && /^-+(?:\|-+)*$/ =~ line.gsub(' ', '')
|
231
|
+
result.read_order_from_headers(prev_line)
|
232
|
+
break
|
109
233
|
end
|
110
|
-
|
111
|
-
|
112
|
-
|
234
|
+
prev_line = line
|
235
|
+
end}
|
236
|
+
rescue Errno::ENOENT
|
237
|
+
# No problem
|
238
|
+
end
|
239
|
+
return result
|
240
|
+
end
|
241
|
+
module_function :read_column_order_from
|
242
|
+
|
243
|
+
##
|
244
|
+
# Generate header lines for the output Markdown table
|
245
|
+
#
|
246
|
+
# Returns an Array of strings, currently two lines giving header text and
|
247
|
+
# divider row.
|
248
|
+
#
|
249
|
+
def header_lines(order, titlizer)
|
250
|
+
col_titles = order.map {|c| titlizer.call(c)}
|
251
|
+
|
252
|
+
# Check that all attributes in #order round-trip through the title name
|
253
|
+
order.zip(col_titles) do |attr, title|
|
254
|
+
unless title.kind_of?(Symbol) || ColumnOrder.col_from_text_name(title) == attr
|
255
|
+
raise "'#{attr}' does not round-trip (rendered as #{result.inspect})"
|
113
256
|
end
|
114
257
|
end
|
258
|
+
|
259
|
+
return [
|
260
|
+
col_titles.join(" | "),
|
261
|
+
(["---"] * col_titles.length).join(" | "),
|
262
|
+
]
|
263
|
+
end
|
264
|
+
module_function :header_lines
|
265
|
+
# TODO: Mark dependencies with colors a la https://stackoverflow.com/a/41247934
|
266
|
+
|
267
|
+
##
|
268
|
+
# Converts an attribute name to the column title for generic attributes
|
269
|
+
#
|
270
|
+
# Dependency info classes (like RubyGemInfo) can delegate common column
|
271
|
+
# title generation to this function.
|
272
|
+
#
|
273
|
+
def common_col_title(attr)
|
274
|
+
case attr
|
275
|
+
when :current_version then 'Our Version'
|
276
|
+
when :age then 'Age (in days)'
|
277
|
+
when :latest_version then 'Latest Version'
|
278
|
+
when :latest_released then 'Date Available'
|
279
|
+
when :recommended_version then 'Recommended Version'
|
280
|
+
when :license then 'License Type'
|
281
|
+
when :changelog then 'Change Log'
|
282
|
+
when :obsolescence then 'How Bad'
|
283
|
+
else
|
284
|
+
warn("'#{attr}' column does not have a mapped name")
|
285
|
+
attr
|
286
|
+
end
|
287
|
+
end
|
288
|
+
module_function :common_col_title
|
289
|
+
|
290
|
+
##
|
291
|
+
# Determine obsolescence level from days
|
292
|
+
#
|
293
|
+
# Returns one of +nil+, +:mild+, +:moderate+, or +:severe+.
|
294
|
+
#
|
295
|
+
# +at_least_moderate:+ allows putting a floor of +:moderate+ obsolescence
|
296
|
+
# on the result.
|
297
|
+
#
|
298
|
+
def obsolescence_by_age(days, at_least_moderate: false)
|
299
|
+
return case
|
300
|
+
when days < 270
|
301
|
+
at_least_moderate ? :moderate : nil
|
302
|
+
when days < 500
|
303
|
+
at_least_moderate ? :moderate : :mild
|
304
|
+
when days < 730
|
305
|
+
:moderate
|
306
|
+
else
|
307
|
+
:severe
|
308
|
+
end
|
309
|
+
end
|
310
|
+
module_function :obsolescence_by_age
|
311
|
+
|
312
|
+
##
|
313
|
+
# Wrap output from processing of an individual dependency with intro and
|
314
|
+
# outro messages
|
315
|
+
#
|
316
|
+
def on_dependency(name)
|
317
|
+
progress_out = $stdout
|
318
|
+
progress_out.puts("--- Reporting on #{name}...")
|
319
|
+
yield
|
320
|
+
progress_out.puts(" (done)")
|
321
|
+
end
|
322
|
+
module_function :on_dependency
|
323
|
+
end
|
324
|
+
|
325
|
+
##
|
326
|
+
# Order of columns in a Markdown table
|
327
|
+
#
|
328
|
+
# Contains the default column ordering when constructed. Columns are
|
329
|
+
# identified by the Symbol commonly used as an attribute on a dependency
|
330
|
+
# info object (e.g. RubyGemInfo instance). Objects of this class behave
|
331
|
+
# to some extent like frozen Array instances.
|
332
|
+
#
|
333
|
+
class ColumnOrder
|
334
|
+
DEFAULT = %i[name current_version age latest_version latest_released recommended_version license changelog].freeze
|
335
|
+
COLUMN_FROM_TEXT_NAME = {
|
336
|
+
'gem' => :name,
|
337
|
+
'package' => :name,
|
338
|
+
'module' => :name,
|
339
|
+
'our version' => :current_version,
|
340
|
+
'how bad' => :obsolescence,
|
341
|
+
'latest version' => :latest_version,
|
342
|
+
'date available' => :latest_released,
|
343
|
+
'age (in days)' => :age,
|
344
|
+
'license type' => :license,
|
345
|
+
/change ?log/ => :changelog,
|
346
|
+
'recommended version' => :recommended_version,
|
347
|
+
}
|
348
|
+
|
349
|
+
def initialize
|
350
|
+
super
|
351
|
+
@order = DEFAULT
|
352
|
+
end
|
353
|
+
|
354
|
+
##
|
355
|
+
# Get the +n+-th column attribute Symbol
|
356
|
+
#
|
357
|
+
def [](n)
|
358
|
+
@order[n]
|
359
|
+
end
|
360
|
+
|
361
|
+
def length
|
362
|
+
@order.length
|
363
|
+
end
|
364
|
+
|
365
|
+
def each(&blk)
|
366
|
+
@order.each(&blk)
|
367
|
+
end
|
368
|
+
include Enumerable
|
369
|
+
|
370
|
+
##
|
371
|
+
# Update the column order to match those in the given line
|
372
|
+
#
|
373
|
+
# Columns not included in the line are appended in the order they
|
374
|
+
# appear in the default order.
|
375
|
+
#
|
376
|
+
def read_order_from_headers(headers_line)
|
377
|
+
headers = headers_line.split('|').map {|h| h.strip.squeeze(' ')}
|
378
|
+
@order = headers.map {|h| self.class.col_from_text_name(h)}.compact
|
379
|
+
|
380
|
+
# Add in any missing columns at the end
|
381
|
+
@order.concat(DEFAULT - @order)
|
382
|
+
|
383
|
+
return @order.dup
|
384
|
+
end
|
385
|
+
|
386
|
+
##
|
387
|
+
# Render a line to include in a Markdown table for the given dependency
|
388
|
+
#
|
389
|
+
# The dependency must know how to respond to (Ruby) messages (i.e.
|
390
|
+
# have attributes) for all columns currently included in the order as
|
391
|
+
# represented by this instance.
|
392
|
+
#
|
393
|
+
def markdown_columns(dependency)
|
394
|
+
map {|attr| dependency.send(attr)}.join(" | ")
|
395
|
+
end
|
396
|
+
|
397
|
+
##
|
398
|
+
# Given a text name, derive the equivalent column attribute
|
399
|
+
#
|
400
|
+
def self.col_from_text_name(n)
|
401
|
+
n = n.downcase
|
402
|
+
entry = COLUMN_FROM_TEXT_NAME.find {|k, v| k === n}
|
403
|
+
return entry && entry[1]
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
##
|
408
|
+
# Extension of String that can accomodate some additional commentary
|
409
|
+
#
|
410
|
+
# The +update_info+ attribute is used to pass information about changes
|
411
|
+
# to licensing between the current and recommended version of a dependency,
|
412
|
+
# and may be +nil+.
|
413
|
+
#
|
414
|
+
class LicenseDescription < String
|
415
|
+
attr_accessor :update_info
|
416
|
+
end
|
417
|
+
|
418
|
+
##
|
419
|
+
# Dependency info wrapper to generate nice Markdown columns
|
420
|
+
#
|
421
|
+
# This wrapper takes basic data from the underlying dependency info object
|
422
|
+
# and returns enhanced Markdown for selected columns (e.g. +name+).
|
423
|
+
#
|
424
|
+
class MarkdownAdapter
|
425
|
+
def initialize(dep)
|
426
|
+
super()
|
427
|
+
@dependency = dep
|
428
|
+
end
|
429
|
+
attr_reader :dependency
|
430
|
+
|
431
|
+
##
|
432
|
+
# Generate Markdown linking the +name+ to the homepage for the dependency
|
433
|
+
#
|
434
|
+
def name
|
435
|
+
cswatch = begin
|
436
|
+
color_swatch + ' '
|
437
|
+
rescue StandardError
|
438
|
+
''
|
439
|
+
end
|
440
|
+
"#{cswatch}[#{dependency.name}](#{dependency.homepage_uri})"
|
441
|
+
rescue StandardError
|
442
|
+
dependency.name
|
443
|
+
end
|
444
|
+
|
445
|
+
##
|
446
|
+
# Include information about temporal difference between current and
|
447
|
+
# recommended versions
|
448
|
+
#
|
449
|
+
def recommended_version
|
450
|
+
recommended_version = dependency.recommended_version
|
451
|
+
if dependency.current_version < recommended_version
|
452
|
+
span_comment = begin
|
453
|
+
if days_newer = dependency.days_between_current_and_recommended
|
454
|
+
" -- #{days_newer} days newer"
|
455
|
+
else
|
456
|
+
""
|
457
|
+
end
|
458
|
+
end
|
459
|
+
"**#{recommended_version}**#{span_comment}"
|
460
|
+
else
|
461
|
+
recommended_version
|
462
|
+
end
|
463
|
+
rescue StandardError
|
464
|
+
recommended_version || "(error)"
|
465
|
+
end
|
466
|
+
|
467
|
+
##
|
468
|
+
# Include update info in the license column
|
469
|
+
#
|
470
|
+
def license
|
471
|
+
value = dependency.license
|
472
|
+
if value.update_info
|
473
|
+
"#{value}<br/>(#{value.update_info})"
|
474
|
+
else
|
475
|
+
value
|
476
|
+
end
|
477
|
+
rescue StandardError
|
478
|
+
"(error)"
|
479
|
+
end
|
480
|
+
|
481
|
+
##
|
482
|
+
# Render short links for http: or https: changelog URLs
|
483
|
+
#
|
484
|
+
def changelog
|
485
|
+
base_val = begin
|
486
|
+
dependency.changelog
|
487
|
+
rescue StandardError
|
488
|
+
return "(error)"
|
489
|
+
end
|
490
|
+
|
491
|
+
begin
|
492
|
+
uri = URI.parse(base_val)
|
493
|
+
if ['http', 'https'].include?(uri.scheme)
|
494
|
+
return "[on #{uri.hostname}](#{base_val})"
|
495
|
+
end
|
496
|
+
rescue StandardError
|
497
|
+
end
|
498
|
+
return base_val
|
499
|
+
end
|
500
|
+
|
501
|
+
def obsolescence
|
502
|
+
color_swatch
|
503
|
+
rescue StandardError
|
504
|
+
''
|
505
|
+
end
|
506
|
+
|
507
|
+
##
|
508
|
+
# Get a CSS-style hex color code corresponding to the obsolescence of the dependency
|
509
|
+
#
|
510
|
+
def color
|
511
|
+
case dependency.obsolescence
|
512
|
+
when :mild then "dde418"
|
513
|
+
when :moderate then "f9b733"
|
514
|
+
when :severe then "fb0e0e"
|
515
|
+
else "4dda1b"
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
##
|
520
|
+
# Markdown for an obsolescence color swatch
|
521
|
+
#
|
522
|
+
# Sourced from: https://stackoverflow.com/a/41247934
|
523
|
+
#
|
524
|
+
def color_swatch
|
525
|
+
"![##{color}](https://placehold.it/15/#{color}/000000?text=+)"
|
526
|
+
end
|
527
|
+
|
528
|
+
##
|
529
|
+
# Delegate other attribute queries to the base dependency object
|
530
|
+
#
|
531
|
+
# Errors are caught and rendered as "(error)"
|
532
|
+
#
|
533
|
+
def method_missing(meth, *args, &blk)
|
534
|
+
dependency.send(meth, *args, &blk)
|
535
|
+
rescue NoMethodError
|
536
|
+
raise
|
537
|
+
rescue StandardError
|
538
|
+
"(error)"
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
module URIModuleMethods
|
543
|
+
def try_parse(s)
|
544
|
+
parse(s)
|
545
|
+
rescue URI::InvalidURIError
|
546
|
+
nil
|
115
547
|
end
|
116
|
-
File.open(repo_name+'.md', 'w') { |f| f.write(final_write.join) }
|
117
|
-
file.close
|
118
548
|
end
|
549
|
+
URI.extend(URIModuleMethods)
|
119
550
|
end
|
120
|
-
|
121
|
-
MyPrecious.update
|