This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "jekyll"
|
||||
require "octokit"
|
||||
|
||||
if Jekyll.env == "development"
|
||||
begin
|
||||
require "dotenv"
|
||||
Dotenv.load
|
||||
rescue LoadError
|
||||
Jekyll.logger.debug "Dotenv not found. Skipping"
|
||||
end
|
||||
end
|
||||
|
||||
module Jekyll
|
||||
module GitHubMetadata
|
||||
autoload :Client, "jekyll-github-metadata/client"
|
||||
autoload :EditLinkTag, "jekyll-github-metadata/edit-link-tag"
|
||||
autoload :MetadataDrop, "jekyll-github-metadata/metadata_drop"
|
||||
autoload :Owner, "jekyll-github-metadata/owner"
|
||||
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
|
||||
|
||||
require_relative "jekyll-github-metadata/site_github_munger" if Jekyll.const_defined? :Site
|
||||
|
||||
class << self
|
||||
attr_reader :repository_finder
|
||||
attr_writer :client, :logger
|
||||
|
||||
def site
|
||||
repository_finder.site
|
||||
end
|
||||
|
||||
def environment
|
||||
Jekyll.env
|
||||
end
|
||||
|
||||
def logger
|
||||
@logger ||= Jekyll.logger
|
||||
end
|
||||
|
||||
def log(severity, message)
|
||||
if logger.method(severity).arity.abs >= 2
|
||||
logger.public_send(severity, "GitHub Metadata:", message.to_s)
|
||||
else
|
||||
logger.public_send(severity, "GitHub Metadata: #{message}")
|
||||
end
|
||||
end
|
||||
|
||||
def client
|
||||
@client ||= Client.new
|
||||
end
|
||||
|
||||
def repository
|
||||
@repository ||= GitHubMetadata::Repository.new(repository_finder.nwo).tap do |repo|
|
||||
log :debug, "Generating for #{repo.nwo}"
|
||||
end
|
||||
end
|
||||
|
||||
def site=(new_site)
|
||||
reset!
|
||||
@repository_finder = GitHubMetadata::RepositoryFinder.new(new_site)
|
||||
end
|
||||
|
||||
def reset!
|
||||
@logger = @client = @repository = @nwo = @site = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Liquid::Template.register_tag("github_edit_link", Jekyll::GitHubMetadata::EditLinkTag)
|
||||
@@ -0,0 +1,158 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "digest"
|
||||
|
||||
module Jekyll
|
||||
module GitHubMetadata
|
||||
class Client
|
||||
InvalidMethodError = Class.new(NoMethodError)
|
||||
BadCredentialsError = Class.new(StandardError)
|
||||
|
||||
# Whitelisted API calls.
|
||||
API_CALLS = Set.new(%w(
|
||||
repository
|
||||
organization
|
||||
user
|
||||
repository?
|
||||
pages
|
||||
contributors
|
||||
releases
|
||||
latest_release
|
||||
list_repos
|
||||
organization_public_members
|
||||
))
|
||||
|
||||
def initialize(options = nil)
|
||||
@client = build_octokit_client(options)
|
||||
end
|
||||
|
||||
def safe_require(gem_name)
|
||||
require gem_name
|
||||
true
|
||||
rescue LoadError
|
||||
false
|
||||
end
|
||||
|
||||
def default_octokit_options
|
||||
{
|
||||
:api_endpoint => Jekyll::GitHubMetadata::Pages.api_url,
|
||||
:web_endpoint => Jekyll::GitHubMetadata::Pages.github_url,
|
||||
:auto_paginate => true,
|
||||
}
|
||||
end
|
||||
|
||||
def build_octokit_client(options = nil)
|
||||
options ||= {}
|
||||
options.merge!(pluck_auth_method) unless options.key?(:access_token)
|
||||
Octokit::Client.new(default_octokit_options.merge(options))
|
||||
end
|
||||
|
||||
def accepts_client_method?(method_name)
|
||||
API_CALLS.include?(method_name.to_s) && @client.respond_to?(method_name)
|
||||
end
|
||||
|
||||
def respond_to_missing?(method_name, include_private = false)
|
||||
accepts_client_method?(method_name) || super
|
||||
end
|
||||
|
||||
def method_missing(method_name, *args, &block)
|
||||
method = method_name.to_s
|
||||
if accepts_client_method?(method_name)
|
||||
key = cache_key(method_name, args)
|
||||
GitHubMetadata.log :debug, "Calling @client.#{method}(#{args.map(&:inspect).join(", ")})"
|
||||
cache[key] ||= save_from_errors { @client.public_send(method_name, *args, &block) }
|
||||
elsif @client.respond_to?(method_name)
|
||||
raise InvalidMethodError, "#{method_name} is not whitelisted on #{inspect}"
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# NOTE: Faraday's error classes have been simplified from v1.0.0.
|
||||
#
|
||||
# In older versions, both `Faraday::Error::ConnectionFailed` and `Faraday::ConnectionFailed`
|
||||
# were valid and equivalent to each other.
|
||||
# From v1.0.0 onwards, `Faraday::Error::ConnectionFailed` no longer exists.
|
||||
#
|
||||
# TODO: Remove in v2.0 of this plugin.
|
||||
FARADAY_FAILED_CONNECTION =
|
||||
begin
|
||||
Faraday::Error::ConnectionFailed
|
||||
rescue NameError
|
||||
Faraday::ConnectionFailed
|
||||
end
|
||||
deprecate_constant :FARADAY_FAILED_CONNECTION
|
||||
|
||||
def save_from_errors(default = false)
|
||||
unless internet_connected?
|
||||
GitHubMetadata.log :warn, "No internet connection. GitHub metadata may be missing or incorrect."
|
||||
return default
|
||||
end
|
||||
|
||||
yield @client
|
||||
rescue Octokit::Unauthorized
|
||||
raise BadCredentialsError, "The GitHub API credentials you provided aren't valid."
|
||||
rescue Faraday::ConnectionFailed, Octokit::TooManyRequests => e
|
||||
GitHubMetadata.log :warn, e.message
|
||||
default
|
||||
rescue Octokit::NotFound
|
||||
default
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class.name} @client=#{client_inspect} @internet_connected=#{internet_connected?}>"
|
||||
end
|
||||
|
||||
def authenticated?
|
||||
!@client.access_token.to_s.empty?
|
||||
end
|
||||
|
||||
def internet_connected?
|
||||
return @internet_connected if defined?(@internet_connected)
|
||||
|
||||
require "resolv"
|
||||
begin
|
||||
Resolv::DNS.open do |dns|
|
||||
dns.timeouts = 2
|
||||
dns.getaddress("api.github.com")
|
||||
end
|
||||
@internet_connected = true
|
||||
rescue Resolv::ResolvError
|
||||
@internet_connected = false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def client_inspect
|
||||
if @client.nil?
|
||||
"nil"
|
||||
else
|
||||
"#<#{@client.class.name} (#{"un" unless authenticated?}authenticated)>"
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def pluck_auth_method
|
||||
if ENV["JEKYLL_GITHUB_TOKEN"] || Octokit.access_token
|
||||
{ :access_token => ENV["JEKYLL_GITHUB_TOKEN"] || Octokit.access_token }
|
||||
elsif !ENV["NO_NETRC"] && File.exist?(File.join(Dir.home, ".netrc")) && safe_require("netrc")
|
||||
{ :netrc => true }
|
||||
else
|
||||
GitHubMetadata.log :warn, "No GitHub API authentication could be found. " \
|
||||
"Some fields may be missing or have incorrect data."
|
||||
{}.freeze
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
def cache_key(method, *args)
|
||||
Digest::SHA1.hexdigest(method.to_s + args.join(", "))
|
||||
end
|
||||
|
||||
def cache
|
||||
@cache ||= {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,125 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jekyll
|
||||
module GitHubMetadata
|
||||
class EditLinkTag < Liquid::Tag
|
||||
attr_reader :context
|
||||
|
||||
# Defines an instance method that delegates to a hash's key
|
||||
#
|
||||
# hash_method - a symbol representing the instance method to delegate to. The
|
||||
# instance method should return a hash or respond to #[]
|
||||
# key - the key to call within the hash
|
||||
# method - (optional) the instance method the key should be aliased to.
|
||||
# If not specified, defaults to the hash key
|
||||
# default - (optional) value to return if value is nil (defaults to nil)
|
||||
#
|
||||
# Returns a symbol representing the instance method
|
||||
def self.def_hash_delegator(hash_method, key, method, default = nil)
|
||||
define_method(method) do
|
||||
hash = send(hash_method)
|
||||
if hash.respond_to? :[]
|
||||
hash[key.to_s] || default
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
MISSING_DATA_MSG = "Cannot generate edit URLs due to missing site.github data"
|
||||
LINK_TEXT_REGEX = %r!(?:\"(.*)\"|'(.*)')!.freeze
|
||||
|
||||
extend Forwardable
|
||||
private def_hash_delegator :site, :github, :site_github, {}
|
||||
private def_hash_delegator :site_github, :repository_url, :repository_url
|
||||
private def_hash_delegator :site_github, :source, :source, {}
|
||||
private def_hash_delegator :source, :branch, :branch
|
||||
private def_hash_delegator :source, :path, :source_path
|
||||
private def_hash_delegator :page, :path, :page_path
|
||||
|
||||
def render(context)
|
||||
@context = context
|
||||
if link_text
|
||||
link
|
||||
else
|
||||
uri.to_s
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def link_text
|
||||
@link_text ||= begin
|
||||
matches = @markup.match LINK_TEXT_REGEX
|
||||
matches[1] || matches[2] if matches
|
||||
end
|
||||
end
|
||||
|
||||
def link
|
||||
"<a href=\"#{uri}\">#{link_text}</a>"
|
||||
end
|
||||
|
||||
def uri
|
||||
if parts.any?(&:nil?)
|
||||
Jekyll.logger.warn "JekyllEditLink: ", MISSING_DATA_MSG
|
||||
""
|
||||
else
|
||||
Addressable::URI.join(*parts_normalized).normalize
|
||||
end
|
||||
end
|
||||
|
||||
def parts
|
||||
memoize_conditionally { [repository_url, "edit/", branch, source_path, page_path] }
|
||||
end
|
||||
|
||||
def parts_normalized
|
||||
memoize_conditionally do
|
||||
parts.map.with_index do |part, index|
|
||||
part = remove_leading_slash(part.to_s)
|
||||
part = ensure_trailing_slash(part) unless index == parts.length - 1
|
||||
ensure_not_just_a_slash(part)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def page
|
||||
memoize_conditionally { context.registers[:page] }
|
||||
end
|
||||
|
||||
# Utility function for compatibility with Jekyll 4.0
|
||||
def memoize_conditionally
|
||||
if renderer_cached?
|
||||
yield
|
||||
else
|
||||
dispatcher = "@#{caller_locations(1..1).first.label}".to_sym
|
||||
if instance_variable_defined?(dispatcher)
|
||||
instance_variable_get(dispatcher)
|
||||
else
|
||||
instance_variable_set(dispatcher, yield)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Utility function to detect Jekyll 4.0
|
||||
def renderer_cached?
|
||||
@renderer_cached ||= context.registers[:site].liquid_renderer.respond_to?(:cache)
|
||||
end
|
||||
|
||||
def site
|
||||
@site ||= context.registers[:site].site_payload["site"]
|
||||
end
|
||||
|
||||
def remove_leading_slash(part)
|
||||
part.start_with?("/") ? part[1..-1] : part
|
||||
end
|
||||
|
||||
def ensure_trailing_slash(part)
|
||||
part.end_with?("/") ? part : "#{part}/"
|
||||
end
|
||||
|
||||
def ensure_not_just_a_slash(part)
|
||||
part == "/" ? "" : part
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,107 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "jekyll"
|
||||
require "forwardable"
|
||||
|
||||
module Jekyll
|
||||
module GitHubMetadata
|
||||
class MetadataDrop < Jekyll::Drops::Drop
|
||||
extend Forwardable
|
||||
|
||||
mutable true
|
||||
|
||||
# See https://github.com/jekyll/jekyll/pull/6338
|
||||
alias_method :invoke_drop, :[]
|
||||
def key?(key)
|
||||
return false if key.nil?
|
||||
return true if self.class.mutable? && mutations.key?(key)
|
||||
|
||||
respond_to?(key) || fallback_data.key?(key)
|
||||
end
|
||||
|
||||
def to_s
|
||||
require "json"
|
||||
JSON.pretty_generate to_h
|
||||
end
|
||||
alias_method :to_str, :to_s
|
||||
|
||||
def content_methods
|
||||
super - %w(to_s to_str)
|
||||
end
|
||||
|
||||
def keys
|
||||
super.sort
|
||||
end
|
||||
|
||||
def_delegator Jekyll::GitHubMetadata::Pages, :env, :environment
|
||||
def_delegator Jekyll::GitHubMetadata::Pages, :env, :pages_env
|
||||
def_delegator Jekyll::GitHubMetadata::Pages, :github_hostname, :hostname
|
||||
def_delegator Jekyll::GitHubMetadata::Pages, :pages_hostname, :pages_hostname
|
||||
def_delegator Jekyll::GitHubMetadata::Pages, :api_url, :api_url
|
||||
def_delegator Jekyll::GitHubMetadata::Pages, :help_url, :help_url
|
||||
|
||||
private def_delegator Jekyll::GitHubMetadata, :repository
|
||||
|
||||
def_delegator :repository, :owner_public_repositories, :public_repositories
|
||||
def_delegator :repository, :organization_public_members, :organization_members
|
||||
def_delegator :repository, :name, :project_title
|
||||
def_delegator :repository, :tagline, :project_tagline
|
||||
def_delegator :repository, :owner_metadata, :owner
|
||||
def_delegator :repository, :owner, :owner_name
|
||||
def_delegator :repository, :owner_display_name, :owner_display_name
|
||||
def_delegator :repository, :owner_url, :owner_url
|
||||
def_delegator :repository, :owner_gravatar_url, :owner_gravatar_url
|
||||
def_delegator :repository, :repository_url, :repository_url
|
||||
def_delegator :repository, :nwo, :repository_nwo
|
||||
def_delegator :repository, :name, :repository_name
|
||||
def_delegator :repository, :zip_url, :zip_url
|
||||
def_delegator :repository, :tar_url, :tar_url
|
||||
def_delegator :repository, :repo_clone_url, :clone_url
|
||||
def_delegator :repository, :releases_url, :releases_url
|
||||
def_delegator :repository, :issues_url, :issues_url
|
||||
def_delegator :repository, :wiki_url, :wiki_url
|
||||
def_delegator :repository, :language, :language
|
||||
def_delegator :repository, :user_page?, :is_user_page
|
||||
def_delegator :repository, :project_page?, :is_project_page
|
||||
def_delegator :repository, :show_downloads?, :show_downloads
|
||||
def_delegator :repository, :html_url, :url
|
||||
def_delegator :repository, :baseurl, :baseurl
|
||||
def_delegator :repository, :contributors, :contributors
|
||||
def_delegator :repository, :releases, :releases
|
||||
def_delegator :repository, :latest_release, :latest_release
|
||||
def_delegator :repository, :private?, :private
|
||||
def_delegator :repository, :archived?, :archived
|
||||
def_delegator :repository, :disabled?, :disabled
|
||||
def_delegator :repository, :license, :license
|
||||
def_delegator :repository, :source, :source
|
||||
|
||||
def versions
|
||||
return @versions if defined?(@versions)
|
||||
|
||||
begin
|
||||
require "github-pages"
|
||||
@versions = GitHubPages.versions
|
||||
rescue LoadError
|
||||
@versions = {}
|
||||
end
|
||||
end
|
||||
|
||||
def build_revision
|
||||
@build_revision ||= begin
|
||||
ENV["JEKYLL_BUILD_REVISION"] || `git rev-parse HEAD`.strip
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Nothing to see here.
|
||||
def fallback_data
|
||||
@fallback_data ||= {}
|
||||
end
|
||||
|
||||
def mutations
|
||||
@mutations ||= {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,90 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jekyll
|
||||
module GitHubMetadata
|
||||
class Owner
|
||||
extend Forwardable
|
||||
|
||||
# Defines an instance method that delegates to a hash's key
|
||||
#
|
||||
# hash - a symbol representing the instance method to delegate to. The
|
||||
# instance method should return a hash or respond to #[]
|
||||
# key - the key to call within the hash
|
||||
# method - (optional) the instance method the key should be aliased to.
|
||||
# If not specified, defaults to the hash key
|
||||
#
|
||||
# Returns a symbol representing the instance method
|
||||
def self.def_hash_delegators(hash, *methods)
|
||||
content_methods.concat(methods)
|
||||
methods.each do |method|
|
||||
define_method(method) do
|
||||
send(hash)[method.to_s]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.content_methods
|
||||
@content_methods ||= []
|
||||
end
|
||||
|
||||
# List of whitelisted keys.
|
||||
def_hash_delegators :owner_info,
|
||||
:avatar_url,
|
||||
:bio,
|
||||
:blog,
|
||||
:collaborators,
|
||||
:company,
|
||||
:created_at,
|
||||
:description,
|
||||
:email,
|
||||
:followers,
|
||||
:following,
|
||||
:has_organization_projects,
|
||||
:has_repository_projects,
|
||||
:hireable,
|
||||
:html_url,
|
||||
:id,
|
||||
:is_verified,
|
||||
:location,
|
||||
:login,
|
||||
:name,
|
||||
:node_id,
|
||||
:public_gists,
|
||||
:public_gists,
|
||||
:public_repos,
|
||||
:public_repos,
|
||||
:type,
|
||||
:updated_at
|
||||
|
||||
attr_reader :owner_login
|
||||
|
||||
def initialize(owner_login)
|
||||
@owner_login = owner_login
|
||||
end
|
||||
|
||||
def to_h
|
||||
@to_h ||= self.class.content_methods
|
||||
.each_with_object({}) { |method, hash| hash[method.to_s] = public_send(method) }
|
||||
end
|
||||
alias_method :to_hash, :to_h
|
||||
def_delegator :to_h, :to_json, :to_json
|
||||
def_delegator :to_h, :to_liquid, :to_liquid
|
||||
|
||||
def_delegator :to_h, :to_s, :to_s
|
||||
alias_method :to_str, :to_s
|
||||
|
||||
private
|
||||
|
||||
def owner_info
|
||||
@owner_info ||= begin
|
||||
Value.new(
|
||||
"owner",
|
||||
proc do |c|
|
||||
(c.organization(owner_login) || c.user(owner_login) || {}).to_h
|
||||
end
|
||||
).render || {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,115 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jekyll
|
||||
module GitHubMetadata
|
||||
class Pages
|
||||
class << self
|
||||
DEFAULTS = {
|
||||
"PAGES_ENV" => "development",
|
||||
"PAGES_API_URL" => "https://api.github.com",
|
||||
"PAGES_HELP_URL" => "https://docs.github.com",
|
||||
"PAGES_GITHUB_HOSTNAME" => "github.com",
|
||||
"PAGES_PAGES_HOSTNAME" => "github.io",
|
||||
"SSL" => "false",
|
||||
"SUBDOMAIN_ISOLATION" => "false",
|
||||
"PAGES_PREVIEW_HTML_URL" => nil,
|
||||
"PAGE_BUILD_ID" => nil,
|
||||
}.freeze
|
||||
|
||||
# Whether the GitHub instance supports HTTPS
|
||||
# Note: this will be the same as how sites are served in Enterprise,
|
||||
# but may be different from how sites are served on GitHub.com.
|
||||
# See Repository#url_scheme
|
||||
def ssl?
|
||||
env_var("SSL") == "true" || test?
|
||||
end
|
||||
|
||||
def scheme
|
||||
ssl? ? "https" : "http"
|
||||
end
|
||||
|
||||
def subdomain_isolation?
|
||||
env_var("SUBDOMAIN_ISOLATION").eql? "true"
|
||||
end
|
||||
|
||||
def test?
|
||||
env == "test"
|
||||
end
|
||||
|
||||
def dotcom?
|
||||
env == "dotcom"
|
||||
end
|
||||
|
||||
def enterprise?
|
||||
env == "enterprise"
|
||||
end
|
||||
|
||||
def development?
|
||||
env == "development"
|
||||
end
|
||||
|
||||
def custom_domains_enabled?
|
||||
dotcom? || test?
|
||||
end
|
||||
|
||||
def env
|
||||
env_var "PAGES_ENV", ENV["JEKYLL_ENV"]
|
||||
end
|
||||
|
||||
def repo_pages_html_url_preview?
|
||||
env_var "PAGES_PREVIEW_HTML_URL"
|
||||
end
|
||||
|
||||
def github_url
|
||||
if dotcom? || github_hostname == "github.com"
|
||||
"https://github.com"
|
||||
else
|
||||
"#{scheme}://#{github_hostname}"
|
||||
end
|
||||
end
|
||||
|
||||
def api_url
|
||||
trim_last_slash env_var("PAGES_API_URL", ENV["API_URL"])
|
||||
end
|
||||
|
||||
def help_url
|
||||
trim_last_slash env_var("PAGES_HELP_URL", ENV["HELP_URL"])
|
||||
end
|
||||
|
||||
def github_hostname
|
||||
trim_last_slash env_var("PAGES_GITHUB_HOSTNAME", ENV["GITHUB_HOSTNAME"])
|
||||
end
|
||||
|
||||
def pages_hostname
|
||||
intermediate_default = ENV["PAGES_HOSTNAME"]
|
||||
intermediate_default ||= "localhost:4000" if development?
|
||||
trim_last_slash env_var("PAGES_PAGES_HOSTNAME", intermediate_default)
|
||||
end
|
||||
|
||||
def page_build?
|
||||
!env_var("PAGE_BUILD_ID").to_s.empty?
|
||||
end
|
||||
|
||||
def configuration
|
||||
(methods - Object.methods - [:configuration]).sort.each_with_object({}) do |meth, memo|
|
||||
memo[meth.to_s] = public_send(meth)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def env_var(key, intermediate_default = nil)
|
||||
!ENV[key].to_s.empty? ? ENV[key] : (intermediate_default || DEFAULTS[key])
|
||||
end
|
||||
|
||||
def trim_last_slash(url)
|
||||
if url[-1] == "/"
|
||||
url[0..-2]
|
||||
else
|
||||
url
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,220 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jekyll
|
||||
module GitHubMetadata
|
||||
class Repository
|
||||
attr_reader :nwo, :owner, :name
|
||||
|
||||
# Defines an instance method that delegates to a hash's key
|
||||
#
|
||||
# hash - a symbol representing the instance method to delegate to. The
|
||||
# instance method should return a hash or respond to #[]
|
||||
# key - the key to call within the hash
|
||||
# method - (optional) the instance method the key should be aliased to.
|
||||
# If not specified, defaults to the hash key
|
||||
#
|
||||
# Returns a symbol representing the instance method
|
||||
def self.def_hash_delegator(hash, key, method)
|
||||
define_method(method) do
|
||||
public_send(hash)[key.to_s]
|
||||
end
|
||||
end
|
||||
|
||||
extend Forwardable
|
||||
def_hash_delegator :repo_info, :license, :license
|
||||
def_hash_delegator :repo_info, :language, :language
|
||||
def_hash_delegator :repo_info, :description, :tagline
|
||||
def_hash_delegator :repo_info, :has_downloads, :show_downloads?
|
||||
def_hash_delegator :repo_info, :private, :private?
|
||||
def_hash_delegator :repo_info, :archived, :archived?
|
||||
def_hash_delegator :repo_info, :disabled, :disabled?
|
||||
def_hash_delegator :latest_release, :url, :latest_release_url
|
||||
def_hash_delegator :source, :branch, :git_ref
|
||||
|
||||
def_delegator :uri, :host, :domain
|
||||
def_delegator :uri, :scheme, :url_scheme
|
||||
def_delegator :uri, :path, :baseurl
|
||||
|
||||
def initialize(name_with_owner)
|
||||
@nwo = name_with_owner
|
||||
@owner = nwo.split("/").first
|
||||
@name = nwo.split("/").last
|
||||
end
|
||||
|
||||
def repo_compat
|
||||
@repo_compat ||= Jekyll::GitHubMetadata::RepositoryCompat.new(self)
|
||||
end
|
||||
|
||||
def repo_info
|
||||
@repo_info ||= begin
|
||||
options = { :accept => "application/vnd.github.drax-preview+json" }
|
||||
(Value.new("repo_info", proc { |c| c.repository(nwo, options) }).render || {})
|
||||
end
|
||||
end
|
||||
|
||||
def repo_pages_info
|
||||
@repo_pages_info ||= (Value.new("repo_pages_info", proc { |c| c.pages(nwo, repo_pages_info_opts) }).render || {})
|
||||
end
|
||||
|
||||
def repo_pages_info_opts
|
||||
if Pages.repo_pages_html_url_preview?
|
||||
{ :accept => "application/vnd.github.mister-fantastic-preview+json" }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def owner_display_name
|
||||
owner_metadata.name
|
||||
end
|
||||
|
||||
def owner_metadata
|
||||
@owner_metadata ||= Jekyll::GitHubMetadata::Owner.new(owner)
|
||||
end
|
||||
|
||||
def owner_url
|
||||
"#{Pages.github_url}/#{owner}"
|
||||
end
|
||||
|
||||
def owner_gravatar_url
|
||||
"#{owner_url}.png"
|
||||
end
|
||||
|
||||
def repo_clone_url
|
||||
"#{repository_url}.git"
|
||||
end
|
||||
|
||||
def repository_url
|
||||
"#{owner_url}/#{name}"
|
||||
end
|
||||
|
||||
def zip_url
|
||||
"#{repository_url}/zipball/#{git_ref}"
|
||||
end
|
||||
|
||||
def tar_url
|
||||
"#{repository_url}/tarball/#{git_ref}"
|
||||
end
|
||||
|
||||
def releases_url
|
||||
"#{repository_url}/releases"
|
||||
end
|
||||
|
||||
def issues_url
|
||||
"#{repository_url}/issues" if repo_info["has_issues"]
|
||||
end
|
||||
|
||||
def wiki_url
|
||||
"#{repository_url}/wiki" if repo_info["has_wiki"]
|
||||
end
|
||||
|
||||
def organization_repository?
|
||||
memoize_value :@is_organization_repository, Value.new("organization_repository?", proc { |c| !!c.organization(owner) })
|
||||
end
|
||||
|
||||
def owner_public_repositories
|
||||
options = {
|
||||
:type => "public",
|
||||
:accept => "application/vnd.github.mercy-preview+json",
|
||||
}
|
||||
memoize_value :@owner_public_repositories, Value.new("owner_public_repositories", proc do |c|
|
||||
repos = c.list_repos(owner, options) || []
|
||||
repos.map do |r|
|
||||
r[:releases] = Value.new("owner_public_repositories_releases", proc { c.releases(r[:full_name]) })
|
||||
r[:contributors] = Value.new("owner_public_repositories_contributors", proc { c.contributors(r[:full_name]) })
|
||||
r
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def organization_public_members
|
||||
memoize_value :@organization_public_members, Value.new("organization_public_members", proc do |c|
|
||||
c.organization_public_members(owner) if organization_repository?
|
||||
end)
|
||||
end
|
||||
|
||||
def contributors
|
||||
memoize_value :@contributors, Value.new("contributors", proc { |c| c.contributors(nwo) })
|
||||
end
|
||||
|
||||
def releases
|
||||
memoize_value :@releases, Value.new("releases", proc { |c| c.releases(nwo) })
|
||||
end
|
||||
|
||||
def latest_release
|
||||
memoize_value :@latest_release, Value.new("latest_release", proc { |c| c.latest_release(nwo) })
|
||||
end
|
||||
|
||||
def source
|
||||
repo_pages_info["source"] || repo_compat.source
|
||||
end
|
||||
|
||||
def project_page?
|
||||
!user_page?
|
||||
end
|
||||
|
||||
def github_repo?
|
||||
!Pages.enterprise? && owner.eql?("github")
|
||||
end
|
||||
|
||||
def primary?
|
||||
if Pages.enterprise?
|
||||
name.downcase == "#{owner.to_s.downcase}.#{Pages.github_hostname}"
|
||||
else
|
||||
user_page_domains.include? name.downcase
|
||||
end
|
||||
end
|
||||
alias_method :user_page?, :primary?
|
||||
|
||||
def user_page_domains
|
||||
domains = [default_user_domain]
|
||||
domains.push "#{owner}.github.com".downcase unless Pages.enterprise?
|
||||
domains
|
||||
end
|
||||
|
||||
def default_user_domain
|
||||
if github_repo?
|
||||
"#{owner}.#{Pages.github_hostname}".downcase
|
||||
elsif Pages.enterprise?
|
||||
Pages.pages_hostname.downcase
|
||||
else
|
||||
"#{owner}.#{Pages.pages_hostname}".downcase
|
||||
end
|
||||
end
|
||||
|
||||
def cname
|
||||
return nil unless Pages.custom_domains_enabled?
|
||||
|
||||
repo_pages_info["cname"]
|
||||
end
|
||||
|
||||
def html_url
|
||||
@html_url ||= (repo_pages_info["html_url"] || repo_compat.pages_url).chomp("/")
|
||||
end
|
||||
|
||||
def uri
|
||||
@uri ||= URI(html_url)
|
||||
end
|
||||
|
||||
def url_without_path
|
||||
uri.dup.tap { |u| u.path = "" }.to_s
|
||||
end
|
||||
|
||||
def stargazers_count
|
||||
repo_pages_info["stargazers_count"] || 0
|
||||
end
|
||||
|
||||
def forks_count
|
||||
repo_pages_info["forks_count"] || 0
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def memoize_value(var_name, value)
|
||||
return instance_variable_get(var_name) if instance_variable_defined?(var_name)
|
||||
|
||||
instance_variable_set(var_name, value.render)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jekyll
|
||||
module GitHubMetadata
|
||||
class RepositoryCompat
|
||||
attr_reader :repo
|
||||
|
||||
def initialize(repo)
|
||||
@repo = repo
|
||||
end
|
||||
|
||||
# In enterprise, the site's scheme will be the same as the instance's
|
||||
# In dotcom, this will be `https` for GitHub-owned sites that end with
|
||||
# `.github.com` and will be `http` for all other sites.
|
||||
# Note: This is not the same as *instance*'s scheme, which may differ
|
||||
def url_scheme
|
||||
if Pages.enterprise?
|
||||
Pages.scheme
|
||||
elsif repo.owner == "github" && domain.end_with?(".github.com")
|
||||
"https"
|
||||
else
|
||||
"http"
|
||||
end
|
||||
end
|
||||
|
||||
def user_domain
|
||||
domain = repo.default_user_domain
|
||||
repo.user_page_domains.each do |user_repo|
|
||||
candidate_nwo = "#{repo.owner}/#{user_repo}"
|
||||
next unless Jekyll::GitHubMetadata.client.repository?(candidate_nwo)
|
||||
|
||||
domain = Jekyll::GitHubMetadata::Repository.new(candidate_nwo).repo_compat.domain
|
||||
end
|
||||
domain
|
||||
end
|
||||
|
||||
def pages_url
|
||||
return enterprise_url unless Pages.custom_domains_enabled?
|
||||
|
||||
if repo.cname || repo.primary?
|
||||
"#{url_scheme}://#{domain}"
|
||||
else
|
||||
URI.join("#{url_scheme}://#{domain}", repo.name).to_s
|
||||
end
|
||||
end
|
||||
alias_method :html_url, :pages_url
|
||||
|
||||
def domain
|
||||
@domain ||=
|
||||
if !Pages.custom_domains_enabled?
|
||||
Pages.github_hostname
|
||||
elsif repo.cname # explicit CNAME
|
||||
repo.cname
|
||||
elsif repo.primary? # user/org repo
|
||||
repo.default_user_domain
|
||||
else # project repo
|
||||
user_domain
|
||||
end
|
||||
end
|
||||
|
||||
def source
|
||||
{
|
||||
"branch" => (repo.user_page? ? "master" : "gh-pages"),
|
||||
"path" => "/",
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def enterprise_url
|
||||
path = repo.user_page? ? repo.owner : repo.nwo
|
||||
if Pages.subdomain_isolation?
|
||||
URI.join("#{Pages.scheme}://#{Pages.pages_hostname}/", path).to_s
|
||||
else
|
||||
URI.join("#{Pages.github_url}/pages/", path).to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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&.is_a?(String) && repo&.include?("/")
|
||||
end
|
||||
|
||||
def git_remotes
|
||||
_process, output = Jekyll::Utils::Exec.run("git", "remote", "--verbose")
|
||||
output.to_s.strip.split("\n")
|
||||
rescue Errno::ENOENT => e
|
||||
Jekyll.logger.warn "Not Installed:", e.message
|
||||
[]
|
||||
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 github_remote_regex
|
||||
matches[2..3].join("/") if matches
|
||||
end
|
||||
|
||||
def github_remote_regex
|
||||
github_host_regex = Regexp.escape(Jekyll::GitHubMetadata::Pages.github_hostname)
|
||||
%r!#{github_host_regex}(:|/)([\w-]+)/([\w\.-]+)!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jekyll
|
||||
module GitHubMetadata
|
||||
module Sanitizer
|
||||
extend self
|
||||
# Sanitize an object.
|
||||
# When the resource is either `false`, `true`, `nil` or a number,
|
||||
# it returns the resource as-is. When the resource is an array,
|
||||
# it maps over the entire array, sanitizing each of its values.
|
||||
# When the resource responds to the #to_hash method, it returns
|
||||
# the value of #sanitize_resource (see below). If none of the
|
||||
# aforementioned conditions are met, the return value of #to_s
|
||||
# is used.
|
||||
#
|
||||
# resource - an Object
|
||||
#
|
||||
# Returns the sanitized resource.
|
||||
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
||||
def sanitize(resource)
|
||||
case resource
|
||||
when Array
|
||||
resource.map { |item| sanitize(item) }
|
||||
when Numeric, Time
|
||||
resource
|
||||
when FalseClass
|
||||
false
|
||||
when TrueClass
|
||||
true
|
||||
when NilClass
|
||||
nil
|
||||
when String
|
||||
resource
|
||||
when Value
|
||||
resource.render
|
||||
else
|
||||
if resource.respond_to?(:to_hash)
|
||||
sanitize_resource(resource)
|
||||
else
|
||||
resource.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
||||
|
||||
# Sanitize the Sawyer Resource or Hash
|
||||
# Note: the object must respond to :to_hash for this to work.
|
||||
# Consider using #sanitize instead of this method directly.
|
||||
#
|
||||
# resource - an Object which responds to :to_hash
|
||||
#
|
||||
# Returns the sanitized sawyer resource or hash as a hash.
|
||||
def sanitize_resource(resource)
|
||||
resource.to_hash.each_with_object({}) do |(k, v), result|
|
||||
result[k.to_s] = sanitize(v)
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
require "jekyll"
|
||||
require "uri"
|
||||
|
||||
module Jekyll
|
||||
module GitHubMetadata
|
||||
class SiteGitHubMunger
|
||||
extend Forwardable
|
||||
|
||||
class << self
|
||||
attr_accessor :global_munger
|
||||
end
|
||||
|
||||
def_delegators Jekyll::GitHubMetadata, :site, :repository
|
||||
private :repository
|
||||
|
||||
def initialize(site)
|
||||
Jekyll::GitHubMetadata.site = site
|
||||
@original_config = site.config["github"]
|
||||
end
|
||||
|
||||
def munge!
|
||||
Jekyll::GitHubMetadata.log :debug, "Initializing..."
|
||||
|
||||
add_title_and_description_fallbacks!
|
||||
add_url_and_baseurl_fallbacks! if should_add_url_fallbacks?
|
||||
end
|
||||
|
||||
def inject_metadata!(payload)
|
||||
payload.site["github"] = github_namespace
|
||||
end
|
||||
|
||||
def uninject_metadata!(payload)
|
||||
payload.site["github"] = @original_config
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def github_namespace
|
||||
case @original_config
|
||||
when nil
|
||||
drop
|
||||
when Hash
|
||||
drop.merge(@original_config)
|
||||
else
|
||||
@original_config
|
||||
end
|
||||
end
|
||||
|
||||
def drop
|
||||
@drop ||= MetadataDrop.new(GitHubMetadata.site)
|
||||
end
|
||||
|
||||
# Set `site.url` and `site.baseurl` if unset.
|
||||
def add_url_and_baseurl_fallbacks!
|
||||
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
|
||||
|
||||
def add_title_and_description_fallbacks!
|
||||
if should_warn_about_site_name?
|
||||
msg = "site.name is set in _config.yml, but many plugins and themes expect "
|
||||
msg << "site.title to be used instead. To avoid potential inconsistency, "
|
||||
msg << "Jekyll GitHub Metadata will not set site.title to the repository's name."
|
||||
Jekyll::GitHubMetadata.log :warn, msg
|
||||
else
|
||||
site.config["title"] ||= Value.new("title", proc { |_context, repository|
|
||||
if repository.project_page?
|
||||
repository.name
|
||||
else
|
||||
repository.owner_display_name || repository.owner
|
||||
end
|
||||
})
|
||||
end
|
||||
site.config["description"] ||= Value.new("description", proc { |_c, r| r.tagline })
|
||||
end
|
||||
|
||||
# Set the baseurl only if it is `nil` or `/`
|
||||
# Baseurls should never be "/". See http://bit.ly/2s1Srid
|
||||
def should_set_baseurl?
|
||||
site.config["baseurl"].nil? || site.config["baseurl"] == "/"
|
||||
end
|
||||
|
||||
def should_add_url_fallbacks?
|
||||
Jekyll.env == "production" || Pages.page_build?
|
||||
end
|
||||
|
||||
def should_warn_about_site_name?
|
||||
site.config["name"] && !site.config["title"]
|
||||
end
|
||||
end
|
||||
|
||||
Jekyll::Hooks.register :site, :after_init do |site|
|
||||
SiteGitHubMunger.global_munger = SiteGitHubMunger.new(site)
|
||||
SiteGitHubMunger.global_munger.munge!
|
||||
end
|
||||
|
||||
Jekyll::Hooks.register :site, :pre_render do |_site, payload|
|
||||
SiteGitHubMunger.global_munger.inject_metadata!(payload)
|
||||
end
|
||||
|
||||
Jekyll::Hooks.register :site, :post_render do |_site, payload|
|
||||
SiteGitHubMunger.global_munger.uninject_metadata!(payload)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "json"
|
||||
|
||||
module Jekyll
|
||||
module GitHubMetadata
|
||||
class Value
|
||||
extend Forwardable
|
||||
def_delegators :render, :+, :to_s, :to_json, :eql?, :hash
|
||||
|
||||
attr_reader :key, :value
|
||||
|
||||
def initialize(*args)
|
||||
case args.size
|
||||
when 1
|
||||
@key = "{anonymous}"
|
||||
@value = args.first
|
||||
when 2
|
||||
@key = args.first.to_s
|
||||
@value = args.last
|
||||
else
|
||||
raise ArgumentError, "#{args.size} args given but expected 1 or 2"
|
||||
end
|
||||
end
|
||||
|
||||
def render
|
||||
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_liquid
|
||||
case render
|
||||
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
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jekyll
|
||||
module GitHubMetadata
|
||||
VERSION = "2.16.1"
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user