Merge pull request #209 from jonabc/ancestor-search

enable file system license searching in parent directories
This commit is contained in:
Ben Balter 2017-07-05 18:14:01 -04:00 committed by GitHub
commit 63b12fd035
9 changed files with 156 additions and 15 deletions

View File

@ -51,7 +51,7 @@ module Licensee
return [] if files.empty? || files.nil?
files = find_files { |n| LicenseFile.name_score(n) }
files = files.map do |file|
LicenseFile.new(load_file(file), file[:name])
LicenseFile.new(load_file(file), file)
end
prioritize_lgpl(files)
@ -90,7 +90,7 @@ module Licensee
# sorted by file score descending
def find_files
return [] if files.empty? || files.nil?
found = files.each { |file| file[:score] = yield(file[:name]) }
found = files.map { |file| file.merge(score: yield(file[:name])) }
found.select! { |file| file[:score] > 0 }
found.sort { |a, b| b[:score] <=> a[:score] }
end

View File

@ -1,7 +1,11 @@
require 'forwardable'
module Licensee
class Project
class File
attr_reader :content, :filename
extend Forwardable
attr_reader :content
ENCODING = Encoding::UTF_8
ENCODING_OPTIONS = {
@ -10,13 +14,28 @@ module Licensee
replace: ''
}.freeze
def initialize(content, filename = nil)
# Create a new Licensee::Project::File with content and metadata
#
# content - file content
# metadata - can be either the string filename, or a hash containing
# metadata about the file content. If a hash is given, the
# filename should be given using the :name key. See individual
# project types for additional available metadata
#
# Returns a new Licensee::Project::File
def initialize(content, metadata = {})
@content = content
@content.force_encoding(ENCODING)
unless @content.valid_encoding?
@content.encode!(ENCODING, ENCODING_OPTIONS)
end
@filename = filename
metadata = { name: metadata } if metadata.is_a? String
@data = metadata || {}
end
def filename
@data[:name]
end
def matcher
@ -34,6 +53,7 @@ module Licensee
alias match license
alias path filename
def_delegator :@data, :[]
end
end
end

View File

@ -1,6 +1,12 @@
require 'pathname'
# Filesystem-based project
#
# Analyze a folder on the filesystem for license information
#
# Project files for this project type will contain the following keys:
# :name - the relative file name
# :dir - the directory path containing the file
module Licensee
class FSProject < Project
def initialize(path, **args)
@ -11,22 +17,28 @@ module Licensee
@pattern = '*'
@dir = path
end
@root = args.delete(:search_root) || @dir
unless valid_search_root?
raise 'Search root must be the FSProject path directory or its ancestor'
end
super(**args)
end
private
# Returns an array of hashes representing the project's files.
# Hashes will have the :name key, with the relative path to the file
# Hashes will have the the following keys:
# :name - the relative file name
# :dir - the directory path containing the file
def files
files = []
Dir.glob(::File.join(@dir, @pattern).tr('\\', '/')) do |file|
next unless ::File.file?(file)
files.push(name: ::File.basename(file))
@files ||= search_directories.flat_map do |dir|
Dir.glob(::File.join(dir, @pattern).tr('\\', '/')).map do |file|
next unless ::File.file?(file)
{ name: ::File.basename(file), dir: dir }
end.compact
end
files
end
# Retrieve a file's content from disk
@ -35,7 +47,32 @@ module Licensee
#
# Returns the file contents as a string
def load_file(file)
::File.read(::File.join(@dir, file[:name]))
::File.read(::File.join(file[:dir], file[:name]))
end
# Returns true if @dir is @root or it's descendant
def valid_search_root?
dir = Pathname.new(@dir)
dir.fnmatch?(@root) || dir.fnmatch?(::File.join(@root, '**'))
end
# Returns the set of unique paths to search for project files
# in order from @dir -> @root
def search_directories
search_enumerator.map(&:to_path)
.push(@root) # ensure root is included in the search
.uniq # don't include the root twice if @dir == @root
end
# Enumerates all directories to search, from @dir to @root
def search_enumerator
root = Pathname.new(@root)
dir = Pathname.new(@dir)
Enumerator.new do |yielder|
dir.relative_path_from(root).ascend do |relative|
yielder.yield root.join(relative)
end
end
end
end
end

View File

@ -1,6 +1,10 @@
# Git-based project
#
# Analyze a given (bare) Git repository for license information
#
# Project files for this project type will contain the following keys:
# :name - the file's path relative to the repo root
# :oid - the file's OID
module Licensee
class GitProject < Project
attr_reader :repository, :revision
@ -51,7 +55,7 @@ module Licensee
# :name - the file's path relative to the repo root
# :oid - the file's OID
def files
commit.tree.map do |entry|
@files ||= commit.tree.map do |entry|
next unless entry[:type] == :blob
{ name: entry[:name], oid: entry[:oid] }
end.compact

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Ben Balter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Ben Balter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -82,6 +82,31 @@
content = subject.send(:load_file, files.first)
expect(content).to match('Permission is hereby granted')
end
if described_class == Licensee::FSProject
context 'with search root argument' do
let(:fixture) { 'license-in-parent-folder/license-folder/package' }
let(:path) { fixture_path(fixture) }
let(:license_folder) { 'license-in-parent-folder/license-folder' }
let(:search_root) { fixture_path(license_folder) }
let(:subject) { described_class.new(path, search_root: search_root) }
let(:files) { subject.send(:files) }
it 'looks for licenses in parent directories up to the search root' do
# should not include the license in 'license-in-parent-folder' dir
expect(files.count).to eql(1)
expect(files.first[:name]).to eql('LICENSE.txt')
end
end
context 'without search root argument' do
let(:fixture) { 'license-in-parent-folder/license-folder/package' }
it 'looks for licenses in current directory only' do
expect(files.count).to eql(0)
end
end
end
end
context 'encoding correctness' do

View File

@ -30,4 +30,17 @@ RSpec.describe Licensee::Project::File do
it 'returns the license' do
expect(subject.license).to eql(mit)
end
context 'with additional metadata' do
subject { described_class.new(content, name: filename, dir: Dir.pwd) }
it 'stores the filename' do
expect(subject.filename).to eql(filename)
expect(subject[:name]).to eql(filename)
end
it 'stores additional metadata' do
expect(subject[:dir]).to eql(Dir.pwd)
end
end
end