ghi 0.9.0.dev1 → 0.9.0.20120627
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 +1 -1
- data/lib/ghi.rb +46 -19
- data/lib/ghi/authorization.rb +11 -15
- data/lib/ghi/client.rb +2 -2
- data/lib/ghi/commands/close.rb +6 -1
- data/lib/ghi/commands/command.rb +24 -15
- data/lib/ghi/commands/comment.rb +39 -15
- data/lib/ghi/commands/config.rb +24 -4
- data/lib/ghi/commands/edit.rb +89 -22
- data/lib/ghi/commands/label.rb +1 -1
- data/lib/ghi/commands/list.rb +33 -22
- data/lib/ghi/commands/milestone.rb +78 -21
- data/lib/ghi/commands/open.rb +39 -14
- data/lib/ghi/commands/show.rb +16 -8
- data/lib/ghi/commands/version.rb +1 -1
- data/lib/ghi/editor.rb +39 -17
- data/lib/ghi/formatting.rb +125 -40
- data/lib/ghi/formatting/colors.rb +1 -1
- data/lib/ghi/web.rb +26 -0
- metadata +57 -48
data/bin/ghi
CHANGED
data/lib/ghi.rb
CHANGED
@@ -6,28 +6,29 @@ module GHI
|
|
6
6
|
autoload :Commands, 'ghi/commands'
|
7
7
|
autoload :Editor, 'ghi/editor'
|
8
8
|
autoload :Formatting, 'ghi/formatting'
|
9
|
+
autoload :Web, 'ghi/web'
|
9
10
|
|
10
11
|
class << self
|
11
12
|
def execute args
|
12
13
|
STDOUT.sync = true
|
13
14
|
|
15
|
+
double_dash = args.index { |arg| arg == '--' }
|
14
16
|
if index = args.index { |arg| arg !~ /^-/ }
|
15
|
-
|
16
|
-
|
17
|
+
if double_dash.nil? || index < double_dash
|
18
|
+
command_name = args.delete_at index
|
19
|
+
command_args = args.slice! index, args.length
|
20
|
+
end
|
17
21
|
end
|
18
22
|
command_args ||= []
|
19
23
|
|
20
24
|
option_parser = OptionParser.new do |opts|
|
21
25
|
opts.banner = <<EOF
|
22
|
-
usage: ghi [--version] [--help] <command> [<args>]
|
26
|
+
usage: ghi [--version] [-p|--paginate|--no-pager] [--help] <command> [<args>]
|
27
|
+
[ -- [<user>/]<repo>]
|
23
28
|
EOF
|
24
|
-
# opts.banner = <<EOF
|
25
|
-
# usage: ghi [--version] [-p|--paginate|--no-pager] [--help] <command> [<args>]
|
26
|
-
# [ -- [<user>/]<repo>]
|
27
|
-
# EOF
|
28
29
|
opts.on('--version') { command_name = 'version' }
|
29
30
|
opts.on '-p', '--paginate', '--[no-]pager' do |paginate|
|
30
|
-
|
31
|
+
GHI::Formatting.paginate = paginate
|
31
32
|
end
|
32
33
|
opts.on '--help' do
|
33
34
|
command_args.unshift(*args)
|
@@ -38,8 +39,17 @@ EOF
|
|
38
39
|
opts.on '--[no-]color' do |colorize|
|
39
40
|
Formatting::Colors.colorize = colorize
|
40
41
|
end
|
41
|
-
opts.on
|
42
|
-
|
42
|
+
opts.on '-l' do
|
43
|
+
if command_name
|
44
|
+
raise OptionParser::InvalidOption
|
45
|
+
else
|
46
|
+
command_name = 'list'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
opts.on '-v' do
|
50
|
+
command_name ? self.v = true : command_name = 'version'
|
51
|
+
end
|
52
|
+
opts.on('-V') { command_name = 'version' }
|
43
53
|
end
|
44
54
|
|
45
55
|
begin
|
@@ -75,40 +85,46 @@ EOF
|
|
75
85
|
end
|
76
86
|
rescue SocketError => e
|
77
87
|
abort "Couldn't find internet."
|
78
|
-
rescue Errno::ECONNREFUSED => e
|
79
|
-
abort "Couldn't
|
80
|
-
rescue Errno::ETIMEDOUT => e
|
81
|
-
abort 'Timed out looking for GitHub.'
|
88
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
89
|
+
abort "Couldn't find GitHub."
|
82
90
|
end
|
83
91
|
end
|
84
92
|
rescue Authorization::Required => e
|
85
93
|
retry if Authorization.authorize!
|
86
94
|
warn e.message
|
87
95
|
if Authorization.token
|
88
|
-
warn <<EOF
|
96
|
+
warn <<EOF.chomp
|
89
97
|
|
90
98
|
Not authorized for this action with your token. To regenerate a new token:
|
91
99
|
EOF
|
92
100
|
end
|
93
101
|
warn <<EOF
|
94
102
|
|
95
|
-
Please run 'ghi config --auth <username
|
103
|
+
Please run 'ghi config --auth <username>'
|
96
104
|
EOF
|
97
105
|
exit 1
|
98
106
|
end
|
99
107
|
|
108
|
+
def config key
|
109
|
+
var = key.gsub('core', 'git').gsub('.', '_').upcase
|
110
|
+
value = ENV[var] || `git config #{key}`.chomp
|
111
|
+
value unless value.empty?
|
112
|
+
end
|
113
|
+
|
100
114
|
attr_accessor :v
|
101
115
|
alias v? v
|
102
116
|
|
103
117
|
private
|
104
118
|
|
105
|
-
ALIASES = {
|
106
|
-
|
119
|
+
ALIASES = Hash.new { |_, key|
|
120
|
+
[key] if /^\d+$/ === key
|
121
|
+
}.update(
|
107
122
|
'claim' => %w(assign),
|
108
123
|
'e' => %w(edit),
|
109
124
|
'l' => %w(list),
|
110
125
|
'L' => %w(label),
|
111
126
|
'm' => %w(comment),
|
127
|
+
'M' => %w(milestone),
|
112
128
|
'o' => %w(open),
|
113
129
|
'reopen' => %w(open),
|
114
130
|
'rm' => %w(close),
|
@@ -116,10 +132,21 @@ EOF
|
|
116
132
|
'st' => %w(list),
|
117
133
|
'tag' => %w(label),
|
118
134
|
'unassign' => %w(assign -d)
|
119
|
-
|
135
|
+
)
|
120
136
|
|
121
137
|
def fetch_alias command, args
|
122
138
|
return command unless fetched = ALIASES[command]
|
139
|
+
|
140
|
+
# If the <command> is an issue number, check the options to see if an
|
141
|
+
# edit or show is desired.
|
142
|
+
if fetched.first =~ /^\d+$/
|
143
|
+
edit_options = Commands::Edit.new([]).options.top.list
|
144
|
+
edit_options.reject! { |arg| !arg.is_a?(OptionParser::Switch) }
|
145
|
+
edit_options.map! { |arg| [arg.short, arg.long] }
|
146
|
+
edit_options.flatten!
|
147
|
+
fetched.unshift((edit_options & args).empty? ? 'show' : 'edit')
|
148
|
+
end
|
149
|
+
|
123
150
|
command = fetched.shift
|
124
151
|
args.unshift(*fetched)
|
125
152
|
command
|
data/lib/ghi/authorization.rb
CHANGED
@@ -1,20 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module GHI
|
2
4
|
module Authorization
|
3
5
|
extend Formatting
|
4
6
|
|
5
7
|
class Required < RuntimeError
|
8
|
+
def message() 'Authorization required.' end
|
6
9
|
end
|
7
10
|
|
8
11
|
class << self
|
9
12
|
def token
|
10
13
|
return @token if defined? @token
|
11
|
-
@token = config 'ghi.token'
|
14
|
+
@token = GHI.config 'ghi.token'
|
12
15
|
end
|
13
16
|
|
14
17
|
def authorize! user = username, pass = password, local = true
|
15
18
|
return false unless user && pass
|
16
19
|
|
17
|
-
res = throb {
|
20
|
+
res = throb(54, "✔\r") {
|
18
21
|
Client.new(user, pass).post(
|
19
22
|
'/authorizations',
|
20
23
|
:scopes => %w(public_repo repo),
|
@@ -23,12 +26,12 @@ module GHI
|
|
23
26
|
)
|
24
27
|
}
|
25
28
|
@token = res.body['token']
|
26
|
-
|
29
|
+
|
27
30
|
run = []
|
28
31
|
unless username
|
29
|
-
run << "git config#{' --global
|
32
|
+
run << "git config#{' --global' unless local} github.user #{user}"
|
30
33
|
end
|
31
|
-
run << "git config#{' --global
|
34
|
+
run << "git config#{' --global' unless local} ghi.token #{token}"
|
32
35
|
|
33
36
|
system run.join('; ')
|
34
37
|
|
@@ -47,24 +50,17 @@ EOF
|
|
47
50
|
end
|
48
51
|
end
|
49
52
|
rescue Client::Error => e
|
50
|
-
abort e.message
|
53
|
+
abort "#{e.message}#{CURSOR[:column][0]}"
|
51
54
|
end
|
52
55
|
|
53
56
|
def username
|
54
57
|
return @username if defined? @username
|
55
|
-
@username = config 'github.user'
|
58
|
+
@username = GHI.config 'github.user'
|
56
59
|
end
|
57
60
|
|
58
61
|
def password
|
59
62
|
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?
|
63
|
+
@password = GHI.config 'github.password'
|
68
64
|
end
|
69
65
|
end
|
70
66
|
end
|
data/lib/ghi/client.rb
CHANGED
@@ -106,9 +106,9 @@ module GHI
|
|
106
106
|
http.use_ssl = true
|
107
107
|
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME 1.8.7
|
108
108
|
|
109
|
-
GHI.v? and puts "===> #{method.to_s.upcase} #{path} #{req.body}"
|
109
|
+
GHI.v? and puts "\r===> #{method.to_s.upcase} #{path} #{req.body}"
|
110
110
|
res = http.start { http.request req }
|
111
|
-
GHI.v? and puts "<=== #{res.code}: #{res.body}"
|
111
|
+
GHI.v? and puts "\r<=== #{res.code}: #{res.body}"
|
112
112
|
|
113
113
|
case res
|
114
114
|
when Net::HTTPSuccess
|
data/lib/ghi/commands/close.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
module GHI
|
2
2
|
module Commands
|
3
3
|
class Close < Command
|
4
|
+
attr_accessor :web
|
5
|
+
|
4
6
|
def options
|
5
7
|
OptionParser.new do |opts|
|
6
8
|
opts.banner = <<EOF
|
@@ -10,6 +12,7 @@ EOF
|
|
10
12
|
opts.on '-l', '--list', 'list closed issues' do
|
11
13
|
assigns[:command] = List
|
12
14
|
end
|
15
|
+
opts.on('-w', '--web') { self.web = true }
|
13
16
|
opts.separator ''
|
14
17
|
opts.separator 'Issue modification options'
|
15
18
|
opts.on '-m', '--message [<text>]', 'close with message' do |text|
|
@@ -24,7 +27,9 @@ EOF
|
|
24
27
|
require_repo
|
25
28
|
|
26
29
|
if list?
|
27
|
-
|
30
|
+
args.unshift(*%W(-sc -- #{repo}))
|
31
|
+
args.unshift '-w' if web
|
32
|
+
List.execute args
|
28
33
|
else
|
29
34
|
require_issue
|
30
35
|
if assigns.key? :comment
|
data/lib/ghi/commands/command.rb
CHANGED
@@ -6,12 +6,16 @@ module GHI
|
|
6
6
|
class Command
|
7
7
|
include Formatting
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
class << self
|
10
|
+
attr_accessor :detected_repo
|
11
|
+
|
12
|
+
def execute args
|
13
|
+
command = new args
|
14
|
+
if i = args.index('--')
|
15
|
+
command.repo = args.slice!(i, args.length)[1] # Raise if too many?
|
16
|
+
end
|
17
|
+
command.execute
|
13
18
|
end
|
14
|
-
command.execute
|
15
19
|
end
|
16
20
|
|
17
21
|
attr_reader :args
|
@@ -33,8 +37,10 @@ module GHI
|
|
33
37
|
|
34
38
|
def repo
|
35
39
|
return @repo if defined? @repo
|
36
|
-
@repo =
|
37
|
-
@repo
|
40
|
+
@repo = GHI.config('ghi.repo') || detect_repo
|
41
|
+
if @repo && !@repo.include?('/')
|
42
|
+
@repo = [Authorization.username, @repo].join '/'
|
43
|
+
end
|
38
44
|
@repo
|
39
45
|
end
|
40
46
|
alias extract_repo repo
|
@@ -57,20 +63,23 @@ module GHI
|
|
57
63
|
end
|
58
64
|
|
59
65
|
def detect_repo
|
60
|
-
|
61
|
-
remotes.
|
66
|
+
remote = remotes.find { |r| r[:remote] == 'upstream' }
|
67
|
+
remote ||= remotes.find { |r| r[:remote] == 'origin' }
|
68
|
+
remote ||= remotes.find { |r| r[:user] == Authorization.username }
|
69
|
+
Command.detected_repo = true and remote[:repo] if remote
|
70
|
+
end
|
62
71
|
|
63
|
-
|
72
|
+
def remotes
|
73
|
+
return @remotes if defined? @remotes
|
74
|
+
@remotes = `git config --get-regexp remote\..+\.url`.split "\n"
|
75
|
+
@remotes.reject! { |r| !r.include? 'github.com'}
|
76
|
+
@remotes.map! { |r|
|
64
77
|
remote, user, repo = r.scan(
|
65
78
|
%r{remote\.([^\.]+)\.url .*?([^:/]+)/([^/\s]+?)(?:\.git)?$}
|
66
79
|
).flatten
|
67
80
|
{ :remote => remote, :user => user, :repo => "#{user}/#{repo}" }
|
68
81
|
}
|
69
|
-
|
70
|
-
remote = remotes.find { |r| r[:remote] == 'upstream' }
|
71
|
-
remote ||= remotes.find { |r| r[:remote] == 'origin' }
|
72
|
-
remote ||= remotes.find { |r| r[:user] == Authorization.username }
|
73
|
-
remote[:repo] if remote
|
82
|
+
@remotes
|
74
83
|
end
|
75
84
|
|
76
85
|
def issue
|
data/lib/ghi/commands/comment.rb
CHANGED
@@ -2,6 +2,8 @@ module GHI
|
|
2
2
|
module Commands
|
3
3
|
class Comment < Command
|
4
4
|
attr_accessor :comment
|
5
|
+
attr_accessor :verbose
|
6
|
+
attr_accessor :web
|
5
7
|
|
6
8
|
def options
|
7
9
|
OptionParser.new do |opts|
|
@@ -12,6 +14,7 @@ EOF
|
|
12
14
|
opts.on '-l', '--list', 'list comments' do
|
13
15
|
self.action = 'list'
|
14
16
|
end
|
17
|
+
opts.on('-w', '--web') { self.web = true }
|
15
18
|
# opts.on '-v', '--verbose', 'list events, too'
|
16
19
|
opts.separator ''
|
17
20
|
opts.separator 'Comment modification options'
|
@@ -27,6 +30,9 @@ EOF
|
|
27
30
|
opts.on '--close', 'close associated issue' do
|
28
31
|
self.action = 'close'
|
29
32
|
end
|
33
|
+
opts.on '-v', '--verbose' do
|
34
|
+
self.verbose = true
|
35
|
+
end
|
30
36
|
opts.separator ''
|
31
37
|
end
|
32
38
|
end
|
@@ -46,7 +52,11 @@ EOF
|
|
46
52
|
res = throb { api.get res.next_page }
|
47
53
|
end
|
48
54
|
when 'create'
|
49
|
-
|
55
|
+
if web
|
56
|
+
Web.new(repo).open "issues/#{issue}#issue_comment_form"
|
57
|
+
else
|
58
|
+
create
|
59
|
+
end
|
50
60
|
when 'update', 'destroy'
|
51
61
|
res = index
|
52
62
|
res = throb { api.get res.last_page } if res.last_page
|
@@ -66,40 +76,54 @@ EOF
|
|
66
76
|
protected
|
67
77
|
|
68
78
|
def index
|
69
|
-
throb { api.get uri }
|
79
|
+
throb { api.get uri, :per_page => 100 }
|
70
80
|
end
|
71
81
|
|
72
|
-
def create
|
73
|
-
require_body
|
82
|
+
def create message = 'Commented.'
|
83
|
+
e = require_body
|
74
84
|
c = throb { api.post uri, assigns }.body
|
75
|
-
puts format_comment
|
76
|
-
puts
|
85
|
+
puts format_comment(c)
|
86
|
+
puts message
|
87
|
+
e.unlink if e
|
77
88
|
end
|
78
89
|
|
79
90
|
def update
|
80
|
-
|
81
|
-
c = throb { api.patch uri, assigns }.body
|
82
|
-
puts format_comment c
|
83
|
-
puts 'Updated.'
|
91
|
+
create 'Comment updated.'
|
84
92
|
end
|
85
93
|
|
86
94
|
def destroy
|
87
95
|
throb { api.delete uri }
|
88
|
-
puts '
|
96
|
+
puts 'Comment deleted.'
|
89
97
|
end
|
90
98
|
|
91
99
|
private
|
92
100
|
|
93
101
|
def uri
|
94
|
-
|
102
|
+
if comment
|
103
|
+
comment['url']
|
104
|
+
else
|
105
|
+
"/repos/#{repo}/issues/#{issue}/comments"
|
106
|
+
end
|
95
107
|
end
|
96
108
|
|
97
109
|
def require_body
|
110
|
+
assigns[:body] = args.join ' ' unless args.empty?
|
98
111
|
return if assigns[:body]
|
99
|
-
|
100
|
-
|
101
|
-
|
112
|
+
if issue && verbose
|
113
|
+
i = throb { api.get "/repos/#{repo}/issues/#{issue}" }.body
|
114
|
+
else
|
115
|
+
i = {'number'=>issue}
|
116
|
+
end
|
117
|
+
filename = "GHI_COMMENT_#{issue}"
|
118
|
+
filename << "_#{comment['id']}" if comment
|
119
|
+
e = Editor.new filename
|
120
|
+
message = e.gets format_comment_editor(i, comment)
|
121
|
+
e.unlink 'No comment.' if message.nil? || message.empty?
|
122
|
+
if comment && message.strip == comment['body'].strip
|
123
|
+
e.unlink 'No change.'
|
124
|
+
end
|
102
125
|
assigns[:body] = message if message
|
126
|
+
e
|
103
127
|
end
|
104
128
|
end
|
105
129
|
end
|
data/lib/ghi/commands/config.rb
CHANGED
@@ -10,11 +10,9 @@ EOF
|
|
10
10
|
opts.on '--local', 'set for local repo only' do
|
11
11
|
assigns[:local] = true
|
12
12
|
end
|
13
|
-
opts.on '--auth [<username
|
13
|
+
opts.on '--auth [<username>]' do |username|
|
14
14
|
self.action = 'auth'
|
15
|
-
username
|
16
|
-
assigns[:username] = username
|
17
|
-
assigns[:password] = password
|
15
|
+
assigns[:username] = username || Authorization.username
|
18
16
|
end
|
19
17
|
opts.separator ''
|
20
18
|
end
|
@@ -25,11 +23,33 @@ EOF
|
|
25
23
|
options.parse! args.empty? ? %w(-h) : args
|
26
24
|
|
27
25
|
if self.action == 'auth'
|
26
|
+
assigns[:password] = Authorization.password || get_password
|
28
27
|
Authorization.authorize!(
|
29
28
|
assigns[:username], assigns[:password], assigns[:local]
|
30
29
|
)
|
31
30
|
end
|
32
31
|
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def get_password
|
36
|
+
print "Enter #{assigns[:username]}'s GitHub password (never stored): "
|
37
|
+
current_tty = `stty -g`
|
38
|
+
system 'stty raw -echo -icanon isig' if $?.success?
|
39
|
+
input = ''
|
40
|
+
while char = $stdin.getbyte and not (char == 13 or char == 10)
|
41
|
+
if char == 127 or char == 8
|
42
|
+
input[-1, 1] = '' unless input.empty?
|
43
|
+
else
|
44
|
+
input << char.chr
|
45
|
+
end
|
46
|
+
end
|
47
|
+
input
|
48
|
+
rescue Interrupt
|
49
|
+
print '^C'
|
50
|
+
ensure
|
51
|
+
system "stty #{current_tty}" unless current_tty.empty?
|
52
|
+
end
|
33
53
|
end
|
34
54
|
end
|
35
55
|
end
|