licensed 1.2.0 → 1.3.0

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