licensee 9.8.0 → 9.9.0.beta.2

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/bin/licensee +25 -52
  3. data/lib/licensee/commands/detect.rb +103 -0
  4. data/lib/licensee/commands/diff.rb +67 -0
  5. data/lib/licensee/commands/license_path.rb +14 -0
  6. data/lib/licensee/commands/version.rb +6 -0
  7. data/lib/licensee/content_helper.rb +6 -0
  8. data/lib/licensee/hash_helper.rb +20 -0
  9. data/lib/licensee/license.rb +6 -1
  10. data/lib/licensee/license_meta.rb +3 -0
  11. data/lib/licensee/license_rules.rb +4 -1
  12. data/lib/licensee/matchers/exact.rb +1 -7
  13. data/lib/licensee/matchers/matcher.rb +7 -0
  14. data/lib/licensee/project_files/project_file.rb +17 -0
  15. data/lib/licensee/projects/project.rb +3 -0
  16. data/lib/licensee/rule.rb +3 -0
  17. data/lib/licensee/version.rb +1 -1
  18. data/lib/licensee.rb +1 -0
  19. data/spec/bin_spec.rb +7 -50
  20. data/spec/fixtures/detect.json +111 -0
  21. data/spec/licensee/commands/detect_spec.rb +86 -0
  22. data/spec/licensee/commands/license_path_spec.rb +21 -0
  23. data/spec/licensee/commands/version_spec.rb +21 -0
  24. data/spec/licensee/hash_helper_spec.rb +78 -0
  25. data/spec/licensee/license_field_spec.rb +11 -2
  26. data/spec/licensee/license_meta_spec.rb +34 -0
  27. data/spec/licensee/license_rules_spec.rb +15 -0
  28. data/spec/licensee/license_spec.rb +24 -2
  29. data/spec/licensee/matchers/matcher_spec.rb +35 -0
  30. data/spec/licensee/project_files/license_file_spec.rb +1 -1
  31. data/spec/licensee/project_files/project_file_spec.rb +21 -0
  32. data/spec/licensee/project_spec.rb +14 -0
  33. data/spec/licensee/rule_spec.rb +27 -8
  34. data/spec/licensee_spec.rb +1 -1
  35. data/spec/spec_helper.rb +14 -6
  36. data/spec/vendored_license_spec.rb +4 -0
  37. data/vendor/choosealicense.com/_data/fields.yml +3 -0
  38. data/vendor/choosealicense.com/_licenses/eupl-1.2.txt +309 -0
  39. data/vendor/choosealicense.com/_licenses/ncsa.txt +22 -22
  40. metadata +45 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6da17475ba74d0aea6d92013e6d141cc88f0d400af3aa9d0be99947c551f3ad6
4
- data.tar.gz: 654876f0d889688a13752497759d2856be48a8714667a167e9781f80b9f1eef1
3
+ metadata.gz: 07cb2f24a84e1c09c9e8b18b8d884fe167075294883c7bdc38a41d96ac43e7cd
4
+ data.tar.gz: 0a5884c9abec6b33e3883cde81533a4b2d9c4b2e6f196dc93a3f8148500f91d0
5
5
  SHA512:
6
- metadata.gz: 208fea169fb835f863d85ec6ac2bbcb87878c10e312938380fd262cfc3b3b7fe0e55b65d74cdd166a1c361d47d071ec5bfc785e4a7854031aa0855fc96105c54
7
- data.tar.gz: 1c23cf247b226d85326ac7c9d388b22e228f07a13388d0e21cac091f6ca8fefd1c4b726448c13e44c429e83c828ef99b56cfaea5c39ae30ef22fc13655027d29
6
+ metadata.gz: 1413f1609224bcc6c0220c9838a9959af8b008231ece109f2d4cf58042c64f6be9df66618b7d15c46ff08b67f90cdb9260693baa189c91a9aa49150731d178b9
7
+ data.tar.gz: f143548da667914c309269e54da37cf449efdf51baa05b1a8bb24a91d82c80def6b01f3d2b804db3be914045f03048a3e111a1145f7674cce0a3086df5eff280
data/bin/licensee CHANGED
@@ -1,64 +1,37 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require_relative '../lib/licensee'
4
-
5
- path = ARGV[0] || Dir.pwd
6
-
7
- # Given a string or object, prepares it for output and human consumption
8
- def humanize(value, type = nil)
9
- case type
10
- when :license
11
- value.name
12
- when :matcher
13
- value.class
14
- when :confidence
15
- Licensee::ContentHelper.format_percent(value)
16
- when :method
17
- value.to_s.tr('_', ' ').capitalize
18
- else
19
- value
20
- end
21
- end
22
-
23
- # Methods to call when displaying information about ProjectFiles
24
- MATCHED_FILE_METHODS = %i[
25
- content_hash attribution confidence matcher license
26
- ].freeze
3
+ require 'dotenv/load'
4
+ require 'thor'
5
+ require 'json'
27
6
 
28
- project = Licensee.project(path, detect_packages: true, detect_readme: true)
29
-
30
- if project.license
31
- puts "License: #{project.license.name}"
32
- elsif project.licenses
33
- puts "Licenses: #{project.licenses.map(&:name)}"
34
- else
35
- puts 'License: Not detected'
36
- end
7
+ require_relative '../lib/licensee'
37
8
 
38
- puts "Matched files: #{project.matched_files.map(&:filename)}"
9
+ class LicenseeCLI < Thor
10
+ package_name 'Licensee'
11
+ class_option :remote, type: :boolean, desc: 'Assume PATH is a GitHub owner/repo path'
12
+ default_task :detect
39
13
 
40
- project.matched_files.each do |matched_file|
41
- puts "#{matched_file.filename}:"
14
+ private
42
15
 
43
- MATCHED_FILE_METHODS.each do |method|
44
- next unless matched_file.respond_to? method
45
- value = matched_file.public_send method
46
- next if value.nil?
47
- puts " #{humanize(method, :method)}: #{humanize(value, method)}"
16
+ def path
17
+ @path ||= if !options[:remote] || args.first =~ %r{^https://}
18
+ args.first || Dir.pwd
19
+ else
20
+ "https://github.com/#{args.first}"
21
+ end
48
22
  end
49
23
 
50
- next unless matched_file.is_a? Licensee::ProjectFiles::LicenseFile
51
- next unless matched_file.confidence != 100
24
+ def project
25
+ @project ||= Licensee.project(path,
26
+ detect_packages: options[:packages], detect_readme: options[:readme])
27
+ end
52
28
 
53
- matcher = Licensee::Matchers::Dice.new(matched_file)
54
- licenses = matcher.licenses_by_similiarity
55
- next if licenses.empty?
56
- puts ' Closest licenses:'
57
- licenses[0...3].each do |license, similarity|
58
- spdx_id = license.meta['spdx-id']
59
- percent = Licensee::ContentHelper.format_percent(similarity)
60
- puts " * #{spdx_id} similarity: #{percent}"
29
+ def remote?
30
+ path =~ %r{^https://}
61
31
  end
62
32
  end
63
33
 
64
- exit !project.licenses.empty?
34
+ commands_dir = File.expand_path 'lib/licensee/commands/'
35
+ Dir["#{commands_dir}/*.rb"].each { |c| require(c) }
36
+
37
+ LicenseeCLI.start(ARGV)
@@ -0,0 +1,103 @@
1
+ class LicenseeCLI < Thor
2
+ # Methods to call when displaying information about ProjectFiles
3
+ MATCHED_FILE_METHODS = %i[
4
+ content_hash attribution confidence matcher license
5
+ ].freeze
6
+
7
+ desc 'detect [PATH]', 'Detect the license of the given project'
8
+ option :json, type: :boolean, desc: 'Return output as JSON'
9
+ option :packages, type: :boolean, default: true, desc: 'Detect licenses in package manager files'
10
+ option :readme, type: :boolean, default: true, desc: 'Detect licenses in README files'
11
+ option :confidence, type: :numeric, default: Licensee.confidence_threshold, desc: 'Confidence threshold'
12
+ option :license, type: :string, desc: 'The SPDX ID or key of the license to compare (implies --diff)'
13
+ option :diff, type: :boolean, desc: 'Compare the license to the closest match'
14
+ def detect(_path = nil)
15
+ Licensee.confidence_threshold = options[:confidence]
16
+
17
+ if options[:json]
18
+ say project.to_h.to_json
19
+ exit !project.licenses.empty?
20
+ end
21
+
22
+ rows = []
23
+ rows << if project.license
24
+ ['License:', project.license.name]
25
+ elsif !project.licenses.empty?
26
+ ['Licenses:', project.licenses.map(&:name)]
27
+ else
28
+ ['License:', set_color('None', :red)]
29
+ end
30
+
31
+ unless project.matched_files.empty?
32
+ rows << ['Matched files:', project.matched_files.map(&:filename).join(', ')]
33
+ end
34
+
35
+ print_table rows
36
+
37
+ project.matched_files.each do |matched_file|
38
+ rows = []
39
+ say "#{matched_file.filename}:"
40
+
41
+ MATCHED_FILE_METHODS.each do |method|
42
+ next unless matched_file.respond_to? method
43
+ value = matched_file.public_send method
44
+ next if value.nil?
45
+ rows << [humanize(method, :method), humanize(value, method)]
46
+ end
47
+ print_table rows, indent: 2
48
+
49
+ next unless matched_file.is_a? Licensee::ProjectFiles::LicenseFile
50
+ next if matched_file.confidence == 100
51
+
52
+ licenses = licenses_by_similiarity(matched_file)
53
+ next if licenses.empty?
54
+ say ' Closest licenses:'
55
+ rows = licenses[0...3].map do |license, similarity|
56
+ spdx_id = license.meta['spdx-id']
57
+ percent = Licensee::ContentHelper.format_percent(similarity)
58
+ ["#{spdx_id} similarity:", percent]
59
+ end
60
+ print_table rows, indent: 4
61
+ end
62
+
63
+ if project.license_file && (options[:license] || options[:diff])
64
+ license = options[:license] || closest_license_key(project.license_file)
65
+ if license
66
+ invoke(:diff, nil,
67
+ license: license, license_to_diff: project.license_file)
68
+ end
69
+ end
70
+
71
+ exit !project.licenses.empty?
72
+ end
73
+
74
+ private
75
+
76
+ # Given a string or object, prepares it for output and human consumption
77
+ def humanize(value, type = nil)
78
+ case type
79
+ when :license
80
+ value.name
81
+ when :matcher
82
+ value.class
83
+ when :confidence
84
+ Licensee::ContentHelper.format_percent(value)
85
+ when :method
86
+ value.to_s.tr('_', ' ').capitalize + ':'
87
+ else
88
+ value
89
+ end
90
+ end
91
+
92
+ def licenses_by_similiarity(matched_file)
93
+ matcher = Licensee::Matchers::Dice.new(matched_file)
94
+ potential_licenses = Licensee.licenses(hidden: true).select(&:wordset)
95
+ matcher.instance_variable_set('@potential_licenses', potential_licenses)
96
+ matcher.licenses_by_similiarity
97
+ end
98
+
99
+ def closest_license_key(matched_file)
100
+ licenses = licenses_by_similiarity(matched_file)
101
+ licenses.first.first.key unless licenses.empty?
102
+ end
103
+ end
@@ -0,0 +1,67 @@
1
+ require 'tmpdir'
2
+
3
+ class LicenseeCLI < Thor
4
+ desc 'diff [PATH]', 'Compare the given license text to a known license'
5
+ option :license, type: :string, desc: 'The SPDX ID or key of the license to compare'
6
+ def diff(_path = nil)
7
+ say "Comparing to #{expected_license.name}:"
8
+ rows = []
9
+
10
+ left = expected_license.content_normalized(wrap: 80)
11
+ right = license_to_diff.content_normalized(wrap: 80)
12
+ similarity = expected_license.similarity(license_to_diff)
13
+ similarity = Licensee::ContentHelper.format_percent(similarity)
14
+
15
+ rows << ['Input Length:', license_to_diff.length]
16
+ rows << ['License length:', expected_license.length]
17
+ rows << ['Similarity:', similarity]
18
+ print_table rows
19
+
20
+ if left == right
21
+ say 'Exact match!', :green
22
+ exit
23
+ end
24
+
25
+ Dir.mktmpdir do |dir|
26
+ path = File.expand_path 'LICENSE', dir
27
+ Dir.chdir(dir) do
28
+ `git init`
29
+ File.write(path, left)
30
+ `git add LICENSE`
31
+ `git commit -m 'left'`
32
+ File.write(path, right)
33
+ say `git diff --word-diff`
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def license_to_diff
41
+ return options[:license_to_diff] if options[:license_to_diff]
42
+ return project.license_file if remote?
43
+
44
+ @license_via_stdin ||= begin
45
+ if STDIN.tty?
46
+ error 'You must pipe license contents to the command via STDIN'
47
+ exit 1
48
+ end
49
+
50
+ Licensee::ProjectFiles::LicenseFile.new(STDIN.read, 'LICENSE')
51
+ end
52
+ end
53
+
54
+ def expected_license
55
+ @expected_license ||= Licensee::License.find options[:license] if options[:license]
56
+ return @expected_license if @expected_license
57
+
58
+ if options[:license]
59
+ error "#{options[:license]} is not a valid license"
60
+ else
61
+ error 'You must provide an expected license'
62
+ end
63
+
64
+ error "Valid licenses: #{Licensee::License.all(hidden: true).map(&:key).join(', ')}"
65
+ exit 1
66
+ end
67
+ end
@@ -0,0 +1,14 @@
1
+ class LicenseeCLI < Thor
2
+ desc 'license-path [PATH]', "Returns the path to the given project's license file"
3
+ def license_path(_path)
4
+ if project.license_file
5
+ if remote?
6
+ say project.license_file.path
7
+ else
8
+ say File.expand_path project.license_file.path
9
+ end
10
+ else
11
+ exit 1
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ class LicenseeCLI < Thor
2
+ desc 'version', 'Return the Licensee version'
3
+ def version
4
+ say Licensee::VERSION
5
+ end
6
+ end
@@ -12,6 +12,7 @@ module Licensee
12
12
  MARKDOWN_HEADING_REGEX = /\A\s*#+/
13
13
  VERSION_REGEX = /\Aversion.*$/i
14
14
  MARKUP_REGEX = /[#_*=~\[\]()`|>]+/
15
+ DEVELOPED_BY_REGEX = /\Adeveloped by:.*?\n\n/im
15
16
 
16
17
  # A set of each word in the license, without duplicates
17
18
  def wordset
@@ -77,6 +78,7 @@ module Licensee
77
78
  string = strip_copyright(string)
78
79
  end
79
80
  string = strip_all_rights_reserved(string)
81
+ string = strip_developed_by(string)
80
82
  string, _partition, _instructions = string.partition(END_OF_TERMS_REGEX)
81
83
  string = strip_markup(string)
82
84
  string = normalize_quotes(string)
@@ -162,6 +164,10 @@ module Licensee
162
164
  strip(string, MARKUP_REGEX)
163
165
  end
164
166
 
167
+ def strip_developed_by(string)
168
+ strip(string, DEVELOPED_BY_REGEX)
169
+ end
170
+
165
171
  def strip(string, regex)
166
172
  string.gsub(regex, ' ').squeeze(' ').strip
167
173
  end
@@ -0,0 +1,20 @@
1
+ module Licensee
2
+ module HashHelper
3
+ def to_h
4
+ hash = {}
5
+ self.class::HASH_METHODS.each do |method|
6
+ key = method.to_s.delete('?').to_sym
7
+ value = public_send(method)
8
+ hash[key] = if value.is_a?(Array)
9
+ value.map { |v| v.respond_to?(:to_h) ? v.to_h : v }
10
+ elsif value.respond_to?(:to_h) && !value.nil?
11
+ value.to_h
12
+ else
13
+ value
14
+ end
15
+ end
16
+
17
+ hash
18
+ end
19
+ end
20
+ end
@@ -92,9 +92,14 @@ module Licensee
92
92
  }.freeze
93
93
 
94
94
  SOURCE_PREFIX = %r{https?://(?:www\.)?}i
95
- SOURCE_SUFFIX = %r{(?:\.html?|\.txt|\/)}i
95
+ SOURCE_SUFFIX = %r{(?:\.html?|\.txt|\/)(?:\?[^\s]*)?}i
96
+
97
+ HASH_METHODS = %i[
98
+ key spdx_id meta url rules fields other? gpl? lgpl? cc?
99
+ ].freeze
96
100
 
97
101
  include Licensee::ContentHelper
102
+ include Licensee::HashHelper
98
103
  extend Forwardable
99
104
  def_delegators :meta, *LicenseMeta.helper_methods
100
105
 
@@ -12,6 +12,9 @@ module Licensee
12
12
 
13
13
  PREDICATE_FIELDS = %i[featured hidden].freeze
14
14
 
15
+ include Licensee::HashHelper
16
+ HASH_METHODS = members - %i[conditions permissions limitations spdx_id]
17
+
15
18
  class << self
16
19
  # Create a new LicenseMeta from YAML
17
20
  #
@@ -1,6 +1,9 @@
1
1
  module Licensee
2
2
  # Exposes #conditions, #permissions, and #limitation arrays of LicenseRules
3
3
  class LicenseRules < Struct.new(:conditions, :permissions, :limitations)
4
+ include Licensee::HashHelper
5
+ HASH_METHODS = Rule.groups
6
+
4
7
  class << self
5
8
  def from_license(license)
6
9
  from_meta(license.meta)
@@ -9,7 +12,7 @@ module Licensee
9
12
  def from_meta(meta)
10
13
  rules = {}
11
14
  Rule.groups.each do |group|
12
- rules[group] = meta[group].map do |tag|
15
+ rules[group] = (meta[group] || []).map do |tag|
13
16
  Rule.find_by_tag_and_group(tag, group)
14
17
  end
15
18
  end
@@ -1,12 +1,6 @@
1
1
  module Licensee
2
2
  module Matchers
3
- class Exact
4
- attr_reader :file
5
-
6
- def initialize(file)
7
- @file = file
8
- end
9
-
3
+ class Exact < Licensee::Matchers::Matcher
10
4
  def match
11
5
  return @match if defined? @match
12
6
  @match = Licensee.licenses(hidden: true).find do |license|
@@ -3,10 +3,17 @@ module Licensee
3
3
  class Matcher
4
4
  attr_reader :file
5
5
 
6
+ include Licensee::HashHelper
7
+ HASH_METHODS = %i[name confidence].freeze
8
+
6
9
  def initialize(file)
7
10
  @file = file
8
11
  end
9
12
 
13
+ def name
14
+ @name ||= self.class.to_s.split('::').last.downcase.to_sym
15
+ end
16
+
10
17
  def match
11
18
  raise 'Not implemented'
12
19
  end
@@ -10,6 +10,11 @@ module Licensee
10
10
 
11
11
  attr_reader :content
12
12
 
13
+ include Licensee::HashHelper
14
+ HASH_METHODS = %i[
15
+ filename content content_hash content_normalized matcher matched_license
16
+ ].freeze
17
+
13
18
  ENCODING = Encoding::UTF_8
14
19
  ENCODING_OPTIONS = {
15
20
  invalid: :replace,
@@ -61,6 +66,10 @@ module Licensee
61
66
  alias match license
62
67
  alias path filename
63
68
 
69
+ def matched_license
70
+ license.key if license
71
+ end
72
+
64
73
  # Is this file a COPYRIGHT file with only a copyright statement?
65
74
  # If so, it can be excluded from determining if a project has >1 license
66
75
  def copyright?
@@ -68,6 +77,14 @@ module Licensee
68
77
  return false unless matcher.is_a?(Matchers::Copyright)
69
78
  filename =~ /\Acopyright(?:#{LicenseFile::OTHER_EXT_REGEX})?\z/i
70
79
  end
80
+
81
+ def content_hash
82
+ nil
83
+ end
84
+
85
+ def content_normalized
86
+ nil
87
+ end
71
88
  end
72
89
  end
73
90
  end
@@ -10,6 +10,9 @@ module Licensee
10
10
  alias detect_readme? detect_readme
11
11
  alias detect_packages? detect_packages
12
12
 
13
+ include Licensee::HashHelper
14
+ HASH_METHODS = %i[licenses matched_files].freeze
15
+
13
16
  def initialize(detect_packages: false, detect_readme: false)
14
17
  @detect_packages = detect_packages
15
18
  @detect_readme = detect_readme
data/lib/licensee/rule.rb CHANGED
@@ -2,6 +2,9 @@ module Licensee
2
2
  class Rule
3
3
  attr_reader :tag, :label, :description, :group
4
4
 
5
+ include Licensee::HashHelper
6
+ HASH_METHODS = %i[tag label description].freeze
7
+
5
8
  def initialize(tag: nil, label: nil, description: nil, group: nil)
6
9
  @tag = tag
7
10
  @label = label
@@ -1,3 +1,3 @@
1
1
  module Licensee
2
- VERSION = '9.8.0'.freeze
2
+ VERSION = '9.9.0.beta.2'.freeze
3
3
  end
data/lib/licensee.rb CHANGED
@@ -6,6 +6,7 @@ require 'yaml'
6
6
 
7
7
  module Licensee
8
8
  autoload :ContentHelper, 'licensee/content_helper'
9
+ autoload :HashHelper, 'licensee/hash_helper'
9
10
  autoload :License, 'licensee/license'
10
11
  autoload :LicenseField, 'licensee/license_field'
11
12
  autoload :LicenseMeta, 'licensee/license_meta'
data/spec/bin_spec.rb CHANGED
@@ -1,64 +1,21 @@
1
1
  RSpec.describe 'command line invocation' do
2
- let(:command) { ['ruby', 'bin/licensee'] }
2
+ let(:command) { ['bundle', 'exec', 'bin/licensee', 'help'] }
3
+ let(:arguments) { [] }
3
4
  let(:output) do
4
5
  Dir.chdir project_root do
5
6
  Open3.capture3(*[command, arguments].flatten)
6
7
  end
7
8
  end
9
+ let(:parsed_output) { YAML.safe_load(stdout) }
8
10
  let(:stdout) { output[0] }
9
11
  let(:stderr) { output[1] }
10
12
  let(:status) { output[2] }
11
- let(:hash) { '46cdc03462b9af57968df67b450cc4372ac41f53' }
12
13
 
13
- context 'without any arguments' do
14
- let(:arguments) { [] }
15
-
16
- it 'Returns a zero exit code' do
17
- expect(status.exitstatus).to eql(0)
18
- end
19
-
20
- it "detects the folder's license" do
21
- expect(stdout).to match('License: MIT License')
22
- end
23
-
24
- it 'outputs the hash' do
25
- expect(stdout).to match(hash)
26
- end
27
-
28
- it 'outputs the attribution' do
29
- expect(stdout).to match('2014-2017 Ben Balter')
30
- end
31
-
32
- it 'outputs the confidence' do
33
- expect(stdout).to match('Confidence: 100.00%')
34
- expect(stdout).to match('Confidence: 90.00%')
35
- end
36
-
37
- it 'outputs the method' do
38
- expect(stdout).to match('Matcher: Licensee::Matchers::Exact')
39
- expect(stdout).to match('Matcher: Licensee::Matchers::Gemspec')
40
- end
41
-
42
- it 'outputs the matched files' do
43
- matched_files = 'Matched files: ["LICENSE.md", "licensee.gemspec"]'
44
- expect(stdout).to include(matched_files)
45
- end
46
- end
47
-
48
- context 'when given a folder path' do
49
- let(:arguments) { [project_root] }
50
-
51
- it "detects the folder's license" do
52
- expect(stdout).to match('License: MIT License')
53
- end
14
+ it 'Returns a zero exit code' do
15
+ expect(status.exitstatus).to eql(0)
54
16
  end
55
17
 
56
- context 'when given a license path' do
57
- let(:license_path) { File.expand_path 'LICENSE.md', project_root }
58
- let(:arguments) { [license_path] }
59
-
60
- it "detects the file's license" do
61
- expect(stdout).to match('License: MIT License')
62
- end
18
+ it 'returns the help text' do
19
+ expect(stdout).to include('Licensee commands:')
63
20
  end
64
21
  end