-
-
Save yanshiyason/3c45080b27ce257317b8f06415218192 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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