licensed 1.2.0 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b1d1f0def7d120443e89895960f41bb7dd90a63f
4
- data.tar.gz: 951687b4ab3804df152f6dbbbfd4ffd2db3e3c71
3
+ metadata.gz: a40ec635869b9918fb610cf744adb0ff82a65410
4
+ data.tar.gz: f810f71a593eb01547b1ffc518b19e3df8b21ca3
5
5
  SHA512:
6
- metadata.gz: b7ca6dc13ebb771ed191e36fde4ab7c75aaadd1ca2e406a925595f3ee98553f660a8d81c677a2d87c346c0bfb32b8693e07cf90f8c5b6e0a817f368326d5f262
7
- data.tar.gz: '0197b0088dd379120614fb725efd41a58d3eed9744e32928220ecb431d63f8be5229888f1857e1ef2b5a3cbfc7810524b10b33c84c71a9990000c26f7f7bb912'
6
+ metadata.gz: c4b254f5fb6bd2a268adfc06971e82421be382d3f83d3eaca29acd5839c56d8c646c151c1b6702fb636c148ab2fff173171e066da315f1e2bf2ceb47349f8b46
7
+ data.tar.gz: 9426edecae8edf19eb61374711f3fdb2eae46468c0c0eff1d7db13f21718ac524a7f21bb1dce5f528d02e8f9bee68e043a393ebe513ee8ef6da5ef71073d0923
data/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## 1.3.0 - 2018-07-25
10
+ ### Added
11
+ - Manifests for the manifest dependency source can be specified using glob patterns in the configuration
12
+ - Paths to licenses for dependencies from the manifest dependency source can be specified in the configuration
13
+ - Manifest dependency source looks for license content in C-style comments if a license file isn't found
14
+
15
+ ## Changes
16
+ - GitHub is no longer queried to find remote license information
17
+ - Removed custom logic around determining whether to use the license key from `licensee`
18
+ - NPM dependency enumeration doesn't use `npm list`
19
+ - Licensed now tracks content from multiple license files when available
20
+
21
+ ### Fixed
22
+ - Fixed regression finding platform-specific ruby gems
23
+
9
24
  ## 1.2.0 - 2018-06-22
10
25
  ### Added
11
26
  - Building and packaging distributable exes for licensed releases
@@ -45,4 +60,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
45
60
 
46
61
  Initial release :tada:
47
62
 
48
- [Unreleased]: https://github.com/github/licensed/compare/1.2.0...HEAD
63
+ [Unreleased]: https://github.com/github/licensed/compare/1.3.0...HEAD
data/README.md CHANGED
@@ -80,7 +80,7 @@ Dependencies will be automatically detected for
80
80
  5. [Go Dep](./docs/sources/dep.md)
81
81
  6. [Manifest lists](./docs/sources/manifests.md)
82
82
  7. [NPM](./docs/sources/npm.md)
83
- 8. [Pip](./docs/source/pip.md)
83
+ 8. [Pip](./docs/sources/pip.md)
84
84
 
85
85
  You can disable any of them in the configuration file:
86
86
 
@@ -1,6 +1,16 @@
1
1
  # Manifests
2
2
 
3
- The manifest source can be used when no package managers are available.
3
+ The manifest source can be used when no package managers are available. The manifest source will be enabled when a manifest file is found or a manifest is configured in the configuration file.
4
+
5
+ ## Manifest sources
6
+
7
+ A dependency file manifest can be specified in two ways - a dependency manifest file or in the licensed configuration file as a set of patterns used to find files.
8
+
9
+ Manifests are loaded from (in order)
10
+ - A manifest file, if found
11
+ - The licensed configuration file, if a manifest file is not found and the `manifest.dependencies` configuration property exists.
12
+
13
+ ### Manifest files
4
14
 
5
15
  Manifest files are used to match source files with their corresponding packages to find package dependencies. Manifest file paths can be specified in the app configuration with the following setting:
6
16
  ```yml
@@ -19,8 +29,119 @@ The manifest can be a JSON or YAML file with a single root object and properties
19
29
  }
20
30
  ```
21
31
 
22
- File paths are relative to the git repository root. Package names will be used for the metadata file names at `path/to/cache/manifest/<package>.txt`
32
+ File paths are relative to the git repository root. A metadata file, `path/to/cache/manifest/<package>.txt`, will be created for each package.
33
+
34
+ **NOTE** It is the responsibility of the repository owner to maintain the manifest file.
35
+
36
+ ### Configured manifest
37
+
38
+ Manifests can be specified using patterns specified in the configuration file.
39
+
40
+ Dependencies are specified at the `manifest.dependencies` configuration property and should map dependency names to one or more patterns of files matching the dependencies.
41
+
42
+ In the following example, there are two dependencies, `package` and `nested`, where `nested` is a sub-dependency in the `vendor/package` folder. Using inclusion and exclusion patterns we can specify that files in the `vendor/package/nested` folder belong to the `nested` package, while all other files belong to `package`.
43
+
44
+ ```yaml
45
+ manifest:
46
+ dependencies:
47
+ package:
48
+ - "vendor/package/**/*"
49
+ - "!vendor/package/nested/*"
50
+ nested: "vendor/package/nested/*"
51
+ ```
52
+
53
+ This demonstrates that
54
+ 1. Dependencies can be mapped to a string or an array of strings
55
+ 2. Inclusion and exclusion patterns apply
56
+ 3. Patterns follow a glob-style format
57
+
58
+ #### Pattern format
59
+
60
+ Patterns are evaluated using [Dir.glob](https://ruby-doc.org/core/Dir.html#method-c-glob) and must follow these rules:
61
+ 1. Patterns are evaluated from the project root directory
62
+ 2. Patterns will only match files that are tracked by Git
63
+ 3. Patterns should follow standard shell glob syntax
64
+ 4. Patterns are evaluated in the order specified - order matters!
65
+ 5. `!` can be appended to any pattern to indicate a negative pattern
66
+ - Negative patterns exclude matching files from the result set
67
+ - If a pattern should match files starting with `!`, escape the leading `!` with `\` -> `\!filename`
68
+ 6. Patterns will match dotfiles
69
+
70
+ **NOTE** If the first, or only, pattern for a dependency is a negative pattern, it will not affect the set of files matched to a dependency. An inclusion pattern should always be specified before any negative patterns for a dependency.
71
+
72
+ #### Restrictions for specifying dependency patterns via configuration
73
+
74
+ The following restrictions will raise errors if they are not met when specifying manifest dependency patterns in the configuration file
75
+
76
+ 1. The dependencies key is specified but is empty
77
+ 2. A file in the project is not attributed to any of the configured dependencies
78
+ - The manifest by default will track all files in the project to ensure that license metadata updates occur when dependencies change
79
+ - See [Globally excluding files](#globally-excluding-files) to limit the scope of files that are tracked
80
+ 3. A file in the project is attributed to multiple configured dependencies
81
+ - All files must be tracked by a single dependency
82
+
83
+ #### Globally excluding files
84
+
85
+ Tracking project files is needed to make sure that any changes to dependencies does not go unnoticed. What about non-dependency code?
86
+
87
+ To reduce friction on changes to project code, global exclusion patterns can be added to the configuration that will cause any matching files to **NOT** be tracked as a dependency of the project.
88
+
89
+ Global exclusion patterns follow the same rules as [dependency patterns](#pattern-format) and are set on the `manifest.exclude` property.
90
+
91
+ The following example excludes all files that are not under the `vendor` folder.
92
+
93
+ ```yaml
94
+ manifest:
95
+ exclude:
96
+ - "**/*"
97
+ - "!vendor/**/*"
98
+ ```
99
+
100
+ ## Finding license content
101
+
102
+ ### From common source file directories
23
103
 
24
104
  If multiple source files map to a single package and they share a common path under the git repository root, that directory will be used to find license information, if available.
25
105
 
26
- It is the responsibility of the repository owner to maintain the manifest file.
106
+ ### From source file comments
107
+
108
+ When a file containing license content is not found for a group of source files,
109
+ Licensed will attempt to parse license text from source file comments.
110
+
111
+ There are some limitations on this functionality:
112
+
113
+ 1. Comments MUST contain a copyright statement
114
+ 2. Comments MUST be C-style multiline comments, e.g. `/* comment */`
115
+ 3. Comments SHOULD contain identical indentation for each content line.
116
+
117
+ The following examples are all :+1:. Licensed will try to preserve formatting,
118
+ however for best results comments should not mix tabs and spaces in leading whitespace.
119
+ ```
120
+ /*
121
+ <copyright statement>
122
+
123
+ <license text>
124
+ */
125
+
126
+ /* <copyright statement>
127
+ <license text>
128
+ */
129
+
130
+ /*
131
+ * <copyright statement>
132
+ * <license text>
133
+ *
134
+ * <license text>
135
+ */
136
+ ```
137
+
138
+ ### From license files specified in the configuration
139
+
140
+ If all else fails, the path to a dependency's license files can be specified manually
141
+ in the configuration. All paths should be relative to the repository root.
142
+
143
+ ```yaml
144
+ manifest:
145
+ licenses:
146
+ package: path/to/LICENSE
147
+ ```
data/lib/licensed.rb CHANGED
@@ -17,32 +17,3 @@ require "licensed/command/cache"
17
17
  require "licensed/command/status"
18
18
  require "licensed/command/list"
19
19
  require "licensed/ui/shell"
20
- require "octokit"
21
-
22
- module Licensed
23
- class << self
24
- attr_accessor :use_github
25
- end
26
-
27
- self.use_github = true
28
-
29
- GITHUB_URL = %r{\Ahttps://github.com/([a-z0-9]+(-[a-z0-9]+)*/(\w|\.|\-)+)}
30
- LICENSE_CONTENT_TYPE = "application/vnd.github.drax-preview+json"
31
-
32
- # Load license content from a GitHub url. Returns nil if the url does not point
33
- # to a GitHub repository, or if the license content is not found
34
- def self.from_github(url)
35
- return unless use_github && match = GITHUB_URL.match(url)
36
-
37
- license_url = Octokit::Repository.path(match[1]) + "/license"
38
- response = octokit.get license_url, accept: LICENSE_CONTENT_TYPE
39
- content = Base64.decode64(response["content"]).force_encoding("UTF-8")
40
- Licensee::ProjectFiles::LicenseFile.new(content, response["name"])
41
- rescue Octokit::NotFound
42
- nil
43
- end
44
-
45
- def self.octokit
46
- @octokit ||= Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
47
- end
48
- end
data/lib/licensed/cli.rb CHANGED
@@ -9,11 +9,10 @@ module Licensed
9
9
  method_option :force, type: :boolean,
10
10
  desc: "Overwrite licenses even if version has not changed."
11
11
  method_option :offline, type: :boolean,
12
- desc: "Do not make network calls."
12
+ desc: "This option is deprecated and will be removed in the next major release."
13
13
  method_option :config, aliases: "-c", type: :string,
14
14
  desc: "Path to licensed configuration file"
15
15
  def cache
16
- Licensed.use_github = false if options[:offline]
17
16
  run Licensed::Command::Cache.new(config), force: options[:force]
18
17
  end
19
18
 
@@ -9,17 +9,10 @@ module Licensed
9
9
  attr_reader :search_root
10
10
 
11
11
  def initialize(path, metadata = {})
12
- @path = path
13
12
  @search_root = metadata.delete("search_root")
14
-
15
- # with licensee providing license_file[:dir],
16
- # enforcing absolute paths makes life much easier when determining
17
- # an absolute file path in notices
18
- unless Pathname.new(path).absolute?
19
- raise "Dependency path #{path} must be absolute"
20
- end
21
-
22
13
  super metadata
14
+
15
+ self.path = path
23
16
  end
24
17
 
25
18
  # Returns a Licensee::Projects::FSProject for the dependency path
@@ -27,12 +20,24 @@ module Licensed
27
20
  @project ||= Licensee::Projects::FSProject.new(path, search_root: search_root, detect_packages: true, detect_readme: true)
28
21
  end
29
22
 
23
+ # Sets the path to source dependency license information
24
+ def path=(path)
25
+ # enforcing absolute paths makes life much easier when determining
26
+ # an absolute file path in #notices
27
+ unless Pathname.new(path).absolute?
28
+ raise "Dependency path #{path} must be absolute"
29
+ end
30
+
31
+ @path = path
32
+ reset_license!
33
+ end
34
+
30
35
  # Detects license information and sets it on this dependency object.
31
36
  # After calling `detect_license!``, the license is set at
32
37
  # `dependency["license"]` and legal text is set to `dependency.text`
33
38
  def detect_license!
34
39
  self["license"] = license_key
35
- self.text = [license_text, *notices].join("\n" + TEXT_SEPARATOR + "\n").strip
40
+ self.text = [license_text, *notices].join("\n" + TEXT_SEPARATOR + "\n").rstrip
36
41
  end
37
42
 
38
43
  # Extract legal notices from the dependency source
@@ -40,7 +45,7 @@ module Licensed
40
45
  local_files.uniq # unique local file paths
41
46
  .sort # sorted by the path
42
47
  .map { |f| File.read(f) } # read the file contents
43
- .map(&:strip) # strip whitespace
48
+ .map(&:rstrip) # strip whitespace
44
49
  .select { |t| t.length > 0 } # files with content only
45
50
  end
46
51
 
@@ -60,46 +65,25 @@ module Licensed
60
65
 
61
66
  private
62
67
 
63
- # Returns the Licensee::ProjectFile representing the matched_project_file
64
- # or remote_license_file
65
- def project_file
66
- matched_project_file || remote_license_file
67
- end
68
-
69
- # Returns the Licensee::LicenseFile, Licensee::PackageManagerFile, or
70
- # Licensee::ReadmeFile with a matched license, in that order or nil
71
- # if no license file matched a known license
72
- def matched_project_file
73
- @matched_project_file ||= project.matched_files
74
- .select { |f| f.license && !f.license.other? }
75
- .first
76
- end
77
-
78
- # Returns a Licensee::LicenseFile with the content of the license in the
79
- # dependency's repository to account for LICENSE files not being distributed
80
- def remote_license_file
81
- return @remote_license_file if defined?(@remote_license_file)
82
- @remote_license_file = Licensed.from_github(self["homepage"])
68
+ # Resets all local project and license information
69
+ def reset_license!
70
+ @project = nil
71
+ self.delete("license")
72
+ self.text = nil
83
73
  end
84
74
 
85
75
  # Regardless of the license detected, try to pull the license content
86
- # from the local LICENSE, remote LICENSE, or the README, in that order
76
+ # from the local LICENSE-type files, remote LICENSE, or the README, in that order
87
77
  def license_text
88
- content_file = project.license_file || remote_license_file || project.readme_file
89
- content_file.content if content_file
78
+ content_files = Array(project.license_files)
79
+ content_files << project.readme_file if content_files.empty? && project.readme_file
80
+ content_files.map(&:content).join("\n#{LICENSE_SEPARATOR}\n")
90
81
  end
91
82
 
92
83
  # Returns a string representing the project's license
93
- # Note, this will be "other" if a license file was found but the license
94
- # could not be identified and "none" if no license file was found at all
95
84
  def license_key
96
- if project_file && project_file.license
97
- project_file.license.key
98
- elsif project.license_file || remote_license_file
99
- "other"
100
- else
101
- "none"
102
- end
85
+ return "none" unless project.license
86
+ project.license.key
103
87
  end
104
88
  end
105
89
  end
data/lib/licensed/git.rb CHANGED
@@ -34,6 +34,12 @@ module Licensed
34
34
  return unless available? && sha
35
35
  Licensed::Shell.execute("git", "show", "-s", "-1", "--format=%ct", sha)
36
36
  end
37
+
38
+ def files
39
+ return unless available?
40
+ output = Licensed::Shell.execute("git", "ls-tree", "--full-tree", "-r", "--name-only", "HEAD")
41
+ output.lines.map(&:strip)
42
+ end
37
43
  end
38
44
  end
39
45
  end
@@ -11,6 +11,7 @@ module Licensed
11
11
 
12
12
  YAML_FRONTMATTER_PATTERN = /\A---\s*\n(.*?\n?)^---\s*$\n?(.*)\z/m
13
13
  TEXT_SEPARATOR = ("-" * 80).freeze
14
+ LICENSE_SEPARATOR = ("*" * 80).freeze
14
15
 
15
16
  # Read an existing license file
16
17
  #
@@ -23,7 +24,7 @@ module Licensed
23
24
  new(YAML.load(match[1]), match[2])
24
25
  end
25
26
 
26
- def_delegators :@metadata, :[], :[]=
27
+ def_delegators :@metadata, :[], :[]=, :delete
27
28
 
28
29
  # The license text and other legal notices
29
30
  attr_accessor :text
@@ -53,7 +54,7 @@ module Licensed
53
54
  # if the text didn't contain the separator, the text itself is the entirety
54
55
  # of the license text
55
56
  split = text.split(TEXT_SEPARATOR)
56
- split.length > 1 ? split.first.strip : text.strip
57
+ split.length > 1 ? split.first.rstrip : text.rstrip
57
58
  end
58
59
  alias_method :content, :license_text # use license_text for content matching
59
60
 
@@ -94,6 +94,13 @@ module Licensed
94
94
  return spec.source.specs.first
95
95
  end
96
96
 
97
+ # spec.source.specs gives access to specifications with more
98
+ # information than spec itself, including platform-specific gems.
99
+ # try to find a specification that matches `spec`
100
+ if source_spec = spec.source.specs.find { |s| s.name == spec.name && s.version == spec.version }
101
+ spec = source_spec
102
+ end
103
+
97
104
  # look for a specification at the bundler specs path
98
105
  spec_path = ::Bundler.specs_path.join("#{spec.full_name}.gemspec")
99
106
  return unless File.exist?(spec_path.to_s)
@@ -13,32 +13,22 @@ module Licensed
13
13
  end
14
14
 
15
15
  def enabled?
16
- File.exist?(manifest_path)
16
+ File.exist?(manifest_path) || generate_manifest?
17
17
  end
18
18
 
19
19
  def dependencies
20
20
  @dependencies ||= packages.map do |package_name, sources|
21
- Dependency.new(sources_license_path(sources), {
22
- "type" => Manifest.type,
23
- "name" => package_name,
24
- "version" => package_version(sources)
25
- })
21
+ Licensed::Source::Manifest::Dependency.new(sources,
22
+ package_license(package_name),
23
+ {
24
+ "type" => Manifest.type,
25
+ "name" => package_name,
26
+ "version" => package_version(sources)
27
+ }
28
+ )
26
29
  end
27
30
  end
28
31
 
29
- # Returns the top-most directory that is common to all paths in `sources`
30
- def sources_license_path(sources)
31
- common_prefix = Pathname.common_prefix(*sources).to_path
32
-
33
- # don't allow the repo root to be used as common prefix
34
- # the project this is run for should be excluded from the manifest,
35
- # or ignored in the config. any license in the root should be ignored.
36
- return common_prefix if common_prefix != Licensed::Git.repository_root
37
-
38
- # use the first source file as the license path.
39
- sources.first
40
- end
41
-
42
32
  # Returns the latest git SHA available from `sources`
43
33
  def package_version(sources)
44
34
  return if sources.nil? || sources.empty?
@@ -48,6 +38,16 @@ module Licensed
48
38
  .max_by { |sha| Licensed::Git.commit_date(sha) }
49
39
  end
50
40
 
41
+ # Returns the license path for a package specified in the configuration.
42
+ def package_license(package_name)
43
+ license_path = @config.dig("manifest", "licenses", package_name)
44
+ return unless license_path
45
+
46
+ license_path = Licensed::Git.repository_root.join(license_path)
47
+ return unless license_path.exist?
48
+ license_path
49
+ end
50
+
51
51
  # Returns a map of package names -> array of full source paths found
52
52
  # in the app manifest
53
53
  def packages
@@ -58,8 +58,10 @@ module Licensed
58
58
  end
59
59
  end
60
60
 
61
- # Returns parsed manifest data for the app
61
+ # Returns parsed or generated manifest data for the app
62
62
  def manifest
63
+ return generate_manifest if generate_manifest?
64
+
63
65
  case manifest_path.extname.downcase.delete "."
64
66
  when "json"
65
67
  JSON.parse(File.read(manifest_path))
@@ -70,11 +72,193 @@ module Licensed
70
72
 
71
73
  # Returns the manifest location for the app
72
74
  def manifest_path
73
- path = @config["manifest"]["path"] if @config["manifest"]
75
+ path = @config.dig("manifest", "path")
74
76
  return Licensed::Git.repository_root.join(path) if path
75
77
 
76
78
  @config.cache_path.join("manifest.json")
77
79
  end
80
+
81
+ # Returns whether a manifest should be generated automatically
82
+ def generate_manifest?
83
+ !File.exist?(manifest_path) && !@config.dig("manifest", "dependencies").nil?
84
+ end
85
+
86
+ # Returns a manifest of files generated automatically based on patterns
87
+ # set in the licensed configuration file
88
+ def generate_manifest
89
+ verify_configured_dependencies!
90
+ configured_dependencies.each_with_object({}) do |(name, files), hsh|
91
+ files.each { |f| hsh[f] = name }
92
+ end
93
+ end
94
+
95
+ # Verify that the licensed configuration file is valid for the current project.
96
+ # Raises errors for issues found with configuration
97
+ def verify_configured_dependencies!
98
+ # verify that dependencies are configured
99
+ if configured_dependencies.empty?
100
+ raise "The manifest \"dependencies\" cannot be empty!"
101
+ end
102
+
103
+ # verify all included files match a single configured dependency
104
+ errors = included_files.map do |file|
105
+ matches = configured_dependencies.select { |name, files| files.include?(file) }
106
+ .map { |name, files| name }
107
+ case matches.size
108
+ when 0
109
+ "#{file} did not match a configured dependency"
110
+ when 1
111
+ nil
112
+ else
113
+ "#{file} matched multiple configured dependencies: #{matches.join(", ")}"
114
+ end
115
+ end
116
+
117
+ errors.compact!
118
+ raise errors.join($/) unless errors.empty?
119
+ end
120
+
121
+ # Returns the project dependencies specified from the licensed configuration
122
+ def configured_dependencies
123
+ @configured_dependencies ||= begin
124
+ dependencies = @config.dig("manifest", "dependencies")&.dup || {}
125
+
126
+ dependencies.each do |name, patterns|
127
+ # map glob pattern(s) listed for the dependency to a listing
128
+ # of files that match the patterns and are not excluded
129
+ dependencies[name] = files_from_pattern_list(patterns) & included_files
130
+ end
131
+
132
+ dependencies
133
+ end
134
+ end
135
+
136
+ # Returns the set of project files that are included in dependency evaluation
137
+ def included_files
138
+ @sources ||= all_files - files_from_pattern_list(@config.dig("manifest", "exclude"))
139
+ end
140
+
141
+ # Finds and returns all files in the project that match
142
+ # the glob pattern arguments.
143
+ def files_from_pattern_list(patterns)
144
+ return Set.new if patterns.nil? || patterns.empty?
145
+
146
+ # evaluate all patterns from the project root
147
+ Dir.chdir Licensed::Git.repository_root do
148
+ Array(patterns).reduce(Set.new) do |files, pattern|
149
+ if pattern.start_with?("!")
150
+ # if the pattern is an exclusion, remove all matching files
151
+ # from the result
152
+ files - Dir.glob(pattern[1..-1], File::FNM_DOTMATCH)
153
+ else
154
+ # if the pattern is an inclusion, add all matching files
155
+ # to the result
156
+ files + Dir.glob(pattern, File::FNM_DOTMATCH)
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ # Returns all tracked files in the project
163
+ def all_files
164
+ # remove files if they are tracked but don't exist on the file system
165
+ @all_files ||= Set.new(Licensed::Git.files || [])
166
+ .delete_if { |f| !File.exist?(f) }
167
+ end
168
+
169
+ class Dependency < Licensed::Dependency
170
+ ANY_EXCEPT_COMMENT_CLOSE_REGEX = /(\*(?!\/)|[^\*])*/m.freeze
171
+ HEADER_LICENSE_REGEX = /
172
+ (
173
+ \/\*
174
+ #{ANY_EXCEPT_COMMENT_CLOSE_REGEX}#{Licensee::Matchers::Copyright::COPYRIGHT_SYMBOLS}#{ANY_EXCEPT_COMMENT_CLOSE_REGEX}
175
+ \*\/
176
+ )
177
+ /imx.freeze
178
+
179
+ def initialize(sources, license_path, metadata = {})
180
+ @sources = sources
181
+ license_path ||= sources_license_path(sources)
182
+ super license_path, metadata
183
+ end
184
+
185
+ # Detects license information and sets it on this dependency object.
186
+ # After calling `detect_license!``, the license is set at
187
+ # `dependency["license"]` and legal text is set to `dependency.text`
188
+ def detect_license!
189
+ # if no license key is found for the project, try to create a
190
+ # temporary LICENSE file from unique source file license headers
191
+ if license_key == "none"
192
+ tmp_license_file = write_license_from_source_licenses(self.path, @sources)
193
+ reset_license!
194
+ end
195
+
196
+ super
197
+ ensure
198
+ File.delete(tmp_license_file) if tmp_license_file && File.exist?(tmp_license_file)
199
+ end
200
+
201
+ private
202
+
203
+ # Returns the top-most directory that is common to all paths in `sources`
204
+ def sources_license_path(sources)
205
+ # return the source directory if there is only one source given
206
+ return source_directory(sources[0]) if sources.size == 1
207
+
208
+ common_prefix = Pathname.common_prefix(*sources).to_path
209
+
210
+ # don't allow the repo root to be used as common prefix
211
+ # the project this is run for should be excluded from the manifest,
212
+ # or ignored in the config. any license in the root should be ignored.
213
+ return common_prefix if common_prefix != Licensed::Git.repository_root
214
+
215
+ # use the first source directory as the license path.
216
+ source_directory(sources.first)
217
+ end
218
+
219
+ # Returns the directory for the source. Checks whether the source
220
+ # is a file or a directory
221
+ def source_directory(source)
222
+ return File.dirname(source) if File.file?(source)
223
+ source
224
+ end
225
+
226
+ # Writes any licenses found in source file comments to a LICENSE
227
+ # file at `dir`
228
+ # Returns the path to the license file
229
+ def write_license_from_source_licenses(dir, sources)
230
+ license_path = File.join(dir, "LICENSE")
231
+ File.open(license_path, "w") do |f|
232
+ licenses = source_comment_licenses(sources).uniq
233
+ f.puts(licenses.join("\n#{LICENSE_SEPARATOR}\n"))
234
+ end
235
+
236
+ license_path
237
+ end
238
+
239
+ # Returns a list of unique licenses parsed from source comments
240
+ def source_comment_licenses(sources)
241
+ comments = sources.select { |s| File.file?(s) }.flat_map do |source|
242
+ content = File.read(source)
243
+ content.scan(HEADER_LICENSE_REGEX).map { |match| match[0] }
244
+ end
245
+
246
+ comments.map do |comment|
247
+ # strip leading "*" and whitespace
248
+ indent = nil
249
+ comment.lines.map do |line|
250
+ # find the length of the indent as the number of characters
251
+ # until the first word character
252
+ indent ||= line[/\A([^\w]*)\w/, 1]&.size
253
+
254
+ # insert newline for each line until a word character is found
255
+ next "\n" unless indent
256
+
257
+ line[/([^\w\r\n]{0,#{indent}})(.*)/m, 2]
258
+ end.join
259
+ end
260
+ end
261
+ end
78
262
  end
79
263
  end
80
264
  end
@@ -19,16 +19,10 @@ module Licensed
19
19
  def dependencies
20
20
  return @dependencies if defined?(@dependencies)
21
21
 
22
- locations = {}
23
- package_location_command.lines.each do |line|
24
- path, id = line.split(":")[0, 2]
25
- locations[id] ||= path
26
- end
27
-
28
22
  packages = recursive_dependencies(JSON.parse(package_metadata_command)["dependencies"])
29
23
 
30
24
  @dependencies = packages.map do |name, package|
31
- path = package["realPath"] || locations["#{package["name"]}@#{package["version"]}"]
25
+ path = package["path"]
32
26
  fail "couldn't locate #{name} under node_modules/" unless path
33
27
  Dependency.new(path, {
34
28
  "type" => NPM.type,
@@ -50,11 +44,6 @@ module Licensed
50
44
  result
51
45
  end
52
46
 
53
- # Returns the output from running `npm list` to get package paths
54
- def package_location_command
55
- npm_list_command("--parseable", "--production", "--long")
56
- end
57
-
58
47
  # Returns the output from running `npm list` to get package metadata
59
48
  def package_metadata_command
60
49
  npm_list_command("--json", "--production", "--long")
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Licensed
3
- VERSION = "1.2.0".freeze
3
+ VERSION = "1.3.0".freeze
4
4
  end
data/licensed.gemspec CHANGED
@@ -25,15 +25,12 @@ Gem::Specification.new do |spec|
25
25
 
26
26
  spec.add_dependency "licensee", "~> 9.0"
27
27
  spec.add_dependency "thor", "~>0.19"
28
- spec.add_dependency "octokit", "~>4.0"
29
28
  spec.add_dependency "pathname-common_prefix", "~>0.0.1"
30
29
  spec.add_dependency "tomlrb", "~>1.2"
31
30
  spec.add_dependency "bundler", "~> 1.10"
32
31
 
33
32
  spec.add_development_dependency "rake", "~> 10.0"
34
33
  spec.add_development_dependency "minitest", "~> 5.8"
35
- spec.add_development_dependency "vcr", "~> 2.9"
36
- spec.add_development_dependency "webmock", "~> 1.21"
37
34
  spec.add_development_dependency "rubocop", "~> 0.49"
38
35
  spec.add_development_dependency "rubocop-github", "~> 0.6"
39
36
  spec.add_development_dependency "byebug", "~> 10.0.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: licensed
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-06-22 00:00:00.000000000 Z
11
+ date: 2018-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: licensee
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.19'
41
- - !ruby/object:Gem::Dependency
42
- name: octokit
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '4.0'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '4.0'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: pathname-common_prefix
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -122,34 +108,6 @@ dependencies:
122
108
  - - "~>"
123
109
  - !ruby/object:Gem::Version
124
110
  version: '5.8'
125
- - !ruby/object:Gem::Dependency
126
- name: vcr
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - "~>"
130
- - !ruby/object:Gem::Version
131
- version: '2.9'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - "~>"
137
- - !ruby/object:Gem::Version
138
- version: '2.9'
139
- - !ruby/object:Gem::Dependency
140
- name: webmock
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - "~>"
144
- - !ruby/object:Gem::Version
145
- version: '1.21'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - "~>"
151
- - !ruby/object:Gem::Version
152
- version: '1.21'
153
111
  - !ruby/object:Gem::Dependency
154
112
  name: rubocop
155
113
  requirement: !ruby/object:Gem::Requirement