licensee 9.8.0 → 9.9.0.beta.2

Sign up to get free protection for your applications and to get access to all the features.
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