Skip to content

Instantly share code, notes, and snippets.

@yanshiyason
Created August 9, 2019 00:14
Show Gist options
  • Save yanshiyason/3c45080b27ce257317b8f06415218192 to your computer and use it in GitHub Desktop.
Save yanshiyason/3c45080b27ce257317b8f06415218192 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# frozen_string_literal: true
# rubocop:disable Metrics/LineLength, Metrics/MethodLength
#
# _________ ________ __________
# \_ ___ \ \_____ \\______ \
# / \ \/ / | \| | _/
# \ \____/ | \ | \
# \______ /\_______ /______ /
# \/ \/ \/
#
# The ULTIMATE "cob" tool with a total of 3 distinct usages.
# examples:
# $ cob # queries github and gives you a list of issues to pick from
# $ cob 2837 # queries github and checks out a git branch using the famous "cob" style
# $ cob "my string with #hashes and, other: . punctuations" # instant cob
# $ cob some sentence without special characters # instant cob
#
#
# this class interfaces with the local `git` command
class Git
def remote
`git config --list | grep remote.origin.url`&.strip&.split(':')&.last
end
def repository
remote.split('/').last.chomp('.git')
end
def repository_owner
remote.split('/').first
end
def checkout_branch(text)
branch = text.downcase.gsub(/[^a-zA-Z0-9#]+/, '_')
`git checkout -b #{branch}`
end
end
# This is the code for the original COB
module LocalCOB
def self.run
Git.new.checkout_branch(ARGV.join(' '))
end
end
# utility method
class String
def all_digits
scan(/\D/).empty?
end
end
# _ _ ___ ___ ___
# | | ___ __ __ _| | / __/ _ \| _ )
# | |__/ _ \/ _/ _` | | | (_| (_) | _ \
# |____\___/\__\__,_|_| \___\___/|___/
#
begin
if (ARGV.length == 1 && !ARGV[0].all_digits) || ARGV.length > 1
LocalCOB.run
exit 0
end
rescue Interrupt
exit 1
end
#
# Asserts libraries are installed
#
required_gems = %w[
json
tty-prompt
fileutils
]
required_gems.each do |lib|
if `gem list '^#{lib}$' -i`.chomp != 'true'
puts "Please run: gem install #{lib}"
exit 1
end
require lib
end
#
# class to handle the credentials
#
# usage:
#
# c = Credentials.new
# c.username
# c.pa_token
#
class Credentials
def initialize
@config_file = ENV['GITHUB_CREDENTIALS_FILE'] ||
"#{ENV['HOME']}/.cob/github_credentials.json"
@config = parse_config
end
def username
# USERNAME
@config['username'] || prompt_username
end
def pa_token
# PA_TOKEN
@config['paToken'] || prompt_pa_token
end
def overwrite_config
File.write(@config_file, @config.to_json)
end
def prompt_username
prompt = TTY::Prompt.new(active_color: :cyan)
username = prompt.ask('What is your github username?') do |q|
q.required true
q.modify :lowercase
end
@config['username'] = username
overwrite_config
username
end
def prompt_pa_token
prompt = TTY::Prompt.new(active_color: :cyan)
token = prompt.ask('What is your github personal access token? You can get one from here: https://github.com/settings/tokens') do |q|
q.required true
end
@config['paToken'] = token
overwrite_config
token
end
def reset_config_file
File.write(@config_file, '{}')
end
def ensure_config_file_is_created
return if File.exist?(@config_file)
segments = @config_file.split('/')
paths = segments[0..-2]
file = segments[-1]
dir = paths.join('/')
FileUtils.mkdir_p(dir)
FileUtils.touch(dir + '/' + file)
reset_config_file
end
private
def parse_config
JSON.parse(File.read(@config_file))
rescue StandardError
puts "Couldn't parse config file. Make sure the JSON is correct:\n#{@config_file}"
exit 1
end
end
# this class fetches and paginates the issues from github.com
class Paginator
attr_reader :page
def initialize(credentials, git)
@credentials = credentials
@git = git
@page = 1
@cache = {}
@next = {}
end
def issues
@cache[@page] ||= begin
issues = fetch_issues
@next[@page] = issues.count.positive?
issues
end
end
def no_issues?
@page == 1 && issues.empty?
end
def next?
issues && @next[@page]
end
def prev?
@page > 1
end
def format_issue(issue)
"#{issue['number']} --- #{issue['title']}"
end
def formatted_issues
issues.map { |i| format_issue(i) }
end
def dec
@page = [@page - 1, 1].max
end
def inc
@page += 1
end
private
def fetch_issues
_header, body = http_get_issues.split("\r\n\r\n")
data = JSON.parse(body)
# TODO: move elsewhere
if data.is_a?(Hash) && data['message'] == 'Bad credentials'
puts 'Your github credentials are wrong.'
@credentials.reset_config_file
exit 1
end
data.select { |i| i['pull_request'].nil? } # ignore pull requests
end
def http_get_issues
uname = @credentials.username
token = @credentials.pa_token
`curl -sS -i https://api.github.com/repos/#{@git.repository_owner}/#{@git.repository}/issues?page=#{@page} \
-u "#{uname}:#{token}" \
-H "Content-Type: application/json"`
end
end
# this class prompts the user for selection and shows "next..." and "prev..."
# when the user can navigate to the next/prev pages.
class Navigator
def initialize(paginator)
@paginator = paginator
@active_index = 1
end
def prompt_for_issue_selection
prompt = TTY::Prompt.new(active_color: :cyan)
if @paginator.no_issues?
puts 'No issues found.'
exit 0
end
@selected_index = prompt.select('Select issue:') do |menu|
menu.default @active_index
items.each.with_index(1) do |item, i|
menu.choice item.to_s, i
end
end
handle_selection
end
def items
ii = @paginator.formatted_issues
ii = ['prev...'] + ii if show_prev?
ii += ['next...'] if show_next?
ii
end
private
def show_prev?
@paginator.prev?
end
def show_next?
@paginator.next?
end
def handle_selection
choice = items[@selected_index - 1]
return choice unless ['next...', 'prev...'].include? choice
if choice == 'next...'
@paginator.inc
@active_index = 1
end
if choice == 'prev...'
@paginator.dec
@active_index = [items.count, 1].max
end
prompt_for_issue_selection
end
end
# this module contain the remote commands
module RemoteCOB
def self.ensure_credentials_and_remote
Credentials.new.ensure_config_file_is_created
git = Git.new
unless git.remote
puts 'could not find remote git repository'
exit 1
end
unless git.repository_owner && git.repository
puts "couldn't retreive repo and owner from remote: #{remote}"
exit 1
end
end
# This is the code for querying github issues and displaying
# a select prompt to the user
module Selection
def self.run
RemoteCOB.ensure_credentials_and_remote
paginator = Paginator.new(Credentials.new, Git.new)
issue = Navigator.new(paginator).prompt_for_issue_selection
Git.new.checkout_branch(issue)
end
end
# This is the code for automatically checking out the matching issue in github
module Fire
def self.run
RemoteCOB.ensure_credentials_and_remote
issue_number = ARGV[0].to_i
paginator = Paginator.new(Credentials.new, Git.new)
while paginator.next?
issue = paginator.issues.detect { |i| i['number'] == issue_number }
if issue
Git.new.checkout_branch(paginator.format_issue(issue))
exit 0
end
puts "couldn't find issue on page: #{paginator.page}, fetching next..."
paginator.inc
end
puts "couldn't find issue on github."
exit 1
end
end
end
#
# ___ _ ___ ___ ___
# | _ \___ _ __ ___| |_ ___ / __/ _ \| _ )
# | / -_) ' \/ _ \ _/ -_) | (_| (_) | _ \
# |_|_\___|_|_|_\___/\__\___| \___\___/|___/
#
begin
RemoteCOB::Selection.run && exit(0) if ARGV.length == 0
RemoteCOB::Fire.run && exit(0) if ARGV.length == 1
rescue Interrupt
exit 1
end
# rubocop:enable Metrics/LineLength, Metrics/MethodLength
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment