cutting_edge 0.0.1
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 +7 -0
- data/Gemfile +10 -0
- data/LICENSE +674 -0
- data/README.md +95 -0
- data/Rakefile +159 -0
- data/bin/cutting_edge +72 -0
- data/cutting_edge.gemspec +57 -0
- data/lib/cutting_edge.rb +3 -0
- data/lib/cutting_edge/app.rb +77 -0
- data/lib/cutting_edge/badge.rb +46 -0
- data/lib/cutting_edge/langs.rb +123 -0
- data/lib/cutting_edge/langs/python.rb +102 -0
- data/lib/cutting_edge/langs/ruby.rb +47 -0
- data/lib/cutting_edge/langs/rust.rb +68 -0
- data/lib/cutting_edge/repo.rb +67 -0
- data/lib/cutting_edge/versions.rb +46 -0
- data/lib/cutting_edge/workers/badge.rb +22 -0
- data/lib/cutting_edge/workers/dependency.rb +100 -0
- data/lib/cutting_edge/workers/helpers.rb +24 -0
- data/spec/langs/python_spec.rb +89 -0
- data/spec/langs/rust_spec.rb +81 -0
- data/spec/spec_helper.rb +111 -0
- metadata +191 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'victor'
|
2
|
+
|
3
|
+
class Badge
|
4
|
+
BADGE_OPTIONS = {
|
5
|
+
up_to_date: {
|
6
|
+
bg_color: '#32CD32',
|
7
|
+
text: 'up-to-date',
|
8
|
+
width: 180
|
9
|
+
},
|
10
|
+
out_of_date: {
|
11
|
+
bg_color: '#ff0000',
|
12
|
+
text: 'out-of-date',
|
13
|
+
width: 190
|
14
|
+
},
|
15
|
+
unknown: {
|
16
|
+
bg_color: '#666',
|
17
|
+
text: 'unknown',
|
18
|
+
width: 170
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
def self.build_badge(status, num=nil)
|
23
|
+
if ! [:up_to_date, :out_of_date, :unknown].include?(status)
|
24
|
+
status = :unknown
|
25
|
+
end
|
26
|
+
number = Integer(num) rescue nil if num
|
27
|
+
|
28
|
+
svg = Victor::SVG.new width: BADGE_OPTIONS[status][:width], height: 32, template: :html
|
29
|
+
|
30
|
+
style = {
|
31
|
+
stroke: '#d3d3d3',
|
32
|
+
stroke_width: 4
|
33
|
+
}
|
34
|
+
|
35
|
+
svg.build do
|
36
|
+
rect x: 0, y: 0, width: BADGE_OPTIONS[status][:width], height: 32, fill: BADGE_OPTIONS[status][:bg_color], style: style
|
37
|
+
|
38
|
+
g font_size: 14, font_family: 'arial', fill: 'white' do
|
39
|
+
text "#{number} Dependencies #{BADGE_OPTIONS[status][:text]}", x: 10, y: 20
|
40
|
+
end
|
41
|
+
end
|
42
|
+
return svg.render
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'toml-rb'
|
3
|
+
|
4
|
+
class Gem::Dependency
|
5
|
+
TYPES = [:runtime, :development, :build]
|
6
|
+
end
|
7
|
+
|
8
|
+
module LanguageHelpers
|
9
|
+
# Return a mock construct that mimicks Gem::Dependency for depedencies we tried to parse, but weren't valid.
|
10
|
+
def unknown_dependency(name, type = :runtime)
|
11
|
+
OpenStruct.new(name: name, type: type, requirement: 'unknown')
|
12
|
+
end
|
13
|
+
|
14
|
+
# For each dependency, find its latest version and return the two together. Takes account of invalid or dependencies (see #unknown_dependency)
|
15
|
+
#
|
16
|
+
# results - Array of Gem::Dependencies and unknown dependencies.
|
17
|
+
#
|
18
|
+
# Returns an Array of tuples of each dependency and its latest version: e.g., [[<Gem::Dependency>, <Gem::Version>]]
|
19
|
+
def dependency_with_latest(results)
|
20
|
+
results.map do |dependency|
|
21
|
+
[dependency, dependency.requirement.to_s == 'unknown' ? nil : latest_version(dependency.name)]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def log_error(message)
|
26
|
+
logger.error(message) if ::CuttingEdge::App.enable_logging
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Language
|
31
|
+
include ::SemanticLogger::Loggable
|
32
|
+
extend LanguageHelpers
|
33
|
+
end
|
34
|
+
|
35
|
+
module LanguageVersionHelpers
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def canonical_version(version)
|
40
|
+
version.match(/^\./) ? "0#{version}" : version
|
41
|
+
end
|
42
|
+
|
43
|
+
# Translate wildcard (*) requirement to Ruby/Gem compatible requirement
|
44
|
+
#
|
45
|
+
# req - String version requirement
|
46
|
+
#
|
47
|
+
# Returns a translated String version requirement
|
48
|
+
def translate_wildcard(req)
|
49
|
+
if req =~ /!=/ # Negative Wildcard
|
50
|
+
# Turn != 1.1.* into >= 1.2, < 1.1
|
51
|
+
req.sub!('.*', '.0')
|
52
|
+
req.sub!('!=', '')
|
53
|
+
begin
|
54
|
+
v = Gem::Version.new(req) # Create the bumped version using Gem::Version so pre-release handling will work
|
55
|
+
rescue ArgumentError => e
|
56
|
+
return nil
|
57
|
+
end
|
58
|
+
lower_bound = ">= #{v.bump.version}"
|
59
|
+
upper_bound = "< #{v.version}"
|
60
|
+
[lower_bound, upper_bound]
|
61
|
+
elsif req =~ /^\s*\*\s*/
|
62
|
+
'>= 0' # Turn * into >= 0
|
63
|
+
else
|
64
|
+
"~> #{req.sub('.*', '.0').sub('=', '')}" # Turn =1.1.* or 1.1.* into ~> 1.1.0
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Translate SemVer Caret requirement to Ruby/Gem compatible requirement
|
69
|
+
# Caret requirement:
|
70
|
+
# lower bound = the unmodified version
|
71
|
+
# upper bound = take the left most non-zero digit and +1 it
|
72
|
+
#
|
73
|
+
# req - String version requirement
|
74
|
+
#
|
75
|
+
# Returns a translated String version requirement
|
76
|
+
def translate_caret(req)
|
77
|
+
req.sub!('^', '')
|
78
|
+
begin
|
79
|
+
version = Gem::Version.new(req)
|
80
|
+
rescue ArgumentError => e
|
81
|
+
return nil
|
82
|
+
end
|
83
|
+
segments = version.version.split('.')
|
84
|
+
index = segments.find_index {|seg| seg.to_i > 0} # Find the leftmost non-zero digit.
|
85
|
+
index = segments.rindex {|seg| seg.to_i == 0} unless index # If there is none, find the last 0.
|
86
|
+
segments[index] = segments[index].to_i + 1
|
87
|
+
upper_bound = segments[0..index].join('.')
|
88
|
+
[">= #{version.version}", "< #{upper_bound}"]
|
89
|
+
end
|
90
|
+
|
91
|
+
def parse_toml(content, sections)
|
92
|
+
begin
|
93
|
+
config = TomlRB.parse(content)
|
94
|
+
rescue TomlRB::ParseError => e
|
95
|
+
log_error("Encountered error when parsing TOML: #{e.class} #{e.message}")
|
96
|
+
return []
|
97
|
+
end
|
98
|
+
results = []
|
99
|
+
|
100
|
+
sections.each do |dependency_type, section_name|
|
101
|
+
packages = config[section_name] || next
|
102
|
+
packages.each do |name, info|
|
103
|
+
requirement = info.fetch('version', nil) rescue info
|
104
|
+
if requirement
|
105
|
+
requirements = requirement.split(',').map {|req| translate_requirement(req)}
|
106
|
+
next if requirements.include?(nil) # If a sub-requirement failed to translate, skip this entire dependency.
|
107
|
+
begin
|
108
|
+
results << Gem::Dependency.new(name, requirements, dependency_type)
|
109
|
+
rescue StandardError => e
|
110
|
+
log_error("Encountered error when parsing requirement #{requirements}: #{e.class} #{e.message}")
|
111
|
+
next
|
112
|
+
end
|
113
|
+
else
|
114
|
+
results << unknown_dependency(name, dependency_type)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
results
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
Dir[File.expand_path('../langs/*.rb', __FILE__)].each { |f| require f }
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'http'
|
3
|
+
|
4
|
+
class PythonLang < Language
|
5
|
+
# For Requirements.txt
|
6
|
+
# See https://iscompatible.readthedocs.io/en/latest/
|
7
|
+
COMPARATORS = />=|>|<=|<|==/
|
8
|
+
VERSION_NUM = /\d[\.\w]*/
|
9
|
+
SUFFIX_OPTION = /\s*(\[.*\])?/
|
10
|
+
NAME = /[^,]+/
|
11
|
+
REGEX = /^(#{NAME})\s*(#{COMPARATORS})\s*(#{VERSION_NUM})(\s*,\s*(#{COMPARATORS})\s*(#{VERSION_NUM}))?#{SUFFIX_OPTION}$/
|
12
|
+
|
13
|
+
API_URL = 'https://pypi.org/pypi/'
|
14
|
+
|
15
|
+
PIPFILE_SECTIONS = {
|
16
|
+
:runtime => 'packages',
|
17
|
+
:development => 'dev-packages'
|
18
|
+
}
|
19
|
+
|
20
|
+
extend LanguageVersionHelpers
|
21
|
+
|
22
|
+
class << self
|
23
|
+
|
24
|
+
# Defaults for projects in this language
|
25
|
+
def locations(name = nil)
|
26
|
+
['requirements.txt', 'Pipfile']
|
27
|
+
end
|
28
|
+
|
29
|
+
# Parse a dependency file
|
30
|
+
#
|
31
|
+
# name - String contents of the file
|
32
|
+
# content - String contents of the file
|
33
|
+
#
|
34
|
+
# Returns an Array of tuples of each dependency and its latest version: [[<Gem::Dependency>, <Gem::Version>]]
|
35
|
+
def parse_file(name, content)
|
36
|
+
return nil unless content
|
37
|
+
if name =~ /\.txt$/
|
38
|
+
results = parse_requirements(content)
|
39
|
+
elsif name =~ /Pipfile/
|
40
|
+
results = parse_toml(content, PIPFILE_SECTIONS)
|
41
|
+
end
|
42
|
+
dependency_with_latest(results) if results
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_requirements(content)
|
46
|
+
results = []
|
47
|
+
content.each_line do |line|
|
48
|
+
next if line =~ /^\s*-e/ # ignore 'editable' dependencies
|
49
|
+
if line =~ COMPARATORS
|
50
|
+
next unless match = line.match(REGEX) # Skip this line if it doesn't conform to our expectations
|
51
|
+
name, first_comp, first_version, _ignore, second_comp, second_version = match.captures
|
52
|
+
first_comp = '=' if first_comp == '=='
|
53
|
+
second_comp = '=' if second_comp == '=='
|
54
|
+
dep = Gem::Dependency.new(name, "#{first_comp} #{first_version}")
|
55
|
+
dep.requirement.concat(["#{second_comp} #{second_version}"]) if second_comp && second_version
|
56
|
+
else
|
57
|
+
dep = Gem::Dependency.new(line.strip) # requries version to be >= 0
|
58
|
+
end
|
59
|
+
results << dep
|
60
|
+
end
|
61
|
+
results
|
62
|
+
end
|
63
|
+
|
64
|
+
# Find the latest versions of a dependency by name
|
65
|
+
#
|
66
|
+
# name - String name of the dependency
|
67
|
+
#
|
68
|
+
# Returns a Gem::Version
|
69
|
+
def latest_version(name)
|
70
|
+
begin
|
71
|
+
content = HTTP.timeout(::CuttingEdge::LAST_VERSION_TIMEOUT).follow(max_hops: 1).get(::File.join(API_URL, name, 'json')).parse
|
72
|
+
version = content['info']['version']
|
73
|
+
Gem::Version.new(canonical_version(version))
|
74
|
+
rescue StandardError, HTTP::TimeoutError => e
|
75
|
+
log_error("Encountered error when fetching latest version of #{name}: #{e.class} #{e.message}")
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Translate version requirement syntax for Pipfiles to a String or Array of Strings that Gem::Dependency.new understands
|
81
|
+
# Pipfile support * and != requirements, which Ruby does not
|
82
|
+
# See https://www.python.org/dev/peps/pep-0440/#version-matching
|
83
|
+
#
|
84
|
+
# req - String version requirement
|
85
|
+
#
|
86
|
+
# Returns a translated String version requirement
|
87
|
+
def translate_requirement(req)
|
88
|
+
req.sub!('~=', '~>')
|
89
|
+
req.sub!('==', '=')
|
90
|
+
case req
|
91
|
+
when /\*/
|
92
|
+
translate_wildcard(req)
|
93
|
+
when '!='
|
94
|
+
req.sub!('!=', '')
|
95
|
+
["< #{req}", "> #{req}"]
|
96
|
+
else
|
97
|
+
req
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'gemnasium/parser'
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
class RubyLang < Language
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
# Defaults for projects in this language
|
9
|
+
def locations(name)
|
10
|
+
["#{name}.gemspec", 'Gemfile']
|
11
|
+
end
|
12
|
+
|
13
|
+
# Parse a dependency file
|
14
|
+
#
|
15
|
+
# name - String contents of the file
|
16
|
+
# content - String contents of the file
|
17
|
+
#
|
18
|
+
# Returns an Array of tuples of each dependency and its latest version: [[<Bundler::Dependency>, <Gem::Version>]]
|
19
|
+
def parse_file(name, content)
|
20
|
+
return nil unless content
|
21
|
+
results = name =~ /gemspec/ ? parse_gemspec(content) : parse_gemfile(content)
|
22
|
+
dependency_with_latest(results)
|
23
|
+
end
|
24
|
+
|
25
|
+
def latest_version(name)
|
26
|
+
# Fancy todo: cache these?
|
27
|
+
begin
|
28
|
+
Gem::SpecFetcher.fetcher.spec_for_dependency(Gem::Dependency.new(name, nil)).flatten.first.version
|
29
|
+
rescue StandardError => e
|
30
|
+
log_error("Encountered error when fetching latest version of #{name}: #{e.class} #{e.message}")
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_ruby(type, content)
|
36
|
+
Gemnasium::Parser.send(type, content).dependencies
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_gemspec(content)
|
40
|
+
parse_ruby(:gemspec, content)
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_gemfile(content)
|
44
|
+
parse_ruby(:gemfile, content)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'http'
|
3
|
+
|
4
|
+
class RustLang < Language
|
5
|
+
API_URL = 'https://crates.io/api/v1/crates'
|
6
|
+
|
7
|
+
CARGOFILE_SECTIONS = {
|
8
|
+
:runtime => 'dependencies',
|
9
|
+
:development => 'dev-dependencies',
|
10
|
+
:build => 'build-dependencies'
|
11
|
+
}
|
12
|
+
|
13
|
+
extend LanguageVersionHelpers
|
14
|
+
|
15
|
+
class << self
|
16
|
+
|
17
|
+
# Defaults for projects in this language
|
18
|
+
def locations(name = nil)
|
19
|
+
['Cargo.toml']
|
20
|
+
end
|
21
|
+
|
22
|
+
# Parse a dependency file
|
23
|
+
#
|
24
|
+
# name - String contents of the file
|
25
|
+
# content - String contents of the file
|
26
|
+
#
|
27
|
+
# Returns an Array of tuples of each dependency and its latest version: [[<Gem::Dependency>, <Gem::Version>]]
|
28
|
+
def parse_file(name, content)
|
29
|
+
return nil unless content
|
30
|
+
results = parse_toml(content, CARGOFILE_SECTIONS)
|
31
|
+
dependency_with_latest(results) if results
|
32
|
+
end
|
33
|
+
|
34
|
+
# Find the latest versions of a dependency by name
|
35
|
+
#
|
36
|
+
# name - String name of the dependency
|
37
|
+
#
|
38
|
+
# Returns a Gem::Version
|
39
|
+
def latest_version(name)
|
40
|
+
begin
|
41
|
+
content = HTTP.timeout(::CuttingEdge::LAST_VERSION_TIMEOUT).get(::File.join(API_URL, name)).parse
|
42
|
+
version = content['crate']['max_version']
|
43
|
+
Gem::Version.new(canonical_version(version))
|
44
|
+
rescue StandardError, HTTP::Error => e
|
45
|
+
log_error("Encountered error when fetching latest version of #{name}: #{e.class} #{e.message}")
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Translate Cargo version requirement syntax to a String or Array of Strings that Gem::Dependency.new understands
|
51
|
+
# Cargo.toml files support * and ^ (wildcard and caret) requirements, which Ruby does not
|
52
|
+
# See: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
|
53
|
+
#
|
54
|
+
# req - String version requirement
|
55
|
+
#
|
56
|
+
# Returns a translated String version requirement
|
57
|
+
def translate_requirement(req)
|
58
|
+
if req =~ /~|<|>|\*|=/
|
59
|
+
return translate_wildcard(req) if req =~ /\*/
|
60
|
+
req.sub!('~', '~>')
|
61
|
+
req
|
62
|
+
else
|
63
|
+
translate_caret(req)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.expand_path('../langs.rb', __FILE__)
|
2
|
+
|
3
|
+
module CuttingEdge
|
4
|
+
class Repository
|
5
|
+
|
6
|
+
DEPENDENCY_TYPES = [:runtime] # Which dependency types to accept (default only :runtime, excludes :development).
|
7
|
+
DEFAULT_LANG = 'ruby'
|
8
|
+
|
9
|
+
attr_reader :token, :locations, :lang
|
10
|
+
attr_accessor :dependency_types
|
11
|
+
|
12
|
+
def initialize(org, name, lang = nil, locations = nil, branch = nil, token = nil)
|
13
|
+
@org = org
|
14
|
+
@name = name
|
15
|
+
@branch = branch || 'master'
|
16
|
+
@token = token
|
17
|
+
@lang = lang || DEFAULT_LANG
|
18
|
+
@locations = {}
|
19
|
+
(locations || get_lang(@lang).locations(name)).each do |loc|
|
20
|
+
@locations[loc] = url_for_file(loc)
|
21
|
+
end
|
22
|
+
@dependency_types = DEPENDENCY_TYPES
|
23
|
+
end
|
24
|
+
|
25
|
+
def source
|
26
|
+
''
|
27
|
+
end
|
28
|
+
|
29
|
+
def identifier
|
30
|
+
File.join(source, @org, @name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def url_for_file(file)
|
34
|
+
file
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def get_lang(lang)
|
40
|
+
Object.const_get("::#{lang.capitalize}Lang")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class GithubRepository < Repository
|
45
|
+
HOST = 'https://raw.githubusercontent.com'
|
46
|
+
|
47
|
+
def source
|
48
|
+
'github'
|
49
|
+
end
|
50
|
+
|
51
|
+
def url_for_file(file)
|
52
|
+
File.join(HOST, @org, @name, @branch, file)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class GitlabRepository < Repository
|
57
|
+
HOST = 'https://gitlab.com/'
|
58
|
+
|
59
|
+
def source
|
60
|
+
'gitlab'
|
61
|
+
end
|
62
|
+
|
63
|
+
def url_for_file(file)
|
64
|
+
File.join(HOST, @org, @name, 'raw', @branch, file)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|