Add support for GitHub projects

Rather than querying the local filesystem, we can use the Github API to
resolve folder contents and the individual files within.

This isn't going to be performant - requests are not made in parallel -
and there is absolutely no error handling.
This commit is contained in:
Nick Campbell 2017-12-24 13:57:58 +00:00
parent 69a67f055b
commit 1b9d1f2b00
No known key found for this signature in database
GPG Key ID: CBF75C1B5FC6A1AB
7 changed files with 235 additions and 8 deletions

View File

@ -2,10 +2,16 @@
## Command line usage
1. `cd` into a project directory
2. Execute the `licensee` command
This gem includes an executable which can be run using the `licensee [PATH]` command,
where `[PATH]` is:
You'll get an output that looks like:
* A directory, for example: `licensee vendor/gems/activesupport`
* A file, for example: `licensee LICENSE.txt`
* A GitHub repository, for example: `licensee https://github.com/facebook/react`
If you don't specify any arguments, `licensee` will just scan the current directory.
In all cases, you'll get an output that looks like:
```
License: MIT
@ -13,8 +19,6 @@ Confidence: 98.42%
Matcher: Licensee::GitMatcher
```
Alternately, `licensee <directory>` will treat the argument as the project directory, and `licensee <file>` will attempt to match the individual file specified, both with output that looks like the above.
## License Ruby API
```ruby
@ -37,6 +41,24 @@ license.meta["permissions"]
=> ["commercial-use","modifications","distribution","private-use"]
```
If you wish to scan private GitHub repositories, or are hitting API rate limits, you can configure the embedded [Octokit](https://github.com/octokit/octokit.rb)
client using environment variables, for example:
```sh
OCTOKIT_ACCESS_TOKEN=abc123 licensee https://github.com/benbalter/licensee
```
Octokit can also be configured using standard module-level configuration:
```ruby
# see https://github.com/octokit/octokit.rb#configuring-module-defaults
Octokit.configure do |c|
c.access_token = "<your 40 char token>"
end
license = Licensee.license "https://github.com/benbalter/licensee"
```
## Advanced API usage
You can gather more information by working with the project object, and the top level Licensee class.

View File

@ -35,7 +35,11 @@ module Licensee
end
def project(path, **args)
Licensee::Projects::GitProject.new(path, args)
if path =~ %r{\Ahttps://github.com}
Licensee::Projects::GitHubProject.new(path, args)
else
Licensee::Projects::GitProject.new(path, args)
end
rescue Licensee::Projects::GitProject::InvalidRepository
Licensee::Projects::FSProject.new(path, args)
end

View File

@ -3,5 +3,6 @@ module Licensee
autoload :Project, 'licensee/projects/project'
autoload :FSProject, 'licensee/projects/fs_project'
autoload :GitProject, 'licensee/projects/git_project'
autoload :GitHubProject, 'licensee/projects/github_project'
end
end

View File

@ -0,0 +1,46 @@
# GitHub project
#
# Analyses a remote GitHub repository for license information
#
# Only the root directory of a repository will be scanned because every
# `#load_file(..)` call incurs a separate API request.
require 'octokit'
module Licensee
module Projects
class GitHubProject < Licensee::Projects::Project
# If there's any trailing data (e.g. `.git`) this pattern will ignore it:
# we're going to use the API rather than clone the repo.
GITHUB_REPO_PATTERN = %r{https://github.com/([^\/]+\/[^\/\.]+).*}
class RepoNotFound < StandardError; end
def initialize(github_url, **args)
@repo = github_url[GITHUB_REPO_PATTERN, 1]
raise ArgumentError, "Not a github URL: #{github_url}" unless @repo
super(**args)
end
attr_reader :repo
private
def files
@files ||= contents.map { |data| { name: data[:name], dir: '/' } }
rescue Octokit::NotFound
raise RepoNotFound,
"Could not load GitHub repo #{repo}, it may be private or deleted"
end
def load_file(file)
Octokit.contents(@repo, path: file[:name],
accept: 'application/vnd.github.v3.raw')
end
def contents
Octokit.contents(@repo).select { |data| data[:type] == 'file' }
end
end
end
end

View File

@ -18,7 +18,9 @@ Gem::Specification.new do |gem|
gem.bindir = 'bin'
gem.executables << 'licensee'
gem.add_dependency('octokit', '~> 4.8.0')
gem.add_dependency('rugged', '~> 0.24')
gem.add_development_dependency('coveralls', '~> 0.8')
gem.add_development_dependency('mustache', '>= 0.9', '< 2.0')
gem.add_development_dependency('pry', '~> 0.9')

View File

@ -0,0 +1,140 @@
RSpec.describe Licensee::Projects::GitHubProject do
describe '#initialize' do
subject(:instance) { described_class.new(github_url) }
context 'with a GitHub URI' do
let(:github_url) { 'https://github.com/benbalter/licensee' }
it 'should set @repo' do
expect(instance.repo).to eq('benbalter/licensee')
end
end
context 'with a GitHub git URI' do
let(:github_url) { 'https://github.com/benbalter/licensee.git' }
it 'should set @repo, stripping the trailing extension' do
expect(instance.repo).to eq('benbalter/licensee')
end
end
context 'with a non-GitHub URI' do
let(:github_url) { 'https://gitlab.com/benbalter/licensee' }
it 'should raise an ArgumentError' do
expect { instance }.to raise_error(ArgumentError)
end
end
context 'with a local folder' do
let(:github_url) { fixture_path('mit') }
it 'should raise an ArgumentError' do
expect { instance }.to raise_error(ArgumentError)
end
end
end
subject { described_class.new(github_url) }
let(:repo) { 'benbalter/licensee' }
let(:github_url) { 'https://github.com/benbalter/licensee' }
let(:mit) { Licensee::License.find('mit') }
let(:readme_file) do
File.read(fixture_path('mit/README.md'))
end
let(:license_file) { File.read(fixture_path('mit/LICENSE.txt')) }
context 'when the repo exists' do
before do
allow(Octokit)
.to receive(:contents)
.with('benbalter/licensee')
.and_return([
{
name: 'LICENSE.txt',
path: 'LICENSE.txt',
sha: 'sha1',
size: 1072,
url: 'https://api.github.com/repos/benbalter/licensee/contents/LICENSE.txt?ref=master',
html_url: 'https://github.com/benbalter/licensee/blob/master/LICENSE.txt',
git_url: 'https://api.github.com/repos/benbalter/licensee/git/blobs/sha1',
download_url: 'https://raw.githubusercontent.com/benbalter/licensee/master/LICENSE.txt',
type: 'file',
_links: {}
},
{ name: 'README.md',
path: 'README.md',
sha: 'sha2',
size: 13_420,
url: 'https://api.github.com/repos/benbalter/licensee/contents/README.md?ref=master',
html_url: 'https://github.com/benbalter/licensee/blob/master/README.md',
git_url: 'https://api.github.com/repos/benbalter/licensee/git/blobs/sha2',
download_url: 'https://raw.githubusercontent.com/benbalter/licensee/master/README.md',
type: 'file',
_links: {} }
])
allow(Octokit)
.to receive(:contents)
.with(repo, path: 'LICENSE.txt',
accept: 'application/vnd.github.v3.raw')
.and_return(license_file)
allow(Octokit)
.to receive(:contents)
.with(repo, path: 'README.md', accept: 'application/vnd.github.v3.raw')
.and_return(readme_file)
end
it 'returns the license' do
expect(subject.license).to be_a(Licensee::License)
expect(subject.license).to eql(mit)
end
it 'returns the matched file' do
expect(subject.matched_file).to be_a(Licensee::ProjectFiles::LicenseFile)
expect(subject.matched_file.filename).to eql('LICENSE.txt')
end
it 'returns the license file' do
expect(subject.license_file).to be_a(Licensee::ProjectFiles::LicenseFile)
expect(subject.license_file.filename).to eql('LICENSE.txt')
end
it "doesn't return the readme" do
expect(subject.readme_file).to be_nil
end
it "doesn't return the package file" do
expect(subject.package_file).to be_nil
end
context 'readme detection' do
subject { described_class.new(github_url, detect_readme: true) }
it 'returns the readme' do
expect(subject.readme_file).to be_a(Licensee::ProjectFiles::ReadmeFile)
expect(subject.readme_file.filename).to eql('README.md')
end
it 'returns the license' do
expect(subject.license).to be_a(Licensee::License)
expect(subject.license).to eql(mit)
end
end
end
context 'when the repo cannot be found' do
let(:github_url) { 'https://github.com/benbalter/not-foundsss' }
before do
allow(Octokit)
.to receive(:contents).with(anything).and_raise(Octokit::NotFound)
end
it 'raises a RepoNotFound error' do
expect { subject.license }.to raise_error(described_class::RepoNotFound)
end
end
end

View File

@ -19,8 +19,20 @@ RSpec.describe Licensee do
expect(Licensee.license(license_path)).to eql(mit_license)
end
it 'inits a project' do
expect(Licensee.project(project_path)).to be_a(Licensee::Projects::Project)
describe '.project' do
subject { Licensee.project(project_path) }
it 'inits a project' do
expect(subject).to be_a(Licensee::Projects::Project)
end
context 'given a GitHub repository' do
let(:project_path) { 'https://github.com/benbalter/licensee' }
it 'creates a GitHubProject' do
expect(subject).to be_a(Licensee::Projects::GitHubProject)
end
end
end
context 'confidence threshold' do