licensee 10.0.0 → 10.1.0
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/lib/licensee/commands/detect.rb +71 -4
- data/lib/licensee/commands/diff.rb +17 -5
- data/lib/licensee/license_meta.rb +6 -1
- data/lib/licensee/matchers/package.rb +38 -0
- data/lib/licensee/matchers/pyproject.rb +18 -0
- data/lib/licensee/matchers.rb +1 -0
- data/lib/licensee/project_files/package_manager_file.rb +6 -4
- data/lib/licensee/projects/git_project.rb +6 -7
- data/lib/licensee/version.rb +1 -1
- data/spec/fixtures/detect.json +1 -1
- data/spec/integration_spec.rb +8 -0
- data/spec/licensee/commands/detect_spec.rb +90 -0
- data/spec/licensee/commands/diff_spec.rb +47 -0
- data/spec/licensee/commands/version_spec.rb +5 -0
- data/spec/licensee/license_meta_spec.rb +14 -0
- data/spec/licensee/matchers/package_matcher_spec.rb +79 -0
- data/spec/licensee_spec.rb +8 -0
- data/spec/script_vendor_licenses_spec.rb +38 -0
- data/vendor/choosealicense.com/_licenses/cc0-1.0.txt +1 -1
- data/vendor/choosealicense.com/_licenses/gfdl-1.3.txt +2 -2
- data/vendor/choosealicense.com/_licenses/gpl-3.0.txt +1 -1
- data/vendor/license-list-XML/src/EUPL-1.2.xml +1 -0
- metadata +8 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7cb258a437a638b51ce88253da3fdc5e9024bbcc4ca39ad85a7d9082e5affc9c
|
|
4
|
+
data.tar.gz: b565687716e0006143184d8b05aef9f01af3c6a69fe76314d98cb83814350274
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c91eb05607ecee1a7d87b968c78078443ba1a961e59d5b9811aab1cfb311671becf52223a82a8f63e7b17874f1f0152e0978c63e0af13b8bdf8bd80a2eb3d85
|
|
7
|
+
data.tar.gz: 06b6501dbd438cb2a218cec4d879c2f8d838642c7fff10753df4f3ac908c84062a1ae47df76750868aade6634e3deb8a1fca50f8027772b11bc33bcd2d64fa93
|
|
@@ -15,13 +15,80 @@ class LicenseeCLI < Thor
|
|
|
15
15
|
option :diff, type: :boolean, desc: 'Compare the license to the closest match'
|
|
16
16
|
option :ref, type: :string, desc: 'The name of the commit/branch/tag to search (github.com only)'
|
|
17
17
|
option :filesystem, type: :boolean, desc: 'Force looking at the filesystem (ignore git data)'
|
|
18
|
+
option :recursive, type: :boolean, desc: 'Recursively detect licenses in subdirectories'
|
|
19
|
+
option :depth, type: :numeric, default: 3, desc: 'Maximum directory depth for --recursive'
|
|
18
20
|
def detect(_path = nil)
|
|
19
21
|
Licensee.confidence_threshold = options[:confidence]
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
if options[:recursive]
|
|
24
|
+
handle_recursive_detect
|
|
25
|
+
else
|
|
26
|
+
handle_json_output if options[:json]
|
|
27
|
+
print_project_summary
|
|
28
|
+
print_matched_files
|
|
29
|
+
maybe_diff_license_file
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def handle_recursive_detect
|
|
36
|
+
root = File.expand_path(path || Dir.pwd)
|
|
37
|
+
results = collect_recursive_results(root, root, options[:depth])
|
|
38
|
+
output_recursive_results(results)
|
|
39
|
+
exit(results.any? { |r| !r[:project].licenses.empty? })
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def output_recursive_results(results)
|
|
43
|
+
if options[:json]
|
|
44
|
+
say results.map { |r| r[:project].to_h.merge(path: r[:path]) }.to_json
|
|
45
|
+
else
|
|
46
|
+
print_recursive_results(results)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def collect_recursive_results(root, current, remaining_depth)
|
|
51
|
+
results = []
|
|
52
|
+
add_recursive_project(root, current, results)
|
|
53
|
+
return results if remaining_depth <= 0
|
|
54
|
+
|
|
55
|
+
subdirectories(current).each do |full_path|
|
|
56
|
+
results.concat(collect_recursive_results(root, full_path, remaining_depth - 1))
|
|
57
|
+
end
|
|
58
|
+
results
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def subdirectories(path)
|
|
62
|
+
Dir.children(path).sort.filter_map do |entry|
|
|
63
|
+
full_path = File.join(path, entry)
|
|
64
|
+
full_path if File.directory?(full_path)
|
|
65
|
+
end
|
|
66
|
+
rescue StandardError
|
|
67
|
+
[]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def add_recursive_project(root, full_path, results)
|
|
71
|
+
proj = Licensee.project(full_path,
|
|
72
|
+
detect_packages: options[:packages],
|
|
73
|
+
detect_readme: options[:readme])
|
|
74
|
+
return if proj.licenses.empty?
|
|
75
|
+
|
|
76
|
+
rel = Pathname.new(full_path).relative_path_from(Pathname.new(root)).to_s
|
|
77
|
+
rel = '.' if rel == ''
|
|
78
|
+
results << { path: rel, project: proj }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def print_recursive_results(results)
|
|
82
|
+
if results.empty?
|
|
83
|
+
say 'No licenses detected.'
|
|
84
|
+
return
|
|
85
|
+
end
|
|
22
86
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
87
|
+
results.each do |result|
|
|
88
|
+
proj = result[:project]
|
|
89
|
+
license_ids = proj.licenses.map(&:spdx_id).join(', ')
|
|
90
|
+
matched_files = proj.matched_files.map(&:path).join(', ')
|
|
91
|
+
say "#{result[:path]} (#{matched_files}): #{license_ids}"
|
|
92
|
+
end
|
|
26
93
|
end
|
|
27
94
|
end
|
|
@@ -6,11 +6,12 @@ require 'tmpdir'
|
|
|
6
6
|
class LicenseeCLI < Thor
|
|
7
7
|
desc 'diff [PATH]', 'Compare the given license text to a known license'
|
|
8
8
|
option :license, type: :string, desc: 'The SPDX ID or key of the license to compare'
|
|
9
|
+
option :line_diff, type: :boolean, desc: 'Use line-based diff instead of word-based diff'
|
|
9
10
|
def diff(_path = nil)
|
|
10
11
|
say "Comparing to #{expected_license.name}:"
|
|
11
12
|
print_table diff_summary_rows
|
|
12
13
|
exit_on_exact_match
|
|
13
|
-
say
|
|
14
|
+
say diff_output
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
private
|
|
@@ -43,13 +44,15 @@ class LicenseeCLI < Thor
|
|
|
43
44
|
@input_text ||= license_to_diff.content_normalized(wrap: 80)
|
|
44
45
|
end
|
|
45
46
|
|
|
46
|
-
def
|
|
47
|
-
Dir.mktmpdir { |dir|
|
|
47
|
+
def diff_output
|
|
48
|
+
Dir.mktmpdir { |dir| diff_output_in_dir(dir) }
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
def
|
|
51
|
+
def diff_output_in_dir(dir)
|
|
51
52
|
path = File.expand_path 'LICENSE', dir
|
|
52
|
-
Dir.chdir(dir)
|
|
53
|
+
Dir.chdir(dir) do
|
|
54
|
+
options[:line_diff] ? git_line_diff(path) : git_word_diff(path)
|
|
55
|
+
end
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
def git_word_diff(path)
|
|
@@ -61,6 +64,15 @@ class LicenseeCLI < Thor
|
|
|
61
64
|
`git diff --word-diff`
|
|
62
65
|
end
|
|
63
66
|
|
|
67
|
+
def git_line_diff(path)
|
|
68
|
+
`git init`
|
|
69
|
+
File.write(path, expected_text)
|
|
70
|
+
`git add LICENSE`
|
|
71
|
+
`git commit -m 'left'`
|
|
72
|
+
File.write(path, input_text)
|
|
73
|
+
`git diff`
|
|
74
|
+
end
|
|
75
|
+
|
|
64
76
|
def license_to_diff
|
|
65
77
|
return options[:license_to_diff] if options[:license_to_diff]
|
|
66
78
|
return project.license_file if remote? || ($stdin.tty? && project.license_file)
|
|
@@ -54,9 +54,14 @@ module Licensee
|
|
|
54
54
|
alias_method :"#{field}?", field
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
# Backward
|
|
57
|
+
# Backward compatibility: `#["spdx-id"]` calls to avoid a breaking change.
|
|
58
|
+
# Also ensure computed fields (e.g. "source") route through the instance
|
|
59
|
+
# method rather than reading the raw nil struct field, keeping hash-style
|
|
60
|
+
# and method-style access consistent.
|
|
58
61
|
def [](key)
|
|
59
62
|
key = 'spdx_id' if key == 'spdx-id'
|
|
63
|
+
return source if key.to_s == 'source'
|
|
64
|
+
|
|
60
65
|
super
|
|
61
66
|
end
|
|
62
67
|
|
|
@@ -8,6 +8,12 @@ module Licensee
|
|
|
8
8
|
# license entry in the database (e.g. LGPL-3.0-or-later → lgpl-3.0).
|
|
9
9
|
SPDX_SUFFIX_REGEX = /-or-later\z|-only\z/i
|
|
10
10
|
|
|
11
|
+
# Regex matching individual SPDX expression tokens (license IDs or operators).
|
|
12
|
+
SPDX_TOKEN_REGEX = /[A-Za-z0-9.\-+]+/
|
|
13
|
+
|
|
14
|
+
# SPDX boolean operators to skip when parsing compound expressions.
|
|
15
|
+
SPDX_OPERATORS = %w[OR AND WITH].freeze
|
|
16
|
+
|
|
11
17
|
def match
|
|
12
18
|
return @match if defined? @match
|
|
13
19
|
|
|
@@ -24,12 +30,44 @@ module Licensee
|
|
|
24
30
|
90
|
|
25
31
|
end
|
|
26
32
|
|
|
33
|
+
# Returns all licenses found in a compound SPDX expression such as
|
|
34
|
+
# "MIT OR Apache-2.0". Unknown identifiers resolve to the 'other'
|
|
35
|
+
# (NOASSERTION) license. Returns nil when the property is absent or
|
|
36
|
+
# contains only a single token (plain license key).
|
|
37
|
+
def spdx_expression_licenses
|
|
38
|
+
return @spdx_expression_licenses if defined? @spdx_expression_licenses
|
|
39
|
+
|
|
40
|
+
@spdx_expression_licenses = resolve_spdx_expression_licenses
|
|
41
|
+
end
|
|
42
|
+
|
|
27
43
|
def license_property
|
|
28
44
|
raise NotImplementedError, "#{self.class}#license_property is not implemented"
|
|
29
45
|
end
|
|
30
46
|
|
|
31
47
|
private
|
|
32
48
|
|
|
49
|
+
def find_license_for_token(token, licenses)
|
|
50
|
+
key = token.downcase
|
|
51
|
+
licenses.find { |l| l.key == key } ||
|
|
52
|
+
match_by_spdx_base_key(key, licenses)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def spdx_expression_tokens(prop)
|
|
56
|
+
prop.scan(SPDX_TOKEN_REGEX).reject { |t| SPDX_OPERATORS.include?(t.upcase) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve_spdx_expression_licenses
|
|
60
|
+
prop = license_property
|
|
61
|
+
return nil if prop.nil? || prop.to_s.empty?
|
|
62
|
+
|
|
63
|
+
tokens = spdx_expression_tokens(prop)
|
|
64
|
+
return nil if tokens.size <= 1
|
|
65
|
+
|
|
66
|
+
licenses = Licensee.licenses(hidden: true)
|
|
67
|
+
other = License.find('other')
|
|
68
|
+
tokens.map { |t| find_license_for_token(t, licenses) || other }
|
|
69
|
+
end
|
|
70
|
+
|
|
33
71
|
def match_by_spdx_base_key(prop, licenses)
|
|
34
72
|
base = prop.sub(SPDX_SUFFIX_REGEX, '')
|
|
35
73
|
return if base == prop
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Licensee
|
|
4
|
+
module Matchers
|
|
5
|
+
# Matches license identifiers in pyproject.toml.
|
|
6
|
+
# Based on Matchers::Cargo
|
|
7
|
+
class PyProject < Licensee::Matchers::Package
|
|
8
|
+
LICENSE_REGEX = /^\s*['"]?license['"]?\s*=\s*['"]([^'"]+)['"]\s*/ix
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def license_property
|
|
13
|
+
match = @file.content.match LICENSE_REGEX
|
|
14
|
+
match[1].downcase if match && match[1]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/licensee/matchers.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Licensee
|
|
|
15
15
|
autoload :NpmBower, 'licensee/matchers/npm_bower'
|
|
16
16
|
autoload :NuGet, 'licensee/matchers/nuget'
|
|
17
17
|
autoload :Package, 'licensee/matchers/package'
|
|
18
|
+
autoload :PyProject, 'licensee/matchers/pyproject'
|
|
18
19
|
autoload :Reference, 'licensee/matchers/reference'
|
|
19
20
|
autoload :Spdx, 'licensee/matchers/spdx'
|
|
20
21
|
end
|
|
@@ -14,16 +14,18 @@ module Licensee
|
|
|
14
14
|
|
|
15
15
|
# Hash of Filename => [possible matchers]
|
|
16
16
|
FILENAMES_EXTENSIONS = {
|
|
17
|
-
'DESCRIPTION'
|
|
18
|
-
'dist.ini'
|
|
19
|
-
'LICENSE.spdx'
|
|
20
|
-
'Cargo.toml'
|
|
17
|
+
'DESCRIPTION' => [Matchers::Cran],
|
|
18
|
+
'dist.ini' => [Matchers::DistZilla],
|
|
19
|
+
'LICENSE.spdx' => [Matchers::Spdx],
|
|
20
|
+
'Cargo.toml' => [Matchers::Cargo],
|
|
21
|
+
'pyproject.toml' => [Matchers::PyProject]
|
|
21
22
|
}.freeze
|
|
22
23
|
|
|
23
24
|
FILENAMES_SCORES = {
|
|
24
25
|
'package.json' => 1.0,
|
|
25
26
|
'LICENSE.spdx' => 1.0,
|
|
26
27
|
'Cargo.toml' => 1.0,
|
|
28
|
+
'pyproject.toml' => 1.0,
|
|
27
29
|
'DESCRIPTION' => 0.9,
|
|
28
30
|
'dist.ini' => 0.8,
|
|
29
31
|
'bower.json' => 0.75,
|
|
@@ -8,12 +8,6 @@
|
|
|
8
8
|
# :name - the file's path relative to the repo root
|
|
9
9
|
# :oid - the file's OID
|
|
10
10
|
|
|
11
|
-
begin
|
|
12
|
-
require 'rugged'
|
|
13
|
-
rescue LoadError
|
|
14
|
-
# rugged is optional; GitProject will raise RuggedNotAvailable when not installed
|
|
15
|
-
end
|
|
16
|
-
|
|
17
11
|
module Licensee
|
|
18
12
|
module Projects
|
|
19
13
|
# Scans a Git repository (via Rugged) for license-related files.
|
|
@@ -24,7 +18,12 @@ module Licensee
|
|
|
24
18
|
class RuggedNotAvailable < InvalidRepository; end
|
|
25
19
|
|
|
26
20
|
def self.available?
|
|
27
|
-
|
|
21
|
+
return true if defined?(Rugged)
|
|
22
|
+
|
|
23
|
+
require 'rugged'
|
|
24
|
+
true
|
|
25
|
+
rescue LoadError
|
|
26
|
+
false
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
def initialize(repo, revision: nil, **args)
|
data/lib/licensee/version.rb
CHANGED
data/spec/fixtures/detect.json
CHANGED
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"filename": "licensee.gemspec",
|
|
96
|
-
"content": "# frozen_string_literal: true\n\nrequire File.expand_path('lib/licensee/version', __dir__)\n\nGem::Specification.new do |gem|\n gem.name = 'licensee'\n gem.version = Licensee::VERSION\n\n gem.summary = 'A Ruby Gem to detect open source project licenses'\n gem.description = <<-DESC\n Licensee automates the process of reading LICENSE files and\n compares their contents to known licenses using a fancy maths.\n DESC\n\n gem.authors = ['Ben Balter']\n gem.email = 'ben.balter@github.com'\n gem.homepage = 'https://github.com/licensee/licensee'\n gem.license = 'MIT'\n gem.metadata['rubygems_mfa_required'] = 'true'\n\n gem.bindir = 'bin'\n gem.executables << 'licensee'\n\n gem.add_dependency('dotenv', '>= 2', '< 4')\n gem.add_dependency('octokit', '>= 4.20', '< 11.0')\n gem.add_dependency('reverse_markdown', '>= 1', '< 4')\n gem.add_dependency('
|
|
96
|
+
"content": "# frozen_string_literal: true\n\nrequire File.expand_path('lib/licensee/version', __dir__)\n\nGem::Specification.new do |gem|\n gem.name = 'licensee'\n gem.version = Licensee::VERSION\n\n gem.summary = 'A Ruby Gem to detect open source project licenses'\n gem.description = <<-DESC\n Licensee automates the process of reading LICENSE files and\n compares their contents to known licenses using a fancy maths.\n DESC\n\n gem.authors = ['Ben Balter']\n gem.email = 'ben.balter@github.com'\n gem.homepage = 'https://github.com/licensee/licensee'\n gem.license = 'MIT'\n gem.metadata['rubygems_mfa_required'] = 'true'\n gem.post_install_message = <<~MSG\n NOTE: The rugged gem is no longer a required dependency of licensee.\n If you scan bare Git repositories or Git repos without a working tree,\n add rugged to your Gemfile or run: gem install rugged\n MSG\n\n gem.bindir = 'bin'\n gem.executables << 'licensee'\n\n gem.add_dependency('dotenv', '>= 2', '< 4')\n gem.add_dependency('octokit', '>= 4.20', '< 11.0')\n gem.add_dependency('reverse_markdown', '>= 1', '< 4')\n gem.add_dependency('thor', '>= 0.19', '< 2.0')\n\n gem.add_development_dependency('gem-release', '~> 2.0')\n gem.add_development_dependency('mustache', '>= 0.9', '< 2.0')\n gem.add_development_dependency('pry', '~> 0.9')\n gem.add_development_dependency('rspec', '~> 3.5')\n gem.add_development_dependency('rubocop', '~> 1.0')\n gem.add_development_dependency('rubocop-performance', '~> 1.5')\n gem.add_development_dependency('rubocop-rspec', '~> 3.0')\n gem.add_development_dependency('rugged', '>= 0.24', '<2.0')\n gem.add_development_dependency('simplecov', '~> 1.0')\n gem.add_development_dependency('webmock', '~> 3.1')\n\n gem.required_ruby_version = '>= 3.2'\n\n # ensure the gem is built out of versioned files\n gem.files = Dir[\n '{bin,lib,man,test,vendor,spec}/**/*',\n 'README*', 'LICENSE*'\n ] & `git ls-files -z`.split(\"\\0\")\nend\n",
|
|
97
97
|
"content_hash": null,
|
|
98
98
|
"content_normalized": null,
|
|
99
99
|
"matcher": {
|
data/spec/integration_spec.rb
CHANGED
|
@@ -368,6 +368,14 @@ RSpec.describe Integration do
|
|
|
368
368
|
expect(project).to have_attributes(license: license, package_file: have_attributes(path: 'package.json'))
|
|
369
369
|
end
|
|
370
370
|
|
|
371
|
+
it 'matches a pyproject.toml file' do
|
|
372
|
+
write_file.call('pyproject.toml', "[project]\nlicense = \"MIT\"")
|
|
373
|
+
project = project_type.new(project_path, detect_packages: true)
|
|
374
|
+
|
|
375
|
+
license = Licensee::License.find('mit')
|
|
376
|
+
expect(project).to have_attributes(license: license, package_file: have_attributes(path: 'pyproject.toml'))
|
|
377
|
+
end
|
|
378
|
+
|
|
371
379
|
it 'matches a README file' do
|
|
372
380
|
write_file.call('README', "## License\n#{Licensee::License.find('mit').content}")
|
|
373
381
|
project = project_type.new(project_path, detect_readme: true)
|
|
@@ -176,4 +176,94 @@ RSpec.describe Licensee::Commands::Detect do
|
|
|
176
176
|
expect(output[1]).to include('Unsupported remote URL')
|
|
177
177
|
end
|
|
178
178
|
end
|
|
179
|
+
|
|
180
|
+
context 'when using --recursive' do
|
|
181
|
+
let(:fixtures_path) { File.expand_path('../../fixtures', __dir__) }
|
|
182
|
+
|
|
183
|
+
context 'when scanning a directory with licensed subdirectories' do
|
|
184
|
+
let(:arguments) { ['--recursive', fixtures_path] }
|
|
185
|
+
|
|
186
|
+
it 'returns a zero exit code' do
|
|
187
|
+
expect(status.exitstatus).to be(0)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it 'outputs at least one result' do
|
|
191
|
+
expect(stdout).not_to be_empty
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'outputs lines in path (file): LICENSE format' do
|
|
195
|
+
expect(stdout.lines).to all(match(/\S+ \(.+\): \S+/))
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
context 'when scanning an empty directory' do
|
|
200
|
+
let(:arguments) { ['--recursive', tmpdir] }
|
|
201
|
+
|
|
202
|
+
after { FileUtils.rm_rf(tmpdir) }
|
|
203
|
+
|
|
204
|
+
def tmpdir
|
|
205
|
+
@tmpdir ||= Dir.mktmpdir
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'returns a non-zero exit code' do
|
|
209
|
+
expect(status.exitstatus).to be(1)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'says no licenses detected' do
|
|
213
|
+
expect(stdout).to include('No licenses detected')
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
context 'when PATH itself has a license' do
|
|
218
|
+
let(:arguments) { ['--recursive', File.join(fixtures_path, 'mit')] }
|
|
219
|
+
|
|
220
|
+
it 'includes the root directory in output' do
|
|
221
|
+
expect(stdout).to include('.')
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it 'returns zero exit code' do
|
|
225
|
+
expect(status.exitstatus).to be(0)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
context 'when using --json' do
|
|
230
|
+
let(:arguments) { ['--recursive', '--json', fixtures_path] }
|
|
231
|
+
|
|
232
|
+
it 'returns valid JSON array' do
|
|
233
|
+
parsed = JSON.parse(stdout)
|
|
234
|
+
expect(parsed).to be_an(Array)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it 'includes path keys in each result' do
|
|
238
|
+
parsed = JSON.parse(stdout)
|
|
239
|
+
expect(parsed.all? { |r| r.key?('path') }).to be true
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
context 'when using --depth' do
|
|
244
|
+
after { FileUtils.rm_rf(shallow_path) }
|
|
245
|
+
|
|
246
|
+
def shallow_path
|
|
247
|
+
@shallow_path ||= begin
|
|
248
|
+
dir = Dir.mktmpdir
|
|
249
|
+
subdir = File.join(dir, 'deep', 'nested', 'project')
|
|
250
|
+
FileUtils.mkdir_p(subdir)
|
|
251
|
+
FileUtils.cp(File.join(fixtures_path, 'mit', 'LICENSE.txt'), subdir)
|
|
252
|
+
dir
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it 'with depth 1 does not find deeply nested licenses' do
|
|
257
|
+
_, _, s = Open3.capture3(*[command, '--recursive', '--depth', '1', shallow_path].flatten,
|
|
258
|
+
chdir: project_root)
|
|
259
|
+
expect(s.exitstatus).to be(1)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it 'with depth 3 finds the nested license' do
|
|
263
|
+
out, = Open3.capture3(*[command, '--recursive', '--depth', '3', shallow_path].flatten,
|
|
264
|
+
chdir: project_root)
|
|
265
|
+
expect(out).to include('MIT')
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
179
269
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Licensee
|
|
4
|
+
module Commands
|
|
5
|
+
module Diff
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
RSpec.describe Licensee::Commands::Diff do
|
|
11
|
+
let(:mit_fixture_path) { fixture_path('mit_markdown') }
|
|
12
|
+
let(:license_file) { File.join(mit_fixture_path, 'LICENSE.md') }
|
|
13
|
+
|
|
14
|
+
def run_diff(*extra_args)
|
|
15
|
+
Dir.chdir project_root do
|
|
16
|
+
Open3.capture3(
|
|
17
|
+
'bundle', 'exec', 'bin/licensee', 'diff', license_file,
|
|
18
|
+
'--license=mit', *extra_args
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def stdout(args = []) = run_diff(*args)[0]
|
|
24
|
+
def status(args = []) = run_diff(*args)[2]
|
|
25
|
+
|
|
26
|
+
context 'with default (word diff)' do
|
|
27
|
+
it 'returns a zero exit code for an exact match' do
|
|
28
|
+
_out, _err, s = run_diff
|
|
29
|
+
expect(s.exitstatus).to be(0)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'outputs similarity information' do
|
|
33
|
+
expect(stdout).to match(/Similarity/i)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context 'with --line-diff' do
|
|
38
|
+
it 'returns a zero exit code for an exact match' do
|
|
39
|
+
_out, _err, s = run_diff('--line-diff')
|
|
40
|
+
expect(s.exitstatus).to be(0)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'outputs similarity information' do
|
|
44
|
+
expect(stdout(['--line-diff'])).to match(/Similarity/i)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -15,6 +15,7 @@ RSpec.describe Licensee::Commands::Version do
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def stdout = output[0]
|
|
18
|
+
def stderr = output[1]
|
|
18
19
|
def status = output[2]
|
|
19
20
|
|
|
20
21
|
it 'returns the version' do
|
|
@@ -24,4 +25,8 @@ RSpec.describe Licensee::Commands::Version do
|
|
|
24
25
|
it 'Returns a zero exit code' do
|
|
25
26
|
expect(status.exitstatus).to be(0)
|
|
26
27
|
end
|
|
28
|
+
|
|
29
|
+
it 'does not output warnings to stderr' do
|
|
30
|
+
expect(stderr).to eq('')
|
|
31
|
+
end
|
|
27
32
|
end
|
|
@@ -68,6 +68,20 @@ RSpec.describe Licensee::LicenseMeta do
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
context 'when accessing the computed "source" field via #[]' do
|
|
72
|
+
it 'returns the SPDX URL when spdx_id is set' do
|
|
73
|
+
expect(meta['source']).to eql('https://spdx.org/licenses/MIT.html')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'is consistent with the #source method' do
|
|
77
|
+
expect(meta['source']).to eql(meta.source)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'is consistent with to_h' do
|
|
81
|
+
expect(meta['source']).to eql(meta.to_h[:source])
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
71
85
|
describe '#from_yaml' do
|
|
72
86
|
subject(:license_meta) { described_class.from_yaml(yaml) }
|
|
73
87
|
|
|
@@ -73,4 +73,83 @@ RSpec.describe Licensee::Matchers::Package do
|
|
|
73
73
|
expect { base_matcher.send(:license_property) }.to raise_error(NotImplementedError, /Package#license_property/)
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
|
+
|
|
77
|
+
describe '#spdx_expression_licenses' do
|
|
78
|
+
context 'with a plain single license key' do
|
|
79
|
+
let(:license_property) { 'mit' }
|
|
80
|
+
|
|
81
|
+
it 'returns nil (not a compound expression)' do
|
|
82
|
+
expect(matcher.spdx_expression_licenses).to be_nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
context 'with a nil license property' do
|
|
87
|
+
let(:license_property) { nil }
|
|
88
|
+
|
|
89
|
+
it 'returns nil' do
|
|
90
|
+
expect(matcher.spdx_expression_licenses).to be_nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
context 'with an "MIT OR Apache-2.0" expression' do
|
|
95
|
+
let(:license_property) { 'MIT OR Apache-2.0' }
|
|
96
|
+
|
|
97
|
+
it 'does not include the OR operator as a license' do
|
|
98
|
+
expect(matcher.spdx_expression_licenses.size).to eq(2)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'includes MIT' do
|
|
102
|
+
expect(matcher.spdx_expression_licenses).to include(Licensee::License.find('mit'))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'includes Apache-2.0' do
|
|
106
|
+
expect(matcher.spdx_expression_licenses).to include(Licensee::License.find('apache-2.0'))
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
context 'with an "MIT AND Apache-2.0" expression' do
|
|
111
|
+
let(:license_property) { 'MIT AND Apache-2.0' }
|
|
112
|
+
|
|
113
|
+
it 'includes MIT' do
|
|
114
|
+
expect(matcher.spdx_expression_licenses).to include(Licensee::License.find('mit'))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'includes Apache-2.0' do
|
|
118
|
+
expect(matcher.spdx_expression_licenses).to include(Licensee::License.find('apache-2.0'))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
context 'with a compound expression containing an unknown identifier' do
|
|
123
|
+
let(:license_property) { 'UnknownFoo OR MIT' }
|
|
124
|
+
|
|
125
|
+
it 'returns other for the unknown identifier' do
|
|
126
|
+
expect(matcher.spdx_expression_licenses).to include(Licensee::License.find('other'))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'returns MIT for the known identifier' do
|
|
130
|
+
expect(matcher.spdx_expression_licenses).to include(Licensee::License.find('mit'))
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
context 'with a compound expression where all identifiers are unknown' do
|
|
135
|
+
let(:license_property) { 'UnknownFoo OR AnotherUnknown' }
|
|
136
|
+
|
|
137
|
+
it 'returns other for all tokens' do
|
|
138
|
+
result = matcher.spdx_expression_licenses
|
|
139
|
+
expect(result).to all(eq(Licensee::License.find('other')))
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
context 'with -or-later suffix tokens in compound expression' do
|
|
144
|
+
let(:license_property) { 'LGPL-2.1-or-later OR GPL-3.0-or-later' }
|
|
145
|
+
|
|
146
|
+
it 'strips -or-later and returns LGPL-2.1' do
|
|
147
|
+
expect(matcher.spdx_expression_licenses).to include(Licensee::License.find('lgpl-2.1'))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'strips -or-later and returns GPL-3.0' do
|
|
151
|
+
expect(matcher.spdx_expression_licenses).to include(Licensee::License.find('gpl-3.0'))
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
76
155
|
end
|
data/spec/licensee_spec.rb
CHANGED
|
@@ -45,6 +45,14 @@ RSpec.describe Licensee do
|
|
|
45
45
|
expect { project }.to raise_error(ArgumentError, /Unsupported remote URL/)
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
|
+
|
|
49
|
+
context 'when rugged is not available' do
|
|
50
|
+
before { allow(Licensee::Projects::GitProject).to receive(:available?).and_return(false) }
|
|
51
|
+
|
|
52
|
+
it 'falls back to FSProject' do
|
|
53
|
+
expect(project).to be_a(Licensee::Projects::FSProject)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
48
56
|
end
|
|
49
57
|
|
|
50
58
|
context 'when using the confidence threshold' do
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable RSpec/DescribeClass
|
|
4
|
+
RSpec.describe 'script/vendor-licenses' do
|
|
5
|
+
let(:script_path) { File.expand_path('script/vendor-licenses', project_root) }
|
|
6
|
+
let(:vendor_data_dir) { File.expand_path('vendor/choosealicense.com/_data', project_root) }
|
|
7
|
+
let(:vendor_licenses_dir) { File.expand_path('vendor/choosealicense.com/_licenses', project_root) }
|
|
8
|
+
|
|
9
|
+
it 'passes a POSIX sh syntax check' do
|
|
10
|
+
_, stderr, status = Open3.capture3('sh', '-n', script_path)
|
|
11
|
+
expect(status.exitstatus).to be(0), "sh -n failed: #{stderr}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'does not contain Bash-specific syntax' do
|
|
15
|
+
content = File.read(script_path)
|
|
16
|
+
expect(content).not_to include('[[')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'uses --no-wildcards-match-slash for GNU tar' do
|
|
20
|
+
content = File.read(script_path)
|
|
21
|
+
expect(content).to include('--no-wildcards-match-slash')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'does not vendor nested _data/i18n files' do
|
|
25
|
+
nested = Dir["#{vendor_data_dir}/i18n/**/*.yml"]
|
|
26
|
+
expect(nested).to be_empty, "Unexpected nested i18n files found: #{nested.join(', ')}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'vendors top-level _data YAML files' do
|
|
30
|
+
top_level = Dir["#{vendor_data_dir}/*.yml"]
|
|
31
|
+
expect(top_level).not_to be_empty
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'vendors _licenses files' do
|
|
35
|
+
expect(Dir["#{vendor_licenses_dir}/*"]).not_to be_empty
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
# rubocop:enable RSpec/DescribeClass
|
|
@@ -12,7 +12,7 @@ note: Creative Commons recommends taking the additional step of adding a boilerp
|
|
|
12
12
|
|
|
13
13
|
using:
|
|
14
14
|
Awesome: https://github.com/sindresorhus/awesome/blob/main/license
|
|
15
|
-
|
|
15
|
+
just: https://github.com/casey/just/blob/master/LICENSE
|
|
16
16
|
psdash: https://github.com/Jahaja/psdash/blob/master/LICENSE
|
|
17
17
|
|
|
18
18
|
permissions:
|
|
@@ -10,8 +10,8 @@ how: Create a text file (typically named COPYING, as per GNU conventions) in the
|
|
|
10
10
|
note: The Free Software Foundation recommends taking the additional step of adding a boilerplate notice after the title of a licensed document. The boilerplate can be found at the "addendum" part of the license.
|
|
11
11
|
|
|
12
12
|
using:
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
Mastodon (Documentation): https://github.com/mastodon/documentation/blob/main/LICENSE
|
|
14
|
+
Regular Expression Museum: https://github.com/thaliaarchi/regexp-museum/blob/main/LICENSE
|
|
15
15
|
Qt (Documentation): https://code.qt.io/cgit/qt/qt.git/tree/LICENSE.FDL
|
|
16
16
|
|
|
17
17
|
permissions:
|
|
@@ -14,7 +14,7 @@ note: The Free Software Foundation recommends taking the additional step of addi
|
|
|
14
14
|
|
|
15
15
|
using:
|
|
16
16
|
Ansible: https://github.com/ansible/ansible/blob/devel/COPYING
|
|
17
|
-
|
|
17
|
+
Niri: https://github.com/niri-wm/niri/blob/main/LICENSE
|
|
18
18
|
uBlock Origin: https://github.com/gorhill/uBlock/blob/master/LICENSE.txt
|
|
19
19
|
|
|
20
20
|
permissions:
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<crossRef>https://joinup.ec.europa.eu/sites/default/files/inline-files/EUPL%20v1_2%20EN(1).txt</crossRef>
|
|
10
10
|
<crossRef>http://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32017D0863</crossRef>
|
|
11
11
|
<crossRef>https://opensource.org/license/EUPL-1.2</crossRef>
|
|
12
|
+
<crossRef>https://interoperable-europe.ec.europa.eu/sites/default/files/inline-files/EUPL%20v1_2%20EN(1).txt</crossRef>
|
|
12
13
|
</crossRefs>
|
|
13
14
|
<notes>
|
|
14
15
|
This license was released: 19 May 2016. This license is available in the
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: licensee
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 10.
|
|
4
|
+
version: 10.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben Balter
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: dotenv
|
|
@@ -219,14 +219,14 @@ dependencies:
|
|
|
219
219
|
requirements:
|
|
220
220
|
- - "~>"
|
|
221
221
|
- !ruby/object:Gem::Version
|
|
222
|
-
version: '0
|
|
222
|
+
version: '1.0'
|
|
223
223
|
type: :development
|
|
224
224
|
prerelease: false
|
|
225
225
|
version_requirements: !ruby/object:Gem::Requirement
|
|
226
226
|
requirements:
|
|
227
227
|
- - "~>"
|
|
228
228
|
- !ruby/object:Gem::Version
|
|
229
|
-
version: '0
|
|
229
|
+
version: '1.0'
|
|
230
230
|
- !ruby/object:Gem::Dependency
|
|
231
231
|
name: webmock
|
|
232
232
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -283,6 +283,7 @@ files:
|
|
|
283
283
|
- lib/licensee/matchers/npm_bower.rb
|
|
284
284
|
- lib/licensee/matchers/nuget.rb
|
|
285
285
|
- lib/licensee/matchers/package.rb
|
|
286
|
+
- lib/licensee/matchers/pyproject.rb
|
|
286
287
|
- lib/licensee/matchers/reference.rb
|
|
287
288
|
- lib/licensee/matchers/spdx.rb
|
|
288
289
|
- lib/licensee/project_files.rb
|
|
@@ -383,6 +384,7 @@ files:
|
|
|
383
384
|
- spec/fixtures/wrk-modified-apache/LICENSE
|
|
384
385
|
- spec/integration_spec.rb
|
|
385
386
|
- spec/licensee/commands/detect_spec.rb
|
|
387
|
+
- spec/licensee/commands/diff_spec.rb
|
|
386
388
|
- spec/licensee/commands/license_path_spec.rb
|
|
387
389
|
- spec/licensee/commands/version_spec.rb
|
|
388
390
|
- spec/licensee/content_helper_spec.rb
|
|
@@ -416,6 +418,7 @@ files:
|
|
|
416
418
|
- spec/licensee/projects/project_spec.rb
|
|
417
419
|
- spec/licensee/rule_spec.rb
|
|
418
420
|
- spec/licensee_spec.rb
|
|
421
|
+
- spec/script_vendor_licenses_spec.rb
|
|
419
422
|
- spec/spec_helper.rb
|
|
420
423
|
- spec/vendored_license_spec.rb
|
|
421
424
|
- vendor/choosealicense.com/_data/fields.yml
|
|
@@ -538,7 +541,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
538
541
|
- !ruby/object:Gem::Version
|
|
539
542
|
version: '0'
|
|
540
543
|
requirements: []
|
|
541
|
-
rubygems_version: 3.6.
|
|
544
|
+
rubygems_version: 3.6.9
|
|
542
545
|
specification_version: 4
|
|
543
546
|
summary: A Ruby Gem to detect open source project licenses
|
|
544
547
|
test_files: []
|