Class: Huborg::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/huborg.rb

Overview

The class that interacts with organizational repositories.

Constant Summary collapse

DEFAULT_REPOSITORY_FILTER =

When listing repositories, this callable will return all repositories.

->(client, repo) { true }

Instance Method Summary collapse

Constructor Details

#initialize(org_names:, logger: default_logger, github_access_token: default_access_token, repository_filter: DEFAULT_REPOSITORY_FILTER) ⇒ Client

Returns a new instance of Client.

Examples:

# Without configuration options. You'll want ENV["GITHUB_ACCESS_TOKEN"]
# to be assigned.
client = Huborg::Client.new(org_names: ["samvera", "samvera-labs"])

# With explicit configuration options. Note, we'll be interacting only
# with repositories that contain the case-insensitive word "hyrax" (case-
# insensitivity is declared as the `i` at the end of the regular
# expression).
client = Huborg::Client.new(
  logger: Logger.new(STDOUT),
  github_access_token: "40-random-characters-for-your-token",
  org_names: ["samvera", "samvera-labs"],
  repository_filter: ->(client, repo) { repo.full_name.match?(/.*hyrax.*/) }
)

Parameters:

  • logger (Logger) (defaults to: default_logger)

    used in logging output of processes

  • github_access_token (String) (defaults to: default_access_token)

    used to connect to the Octokit::Client. The given token will need to have permission to interact with repositories. Defaults to ENV

  • org_names (Array<String>, String)

    list of GitHub organizations Huborg will act on

  • repository_filter (Proc<Octokit::Client,Octokit::Repository>) (defaults to: DEFAULT_REPOSITORY_FILTER)

    filter the list of repositories to those for those which the callable returns true; defaults to ALL

See Also:

Since:

  • v0.1.0



56
57
58
59
60
61
62
63
64
# File 'lib/huborg.rb', line 56

def initialize(org_names:,
               logger: default_logger,
               github_access_token: default_access_token,
               repository_filter: DEFAULT_REPOSITORY_FILTER)
  @org_names = Array(org_names)
  @logger = logger
  @client = Octokit::Client.new(access_token: github_access_token)
  @repository_filter = repository_filter
end

Instance Method Details

#audit_license(skip_private: true, skip_archived: true, allowed_licenses: :all) ⇒ True

Responsible for logging (as an error) repositories that do not have a license.

Parameters:

  • skip_private (Boolean) (defaults to: true)

    do not check private repositories for a license

  • skip_archived (Boolean) (defaults to: true)

    do not check archived repositories for license

  • allowed_licenses (Array<String>, :all) (defaults to: :all)

    the licenses which are allowed, in all other cases, log as an error. This checks the :key of a license object in Github’s API (see api.github.com/licenses)

Returns:

  • (True)

    the task completed without exception (there may be logged errors)

See Also:

Since:

  • v0.2.0



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/huborg.rb', line 140

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")
    end
  end
  true
end

#clone_and_rebase!(directory:, skip_forked: false, skip_archived: false, skip_dirty: true, force: false, shallow: false) ⇒ True

Note:

The Product Owner decided to set ‘shallow: false` as the default, as other local scripts run by them made use of those directory structures.

Clone all repositories (that match the #repository_filter for the given organization(s). Then and rebase any existing repositories.

Let’s say we have a Github Organization “penguin” which has the repositories “paradigm” and “raft”. In the above example, if we specified the ‘DIRECTORY` as “/Iceflow”, we would end up with the following local directory tree within /Iceflow:

.
└── penguin
    ├── paradigm
    └── raft

In the case of ‘shallow: true`, we would have the following tree within /Iceflow:

.
├── paradigm
└── raft

Examples:

client = Huborg::Client.new(
  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") }
client.clone_and_rebase!(directory: directory)

Parameters:

  • directory (String)

    the directory in which to clone the repositories (as a sub-directory)

  • skip_dirty (Boolean) (defaults to: true)

    if the repository already exists there, don’t clone or pull down changes

  • force (Boolean) (defaults to: false)

    if we want to obliterate any changes in an existing repository

  • shallow (Boolean) (defaults to: false)

    when true, instead of cloning into a subdirectory of ‘org/repo`, clone into `repo`.

  • skip_forked (Boolean) (defaults to: false)

    when true, don’t clone a repository that is a fork of some other repository.

  • skip_archived (Boolean) (defaults to: false)

    when true, don’t clone a repository that is archived on Github.

Returns:

  • (True)

    if successfully completed

Since:

  • v0.2.0



257
258
259
260
261
262
263
264
# File 'lib/huborg.rb', line 257

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

#each_pull_request_with_repo(skip_archived: true, query: { state: :open}) {|responds, responds| ... } ⇒ Object

Yield each pull request, and associated repository that matches the given parameters

Examples:

require 'huborg'
client = Huborg::Client.new(org_names: ["samvera", "samvera-labs"])
File.open(File.join(ENV["HOME"], "/Desktop/pull-requests.tsv"), "w+") do |file|
  file.puts "REPO_FULL_NAME\tPR_CREATED_AT\tPR_URL\tPR_TITLE"
  client.each_pull_request_with_repo do |pull, repo|
    file.puts "#{repo.full_name}\t#{pull.created_at}\t#{pull.html_url}\t#{pull.title}"
  end
end

Parameters:

  • skip_archived (Boolean) (defaults to: true)

    skip any archived projects

  • query (Hash) (defaults to: { state: :open})

    the query params to use when selecting pull requests

Yield Parameters:

  • responds (Octokit::PullRequest)

    to #created_at, #title, #html_url, etc

  • responds (Octokit::Repository)

    to #full_name

See Also:

Since:

  • v0.3.0



289
290
291
292
293
294
295
296
297
# File 'lib/huborg.rb', line 289

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
  end
  true
end

#list_repositories {|responds| ... } ⇒ True

List every repository that will be acted upon. This is primarily to provide extra assurance to the user.

Examples:

require 'huborg'
client = Huborg::Client.new(
  org_names: ["samvera", "samvera-labs"],
  repository_filter: ->(client, repo) {
    ['infrastructure', 'gem'].all? { |topic|
      client.topics(repo.full_name, accept: Octokit::Preview::PREVIEW_TYPES[:topics])[:names].include?(topic)
    }
  }
)
client.list_repositories do |repo|
  puts repo.full_name
end

Yield Parameters:

  • responds (Octokit::Repository)

    to #full_name

Returns:

  • (True)

See Also:

Since:

  • v0.4.0



325
326
327
328
329
330
331
# File 'lib/huborg.rb', line 325

def list_repositories
  each_github_repository do |repo|
    yield repo
  end

  true
end

#push_template!(template:, filename:, overwrite: false) ⇒ True

TODO:

Verify that the template exists

Note:

This skips archived repositories

Responsible for pushing the given template file to all of the organizations repositories. As we are pushing changes to repositories, this process will skip archived repositories.

Examples:

client = Huborg::Client.new(
  github_access_token: ENV.fetch("GITHUB_ACCESS_TOKEN"),
  org_names: ENV.fetch("GITHUB_ORG_NAME")
)
client.push_template!(
  template: "/path/to/file/on/your/system",
  filename: "relative/path/in/repository"
)

Parameters:

  • template (String)

    name of the template to push out to all repositories

  • filename (String)

    where in the repository should we write the template. This is a relative pathname, each directory and filename.

  • overwrite (Boolean) (defaults to: false)

    because sometimes you shouldn’t 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.

Returns:

  • (True)

    if successfully completed

Since:

  • v0.1.0



115
116
117
118
119
120
# File 'lib/huborg.rb', line 115

def push_template!(template:, filename:, overwrite: false)
  each_github_repository do |repo|
    push_template_to!(repo: repo, template: template, filename: filename, overwrite: overwrite)
  end
  true
end

#synchronize_mailmap!(template:, consolidated_template: template) ⇒ True

TODO:

Ensure that this doesn’t create a pull request if nothing has changed.

Responsible for taking a template that confirms to Git’s .mailmap file format (e.g. github.com/samvera/maintenance/blob/master/templates/MAILMAP) and adding in all .mailmap entries that exist within the organizations repositories. This merged .mailmap is then pushed out, via a pull request, to all of the non-archived repositories.

Parameters:

  • template (String)

    path to the source template for .mailmap This does assume that there were prior efforts at consolidating a .mailmap file. If you don’t have this, pass an empty file.

  • consolidated_template (String) (defaults to: template)

    path that we will write our changes, this file will be pushed to all non-archived repositories

Returns:

  • (True)

    if successfully completed

See Also:

Since:

  • v0.2.0



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/huborg.rb', line 178

def synchronize_mailmap!(template:, consolidated_template: template)
  mailmap_lines = Set.new
  File.read(template).split("\n").each do |line|
    mailmap_lines << line unless line.empty?
  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
    end
  end

  # Write the contents to a file
  File.open(consolidated_template, "w+") do |file|
    mailmap_lines.to_a.sort.each do |line|
      file.puts line
    end
  end

  each_github_repository do |repo|
    next if repo.archived?
    push_template_to!(filename: ".mailmap", template: consolidated_template, repo: repo, overwrite: true)
  end

  true
end