diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..04ed2aa --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--require spec_helper +--order random diff --git a/.rubocop.yml b/.rubocop.yml index 481cf9c..e5e83d1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,3 +11,7 @@ Metrics/LineLength: Metrics/BlockLength: Exclude: - spec/**/* + +Metrics/MethodLength: + Exclude: + - spec/**/ diff --git a/lib/jekyll-github-metadata.rb b/lib/jekyll-github-metadata.rb index d1dcf36..e7895fa 100644 --- a/lib/jekyll-github-metadata.rb +++ b/lib/jekyll-github-metadata.rb @@ -18,25 +18,30 @@ module Errors end module GitHubMetadata - NoRepositoryError = Class.new(Jekyll::Errors::FatalException) - autoload :Client, "jekyll-github-metadata/client" autoload :MetadataDrop, "jekyll-github-metadata/metadata_drop" autoload :Pages, "jekyll-github-metadata/pages" autoload :Repository, "jekyll-github-metadata/repository" + autoload :RepositoryFinder, "jekyll-github-metadata/repository_finder" autoload :RepositoryCompat, "jekyll-github-metadata/repository_compat" autoload :Sanitizer, "jekyll-github-metadata/sanitizer" autoload :Value, "jekyll-github-metadata/value" autoload :VERSION, "jekyll-github-metadata/version" + NoRepositoryError = RepositoryFinder::NoRepositoryError + if Jekyll.const_defined? :Site require_relative "jekyll-github-metadata/site_github_munger" end class << self - attr_accessor :repository + attr_reader :repository_finder attr_writer :client, :logger + def site + repository_finder.site + end + def environment Jekyll.respond_to?(:env) ? Jekyll.env : (Pages.env || "development") end @@ -61,9 +66,19 @@ def client @client ||= Client.new end + def repository + @repository ||= GitHubMetadata::Repository.new(repository_finder.nwo).tap do |repo| + Jekyll::GitHubMetadata.log :debug, "Generating for #{repo.nwo}" + end + end + + def site=(new_site) + reset! + @repository_finder = GitHubMetadata::RepositoryFinder.new(new_site) + end + def reset! - @logger = nil - @client = nil + @logger = @client = @repository = @nwo = @site = nil end end end diff --git a/lib/jekyll-github-metadata/metadata_drop.rb b/lib/jekyll-github-metadata/metadata_drop.rb index adb7591..57b4bc3 100644 --- a/lib/jekyll-github-metadata/metadata_drop.rb +++ b/lib/jekyll-github-metadata/metadata_drop.rb @@ -8,11 +8,6 @@ class MetadataDrop < Jekyll::Drops::Drop mutable true - def initialize(site) - @site = site - super(nil) - end - def to_s require "json" JSON.pretty_generate to_h @@ -34,20 +29,7 @@ def keys def_delegator :"Jekyll::GitHubMetadata::Pages", :api_url, :api_url def_delegator :"Jekyll::GitHubMetadata::Pages", :help_url, :help_url - def versions - @versions ||= begin - require "github-pages" - GitHubPages.versions - rescue LoadError - {} - end - end - - def build_revision - @build_revision ||= begin - ENV["JEKYLL_BUILD_REVISION"] || `git rev-parse HEAD`.strip - end - end + private def_delegator :"Jekyll::GitHubMetadata", :repository def_delegator :repository, :owner_public_repositories, :public_repositories def_delegator :repository, :organization_public_members, :organization_members @@ -76,66 +58,22 @@ def build_revision def_delegator :repository, :latest_release, :latest_release def_delegator :repository, :private?, :private - private - attr_reader :site - - def repository - @repository ||= GitHubMetadata::Repository.new(nwo(site)).tap do |repo| - Jekyll::GitHubMetadata.log :debug, "Generating for #{repo.nwo}" + def versions + @versions ||= begin + require "github-pages" + GitHubPages.versions + rescue LoadError + {} end end - def git_exe_path - ENV["PATH"].to_s.split(File::PATH_SEPARATOR).map { |path| File.join(path, "git") }.find { |path| File.exist?(path) } - end - - def git_remotes - return [] if git_exe_path.nil? - `#{git_exe_path} remote --verbose`.to_s.strip.split("\n") - end - - def git_remote_url - git_remotes.grep(%r!\Aorigin\t!).map do |remote| - remote.sub(%r!\Aorigin\t(.*) \([a-z]+\)!, "\\1") - end.uniq.first || "" - end - - def nwo_from_git_origin_remote - return unless Jekyll.env == "development" || Jekyll.env == "test" - matches = git_remote_url.chomp(".git").match %r!github.com(:|/)([\w-]+)/([\w\.-]+)! - matches[2..3].join("/") if matches - end - - def nwo_from_env - ENV["PAGES_REPO_NWO"] - end - - def nwo_from_config(site) - repo = site.config["repository"] - repo if repo && repo.is_a?(String) && repo.include?("/") + def build_revision + @build_revision ||= begin + ENV["JEKYLL_BUILD_REVISION"] || `git rev-parse HEAD`.strip + end end - # Public: fetches the repository name with owner to fetch metadata for. - # In order of precedence, this method uses: - # 1. the environment variable $PAGES_REPO_NWO - # 2. 'repository' variable in the site config - # 3. the 'origin' git remote's URL - # - # site - the Jekyll::Site being processed - # - # Return the name with owner (e.g. 'parkr/my-repo') or raises an - # error if one cannot be found. - def nwo(site) - nwo_from_env || \ - nwo_from_config(site) || \ - nwo_from_git_origin_remote || \ - proc do - raise GitHubMetadata::NoRepositoryError, "No repo name found. " \ - "Specify using PAGES_REPO_NWO environment variables, " \ - "'repository' in your configuration, or set up an 'origin' " \ - "git remote pointing to your github.com repository." - end.call - end + private # Nothing to see here. def fallback_data diff --git a/lib/jekyll-github-metadata/repository_finder.rb b/lib/jekyll-github-metadata/repository_finder.rb new file mode 100644 index 0000000..d0d8524 --- /dev/null +++ b/lib/jekyll-github-metadata/repository_finder.rb @@ -0,0 +1,71 @@ +module Jekyll + module GitHubMetadata + class RepositoryFinder + NoRepositoryError = Class.new(Jekyll::Errors::FatalException) + + attr_reader :site + def initialize(site) + @site = site + end + + # Public: fetches the repository name with owner to fetch metadata for. + # In order of precedence, this method uses: + # 1. the environment variable $PAGES_REPO_NWO + # 2. 'repository' variable in the site config + # 3. the 'origin' git remote's URL + # + # site - the Jekyll::Site being processed + # + # Return the name with owner (e.g. 'parkr/my-repo') or raises an + # error if one cannot be found. + def nwo + @nwo ||= begin + nwo_from_env || \ + nwo_from_config || \ + nwo_from_git_origin_remote || \ + proc do + raise NoRepositoryError, "No repo name found. " \ + "Specify using PAGES_REPO_NWO environment variables, " \ + "'repository' in your configuration, or set up an 'origin' " \ + "git remote pointing to your github.com repository." + end.call + end + end + + private + + def nwo_from_env + ENV["PAGES_REPO_NWO"] + end + + def nwo_from_config + repo = site.config["repository"] + repo if repo && repo.is_a?(String) && repo.include?("/") + end + + def git_exe_path + ENV["PATH"].to_s + .split(File::PATH_SEPARATOR) + .map { |path| File.join(path, "git") } + .find { |path| File.exist?(path) } + end + + def git_remotes + return [] if git_exe_path.nil? + `#{git_exe_path} remote --verbose`.to_s.strip.split("\n") + end + + def git_remote_url + git_remotes.grep(%r!\Aorigin\t!).map do |remote| + remote.sub(%r!\Aorigin\t(.*) \([a-z]+\)!, "\\1") + end.uniq.first || "" + end + + def nwo_from_git_origin_remote + return unless Jekyll.env == "development" || Jekyll.env == "test" + matches = git_remote_url.chomp(".git").match %r!github.com(:|/)([\w-]+)/([\w\.-]+)! + matches[2..3].join("/") if matches + end + end + end +end diff --git a/lib/jekyll-github-metadata/site_github_munger.rb b/lib/jekyll-github-metadata/site_github_munger.rb index 2beea7d..69a6d4f 100644 --- a/lib/jekyll-github-metadata/site_github_munger.rb +++ b/lib/jekyll-github-metadata/site_github_munger.rb @@ -4,10 +4,13 @@ module Jekyll module GitHubMetadata class SiteGitHubMunger - attr_reader :site + extend Forwardable + + def_delegators :"Jekyll::GitHubMetadata", :site + private def_delegator :"Jekyll::GitHubMetadata", :repository def initialize(site) - @site = site + Jekyll::GitHubMetadata.site = site end def munge! @@ -16,9 +19,8 @@ def munge! # This is the good stuff. site.config["github"] = github_namespace - return unless should_add_fallbacks? - add_url_and_baseurl_fallbacks! add_title_and_description_fallbacks! + add_url_and_baseurl_fallbacks! if should_add_url_fallbacks? end private @@ -35,13 +37,14 @@ def github_namespace end def drop - @drop ||= MetadataDrop.new(site) + @drop ||= MetadataDrop.new(GitHubMetadata.site) end # Set `site.url` and `site.baseurl` if unset. def add_url_and_baseurl_fallbacks! - site.config["url"] ||= repository.url_without_path - site.config["baseurl"] = repository.baseurl if should_set_baseurl? + site.config["url"] ||= Value.new("url", proc { |_c, r| r.url_without_path }) + return unless should_set_baseurl? + site.config["baseurl"] = Value.new("baseurl", proc { |_c, r| r.baseurl }) end # Set the baseurl only if it is `nil` or `/` @@ -51,17 +54,13 @@ def should_set_baseurl? end def add_title_and_description_fallbacks! - site.config["title"] ||= repository.name - site.config["description"] ||= repository.tagline + site.config["title"] ||= Value.new("title", proc { |_c, r| r.name }) + site.config["description"] ||= Value.new("description", proc { |_c, r| r.tagline }) end - def should_add_fallbacks? + def should_add_url_fallbacks? Jekyll.env == "production" || Pages.page_build? end - - def repository - drop.send(:repository) - end end end end diff --git a/lib/jekyll-github-metadata/value.rb b/lib/jekyll-github-metadata/value.rb index e1ded5d..1860a78 100644 --- a/lib/jekyll-github-metadata/value.rb +++ b/lib/jekyll-github-metadata/value.rb @@ -3,6 +3,9 @@ module Jekyll module GitHubMetadata class Value + extend Forwardable + def_delegators :render, :+, :to_s, :to_json, :eql?, :hash + attr_reader :key, :value def initialize(*args) @@ -19,48 +22,39 @@ def initialize(*args) end def render - @value = if @value.respond_to?(:call) - case @value.arity - when 0 - @value.call - when 1 - @value.call(GitHubMetadata.client) - when 2 - @value.call(GitHubMetadata.client, GitHubMetadata.repository) - else - raise ArgumentError, "Whoa, arity of 0, 1, or 2 please in your procs." - end - else - @value - end - @value = Sanitizer.sanitize(@value) + return @rendered if defined? @rendered + @rendered = @value = Sanitizer.sanitize(call_or_value) rescue RuntimeError, NameError => e Jekyll::GitHubMetadata.log :error, "Error processing value '#{key}':" raise e end - def to_s - render.to_s - end - - def to_json(*) - render.to_json - end - def to_liquid case render - when nil - nil - when true, false - value - when Hash - value - when String, Numeric, Array + when nil, true, false, Hash, String, Numeric, Array value else to_json end end + + private + + # Calls the value Proc with the appropriate number of arguments + # or returns the raw value if it's a literal + def call_or_value + return value unless value.respond_to?(:call) + case value.arity + when 0 + value.call + when 1 + value.call(GitHubMetadata.client) + when 2 + value.call(GitHubMetadata.client, GitHubMetadata.repository) + else + raise ArgumentError, "Whoa, arity of 0, 1, or 2 please in your procs." + end + end end end end diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index 6efaa29..69e7c8d 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -15,6 +15,7 @@ def dest_dir(*files) before(:each) do # Run Jekyll ENV.delete("JEKYLL_ENV") + ENV["JEKYLL_ENV"] = "production" ENV["PAGES_ENV"] = "dotcom" Jekyll.logger.log_level = :error Jekyll::Commands::Build.process({ @@ -30,41 +31,7 @@ def dest_dir(*files) end subject { SafeYAML.load(dest_dir("rendered.txt").read) } - { - "environment" => "dotcom", - "hostname" => "github.com", - "pages_env" => "dotcom", - "pages_hostname" => "github.io", - "help_url" => "https://help.github.com", - "api_url" => "https://api.github.com", - "versions" => {}, - "public_repositories" => Regexp.new('"id"=>17261694, "name"=>"atom-jekyll"'), - "organization_members" => Regexp.new('"login"=>"parkr", "id"=>237985'), - "build_revision" => %r![a-f0-9]{40}!, - "project_title" => "github-metadata", - "project_tagline" => ":octocat: `site.github`", - "owner_name" => "jekyll", - "owner_url" => "https://github.com/jekyll", - "owner_gravatar_url" => "https://github.com/jekyll.png", - "repository_url" => "https://github.com/jekyll/github-metadata", - "repository_nwo" => "jekyll/github-metadata", - "repository_name" => "github-metadata", - "zip_url" => "https://github.com/jekyll/github-metadata/zipball/gh-pages", - "tar_url" => "https://github.com/jekyll/github-metadata/tarball/gh-pages", - "clone_url" => "https://github.com/jekyll/github-metadata.git", - "releases_url" => "https://github.com/jekyll/github-metadata/releases", - "issues_url" => "https://github.com/jekyll/github-metadata/issues", - "wiki_url" => nil, # disabled - "language" => "Ruby", - "is_user_page" => false, - "is_project_page" => true, - "show_downloads" => true, - "url" => "http://jekyll.github.io/github-metadata", - "baseurl" => "/github-metadata", - "contributors" => %r!"login"=>"parkr", "id"=>237985!, - "releases" => %r!"tag_name"=>"v1.1.0"!, - "private" => false, - }.each do |key, value| + expected_values.each do |key, value| it "contains the correct #{key}" do expect(subject).to have_key(key) if value.is_a? Regexp diff --git a/spec/metadata_drop_spec.rb b/spec/metadata_drop_spec.rb index b9cc102..6c66f8d 100644 --- a/spec/metadata_drop_spec.rb +++ b/spec/metadata_drop_spec.rb @@ -5,10 +5,10 @@ let(:config) { Jekyll::Configuration.from(overrides) } let(:site) { Jekyll::Site.new config } subject { described_class.new(site) } + before { stub_all_api_requests } context "in Liquid" do before(:each) do - stub_all_api_requests site.config.delete("repository") site.config["github"] = subject end @@ -25,83 +25,27 @@ end end - context "with no repository set" do - before(:each) do - site.config.delete("repository") - ENV["PAGES_REPO_NWO"] = nil - end + context "payload" do + let!(:payload) { subject.to_h } - context "without a git nwo" do - it "raises a NoRepositoryError" do - allow(subject).to receive(:git_remote_url).and_return("") - expect(lambda do - subject.send(:nwo, site) - end).to raise_error(Jekyll::GitHubMetadata::NoRepositoryError) + expected_values.each do |key, value| + it "contains the #{key} key" do + expect(payload).to have_key(key) end - end - - it "retrieves the git remote" do - allow(subject).to receive(:git_remote_url).and_call_original - expect(subject.send(:git_remote_url)).to include("jekyll/github-metadata") - end - { - :https => "https://github.com/foo/bar", - :ssh => "git@github.com:foo/bar.git", - }.each do |type, url| - context "with a #{type} git URL" do - before(:each) do - site.config.delete("repository") - ENV["PAGES_REPO_NWO"] = nil - ENV.delete("JEKYLL_ENV") - end - after(:each) { ENV["JEKYLL_ENV"] = "test" } - - it "parses the name with owner from the git URL" do - allow(subject).to receive(:git_remote_url).and_return(url) - expect(subject.send(:nwo, site)).to eql("foo/bar") + it "contains the correct value for #{key}" do + if value.is_a? Regexp + expect(payload[key].to_s).to match value + else + expect(payload[key]).to eql value end end end - end - - context "with PAGES_REPO_NWO and site.repository set" do - before(:each) { ENV["PAGES_REPO_NWO"] = "jekyll/some-repo" } - - it "uses the value from PAGES_REPO_NWO" do - expect(subject.send(:nwo, site)).to eql("jekyll/some-repo") - end - end - - context "with only site.repository set" do - before(:each) { ENV["PAGES_REPO_NWO"] = nil } - - it "uses the value from site.repository" do - expect(subject.send(:nwo, site)).to eql("jekyll/another-repo") - end - end - context "when determining the nwo via git" do - before(:each) { ENV.delete("JEKYLL_ENV") } - after(:each) { ENV["JEKYLL_ENV"] = "test" } - - it "handles periods in repo names" do - allow(subject).to receive(:git_remote_url).and_return <<-EOS - origin https://github.com/afeld/hackerhours.org.git (fetch) - origin https://github.com/afeld/hackerhours.org.git (push) - EOS - expect(subject.send(:nwo_from_git_origin_remote)).to include("afeld/hackerhours.org") - end - - context "when git doesn't exist" do - before(:each) { @old_path = ENV.delete("PATH").to_s.split(File::PATH_SEPARATOR) } - after(:each) { ENV["PATH"] = @old_path.join(File::PATH_SEPARATOR) } - - it "fails with a nice error message" do - allow(subject).to receive(:git_remote_url).and_call_original - expect(subject.send(:git_exe_path)).to eql(nil) - expect(subject.send(:git_remote_url)).to be_empty - end + # Test to ensure we're testing all the values in the drop payload + # If this test fails, you likely need to update a value in spec_helper.rb + it "validates all values" do + expect(payload.keys).to match_array(expected_values.keys) end end end diff --git a/spec/repository_finder_spec.rb b/spec/repository_finder_spec.rb new file mode 100644 index 0000000..c14fd9f --- /dev/null +++ b/spec/repository_finder_spec.rb @@ -0,0 +1,88 @@ +require "spec_helper" + +RSpec.describe Jekyll::GitHubMetadata::RepositoryFinder do + let(:overrides) { { "repository" => "jekyll/another-repo" } } + let(:config) { Jekyll::Configuration.from(overrides) } + let(:site) { Jekyll::Site.new config } + subject { described_class.new(site) } + + context "with no repository set" do + before(:each) do + site.config.delete("repository") + ENV["PAGES_REPO_NWO"] = nil + end + + context "without a git nwo" do + it "raises a NoRepositoryError" do + allow(subject).to receive(:git_remote_url).and_return("") + expect(lambda do + subject.send(:nwo) + end).to raise_error(Jekyll::GitHubMetadata::NoRepositoryError) + end + end + + it "retrieves the git remote" do + allow(subject).to receive(:git_remote_url).and_call_original + expect(subject.send(:git_remote_url)).to include("jekyll/github-metadata") + end + + { + :https => "https://github.com/foo/bar", + :ssh => "git@github.com:foo/bar.git", + }.each do |type, url| + context "with a #{type} git URL" do + before(:each) do + site.config.delete("repository") + ENV["PAGES_REPO_NWO"] = nil + ENV.delete("JEKYLL_ENV") + end + after(:each) { ENV["JEKYLL_ENV"] = "test" } + + it "parses the name with owner from the git URL" do + allow(subject).to receive(:git_remote_url).and_return(url) + expect(subject.send(:nwo)).to eql("foo/bar") + end + end + end + end + + context "with PAGES_REPO_NWO and site.repository set" do + before(:each) { ENV["PAGES_REPO_NWO"] = "jekyll/some-repo" } + + it "uses the value from PAGES_REPO_NWO" do + expect(subject.send(:nwo)).to eql("jekyll/some-repo") + end + end + + context "with only site.repository set" do + before(:each) { ENV["PAGES_REPO_NWO"] = nil } + + it "uses the value from site.repository" do + expect(subject.send(:nwo)).to eql("jekyll/another-repo") + end + end + + context "when determining the nwo via git" do + before(:each) { ENV.delete("JEKYLL_ENV") } + after(:each) { ENV["JEKYLL_ENV"] = "test" } + + it "handles periods in repo names" do + allow(subject).to receive(:git_remote_url).and_return <<-EOS + origin https://github.com/afeld/hackerhours.org.git (fetch) + origin https://github.com/afeld/hackerhours.org.git (push) + EOS + expect(subject.send(:nwo_from_git_origin_remote)).to include("afeld/hackerhours.org") + end + + context "when git doesn't exist" do + before(:each) { @old_path = ENV.delete("PATH").to_s.split(File::PATH_SEPARATOR) } + after(:each) { ENV["PATH"] = @old_path.join(File::PATH_SEPARATOR) } + + it "fails with a nice error message" do + allow(subject).to receive(:git_remote_url).and_call_original + expect(subject.send(:git_exe_path)).to eql(nil) + expect(subject.send(:git_remote_url)).to be_empty + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 88a9176..6b12e12 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ require "jekyll-github-metadata" require "webmock/rspec" require "pathname" +require "jekyll" SPEC_DIR = Pathname.new(File.expand_path("../", __FILE__)) @@ -112,6 +113,45 @@ def env_args_to_hash(*args) end end +def expected_values + { + "environment" => "dotcom", + "hostname" => "github.com", + "pages_env" => "dotcom", + "pages_hostname" => "github.io", + "help_url" => "https://help.github.com", + "api_url" => "https://api.github.com", + "versions" => {}, + "public_repositories" => Regexp.new('"id"=>17261694, "name"=>"atom-jekyll"'), + "organization_members" => Regexp.new('"login"=>"parkr", "id"=>237985'), + "build_revision" => %r![a-f0-9]{40}!, + "project_title" => "github-metadata", + "project_tagline" => ":octocat: `site.github`", + "owner_name" => "jekyll", + "owner_url" => "https://github.com/jekyll", + "owner_gravatar_url" => "https://github.com/jekyll.png", + "repository_url" => "https://github.com/jekyll/github-metadata", + "repository_nwo" => "jekyll/github-metadata", + "repository_name" => "github-metadata", + "zip_url" => "https://github.com/jekyll/github-metadata/zipball/gh-pages", + "tar_url" => "https://github.com/jekyll/github-metadata/tarball/gh-pages", + "clone_url" => "https://github.com/jekyll/github-metadata.git", + "releases_url" => "https://github.com/jekyll/github-metadata/releases", + "issues_url" => "https://github.com/jekyll/github-metadata/issues", + "wiki_url" => nil, # disabled + "language" => "Ruby", + "is_user_page" => false, + "is_project_page" => true, + "show_downloads" => true, + "url" => "http://jekyll.github.io/github-metadata", + "baseurl" => "/github-metadata", + "contributors" => %r!"login"=>"parkr", "id"=>237985!, + "releases" => %r!"tag_name"=>"v1.1.0"!, + "latest_release" => %r!assets_url!, + "private" => false, + } +end + RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true diff --git a/spec/test-site/index.html b/spec/test-site/index.html index bc36926..41a95f5 100644 --- a/spec/test-site/index.html +++ b/spec/test-site/index.html @@ -9,7 +9,7 @@
- +