class Publicity::Main

Public Class Methods

new() click to toggle source
# File lib/publicity/prompt.rb, line 10
def initialize
  @credential_helper = `git config --global credential.helper`.delete("\n")
  @github = nil
  @username = nil
  @auth_type = nil

  prompt
end

Public Instance Methods

create_readme!(repo, data, remote_repo_name) click to toggle source
# File lib/publicity/prompt.rb, line 288
    def create_readme!(repo, data, remote_repo_name)
      text = <<EOS
##{remote_repo_name}

This is a copy of the work I did on a private repo, originally a project from
the [Hack Reactor](http://hackreactor.com) curriculum. This project was worked
on with a pair, and as such is representative of the kind of problems that I've
tackled, but not of my solo work.

For a better perspective on my own work, please see [#{data[:name]}](#{data[:url]}).
EOS

      oid = repo.write(text, :blob)
      index = repo.index
      # Read the current tree into the index. This prevents us from overwriting
      # the current index (and thus replacing the entire current tree)
      index.read_tree(repo.lookup(repo.head.target).tree)
      index.add(:path => "README.md", :oid => oid, :mode => 0100644)

      author = { :email => "accounts@hackreactor.com", :name => 'Hack Reactor', :time => Time.now }

      Rugged::Commit.create(repo,
        :tree       => repo.lookup(index.write_tree),
        :author     => author,
        :committer  => author,
        :message    => "Add a readme (automated commit)",
        :parents    => repo.empty? ? [] : [repo.head.target].compact,
        :update_ref => 'HEAD')
    end
get_github_credentials() click to toggle source
# File lib/publicity/prompt.rb, line 206
def get_github_credentials
  # TODO: Set a flag guarding Keychain access. If we try to access GitHub and the
  # user's credentials fail, this will prevent us from looping over invalid
  # keychain credentials over and over.

  # If we're on OS X and can retrieve GitHub credentials from the keychain,
  # use those. If the keychain is unavailable, or if we weren't able to
  # retrieve GitHub credentials from it, we'll prompt the user for their
  # credentials.
  if Publicity::HOST_IS_OSX
    item = Keychain.items.find { |i| i.label =~ /github\.com/ }
  end

  # Create an instance of the GitHub API. We'll use this to make all GitHub
  # requests
  @github = Github.new do |config|
    if item
      @auth_type = 'keychain'
      config.login = item.account
      config.password = item.password
    else
      @auth_type = 'basic'
      config.login = ask("Enter your GitHub username: ") { |q| q.echo = true }
      config.password = ask("And your password: ") { |q| q.echo = "*" }
    end
  end
end
get_github_credentials!() click to toggle source
# File lib/publicity/prompt.rb, line 234
def get_github_credentials!
  begin
    get_github_credentials
    # Try to access something that requires credentials to test if these
    # credentials are valid
    @github.authorizations.list
  rescue
    puts '', "Sorry, login to GitHub failed. Please re-enter your credentials."
    retry
  end
  @username = @github.current_options[:login]
  @github
end
get_private_repos() click to toggle source
# File lib/publicity/prompt.rb, line 256
def get_private_repos
  result = Array.new

  @github.repos.list do |repo|
    # If the repo isn't a fork, we need to skip it. This prevents us from
    # inadvertently trying to access a repo on which a user has been
    # granted collaborator status, but hasn't forked.
    unless repo.fork
      next
    end

    # The version of repo that comes with github.repos.list only has a
    # limited subset of the repo's metadata--get the full version
    #
    # TODO: This is jank. Jesus, please fix this. Rescue any errors and
    # just skip to the next repo.
    begin
      repo = @github.repos.get(@username, repo[:name])
    rescue
      next
    end

    # If the repo is still private and it's a fork of a Hack Reactor repo,
    # it qualifies for liberation
    if repo[:private] and repo[:parent].andand[:owner].andand[:login] === 'hackreactor'
      result.push repo
    end
  end

  result
end
get_project_info() click to toggle source
# File lib/publicity/prompt.rb, line 248
def get_project_info
  project = Hash.new
  project[:name] = ask("Please enter the name of that project: ")
  project[:url] = ask("Please enter the URL to that project's GitHub repository, or to where it's hosted:")
  puts ''
  project
end
prompt() click to toggle source
# File lib/publicity/prompt.rb, line 19
    def prompt
      puts <<EOS
PUBLICITY
=========

Hello! I'm a script that will publicize all of your forked repositories.

EOS
      ask("Press enter when you're ready.") { |q| q.echo = '' }

      # Clear the screen
      print "\e[2J\e[f"

      puts "I'll need your GitHub credentials to get started.", ''
      get_github_credentials!

      puts <<EOS

Thanks.

One more thing--this script will create a new readme for each of your projects.
The readme will point to a project you think shows off your skills; you should
pick a project you're proud of, and you should avoid picking a group project.

EOS
      project = get_project_info

      # Gather a list of the user's repos which are forked from the hackreactor
      # organization and are private. Ask the user which of those repos they'd like
      # to publicize.
      remote_repos = Array.new
      private_repos = get_private_repos
      if private_repos.length === 0
        puts "Sorry, doesn't look like you have any private forks. Exiting..."
        exit 0
      end

      private_repos.each do |private_repo|
        response = ask("Would you like to publicize the #{private_repo[:name]} repository? (y/n): ") do |q|
          q.validate = /^(y|n)$/i
          q.responses[:not_valid] = "Sorry, that's an invalid option. Please enter either y or n: "
        end
        remote_repos.push(private_repo) if response === 'y'
      end

      # Create a scratch directory where we'll clone all our repos. This repo
      # will get cleaned up after the begin block.
      tmpdir = Dir.mktmpdir(nil, '/tmp')
      begin
        # Determine the credential helper we'll be using
        if @auth_type === 'basic'
          # If the user is using basic auth, we'll need to put their credentials
          # somewhere Git can access.
          `git config --global credential.helper store`
          tmpcreds = File.open("#{Dir.home}/.git-credentials", 'w', 0664) do |f|
            f.write("https://#{@username}:#{@github.current_options[:password]}@github.com")
            f
          end
        elsif @auth_type === 'keychain'
          `git config --global credential.helper osxkeychain`
        else
          raise StandardError, "Unknown credential helper"
        end

        remote_repos.each do |remote_repo|
          puts "Cloning down #{remote_repo[:name]}..."

          dir = "#{tmpdir}/#{remote_repo[:name]}.git"

          # Clone the original repo
          `git clone --bare #{remote_repo.clone_url} #{dir} 1> /dev/null`

          puts "Finished cloning."

          # Wrap the cloned git directory in a libgit wrapper
          local_repo = Rugged::Repository.new(dir)

          # Get a list of all the branches, and filter out irrelevant branches
          branches = []
          regex = /(#{Regexp.quote(@username)}|master|.*2013)/
          Rugged::Branch.each(local_repo) do |branch|
            branches.push(branch) if branch.name =~ regex
          end

          # Ask the user which branch they want to keep; we'll end up deleting
          # all other branches. Skip over this prompt if the repo only has a
          # :master branch.
          puts <<EOS

I'll now take a branch of your choosing and make it your master branch. This
way, anyone who looks at your repository will see your code right away, rather
than the repository's boilerplate code.

EOS
          if branches.any? { |b| b.name == 'master' } and branches.length === 1
            branch = branches[0]
            puts "Only found branch `#{branch.name}`. Using that one."
          else
            branch = choose do |menu|
              menu.index = :number
              menu.select_by = :index
              menu.prompt = "Please enter the number of the branch you'd like to keep: "
              menu.choices(*branches.map { |item| item.name }) do |selection|
                branches.detect { |b| b.name == selection }
              end
            end
          end

          # Replace the master branch with the user's branch. This ensures that
          # anyone viewing the repo on GitHub is first presented with the user's
          # code, not some empty boilerplate project.
          if not branch.name =~ /^master$/
            branch.move('master')
          end

          # Delete all remote branches
          Rugged::Branch.each_name(local_repo, :remote).sort.each do |r|
            if not r =~ /^master$/
              Rugged::Branch.lookup(local_repo, r).delete! if Rugged::Branch.lookup(local_repo, r)
            end
          end

          # Generate a readme and commit it to the repository.
          create_readme!(local_repo, project, remote_repo[:name])

          # Rename the source repo on GitHub to make way for the (nicenamed) new
          # repository.
          @github.repos.edit @username, "#{remote_repo[:name]}",
            :name => "#{remote_repo[:name]}.old",
            :private => true

          # Create an endpoint for the new repository.
          options = {
            :name => remote_repo[:name],
            :has_issues => false,
            :has_wiki => false
          }
          @github.repos.create options

          # Push the processed repository back up
          puts '', 'Pushing up your processed repository...'
          Dir.chdir(dir)
          # Write to ~/.netrc and ensure delete afterward. Move mktmpdir into
          # that block.
          `git push -u #{remote_repo[:clone_url]} master 1> /dev/null`

          # Display a different message if this is the last item
          if remote_repos.index(remote_repo) === remote_repos.length - 1
            puts "Finished with #{remote_repo[:name]}. Moving on to the next repository...", ''
          else
            puts "Finished with #{remote_repo[:name]}. All repos processed.", ''
          end
        end
        # Each user currently has two versions of each repo up on GitHub--a
        # private one called reponame.old, and a public one without the .old
        # suffix. Offer to delete all the old, private repos.
        puts <<EOS
All your repos have been pushed up to GitHub. Please review all your newly
pushed repos to make sure they look okay.

EOS
        delete_branches = ask("Would you like me to delete the old, private versions of your repos? (y/n) ") do |q|
          q.validate = /^(y|n)$/i
          q.responses[:not_valid] = "Sorry, that's an invalid option. Please enter either y or n: "
        end

        if delete_branches === 'y'
          remote_repos.each do |private_repo|
            @github.repos.delete(@username, "#{private_repo[:name]}.old")
          end
        end
      ensure
        # Make sure we tidy up after ourselves
        `git config --global credential.helper '#{@credential_helper}'`
        FileUtils.remove_entry(tmpdir) if tmpdir
        FileUtils.remove_entry("#{Dir.home}/.git-credentials") if tmpcreds
      end
      puts <<EOS

All your repos are now public. Please note that any of the repos this script
processed are now out of sync with your local versions; before making any
changes to local copies of these repos, please refresh from GitHub.

Enjoy!
EOS
    end