licensed 1.5.2 → 2.0.0

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