licensed 1.5.2 → 2.0.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +22 -1
  4. data/CONTRIBUTING.md +2 -2
  5. data/README.md +17 -24
  6. data/Rakefile +2 -2
  7. data/docs/adding_a_new_source.md +93 -0
  8. data/docs/commands.md +81 -0
  9. data/docs/configuration.md +8 -8
  10. data/docs/migrating_to_newer_versions.md +3 -0
  11. data/docs/reporters.md +174 -0
  12. data/docs/sources/bundler.md +5 -5
  13. data/lib/licensed.rb +5 -14
  14. data/lib/licensed/cli.rb +23 -9
  15. data/lib/licensed/commands.rb +9 -0
  16. data/lib/licensed/commands/cache.rb +82 -0
  17. data/lib/licensed/commands/command.rb +112 -0
  18. data/lib/licensed/commands/list.rb +24 -0
  19. data/lib/licensed/commands/status.rb +49 -0
  20. data/lib/licensed/configuration.rb +3 -8
  21. data/lib/licensed/dependency.rb +116 -58
  22. data/lib/licensed/dependency_record.rb +76 -0
  23. data/lib/licensed/migrations.rb +7 -0
  24. data/lib/licensed/migrations/v2.rb +65 -0
  25. data/lib/licensed/reporters.rb +9 -0
  26. data/lib/licensed/reporters/cache_reporter.rb +76 -0
  27. data/lib/licensed/reporters/list_reporter.rb +69 -0
  28. data/lib/licensed/reporters/reporter.rb +119 -0
  29. data/lib/licensed/reporters/status_reporter.rb +67 -0
  30. data/lib/licensed/shell.rb +8 -10
  31. data/lib/licensed/sources.rb +15 -0
  32. data/lib/licensed/{source → sources}/bower.rb +14 -19
  33. data/lib/licensed/{source → sources}/bundler.rb +73 -48
  34. data/lib/licensed/{source → sources}/cabal.rb +40 -46
  35. data/lib/licensed/{source → sources}/dep.rb +15 -27
  36. data/lib/licensed/{source → sources}/git_submodule.rb +14 -19
  37. data/lib/licensed/{source → sources}/go.rb +28 -35
  38. data/lib/licensed/{source → sources}/manifest.rb +68 -90
  39. data/lib/licensed/{source → sources}/npm.rb +16 -25
  40. data/lib/licensed/{source → sources}/pip.rb +23 -25
  41. data/lib/licensed/sources/source.rb +69 -0
  42. data/lib/licensed/ui/shell.rb +4 -0
  43. data/lib/licensed/version.rb +6 -1
  44. data/licensed.gemspec +4 -4
  45. data/script/source-setup/bundler +1 -1
  46. metadata +32 -18
  47. data/lib/licensed/command/cache.rb +0 -82
  48. data/lib/licensed/command/list.rb +0 -43
  49. data/lib/licensed/command/status.rb +0 -79
  50. data/lib/licensed/command/version.rb +0 -18
  51. data/lib/licensed/license.rb +0 -68
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+ require "fileutils"
3
+ require "forwardable"
4
+ require "licensee"
5
+
6
+ module Licensed
7
+ class DependencyRecord
8
+ include Licensee::ContentHelper
9
+ extend Forwardable
10
+
11
+ EXTENSION = "dep.yml".freeze
12
+
13
+ # Read an existing record file
14
+ #
15
+ # filename - A String path to the file
16
+ #
17
+ # Returns a Licensed::DependencyRecord
18
+ def self.read(filename)
19
+ return unless File.exist?(filename)
20
+ data = YAML.load_file(filename)
21
+ return if data.nil? || data.empty?
22
+ new(
23
+ licenses: data.delete("licenses"),
24
+ notices: data.delete("notices"),
25
+ metadata: data
26
+ )
27
+ end
28
+
29
+ def_delegators :@metadata, :[], :[]=
30
+ attr_reader :licenses
31
+ attr_reader :notices
32
+
33
+ # Construct a new record
34
+ #
35
+ # licenses - a string, or array of strings, representing the content of each license
36
+ # notices - a string, or array of strings, representing the content of each legal notice
37
+ # metadata - a Hash of the metadata for the package
38
+ def initialize(licenses: [], notices: [], metadata: {})
39
+ @licenses = [licenses].flatten.compact
40
+ @notices = [notices].flatten.compact
41
+ @metadata = metadata
42
+ end
43
+
44
+ # Save the metadata and text to a file
45
+ #
46
+ # filename - The destination file to save record contents at
47
+ def save(filename)
48
+ data_to_save = @metadata.merge({
49
+ "licenses" => licenses,
50
+ "notices" => notices
51
+ })
52
+
53
+ FileUtils.mkdir_p(File.dirname(filename))
54
+ File.write(filename, data_to_save.to_yaml)
55
+ end
56
+
57
+ # Returns the content used to compare two licenses using normalization from
58
+ # `Licensee::CotentHelper`
59
+ def content
60
+ return if licenses.nil? || licenses.empty?
61
+ licenses.map do |license|
62
+ if license.is_a?(String)
63
+ license
64
+ elsif license.respond_to?(:[])
65
+ license["text"]
66
+ end
67
+ end.join
68
+ end
69
+
70
+ # Returns whether two records match based on their contents
71
+ def matches?(other)
72
+ return false unless other.is_a?(DependencyRecord)
73
+ self.content_normalized == other.content_normalized
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Licensed
4
+ module Migrations
5
+ require "licensed/migrations/v2"
6
+ end
7
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+ require "licensed/shell"
3
+
4
+ module Licensed
5
+ module Migrations
6
+ class V2
7
+ YAML_FRONTMATTER_PATTERN = /\A---\s*\n(.*?\n?)^---\s*$\n?(.*)\z/m
8
+ TEXT_SEPARATOR = ("-" * 80).freeze
9
+ LICENSE_SEPARATOR = ("*" * 80).freeze
10
+
11
+ def self.migrate(config_path, shell = Licensed::UI::Shell.new)
12
+ shell.info "updating to v2"
13
+
14
+ shell.info "updating bundler configuration keys"
15
+ # replace all "rubygem" and "rubygems" configuration keys with "bundler"
16
+ # to account for the bundler source's `type` change from `rubygem` to `bundler`
17
+ File.write(config_path, File.read(config_path).gsub(/("?)rubygems?("?):/, "\\1bundler\\2:"))
18
+
19
+ shell.info "updating cached records"
20
+ # load the configuration to find and update cached contents
21
+ configuration = Licensed::Configuration.load_from(config_path)
22
+ configuration.apps.each do |app|
23
+
24
+ # move any bundler records from the `rubygem` folder to the `bundler` folder
25
+ rubygem_cache = app.cache_path.join("rubygem")
26
+ if rubygem_cache.exist?
27
+ File.rename rubygem_cache, app.cache_path.join("bundler")
28
+ end
29
+
30
+ app.sources.each do |source|
31
+ Dir.chdir app.cache_path.join(source.class.type) do
32
+ # licensed v1 cached records were stored as .txt files with YAML frontmatter
33
+ Dir["**/*.txt"].each do |file|
34
+ yaml, licenses, notices = parse_file(file)
35
+
36
+ # rename the rubygem type to bundler
37
+ yaml["type"] = "bundler" if yaml["type"] == "rubygem"
38
+
39
+ # set licenses and notices as yaml properties
40
+ yaml["licenses"] = licenses.map { |text| { "text" => text } }
41
+ yaml["notices"] = notices.map { |text| { "text" => text } }
42
+
43
+ # v2 records are stored in `.dep.yml` files
44
+ # write the new yaml contents to the new file and delete old file
45
+ new_file = file.gsub(".txt", ".dep.yml")
46
+ File.write(new_file, yaml.to_yaml)
47
+ File.delete(file)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ # find the yaml and non-yaml data according to parsing logic from v1
55
+ def self.parse_file(filename)
56
+ match = File.read(filename).scrub.match(YAML_FRONTMATTER_PATTERN)
57
+ yaml = YAML.load(match[1])
58
+ # in v1, licenses and notices are separated by special text dividers
59
+ licenses, *notices = match[2].split(TEXT_SEPARATOR).map(&:strip)
60
+ licenses = licenses.split(LICENSE_SEPARATOR).map(&:strip)
61
+ [yaml, licenses, notices]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ module Licensed
3
+ module Reporters
4
+ require "licensed/reporters/reporter"
5
+ require "licensed/reporters/cache_reporter"
6
+ require "licensed/reporters/status_reporter"
7
+ require "licensed/reporters/list_reporter"
8
+ end
9
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+ module Licensed
3
+ module Reporters
4
+ class CacheReporter < Reporter
5
+ # Reports on an application configuration in a cache command run
6
+ #
7
+ # app - An application configuration
8
+ #
9
+ # Returns the result of the yielded method
10
+ # Note - must be called from inside the `report_run` scope
11
+ def report_app(app)
12
+ super do |report|
13
+ shell.info "Caching dependency records for #{app["name"]}"
14
+ yield report
15
+ end
16
+ end
17
+
18
+ # Reports on a dependency source enumerator in a cache command run.
19
+ # Shows the type and count of dependencies found by the source.
20
+ #
21
+ # source - A dependency source enumerator
22
+ #
23
+ # Returns the result of the yielded method
24
+ # Note - must be called from inside the `report_run` scope
25
+ def report_source(source)
26
+ super do |report|
27
+ shell.info " #{source.class.type}"
28
+ result = yield report
29
+
30
+ errored_reports = report.all_reports.select { |report| report.errors.any? }.to_a
31
+ if errored_reports.any?
32
+ shell.newline
33
+ shell.error " * Errors:"
34
+ errored_reports.each do |report|
35
+ display_metadata = report.map { |k, v| "#{k}: #{v}" }.join(", ")
36
+
37
+ shell.error " * #{report.name}"
38
+ shell.error " #{display_metadata}" unless display_metadata.empty?
39
+ report.errors.each do |error|
40
+ shell.error " - #{error}"
41
+ end
42
+ shell.newline
43
+ end
44
+ else
45
+ shell.confirm " * #{report.reports.size} #{source.class.type} dependencies"
46
+ end
47
+
48
+ result
49
+ end
50
+ end
51
+
52
+ # Reports on a dependency in a cache command run.
53
+ # Shows whether the dependency's record was cached or reused.
54
+ #
55
+ # dependency - An application dependency
56
+ #
57
+ # Returns the result of the yielded method
58
+ # Note - must be called from inside the `report_run` scope
59
+ def report_dependency(dependency)
60
+ super do |report|
61
+ result = yield report
62
+
63
+ if report.errors.any?
64
+ shell.error " Error #{dependency.name} (#{dependency.version})"
65
+ elsif report["cached"]
66
+ shell.info " Caching #{dependency.name} (#{dependency.version})"
67
+ else
68
+ shell.info " Using #{dependency.name} (#{dependency.version})"
69
+ end
70
+
71
+ result
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Licensed
4
+ module Reporters
5
+ class ListReporter < Reporter
6
+ # Reports on an application configuration in a list command run
7
+ #
8
+ # app - An application configuration
9
+ #
10
+ # Returns the result of the yielded method
11
+ # Note - must be called from inside the `report_run` scope
12
+ def report_app(app)
13
+ super do |report|
14
+ shell.info "Listing dependencies for #{app["name"]}"
15
+ yield report
16
+ end
17
+ end
18
+
19
+ # Reports on a dependency source enumerator in a list command run.
20
+ # Shows the type and count of dependencies found by the source.
21
+ #
22
+ # source - A dependency source enumerator
23
+ #
24
+ # Returns the result of the yielded method
25
+ # Note - must be called from inside the `report_run` scope
26
+ def report_source(source)
27
+ super do |report|
28
+ shell.info " #{source.class.type}"
29
+ result = yield report
30
+
31
+ errored_reports = report.all_reports.select { |report| report.errors.any? }.to_a
32
+ if errored_reports.any?
33
+ shell.newline
34
+ shell.error " * Errors:"
35
+ errored_reports.each do |report|
36
+ display_metadata = report.map { |k, v| "#{k}: #{v}" }.join(", ")
37
+
38
+ shell.error " * #{report.name}"
39
+ shell.error " #{display_metadata}" unless display_metadata.empty?
40
+ report.errors.each do |error|
41
+ shell.error " - #{error}"
42
+ end
43
+ shell.newline
44
+ end
45
+ else
46
+ shell.confirm " * #{report.reports.size} #{source.class.type} dependencies"
47
+ end
48
+
49
+ result
50
+ end
51
+ end
52
+
53
+ # Reports on a dependency in a list command run.
54
+ #
55
+ # dependency - An application dependency
56
+ #
57
+ # Returns the result of the yielded method
58
+ # Note - must be called from inside the `report_run` scope
59
+ def report_dependency(dependency)
60
+ super do |report|
61
+ result = yield report
62
+ shell.info " #{dependency.name} (#{dependency.version})"
63
+
64
+ result
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+ module Licensed
3
+ module Reporters
4
+ class Reporter
5
+ class Report < Hash
6
+ attr_reader :name
7
+ attr_reader :target
8
+ def initialize(name:, target:)
9
+ super()
10
+ @name = name
11
+ @target = target
12
+ end
13
+
14
+ def reports
15
+ @reports ||= []
16
+ end
17
+
18
+ def errors
19
+ @errors ||= []
20
+ end
21
+
22
+ def all_reports
23
+ result = []
24
+ result << self
25
+ result.push(*reports.flat_map(&:all_reports))
26
+ end
27
+ end
28
+
29
+ class ReportingError < StandardError; end;
30
+
31
+ def initialize(shell = Licensed::UI::Shell.new)
32
+ @shell = shell
33
+ end
34
+
35
+ # Generate a report for a licensed command execution
36
+ # Yields a report object which can be used to view or add
37
+ # data generated for this run
38
+ #
39
+ # Returns the result of the yielded method
40
+ def report_run(command)
41
+ result = nil
42
+ @run_report = Report.new(name: nil, target: command)
43
+ begin
44
+ result = yield @run_report
45
+ ensure
46
+ @run_report = nil
47
+ end
48
+
49
+ result
50
+ end
51
+
52
+ # Generate a report for a licensed app configuration
53
+ # Yields a report object which can be used to view or add
54
+ # data generated for this app
55
+ #
56
+ # app - An application configuration
57
+ #
58
+ # Returns the result of the yielded method
59
+ # Note - must be called from inside the `report_run` scope
60
+ def report_app(app)
61
+ raise ReportingError.new("Cannot call report_app with active app context") unless @app_report.nil?
62
+ raise ReportingError.new("Call report_run before report_app") if @run_report.nil?
63
+ result = nil
64
+ @app_report = Report.new(name: app["name"], target: app)
65
+ begin
66
+ result = yield @app_report
67
+ ensure
68
+ @run_report.reports << @app_report
69
+ @app_report = nil
70
+ end
71
+
72
+ result
73
+ end
74
+
75
+ # Generate a report for a licensed dependency source enumerator
76
+ # Yields a report object which can be used to view or add
77
+ # data generated for this dependency source
78
+ #
79
+ # source - A dependency source enumerator
80
+ #
81
+ # Returns the result of the yielded method
82
+ # Note - must be called from inside the `report_app` scope
83
+ def report_source(source)
84
+ raise ReportingError.new("Cannot call report_source with active source context") unless @source_report.nil?
85
+ raise ReportingError.new("Call report_app before report_source") if @app_report.nil?
86
+ result = nil
87
+ @source_report = Report.new(name: [@app_report.name, source.class.type].join("."), target: source)
88
+ begin
89
+ result = yield @source_report
90
+ ensure
91
+ @app_report.reports << @source_report
92
+ @source_report = nil
93
+ end
94
+
95
+ result
96
+ end
97
+
98
+ # Generate a report for a licensed dependency
99
+ # Yields a report object which can be used to view or add
100
+ # data generated for this dependency
101
+ #
102
+ # dependency - An application dependency
103
+ #
104
+ # Returns the result of the yielded method
105
+ # Note - must be called from inside the `report_source` scope
106
+ def report_dependency(dependency)
107
+ raise ReportingError.new("Call report_source before report_dependency") if @source_report.nil?
108
+
109
+ dependency_report = Report.new(name: [@source_report.name, dependency.name].join("."), target: dependency)
110
+ @source_report.reports << dependency_report
111
+ yield dependency_report
112
+ end
113
+
114
+ protected
115
+
116
+ attr_reader :shell
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Licensed
4
+ module Reporters
5
+ class StatusReporter < Reporter
6
+ # Generate a report for a licensed status command run
7
+ # Shows the errors found when checking status, as well as
8
+ # overall number of dependencies checked
9
+ #
10
+ # Returns the result of the yielded method
11
+ def report_app(app)
12
+ super do |report|
13
+ shell.info "Checking cached dependency records for #{app["name"]}"
14
+
15
+ result = yield report
16
+
17
+ all_reports = report.all_reports
18
+ errored_reports = all_reports.select { |report| report.errors.any? }.to_a
19
+
20
+ dependency_count = all_reports.select { |report| report.target.is_a?(Licensed::Dependency) }.size
21
+ error_count = errored_reports.sum { |report| report.errors.size }
22
+
23
+ if error_count > 0
24
+ shell.newline
25
+ shell.error "Errors:"
26
+ errored_reports.each do |report|
27
+ display_metadata = report.map { |k, v| "#{k}: #{v}" }.join(", ")
28
+
29
+ shell.error "* #{report.name}"
30
+ shell.error " #{display_metadata}" unless display_metadata.empty?
31
+ report.errors.each do |error|
32
+ shell.error " - #{error}"
33
+ end
34
+ shell.newline
35
+ end
36
+ end
37
+
38
+ shell.newline
39
+ shell.info "#{dependency_count} dependencies checked, #{error_count} errors found."
40
+
41
+ result
42
+ end
43
+ end
44
+
45
+ # Reports on a dependency in a status command run.
46
+ # Shows whether the dependency's status is valid in dot format
47
+ #
48
+ # dependency - An application dependency
49
+ #
50
+ # Returns the result of the yielded method
51
+ # Note - must be called from inside the `report_run` scope
52
+ def report_dependency(dependency)
53
+ super do |report|
54
+ result = yield report
55
+
56
+ if report.errors.empty?
57
+ shell.confirm(".", false)
58
+ else
59
+ shell.error("F", false)
60
+ end
61
+
62
+ result
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end