class OctofactsUpdater::Service::GitHub

Attributes

options[R]

Public Class Methods

new(options = {}) click to toggle source

Constructor.

options - Hash with options

# File lib/octofacts_updater/service/github.rb, line 37
def initialize(options = {})
  @options = options
  @verbose = github_options.fetch("verbose", false)
  @changes = []
end
run(root, paths, options = {}) click to toggle source

Callable external method: Push all changes to the indicated paths to GitHub.

root - A String with the root directory, to which paths are relative. paths - An Array of Strings, which are relative to the repository root. options - A Hash with configuration options.

Returns true if there were any changes made, false otherwise.

# File lib/octofacts_updater/service/github.rb, line 19
def self.run(root, paths, options = {})
  root ||= options.fetch("github", {})["base_directory"]
  unless root && File.directory?(root)
    raise ArgumentError, "Base directory must be specified"
  end
  github = new(options)
  project_root = Pathname.new(root)
  paths.each do |path|
    absolute_path = Pathname.new(path)
    stripped_path = absolute_path.relative_path_from(project_root)
    github.commit_data(stripped_path.to_s, File.read(path))
  end
  github.finalize_commit
end

Public Instance Methods

commit_data(path, new_content) click to toggle source

Commit a file to a location in the repository with the provided message. This will return true if there was an actual change, and false otherwise. This method does not actually do the commit, but rather it batches up all of the changes which must be realized later.

path - A String with the path at which to commit the file new_content - A String with the new contents

Returns true (and updates @changes) if there was actually a change, false otherwise.

# File lib/octofacts_updater/service/github.rb, line 51
def commit_data(path, new_content)
  ensure_branch_exists

  old_content = nil
  begin
    contents = octokit.contents(repository, path: path, ref: branch)
    old_content = Base64.decode64(contents.content)
  rescue Octokit::NotFound
    verbose("No old content found in #{repository.inspect} at #{path.inspect} in #{branch.inspect}")
    # Fine, we will add below.
  end

  if new_content == old_content
    verbose("Content of #{path} matches, no commit needed")
    return false
  else
    verbose("Content of #{path} does not match. A commit is needed.")
    verbose(Diffy::Diff.new(old_content, new_content))
  end

  @changes << Hash(
    path: path,
    mode: "100644",
    type: "blob",
    sha: octokit.create_blob(repository, new_content)
  )

  verbose("Batched update of #{path}")
  true
end
delete_file(path, message = commit_message) click to toggle source

Delete a file from the repository. Because of the way the GitHub API works, this will generate an immediate commit and push. It will NOT be batched for later application.

path - A String with the path at which to commit the file message - A String with a commit message, defaults to the overall configured commit message.

Returns true if the file existed before and was deleted. Returns false if the file didn't exist anyway.

# File lib/octofacts_updater/service/github.rb, line 107
def delete_file(path, message = commit_message)
  ensure_branch_exists
  contents = octokit.contents(repository, path: path, ref: branch)
  blob_sha = contents.sha
  octokit.delete_contents(repository, path, message, blob_sha, branch: branch)
  verbose("Deleted #{path}")
  find_or_create_pull_request
  true
rescue Octokit::NotFound
  verbose("Deleted #{path} (already gone)")
  false
end
finalize_commit(message = commit_message) click to toggle source

Finalize the GitHub commit by actually pushing any of the changes. This will not do anything if there are not any changes batched via the `commit_data` method.

message - A String with a commit message, defaults to the overall configured commit message.

# File lib/octofacts_updater/service/github.rb, line 86
def finalize_commit(message = commit_message)
  return unless @changes.any?

  ensure_branch_exists
  branch_ref = octokit.branch(repository, branch)
  commit = octokit.git_commit(repository, branch_ref[:commit][:sha])
  tree = commit["tree"]
  new_tree = octokit.create_tree(repository, @changes, base_tree: tree["sha"])
  new_commit = octokit.create_commit(repository, message, new_tree["sha"], commit["sha"])
  octokit.update_ref(repository, "heads/#{branch}", new_commit["sha"])
  verbose("Committed #{@changes.size} change(s) to GitHub")
  find_or_create_pull_request
end

Private Instance Methods

branch() click to toggle source
# File lib/octofacts_updater/service/github.rb, line 212
def branch
  github_options.fetch("branch")
end
commit_message() click to toggle source
# File lib/octofacts_updater/service/github.rb, line 216
def commit_message
  github_options.fetch("commit_message")
end
default_branch() click to toggle source

Private: Get the default branch from the repository. Unless default_branch is specified in the options, then use that instead.

Returns a String with the name of the default branch.

# File lib/octofacts_updater/service/github.rb, line 140
def default_branch
  github_options["default_branch"] || octokit.repo(repository)[:default_branch]
end
ensure_branch_exists() click to toggle source

Private: Ensure branch exists. This will use octokit to create the branch on GitHub if the branch does not already exist.

# File lib/octofacts_updater/service/github.rb, line 146
def ensure_branch_exists
  @ensure_branch_exists ||= begin
    created = false
    begin
      if octokit.branch(repository, branch)
        verbose("Branch #{branch} already exists in #{repository}.")
        created = true
      end
    rescue Octokit::NotFound
      # Fine, we'll create it
    end

    unless created
      base_sha = octokit.branch(repository, default_branch)[:commit][:sha]
      octokit.create_ref(repository, "heads/#{branch}", base_sha)
      verbose("Created branch #{branch} based on #{default_branch} #{base_sha}.")
    end

    true
  end
end
find_or_create_pull_request() click to toggle source

Private: Find an existing pull request for the branch, and commit a new pull request if there was not an existing one open.

Returns the pull request object that was created.

# File lib/octofacts_updater/service/github.rb, line 172
def find_or_create_pull_request
  @find_or_create_pull_request ||= begin
    prs = octokit.pull_requests(repository, head: "github:#{branch}", state: "open")
    if prs && !prs.empty?
      verbose("Found existing PR #{prs.first.html_url}")
      prs.first
    else
      new_pr = octokit.create_pull_request(
        repository,
        default_branch,
        branch,
        pr_subject,
        pr_body
      )
      verbose("Created a new PR #{new_pr.html_url}")
      new_pr
    end
  end
end
github_options() click to toggle source
# File lib/octofacts_updater/service/github.rb, line 203
def github_options
  return {} unless options.is_a?(Hash)
  options.fetch("github", {})
end
octokit() click to toggle source

Private: Build an octokit object from the provided options.

Returns an octokit object.

# File lib/octofacts_updater/service/github.rb, line 125
def octokit
  @octokit ||= begin
    token = options.fetch("github", {})["token"] || ENV["OCTOKIT_TOKEN"]
    if token
      Octokit::Client.new(access_token: token)
    else
      raise ArgumentError, "Access token must be provided in config file or OCTOKIT_TOKEN environment variable."
    end
  end
end
pr_body() click to toggle source
# File lib/octofacts_updater/service/github.rb, line 224
def pr_body
  github_options.fetch("pr_body")
end
pr_subject() click to toggle source
# File lib/octofacts_updater/service/github.rb, line 220
def pr_subject
  github_options.fetch("pr_subject")
end
repository() click to toggle source
# File lib/octofacts_updater/service/github.rb, line 208
def repository
  github_options.fetch("repository")
end
verbose(message) click to toggle source

Log a verbose message.

message - A String with the message to print.

# File lib/octofacts_updater/service/github.rb, line 198
def verbose(message)
  return unless @verbose
  puts "*** #{Time.now}: #{message}"
end