diff --git a/.circleci/config.yml b/.circleci/config.yml index 9cee232..f0b333f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,7 +39,8 @@ jobs: - samvera/rubocop - - samvera/parallel_rspec + # RSpec tests have not been implemented + # - samvera/parallel_rspec workflows: ci: diff --git a/Gemfile b/Gemfile index 05582cb..6314fca 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,8 @@ -source "https://rubygems.org" +# frozen_string_literal: true + +source 'https://rubygems.org' # Specify your gem's dependencies in huborg.gemspec gemspec -gem "rake", "~> 12.0" +gem 'rake', '~> 12.0' diff --git a/Rakefile b/Rakefile index d0125b0..5a3253d 100644 --- a/Rakefile +++ b/Rakefile @@ -1,34 +1,37 @@ -require "bundler/gem_tasks" +# frozen_string_literal: true +require 'bundler/gem_tasks' + +# rubocop:disable Metrics/BlockLength namespace :test do - desc "Push template to organization (set ENV[GITHUB_ACCESS_TOKEN] and ENV[GITHUB_ORG_NAME])" + desc 'Push template to organization (set ENV[GITHUB_ACCESS_TOKEN] and ENV[GITHUB_ORG_NAME])' task :push_template do require 'huborg' client = Huborg::Client.new( - github_access_token: ENV.fetch("GITHUB_ACCESS_TOKEN"), - org_names: ENV.fetch("GITHUB_ORG_NAME") + github_access_token: ENV.fetch('GITHUB_ACCESS_TOKEN'), + org_names: ENV.fetch('GITHUB_ORG_NAME') ) client.push_template!( template: __FILE__, - filename: "disposable-#{Time.now.utc.to_s.gsub(/\D+/,'')}.rake" + filename: "disposable-#{Time.now.utc.to_s.gsub(/\D+/, '')}.rake" ) end task :clone_and_rebase do require 'huborg' client = Huborg::Client.new( - github_access_token: ENV.fetch("GITHUB_ACCESS_TOKEN"), - org_names: ENV.fetch("GITHUB_ORG_NAME") + github_access_token: ENV.fetch('GITHUB_ACCESS_TOKEN'), + org_names: ENV.fetch('GITHUB_ORG_NAME') ) - directory = ENV.fetch("DIRECTORY") { File.join(ENV.fetch("HOME"), "git") } + directory = ENV.fetch('DIRECTORY') { File.join(ENV.fetch('HOME'), 'git') } client.clone_and_rebase!(directory: directory) end task :audit_license do require 'huborg' client = Huborg::Client.new( - github_access_token: ENV.fetch("GITHUB_ACCESS_TOKEN"), - org_names: ENV.fetch("GITHUB_ORG_NAME") + github_access_token: ENV.fetch('GITHUB_ACCESS_TOKEN'), + org_names: ENV.fetch('GITHUB_ORG_NAME') ) client.audit_license end @@ -36,27 +39,29 @@ namespace :test do task :mailmap do require 'huborg' client = Huborg::Client.new( - github_access_token: ENV.fetch("GITHUB_ACCESS_TOKEN"), - org_names: ENV.fetch("GITHUB_ORG_NAME") + github_access_token: ENV.fetch('GITHUB_ACCESS_TOKEN'), + org_names: ENV.fetch('GITHUB_ORG_NAME') ) - client.synchronize_mailmap!(template: ENV.fetch("MAILMAP_TEMPLATE_FILENAME")) + client.synchronize_mailmap!(template: ENV.fetch('MAILMAP_TEMPLATE_FILENAME')) end end - require 'github_changelog_generator/task' -desc "Generate CHANGELOG.md based on lib/huborg/version.md (change that to the new version before you run rake changelog)" +# rubocop:disable Metrics/LineLength +desc 'Generate CHANGELOG.md based on lib/huborg/version.md (change that to the new version before you run rake changelog)' +# rubocop:enable Metrics/LineLength GitHubChangelogGenerator::RakeTask.new :changelog do |config| begin - ENV.fetch("CHANGELOG_GITHUB_TOKEN") + ENV.fetch('CHANGELOG_GITHUB_TOKEN') rescue KeyError - $stderr.puts %(To run `rake changelog` you need to have a CHANGELOG_GITHUB_TOKEN) - $stderr.puts %(set in ENV. (`export CHANGELOG_GITHUB_TOKEN="«your-40-digit-github-token»"`)) + warn %(To run `rake changelog` you need to have a CHANGELOG_GITHUB_TOKEN) + warn %(set in ENV. (`export CHANGELOG_GITHUB_TOKEN="«your-40-digit-github-token»"`)) exit!(1) end config.user = 'samvera-labs' config.project = 'huborg' config.since_tag = 'v0.1.0' # The changes before v0.1.0 were not as helpful - config.future_release = %(v#{ENV.fetch("FUTURE_RELEASE", Huborg::VERSION)}) + config.future_release = %(v#{ENV.fetch('FUTURE_RELEASE', Huborg::VERSION)}) config.base = 'CHANGELOG.md' end +# rubocop:enable Metrics/BlockLength diff --git a/bin/console b/bin/console index 137a6d3..29a1ec5 100755 --- a/bin/console +++ b/bin/console @@ -1,7 +1,8 @@ #!/usr/bin/env ruby +# frozen_string_literal: true -require "bundler/setup" -require "huborg" +require 'bundler/setup' +require 'huborg' # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. @@ -10,5 +11,5 @@ require "huborg" # require "pry" # Pry.start -require "irb" +require 'irb' IRB.start(__FILE__) diff --git a/huborg.gemspec b/huborg.gemspec index 2805d8d..17c6cee 100644 --- a/huborg.gemspec +++ b/huborg.gemspec @@ -1,31 +1,34 @@ +# frozen_string_literal: true + require_relative 'lib/huborg/version' Gem::Specification.new do |spec| - spec.name = "huborg" + spec.name = 'huborg' spec.version = Huborg::VERSION - spec.authors = ["Jeremy Friesen"] - spec.email = ["jeremy.n.friesen@gmail.com"] + spec.authors = ['Jeremy Friesen'] + spec.email = ['jeremy.n.friesen@gmail.com'] - spec.summary = %q{Make changes to Organization Repositories en-masse.} - spec.description = %q{Make changes to Organization Repositories en-masse.} - spec.homepage = "https://github.com/samvera-labs/huborg/" - spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") + spec.summary = 'Make changes to Organization Repositories en-masse.' + spec.description = 'Make changes to Organization Repositories en-masse.' + spec.homepage = 'https://github.com/samvera-labs/huborg/' + spec.required_ruby_version = Gem::Requirement.new('>= 2.7') - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "https://github.com/samvera-labs/huborg/" - spec.metadata["changelog_uri"] = "https://github.com/samvera-labs/huborg/blob/master/CHANGELOG.md" - spec.metadata["documentation_uri"] = "https://www.rubydoc.info/gems/huborg/" + spec.metadata['homepage_uri'] = spec.homepage + spec.metadata['source_code_uri'] = 'https://github.com/samvera-labs/huborg/' + spec.metadata['changelog_uri'] = 'https://github.com/samvera-labs/huborg/blob/master/CHANGELOG.md' + spec.metadata['documentation_uri'] = 'https://www.rubydoc.info/gems/huborg/' # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do + spec.files = Dir.chdir(File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end - spec.bindir = "exe" + spec.bindir = 'exe' spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } - spec.require_paths = ["lib"] - spec.add_dependency "octokit", "~> 4.16" - spec.add_dependency "git", "~> 1.6" - spec.add_development_dependency "byebug" - spec.add_development_dependency "github_changelog_generator" + spec.require_paths = ['lib'] + spec.add_dependency 'git', '~> 1.6' + spec.add_dependency 'octokit', '~> 4.16' + spec.add_development_dependency 'byebug' + spec.add_development_dependency 'github_changelog_generator' + spec.add_development_dependency 'rubocop' end diff --git a/lib/huborg.rb b/lib/huborg.rb index bf7044f..c18aacb 100644 --- a/lib/huborg.rb +++ b/lib/huborg.rb @@ -1,5 +1,6 @@ -# coding: utf-8 -require "huborg/version" +# frozen_string_literal: true + +require 'huborg/version' require 'octokit' require 'git' require 'fileutils' @@ -18,10 +19,12 @@ class Error < RuntimeError; end # * {#audit_license} - tooling to check the licenses of your org # * {#synchronize_mailmap!} - ensure all git .mailmap files are # synchronized + + # rubocop:disable Metrics/ClassLength class Client # When listing repositories, this callable will return all repositories. # @see #initialize `#initialize` for details on the repository filter. - DEFAULT_REPOSITORY_FILTER = ->(client, repo) { true } + DEFAULT_REPOSITORY_FILTER = ->(_client, _repo) { true } # @since v0.1.0 # @@ -51,8 +54,11 @@ class Client # repository_filter: ->(client, repo) { repo.full_name.match?(/.*hyrax.*/) } # ) # + # rubocop:disable Layout/LineLength # @see https://github.com/octokit/octokit.rb#oauth-access-tokens Octokit's documentation for OAuth Tokens # @see https://developer.github.com/v3/repos/#list-organization-repositories Github's documentation for repository data structures + # rubocop:enable Layout/LineLength + def initialize(org_names:, logger: default_logger, github_access_token: default_access_token, @@ -69,15 +75,15 @@ def initialize(org_names:, def default_logger require 'logger' - Logger.new(STDOUT) + Logger.new($stdout) end def default_access_token ENV.fetch('GITHUB_ACCESS_TOKEN') - rescue KeyError => e + rescue KeyError message = "You need to provide an OAuth Access Token.\nSee: https://github.com/octokit/octokit.rb#oauth-access-tokens" - $stderr.puts message - raise Error.new(message) + warn message + raise Error, message end public @@ -137,15 +143,22 @@ def push_template!(template:, filename:, overwrite: false) # @see https://api.github.com/licenses Github's documentation for a list of license keys # @return [True] the task completed without exception (there may be # logged errors) + + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/PerceivedComplexity def audit_license(skip_private: true, skip_archived: true, allowed_licenses: :all) license_list = Array(allowed_licenses) each_github_repository do |repo| next if skip_private && repo.private? next if skip_archived && repo.archived? + if repo.license logger.info(%(#{repo.fullname} has "#{repo.license.key}")) next if allowed_licenses == :all next if license_list.include?(repo.license.key) + logger.error(%(#{repo.full_name} has "#{repo.license.key}" which is not in #{license_list.inspect})) else logger.error("#{repo.full_name} is missing a license") @@ -153,6 +166,10 @@ def audit_license(skip_private: true, skip_archived: true, allowed_licenses: :al end true end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/CyclomaticComplexity + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/PerceivedComplexity # @api public # @since v0.2.0 @@ -175,6 +192,9 @@ def audit_license(skip_private: true, skip_archived: true, allowed_licenses: :al # for more on git's .mailmap file # @todo Ensure that this doesn't create a pull request if nothing # has changed. + + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength def synchronize_mailmap!(template:, consolidated_template: template) mailmap_lines = Set.new File.read(template).split("\n").each do |line| @@ -182,19 +202,17 @@ def synchronize_mailmap!(template:, consolidated_template: template) end each_github_repository do |repo| - begin - mailmap = client.contents(repo.full_name, path: '.mailmap') - lines = mailmap.rels[:download].get.data - lines.split("\n").each do |line| - mailmap_lines << line - end - rescue Octokit::NotFound - next + mailmap = client.contents(repo.full_name, path: '.mailmap') + lines = mailmap.rels[:download].get.data + lines.split("\n").each do |line| + mailmap_lines << line end + rescue Octokit::NotFound + next end # Write the contents to a file - File.open(consolidated_template, "w+") do |file| + File.open(consolidated_template, 'w+') do |file| mailmap_lines.to_a.sort.each do |line| file.puts line end @@ -202,11 +220,14 @@ def synchronize_mailmap!(template:, consolidated_template: template) each_github_repository do |repo| next if repo.archived? - push_template_to!(filename: ".mailmap", template: consolidated_template, repo: repo, overwrite: true) + + push_template_to!(filename: '.mailmap', template: consolidated_template, repo: repo, overwrite: true) end true end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength # @api public # @since v0.2.0 @@ -254,14 +275,18 @@ def synchronize_mailmap!(template:, consolidated_template: template) # └── raft # # @return [True] if successfully completed - def clone_and_rebase!(directory:, skip_forked: false, skip_archived: false, skip_dirty: true, force: false, shallow: false) + # rubocop:disable Metrics/ParameterLists + def clone_and_rebase!(directory:, skip_forked: false, skip_archived: false, skip_dirty: true, force: false, + shallow: false) each_github_repository do |repo| next if skip_archived && repo.archived? next if skip_forked && repo.fork? + clone_and_rebase_one!(repo: repo, directory: directory, skip_dirty: skip_dirty, force: force, shallow: shallow) end true end + # rubocop:enable Metrics/ParameterLists # @api public # @since v0.3.0 @@ -286,9 +311,10 @@ def clone_and_rebase!(directory:, skip_forked: false, skip_archived: false, skip # end # # @see https://developer.github.com/v3/pulls/#list-pull-requests - def each_pull_request_with_repo(skip_archived: true, query: { state: :open}) + def each_pull_request_with_repo(skip_archived: true, query: { state: :open }) each_github_repository do |repo| next if skip_archived && repo.archived? + fetch_rel_for(rel: :pulls, from: repo, query: query).each do |pull| yield(pull, repo) end @@ -322,10 +348,8 @@ def each_pull_request_with_repo(skip_archived: true, query: { state: :open}) # for the response document # # @return [True] - def list_repositories - each_github_repository do |repo| - yield repo - end + def list_repositories(&block) + each_github_repository(&block) true end @@ -370,9 +394,13 @@ def each_github_repository(&block) # overwrite what already exists. In the case of a LICENSE, we # would not want to do that. In the case of a .mailmap, we # would likely want to overwrite. + + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength def push_template_to!(repo:, template:, filename:, overwrite: false) return if repo.archived - # Note: Sometimes I'm using "heads/" and other times I'm using + + # NOTE: Sometimes I'm using "heads/" and other times I'm using # "refs/heads/". There appears to be an inconsistency in # the implementation of octokit. default_branch = client.ref(repo.full_name, "heads/#{repo.default_branch}") @@ -386,32 +414,39 @@ def push_template_to!(repo:, template:, filename:, overwrite: false) end commit_message = "Adding/updating #{filename}\n\nThis was uploaded via automation." logger.info("Creating pull request for #{filename} on #{repo.full_name}") - target_branch_name = "refs/heads/autoupdate-#{Time.now.utc.to_s.gsub(/\D+/,'')}" + target_branch_name = "refs/heads/autoupdate-#{Time.now.utc.to_s.gsub(/\D+/, '')}" if filename_ref_on_default_branch return unless overwrite + client.create_reference(repo.full_name, target_branch_name, default_branch.object.sha) client.update_contents( repo.full_name, filename, commit_message, filename_ref_on_default_branch.sha, - file: File.new(template, "r"), + file: File.new(template, 'r'), branch: target_branch_name ) - client.create_pull_request(repo.full_name, "refs/heads/#{repo.default_branch}", target_branch_name, commit_message) else client.create_reference(repo.full_name, target_branch_name, default_branch.object.sha) client.create_contents( repo.full_name, filename, commit_message, - file: File.new(template, "r"), + file: File.new(template, 'r'), branch: target_branch_name ) - client.create_pull_request(repo.full_name, "refs/heads/#{repo.default_branch}", target_branch_name, commit_message) end + client.create_pull_request(repo.full_name, "refs/heads/#{repo.default_branch}", target_branch_name, + commit_message) end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/PerceivedComplexity def clone_and_rebase_one!(repo:, directory:, skip_dirty: true, force: false, shallow: false) repo_path = shallow ? File.join(directory, repo.name) : File.join(directory, repo.full_name) if File.directory?(repo_path) @@ -437,7 +472,7 @@ def clone_and_rebase_one!(repo:, directory:, skip_dirty: true, force: false, sha end git.branch(repo.default_branch).checkout logger.info("Pulling down #{repo.default_branch} branch from origin for #{repo_path}") - git.pull("origin", repo.default_branch) + git.pull('origin', repo.default_branch) else parent_directory = File.dirname(repo_path) logger.info("Creating #{parent_directory}") @@ -448,6 +483,10 @@ def clone_and_rebase_one!(repo:, directory:, skip_dirty: true, force: false, sha logger.info("Finished cloning #{repo.name} into #{parent_directory}") end end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/CyclomaticComplexity + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/PerceivedComplexity # Responsible for fetching an array of the given :rel # @@ -458,27 +497,31 @@ def clone_and_rebase_one!(repo:, directory:, skip_dirty: true, force: false, sha # Octokit::Repository. # # @return [Array] + + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength def fetch_rel_for(rel:, from:, query: {}) # Build a list of repositories, note per Github's API, these are # paginated. from_to_s = from.respond_to?(:name) ? from.name : from.to_s + # rubocop:disable Layout/LineLength logger.info "Fetching rels[#{rel.inspect}] for '#{from_to_s}' with filter #{repository_filter.inspect}, and query #{query.inspect}" + # rubocop:enable Layout/LineLength source = from.rels[rel].get(query) rels = [] while source rels += source.data - if source.rels[:next] - source = source.rels[:next].get(query) - else - source = nil - end + source = source.rels[:next]&.get(query) end + # rubocop:disable Layout/LineLength logger.info "Finished fetching rels[#{rel.inspect}] for '#{from_to_s}' with filter #{repository_filter.inspect}, and query #{query.inspect}" - if block_given? - rels - else - return rels - end + # rubocop:enable Layout/LineLength + return rels unless block_given? + + rels end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength end + # rubocop:enable Metrics/ClassLength end diff --git a/lib/huborg/version.rb b/lib/huborg/version.rb index 0d06455..8f4387c 100644 --- a/lib/huborg/version.rb +++ b/lib/huborg/version.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module Huborg # The current version of Huborg, as per Semantic Versioning # @see https://semver.org - VERSION = "0.3.1" + VERSION = '0.3.1' end