ghi 0.3.1 → 0.9.0.dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/ghi +2 -4
- data/lib/ghi.rb +112 -42
- data/lib/ghi/authorization.rb +71 -0
- data/lib/ghi/client.rb +126 -0
- data/lib/ghi/commands.rb +20 -0
- data/lib/ghi/commands/assign.rb +53 -0
- data/lib/ghi/commands/close.rb +45 -0
- data/lib/ghi/commands/command.rb +114 -0
- data/lib/ghi/commands/comment.rb +104 -0
- data/lib/ghi/commands/config.rb +35 -0
- data/lib/ghi/commands/edit.rb +49 -0
- data/lib/ghi/commands/help.rb +62 -0
- data/lib/ghi/commands/label.rb +153 -0
- data/lib/ghi/commands/list.rb +133 -0
- data/lib/ghi/commands/milestone.rb +150 -0
- data/lib/ghi/commands/open.rb +69 -0
- data/lib/ghi/commands/show.rb +21 -0
- data/lib/ghi/commands/version.rb +16 -0
- data/lib/ghi/formatting.rb +301 -0
- data/lib/ghi/formatting/colors.rb +295 -0
- data/lib/ghi/json.rb +1304 -0
- metadata +71 -49
- data/README.rdoc +0 -126
- data/lib/ghi/api.rb +0 -145
- data/lib/ghi/cli.rb +0 -657
- data/lib/ghi/issue.rb +0 -30
- data/spec/ghi/api_spec.rb +0 -218
- data/spec/ghi/cli_spec.rb +0 -267
- data/spec/ghi/issue_spec.rb +0 -26
- data/spec/ghi_spec.rb +0 -62
data/bin/ghi
CHANGED
data/lib/ghi.rb
CHANGED
@@ -1,57 +1,127 @@
|
|
1
|
-
require
|
2
|
-
require "yaml"
|
3
|
-
YAML::ENGINE.yamler = "syck" if YAML.const_defined? :ENGINE
|
1
|
+
require 'optparse'
|
4
2
|
|
5
3
|
module GHI
|
6
|
-
|
4
|
+
autoload :Authorization, 'ghi/authorization'
|
5
|
+
autoload :Client, 'ghi/client'
|
6
|
+
autoload :Commands, 'ghi/commands'
|
7
|
+
autoload :Formatting, 'ghi/formatting'
|
7
8
|
|
8
9
|
class << self
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
if
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
10
|
+
def execute args
|
11
|
+
STDOUT.sync = true
|
12
|
+
|
13
|
+
if index = args.index { |arg| arg !~ /^-/ }
|
14
|
+
command_name = args.delete_at index
|
15
|
+
command_args = args.slice! index, args.length
|
16
|
+
end
|
17
|
+
command_args ||= []
|
18
|
+
|
19
|
+
option_parser = OptionParser.new do |opts|
|
20
|
+
opts.banner = <<EOF
|
21
|
+
usage: ghi [--version] [--help] <command> [<args>] [ -- [<user>/]<repo>]
|
22
|
+
EOF
|
23
|
+
# opts.banner = <<EOF
|
24
|
+
# usage: ghi [--version] [-p|--paginate|--no-pager] [--help] <command> [<args>]
|
25
|
+
# [ -- [<user>/]<repo>]
|
26
|
+
# EOF
|
27
|
+
opts.on('--version') { command_name = 'version' }
|
28
|
+
opts.on '-p', '--paginate', '--[no-]pager' do |paginate|
|
29
|
+
|
30
|
+
end
|
31
|
+
opts.on '--help' do
|
32
|
+
command_args.unshift(*args)
|
33
|
+
command_args.unshift command_name if command_name
|
34
|
+
args.clear
|
35
|
+
command_name = 'help'
|
36
|
+
end
|
37
|
+
opts.on '--[no-]color' do |colorize|
|
38
|
+
Formatting::Colors.colorize = colorize
|
39
|
+
end
|
40
|
+
opts.on('-v') { self.v = true }
|
41
|
+
opts.on('-h') { raise OptionParser::InvalidOption }
|
18
42
|
end
|
19
|
-
@login
|
20
|
-
end
|
21
43
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
warn "Please configure your GitHub token."
|
28
|
-
puts
|
29
|
-
puts "E.g., git config --global github.token [your token]"
|
30
|
-
puts "Or set the environment variable GITHUB_TOKEN"
|
31
|
-
puts
|
32
|
-
puts "Find your token here: https://github.com/account/admin"
|
33
|
-
abort
|
34
|
-
elsif @token.sub!(/^!/, '')
|
35
|
-
@token = `#@token`
|
44
|
+
begin
|
45
|
+
option_parser.parse! args
|
46
|
+
rescue OptionParser::InvalidOption => e
|
47
|
+
warn e.message.capitalize
|
48
|
+
abort option_parser.banner
|
36
49
|
end
|
37
|
-
|
50
|
+
|
51
|
+
if command_name.nil? || command_name == 'help'
|
52
|
+
Commands::Help.execute command_args, option_parser.banner
|
53
|
+
else
|
54
|
+
command_name = fetch_alias command_name, command_args
|
55
|
+
begin
|
56
|
+
command = Commands.const_get command_name.capitalize
|
57
|
+
rescue NameError
|
58
|
+
abort "ghi: '#{command_name}' is not a ghi command. See 'ghi --help'."
|
59
|
+
end
|
60
|
+
|
61
|
+
# Post-command help option parsing.
|
62
|
+
Commands::Help.execute [command_name] if command_args.first == '--help'
|
63
|
+
|
64
|
+
begin
|
65
|
+
command.execute command_args
|
66
|
+
rescue OptionParser::ParseError, Commands::MissingArgument => e
|
67
|
+
warn "#{e.message.capitalize}\n"
|
68
|
+
abort command.new([]).options.to_s
|
69
|
+
rescue Client::Error => e
|
70
|
+
if e.response.is_a?(Net::HTTPNotFound) && Authorization.token.nil?
|
71
|
+
raise Authorization::Required
|
72
|
+
else
|
73
|
+
abort e.message
|
74
|
+
end
|
75
|
+
rescue SocketError => e
|
76
|
+
abort "Couldn't find internet."
|
77
|
+
rescue Errno::ECONNREFUSED => e
|
78
|
+
abort "Couldn't connect to GitHub."
|
79
|
+
rescue Errno::ETIMEDOUT => e
|
80
|
+
abort 'Timed out looking for GitHub.'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
rescue Authorization::Required => e
|
84
|
+
retry if Authorization.authorize!
|
85
|
+
warn e.message
|
86
|
+
if Authorization.token
|
87
|
+
warn <<EOF
|
88
|
+
|
89
|
+
Not authorized for this action with your token. To regenerate a new token:
|
90
|
+
EOF
|
91
|
+
end
|
92
|
+
warn <<EOF
|
93
|
+
|
94
|
+
Please run 'ghi config --auth <username>:<password>'
|
95
|
+
EOF
|
96
|
+
exit 1
|
38
97
|
end
|
39
98
|
|
99
|
+
attr_accessor :v
|
100
|
+
alias v? v
|
101
|
+
|
40
102
|
private
|
41
103
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
104
|
+
ALIASES = {
|
105
|
+
'c' => %w(close),
|
106
|
+
'claim' => %w(assign),
|
107
|
+
'e' => %w(edit),
|
108
|
+
'l' => %w(list),
|
109
|
+
'L' => %w(label),
|
110
|
+
'm' => %w(comment),
|
111
|
+
'o' => %w(open),
|
112
|
+
'reopen' => %w(open),
|
113
|
+
'rm' => %w(close),
|
114
|
+
's' => %w(show),
|
115
|
+
'st' => %w(list),
|
116
|
+
'tag' => %w(label),
|
117
|
+
'unassign' => %w(assign -d)
|
118
|
+
}
|
48
119
|
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
false
|
120
|
+
def fetch_alias command, args
|
121
|
+
return command unless fetched = ALIASES[command]
|
122
|
+
command = fetched.shift
|
123
|
+
args.unshift(*fetched)
|
124
|
+
command
|
55
125
|
end
|
56
126
|
end
|
57
127
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module GHI
|
2
|
+
module Authorization
|
3
|
+
extend Formatting
|
4
|
+
|
5
|
+
class Required < RuntimeError
|
6
|
+
end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def token
|
10
|
+
return @token if defined? @token
|
11
|
+
@token = config 'ghi.token'
|
12
|
+
end
|
13
|
+
|
14
|
+
def authorize! user = username, pass = password, local = true
|
15
|
+
return false unless user && pass
|
16
|
+
|
17
|
+
res = throb {
|
18
|
+
Client.new(user, pass).post(
|
19
|
+
'/authorizations',
|
20
|
+
:scopes => %w(public_repo repo),
|
21
|
+
:note => 'ghi',
|
22
|
+
:note_url => 'https://github.com/stephencelis/ghi'
|
23
|
+
)
|
24
|
+
}
|
25
|
+
@token = res['token']
|
26
|
+
|
27
|
+
run = []
|
28
|
+
unless username
|
29
|
+
run << "git config#{' --global ' unless local} github.user #{user}"
|
30
|
+
end
|
31
|
+
run << "git config#{' --global ' unless local} ghi.token #{token}"
|
32
|
+
|
33
|
+
system run.join('; ')
|
34
|
+
|
35
|
+
unless local
|
36
|
+
at_exit do
|
37
|
+
warn <<EOF
|
38
|
+
Your ~/.gitconfig has been modified by way of:
|
39
|
+
|
40
|
+
#{run.join "\n "}
|
41
|
+
|
42
|
+
#{bright { blink { 'Do not check this change into public source control!' } }}
|
43
|
+
Alternatively, set the following env var in a private dotfile:
|
44
|
+
|
45
|
+
export GHI_TOKEN="#{token}"
|
46
|
+
EOF
|
47
|
+
end
|
48
|
+
end
|
49
|
+
rescue Client::Error => e
|
50
|
+
abort e.message
|
51
|
+
end
|
52
|
+
|
53
|
+
def username
|
54
|
+
return @username if defined? @username
|
55
|
+
@username = config 'github.user'
|
56
|
+
end
|
57
|
+
|
58
|
+
def password
|
59
|
+
return @password if defined? @password
|
60
|
+
@password = config 'github.password'
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def config key
|
66
|
+
value = ENV["#{key.upcase.gsub '.', '_'}"] || `git config #{key}`.chomp
|
67
|
+
value unless value.empty?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/ghi/client.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'net/https'
|
3
|
+
|
4
|
+
unless defined? Net::HTTP::Patch
|
5
|
+
Net::HTTP::Patch = Class.new Net::HTTP::Post do
|
6
|
+
METHOD = 'PATCH'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module GHI
|
11
|
+
class Client
|
12
|
+
autoload :JSON, 'ghi/json'
|
13
|
+
|
14
|
+
class Error < RuntimeError
|
15
|
+
attr_reader :response
|
16
|
+
def initialize response
|
17
|
+
@response, @json = response, JSON.parse(response.body)
|
18
|
+
end
|
19
|
+
|
20
|
+
def body() @json end
|
21
|
+
def message() body['message'] end
|
22
|
+
def errors() [*body['errors']] end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Response
|
26
|
+
def initialize response
|
27
|
+
@response = response
|
28
|
+
end
|
29
|
+
|
30
|
+
def body
|
31
|
+
@body ||= JSON.parse @response.body
|
32
|
+
end
|
33
|
+
|
34
|
+
def next_page() links['next'] end
|
35
|
+
def last_page() links['last'] end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def links
|
40
|
+
return @links if defined? @links
|
41
|
+
@links = {}
|
42
|
+
if links = @response['Link']
|
43
|
+
links.scan(/<([^>]+)>; rel="([^"]+)"/).each { |l, r| @links[r] = l }
|
44
|
+
end
|
45
|
+
@links
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
CONTENT_TYPE = 'application/vnd.github+json'
|
50
|
+
METHODS = {
|
51
|
+
:head => Net::HTTP::Head,
|
52
|
+
:get => Net::HTTP::Get,
|
53
|
+
:post => Net::HTTP::Post,
|
54
|
+
:put => Net::HTTP::Put,
|
55
|
+
:patch => Net::HTTP::Patch,
|
56
|
+
:delete => Net::HTTP::Delete
|
57
|
+
}
|
58
|
+
|
59
|
+
attr_reader :username, :password
|
60
|
+
def initialize username = nil, password = nil
|
61
|
+
@username, @password = username, password
|
62
|
+
end
|
63
|
+
|
64
|
+
def head path, options = {}
|
65
|
+
request :head, path, options
|
66
|
+
end
|
67
|
+
|
68
|
+
def get path, params = {}, options = {}
|
69
|
+
request :get, path, options.merge(:params => params)
|
70
|
+
end
|
71
|
+
|
72
|
+
def post path, body = nil, options = {}
|
73
|
+
request :post, path, options.merge(:body => body)
|
74
|
+
end
|
75
|
+
|
76
|
+
def put path, body = nil, options = {}
|
77
|
+
request :put, path, options.merge(:body => body)
|
78
|
+
end
|
79
|
+
|
80
|
+
def patch path, body = nil, options = {}
|
81
|
+
request :patch, path, options.merge(:body => body)
|
82
|
+
end
|
83
|
+
|
84
|
+
def delete path, options = {}
|
85
|
+
request :delete, path, options
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def request method, path, options
|
91
|
+
if params = options[:params] and !params.empty?
|
92
|
+
q = params.map { |k, v| "#{CGI.escape k.to_s}=#{CGI.escape v.to_s}" }
|
93
|
+
path += "?#{q.join '&'}"
|
94
|
+
end
|
95
|
+
|
96
|
+
req = METHODS[method].new path, 'Accept' => CONTENT_TYPE
|
97
|
+
if GHI::Authorization.token
|
98
|
+
req['Authorization'] = "token #{GHI::Authorization.token}"
|
99
|
+
end
|
100
|
+
if options.key? :body
|
101
|
+
req['Content-Type'] = CONTENT_TYPE
|
102
|
+
req.body = options[:body] ? JSON.dump(options[:body]) : ''
|
103
|
+
end
|
104
|
+
req.basic_auth username, password if username && password
|
105
|
+
|
106
|
+
http = Net::HTTP.new 'api.github.com', 443
|
107
|
+
http.use_ssl = true
|
108
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME 1.8.7
|
109
|
+
|
110
|
+
GHI.v? and puts "===> #{method.to_s.upcase} #{path} #{req.body}"
|
111
|
+
res = http.start { http.request req }
|
112
|
+
GHI.v? and puts "<=== #{res.code}: #{res.body}"
|
113
|
+
|
114
|
+
case res
|
115
|
+
when Net::HTTPSuccess
|
116
|
+
return Response.new(res)
|
117
|
+
when Net::HTTPUnauthorized
|
118
|
+
if password.nil?
|
119
|
+
raise Authorization::Required, 'Authorization required'
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
raise Error, res
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/lib/ghi/commands.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module GHI
|
2
|
+
module Commands
|
3
|
+
autoload :Command, 'ghi/commands/command'
|
4
|
+
|
5
|
+
autoload :List, 'ghi/commands/list'
|
6
|
+
autoload :Open, 'ghi/commands/open'
|
7
|
+
autoload :Assign, 'ghi/commands/assign'
|
8
|
+
autoload :Close, 'ghi/commands/close'
|
9
|
+
autoload :Comment, 'ghi/commands/comment'
|
10
|
+
autoload :Config, 'ghi/commands/config'
|
11
|
+
autoload :Edit, 'ghi/commands/edit'
|
12
|
+
autoload :Help, 'ghi/commands/help'
|
13
|
+
autoload :Label, 'ghi/commands/label'
|
14
|
+
autoload :Milestone, 'ghi/commands/milestone'
|
15
|
+
autoload :Reopen, 'ghi/commands/reopen'
|
16
|
+
autoload :Show, 'ghi/commands/show'
|
17
|
+
autoload :Unassign, 'ghi/commands/unassign'
|
18
|
+
autoload :Version, 'ghi/commands/version'
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module GHI
|
2
|
+
module Commands
|
3
|
+
class Assign < Command
|
4
|
+
def options
|
5
|
+
OptionParser.new do |opts|
|
6
|
+
opts.banner = <<EOF
|
7
|
+
usage: ghi assign [options] [<issueno>]
|
8
|
+
or: ghi assign <issueno> <user>
|
9
|
+
or: ghi unassign <issueno>
|
10
|
+
EOF
|
11
|
+
opts.separator ''
|
12
|
+
opts.on(
|
13
|
+
'-u', '--assignee <user>', 'assign to specified user'
|
14
|
+
) do |assignee|
|
15
|
+
assigns[:assignee] = assignee
|
16
|
+
end
|
17
|
+
opts.on '-d', '--no-assignee', 'unassign this issue' do
|
18
|
+
assigns[:assignee] = nil
|
19
|
+
end
|
20
|
+
opts.on '-l', '--list', 'list assigned issues' do
|
21
|
+
self.action = 'list'
|
22
|
+
end
|
23
|
+
opts.separator ''
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def execute
|
28
|
+
self.action = 'edit'
|
29
|
+
assigns[:args] = []
|
30
|
+
|
31
|
+
require_repo
|
32
|
+
extract_issue
|
33
|
+
options.parse! args
|
34
|
+
|
35
|
+
unless assigns.key? :assignee
|
36
|
+
assigns[:assignee] = args.pop || Authorization.username
|
37
|
+
end
|
38
|
+
if assigns.key? :assignee
|
39
|
+
assigns[:args].concat(
|
40
|
+
assigns[:assignee] ? %W(-u #{assigns[:assignee]}) : %w(--no-assign)
|
41
|
+
)
|
42
|
+
end
|
43
|
+
assigns[:args] << issue if issue
|
44
|
+
assigns[:args].concat %W(-- #{repo})
|
45
|
+
|
46
|
+
case action
|
47
|
+
when 'list' then List.execute assigns[:args]
|
48
|
+
when 'edit' then Edit.execute assigns[:args]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|