ghi 0.2.0 → 1.2.0
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.
- checksums.yaml +7 -0
- data/bin/ghi +2 -4
- data/lib/ghi/authorization.rb +142 -0
- data/lib/ghi/client.rb +148 -0
- data/lib/ghi/commands/assign.rb +54 -0
- data/lib/ghi/commands/close.rb +51 -0
- data/lib/ghi/commands/command.rb +133 -0
- data/lib/ghi/commands/comment.rb +168 -0
- data/lib/ghi/commands/config.rb +57 -0
- data/lib/ghi/commands/disable.rb +35 -0
- data/lib/ghi/commands/edit.rb +139 -0
- data/lib/ghi/commands/enable.rb +36 -0
- data/lib/ghi/commands/help.rb +65 -0
- data/lib/ghi/commands/label.rb +196 -0
- data/lib/ghi/commands/list.rb +204 -0
- data/lib/ghi/commands/milestone.rb +206 -0
- data/lib/ghi/commands/open.rb +102 -0
- data/lib/ghi/commands/show.rb +65 -0
- data/lib/ghi/commands/status.rb +32 -0
- data/lib/ghi/commands/version.rb +16 -0
- data/lib/ghi/commands.rb +23 -0
- data/lib/ghi/editor.rb +48 -0
- data/lib/ghi/formatting/colors.rb +364 -0
- data/lib/ghi/formatting.rb +523 -0
- data/lib/ghi/web.rb +37 -0
- data/lib/ghi.rb +151 -39
- metadata +95 -53
- data/History.rdoc +0 -180
- data/Manifest.txt +0 -14
- data/README.rdoc +0 -92
- data/lib/ghi/api.rb +0 -106
- data/lib/ghi/cli.rb +0 -608
- data/lib/ghi/issue.rb +0 -29
- data/spec/ghi/api_spec.rb +0 -194
- data/spec/ghi/cli_spec.rb +0 -258
- data/spec/ghi/issue_spec.rb +0 -26
- data/spec/ghi_spec.rb +0 -91
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
require 'date'
|
|
2
|
+
|
|
3
|
+
module GHI
|
|
4
|
+
module Commands
|
|
5
|
+
class List < Command
|
|
6
|
+
attr_accessor :web
|
|
7
|
+
attr_accessor :reverse
|
|
8
|
+
attr_accessor :quiet
|
|
9
|
+
attr_accessor :exclude_pull_requests
|
|
10
|
+
attr_accessor :pull_requests_only
|
|
11
|
+
|
|
12
|
+
def options
|
|
13
|
+
OptionParser.new do |opts|
|
|
14
|
+
opts.banner = 'usage: ghi list [options]'
|
|
15
|
+
opts.separator ''
|
|
16
|
+
opts.on '-g', '--global', 'all of your issues on GitHub' do
|
|
17
|
+
assigns[:filter] = 'all'
|
|
18
|
+
@repo = nil
|
|
19
|
+
end
|
|
20
|
+
opts.on(
|
|
21
|
+
'-s', '--state <in>', %w(open closed),
|
|
22
|
+
{'o'=>'open', 'c'=>'closed'}, "'open' or 'closed'"
|
|
23
|
+
) do |state|
|
|
24
|
+
assigns[:state] = state
|
|
25
|
+
end
|
|
26
|
+
opts.on(
|
|
27
|
+
'-L', '--label <labelname>...', Array, 'by label(s)'
|
|
28
|
+
) do |labels|
|
|
29
|
+
(assigns[:labels] ||= []).concat labels
|
|
30
|
+
end
|
|
31
|
+
opts.on(
|
|
32
|
+
'-N', '--not-label <labelname>...', Array, 'exclude with label(s)'
|
|
33
|
+
) do |labels|
|
|
34
|
+
(assigns[:exclude_labels] ||= []).concat labels
|
|
35
|
+
end
|
|
36
|
+
opts.on(
|
|
37
|
+
'--no-labels', 'do not print labels'
|
|
38
|
+
) do
|
|
39
|
+
assigns[:dont_print_labels] = true
|
|
40
|
+
end
|
|
41
|
+
opts.on(
|
|
42
|
+
'-S', '--sort <by>', %w(created updated comments),
|
|
43
|
+
{'c'=>'created','u'=>'updated','m'=>'comments'},
|
|
44
|
+
"'created', 'updated', or 'comments'"
|
|
45
|
+
) do |sort|
|
|
46
|
+
assigns[:sort] = sort
|
|
47
|
+
end
|
|
48
|
+
opts.on '--reverse', 'reverse (ascending) sort order' do
|
|
49
|
+
self.reverse = !reverse
|
|
50
|
+
end
|
|
51
|
+
opts.on('-p', '--pulls','list only pull requests') { self.pull_requests_only = true }
|
|
52
|
+
opts.on('-P', '--no-pulls','exclude pull requests') { self.exclude_pull_requests = true }
|
|
53
|
+
opts.on(
|
|
54
|
+
'--since <date>', 'issues more recent than',
|
|
55
|
+
"e.g., '2011-04-30'"
|
|
56
|
+
) do |date|
|
|
57
|
+
begin
|
|
58
|
+
assigns[:since] = DateTime.parse date # TODO: Better parsing.
|
|
59
|
+
rescue ArgumentError => e
|
|
60
|
+
raise OptionParser::InvalidArgument, e.message
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
opts.on('-v', '--verbose') { self.verbose = true }
|
|
64
|
+
opts.on('-w', '--web') { self.web = true }
|
|
65
|
+
opts.separator ''
|
|
66
|
+
opts.separator 'Global options'
|
|
67
|
+
opts.on(
|
|
68
|
+
'-f', '--filter <by>',
|
|
69
|
+
filters = %w[all assigned created mentioned subscribed],
|
|
70
|
+
Hash[filters.map { |f| [f[0, 1], f] }],
|
|
71
|
+
filters.map { |f| "'#{f}'" }.join(', ')
|
|
72
|
+
) do |filter|
|
|
73
|
+
assigns[:filter] = filter
|
|
74
|
+
end
|
|
75
|
+
opts.on '--mine', 'assigned to you' do
|
|
76
|
+
assigns[:filter] = 'assigned'
|
|
77
|
+
assigns[:assignee] = Authorization.username
|
|
78
|
+
end
|
|
79
|
+
opts.separator ''
|
|
80
|
+
opts.separator 'Project options'
|
|
81
|
+
opts.on(
|
|
82
|
+
'-M', '--[no-]milestone [<n>]', Integer,
|
|
83
|
+
'with (specified) milestone'
|
|
84
|
+
) do |milestone|
|
|
85
|
+
assigns[:milestone] = any_or_none_or milestone
|
|
86
|
+
end
|
|
87
|
+
opts.on(
|
|
88
|
+
'-u', '--[no-]assignee [<user>]', 'assigned to specified user'
|
|
89
|
+
) do |assignee|
|
|
90
|
+
assignee = assignee.sub /^@/, '' if assignee
|
|
91
|
+
assigns[:assignee] = any_or_none_or assignee
|
|
92
|
+
end
|
|
93
|
+
opts.on '--mine', 'assigned to you' do
|
|
94
|
+
assigns[:filter] = 'assigned'
|
|
95
|
+
assigns[:assignee] = Authorization.username
|
|
96
|
+
end
|
|
97
|
+
opts.on(
|
|
98
|
+
'--creator [<user>]', 'created by you or specified user'
|
|
99
|
+
) do |creator|
|
|
100
|
+
creator = creator.sub /^@/, '' if creator
|
|
101
|
+
assigns[:creator] = creator || Authorization.username
|
|
102
|
+
end
|
|
103
|
+
opts.on(
|
|
104
|
+
'-U', '--mentioned [<user>]', 'mentioning you or specified user'
|
|
105
|
+
) do |mentioned|
|
|
106
|
+
assigns[:mentioned] = mentioned || Authorization.username
|
|
107
|
+
end
|
|
108
|
+
opts.on(
|
|
109
|
+
'-O', '--org <organization>', 'in repos within an organization you belong to'
|
|
110
|
+
) do |org|
|
|
111
|
+
assigns[:org] = org
|
|
112
|
+
@repo = nil
|
|
113
|
+
end
|
|
114
|
+
opts.separator ''
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def execute
|
|
119
|
+
if index = args.index { |arg| /^@/ === arg }
|
|
120
|
+
assigns[:assignee] = args.delete_at(index)[1..-1]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
begin
|
|
124
|
+
options.parse! args
|
|
125
|
+
@repo ||= ARGV[0] if ARGV.one?
|
|
126
|
+
rescue OptionParser::InvalidOption => e
|
|
127
|
+
fallback.parse! e.args
|
|
128
|
+
retry
|
|
129
|
+
end
|
|
130
|
+
assigns[:labels] = assigns[:labels].join ',' if assigns[:labels]
|
|
131
|
+
if assigns[:exclude_labels]
|
|
132
|
+
assigns[:exclude_labels] = assigns[:exclude_labels].join ','
|
|
133
|
+
end
|
|
134
|
+
if reverse
|
|
135
|
+
assigns[:sort] ||= 'created'
|
|
136
|
+
assigns[:direction] = 'asc'
|
|
137
|
+
end
|
|
138
|
+
if web
|
|
139
|
+
Web.new(repo || 'dashboard').open 'issues', assigns
|
|
140
|
+
else
|
|
141
|
+
assigns[:per_page] = 100
|
|
142
|
+
unless quiet
|
|
143
|
+
print header = format_issues_header
|
|
144
|
+
print "\n" unless paginate?
|
|
145
|
+
end
|
|
146
|
+
res = throb(
|
|
147
|
+
0, format_state(assigns[:state], quiet ? CURSOR[:up][1] : '#')
|
|
148
|
+
) { api.get uri, assigns }
|
|
149
|
+
print "\r#{CURSOR[:up][1]}" if header && paginate?
|
|
150
|
+
page header do
|
|
151
|
+
issues = res.body
|
|
152
|
+
|
|
153
|
+
if exclude_pull_requests || pull_requests_only
|
|
154
|
+
prs, issues = issues.partition { |i| i['pull_request'].values.any? }
|
|
155
|
+
issues = prs if pull_requests_only
|
|
156
|
+
end
|
|
157
|
+
if assigns[:exclude_labels]
|
|
158
|
+
issues = issues.reject do |i|
|
|
159
|
+
i["labels"].any? do |label|
|
|
160
|
+
assigns[:exclude_labels].include? label["name"]
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
if verbose
|
|
165
|
+
puts issues.map { |i| format_issue i }
|
|
166
|
+
else
|
|
167
|
+
puts format_issues(issues, repo.nil?)
|
|
168
|
+
end
|
|
169
|
+
break unless res.next_page
|
|
170
|
+
res = throb { api.get res.next_page }
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
rescue Client::Error => e
|
|
174
|
+
if e.response.code == '422'
|
|
175
|
+
e.errors.any? { |err|
|
|
176
|
+
err['code'] == 'missing' && err['field'] == 'milestone'
|
|
177
|
+
} and abort 'No such milestone.'
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
raise
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def uri
|
|
186
|
+
url = ''
|
|
187
|
+
if repo
|
|
188
|
+
url = "/repos/#{repo}"
|
|
189
|
+
end
|
|
190
|
+
if assigns[:org]
|
|
191
|
+
url = "/orgs/#{assigns[:org]}"
|
|
192
|
+
end
|
|
193
|
+
return url << '/issues'
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def fallback
|
|
197
|
+
OptionParser.new do |opts|
|
|
198
|
+
opts.on('-c', '--closed') { assigns[:state] = 'closed' }
|
|
199
|
+
opts.on('-q', '--quiet') { self.quiet = true }
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
require 'date'
|
|
2
|
+
|
|
3
|
+
module GHI
|
|
4
|
+
module Commands
|
|
5
|
+
class Milestone < Command
|
|
6
|
+
attr_accessor :edit
|
|
7
|
+
attr_accessor :reverse
|
|
8
|
+
attr_accessor :web
|
|
9
|
+
|
|
10
|
+
#--
|
|
11
|
+
# FIXME: Opt for better interface, e.g.,
|
|
12
|
+
#
|
|
13
|
+
# ghi milestone [-v | --verbose] [--[no-]closed]
|
|
14
|
+
# ghi milestone add <name> <description>
|
|
15
|
+
# ghi milestone rm <milestoneno>
|
|
16
|
+
#++
|
|
17
|
+
def options
|
|
18
|
+
OptionParser.new do |opts|
|
|
19
|
+
opts.banner = <<EOF
|
|
20
|
+
usage: ghi milestone [<modification options>] [<milestoneno>]
|
|
21
|
+
or: ghi milestone -D <milestoneno>
|
|
22
|
+
or: ghi milestone -l [-c] [-v]
|
|
23
|
+
EOF
|
|
24
|
+
opts.separator ''
|
|
25
|
+
opts.on '-l', '--list', 'list milestones' do
|
|
26
|
+
self.action = 'index'
|
|
27
|
+
end
|
|
28
|
+
opts.on '-c', '--[no-]closed', 'show closed milestones' do |closed|
|
|
29
|
+
assigns[:state] = closed ? 'closed' : 'open'
|
|
30
|
+
end
|
|
31
|
+
opts.on(
|
|
32
|
+
'-S', '--sort <on>', %w(due_date completeness),
|
|
33
|
+
{'d'=>'due_date', 'due'=>'due_date', 'c'=>'completeness'},
|
|
34
|
+
"'due_date' or 'completeness'"
|
|
35
|
+
) do |sort|
|
|
36
|
+
assigns[:sort] = sort
|
|
37
|
+
end
|
|
38
|
+
opts.on '--reverse', 'reverse (ascending) sort order' do
|
|
39
|
+
self.reverse = !reverse
|
|
40
|
+
end
|
|
41
|
+
opts.on '-v', '--verbose', 'list milestones verbosely' do
|
|
42
|
+
self.verbose = true
|
|
43
|
+
end
|
|
44
|
+
opts.on('-w', '--web') { self.web = true }
|
|
45
|
+
opts.separator ''
|
|
46
|
+
opts.separator 'Milestone modification options'
|
|
47
|
+
opts.on(
|
|
48
|
+
'-m', '--message [<text>]', 'change milestone description'
|
|
49
|
+
) do |text|
|
|
50
|
+
self.action = 'create'
|
|
51
|
+
self.edit = true
|
|
52
|
+
next unless text
|
|
53
|
+
assigns[:title], assigns[:description] = text.split(/\n+/, 2)
|
|
54
|
+
end
|
|
55
|
+
# FIXME: We already describe --[no-]closed; describe this, too?
|
|
56
|
+
opts.on(
|
|
57
|
+
'-s', '--state <in>', %w(open closed),
|
|
58
|
+
{'o'=>'open', 'c'=>'closed'}, "'open' or 'closed'"
|
|
59
|
+
) do |state|
|
|
60
|
+
self.action = 'create'
|
|
61
|
+
assigns[:state] = state
|
|
62
|
+
end
|
|
63
|
+
opts.on(
|
|
64
|
+
'--due <on>', 'when milestone should be complete',
|
|
65
|
+
"e.g., '2012-04-30'"
|
|
66
|
+
) do |date|
|
|
67
|
+
self.action = 'create'
|
|
68
|
+
begin
|
|
69
|
+
# TODO: Better parsing.
|
|
70
|
+
assigns[:due_on] = DateTime.parse(date).strftime
|
|
71
|
+
rescue ArgumentError => e
|
|
72
|
+
raise OptionParser::InvalidArgument, e.message
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
opts.on '-D', '--delete', 'delete milestone' do
|
|
76
|
+
self.action = 'destroy'
|
|
77
|
+
end
|
|
78
|
+
opts.separator ''
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def execute
|
|
83
|
+
self.action = 'index'
|
|
84
|
+
require_repo
|
|
85
|
+
extract_milestone
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
options.parse! args
|
|
89
|
+
rescue OptionParser::AmbiguousOption => e
|
|
90
|
+
fallback.parse! e.args
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
milestone and case action
|
|
94
|
+
when 'create' then self.action = 'update'
|
|
95
|
+
when 'index' then self.action = 'show'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if reverse
|
|
99
|
+
assigns[:sort] ||= 'created'
|
|
100
|
+
assigns[:direction] = 'asc'
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
case action
|
|
104
|
+
when 'index'
|
|
105
|
+
if web
|
|
106
|
+
Web.new(repo).open 'milestones', assigns
|
|
107
|
+
else
|
|
108
|
+
assigns[:per_page] = 100
|
|
109
|
+
state = assigns[:state] || 'open'
|
|
110
|
+
print format_state state, "# #{repo} #{state} milestones"
|
|
111
|
+
print "\n" unless paginate?
|
|
112
|
+
res = throb(0, format_state(state, '#')) { api.get uri, assigns }
|
|
113
|
+
page do
|
|
114
|
+
milestones = res.body
|
|
115
|
+
if verbose
|
|
116
|
+
puts milestones.map { |m| format_milestone m }
|
|
117
|
+
else
|
|
118
|
+
puts format_milestones(milestones)
|
|
119
|
+
end
|
|
120
|
+
break unless res.next_page
|
|
121
|
+
res = throb { api.get res.next_page }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
when 'show'
|
|
125
|
+
if web
|
|
126
|
+
List.execute %W(-w -M #{milestone} -- #{repo})
|
|
127
|
+
else
|
|
128
|
+
m = throb { api.get uri }.body
|
|
129
|
+
page do
|
|
130
|
+
puts format_milestone(m)
|
|
131
|
+
puts 'Issues:'
|
|
132
|
+
args.unshift(*%W(-q -M #{milestone} -- #{repo}))
|
|
133
|
+
args.unshift '-v' if verbose
|
|
134
|
+
List.execute args
|
|
135
|
+
break
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
when 'create'
|
|
139
|
+
if web
|
|
140
|
+
Web.new(repo).open 'issues/milestones/new'
|
|
141
|
+
else
|
|
142
|
+
if assigns[:title].nil?
|
|
143
|
+
e = Editor.new 'GHI_MILESTONE'
|
|
144
|
+
message = e.gets format_milestone_editor
|
|
145
|
+
e.unlink 'Empty milestone.' if message.nil? || message.empty?
|
|
146
|
+
assigns[:title], assigns[:description] = message.split(/\n+/, 2)
|
|
147
|
+
end
|
|
148
|
+
m = throb { api.post uri, assigns }.body
|
|
149
|
+
puts 'Milestone #%d created.' % m['number']
|
|
150
|
+
e.unlink if e
|
|
151
|
+
end
|
|
152
|
+
when 'update'
|
|
153
|
+
if web
|
|
154
|
+
Web.new(repo).open "issues/milestones/#{milestone}/edit"
|
|
155
|
+
else
|
|
156
|
+
if edit || assigns.empty?
|
|
157
|
+
m = throb { api.get "/repos/#{repo}/milestones/#{milestone}" }.body
|
|
158
|
+
e = Editor.new "GHI_MILESTONE_#{milestone}"
|
|
159
|
+
message = e.gets format_milestone_editor(m)
|
|
160
|
+
e.unlink 'Empty milestone.' if message.nil? || message.empty?
|
|
161
|
+
assigns[:title], assigns[:description] = message.split(/\n+/, 2)
|
|
162
|
+
end
|
|
163
|
+
if assigns[:title] && m
|
|
164
|
+
t_match = assigns[:title].strip == m['title'].strip
|
|
165
|
+
if assigns[:description]
|
|
166
|
+
b_match = assigns[:description].strip == m['description'].strip
|
|
167
|
+
end
|
|
168
|
+
if t_match && b_match
|
|
169
|
+
e.unlink if e
|
|
170
|
+
abort 'No change.' if assigns.dup.delete_if { |k, v|
|
|
171
|
+
[:title, :description].include? k
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
m = throb { api.patch uri, assigns }.body
|
|
176
|
+
puts format_milestone(m)
|
|
177
|
+
puts 'Updated.'
|
|
178
|
+
e.unlink if e
|
|
179
|
+
end
|
|
180
|
+
when 'destroy'
|
|
181
|
+
require_milestone
|
|
182
|
+
throb { api.delete uri }
|
|
183
|
+
puts 'Milestone deleted.'
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def uri
|
|
190
|
+
if milestone
|
|
191
|
+
"/repos/#{repo}/milestones/#{milestone}"
|
|
192
|
+
else
|
|
193
|
+
"/repos/#{repo}/milestones"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def fallback
|
|
198
|
+
OptionParser.new do |opts|
|
|
199
|
+
opts.on '-d' do
|
|
200
|
+
self.action = 'destroy'
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
module GHI
|
|
2
|
+
module Commands
|
|
3
|
+
class Open < Command
|
|
4
|
+
attr_accessor :editor
|
|
5
|
+
attr_accessor :web
|
|
6
|
+
|
|
7
|
+
def options
|
|
8
|
+
OptionParser.new do |opts|
|
|
9
|
+
opts.banner = <<EOF
|
|
10
|
+
usage: ghi open [options]
|
|
11
|
+
or: ghi reopen [options] <issueno>
|
|
12
|
+
EOF
|
|
13
|
+
opts.separator ''
|
|
14
|
+
opts.on '-l', '--list', 'list open tickets' do
|
|
15
|
+
self.action = 'index'
|
|
16
|
+
end
|
|
17
|
+
opts.on('-w', '--web') { self.web = true }
|
|
18
|
+
opts.separator ''
|
|
19
|
+
opts.separator 'Issue modification options'
|
|
20
|
+
opts.on(
|
|
21
|
+
'-m', '--message [<text>]', 'describe issue',
|
|
22
|
+
"use line breaks to separate title from description"
|
|
23
|
+
) do |text|
|
|
24
|
+
if text
|
|
25
|
+
assigns[:title], assigns[:body] = text.split(/\n+/, 2)
|
|
26
|
+
else
|
|
27
|
+
self.editor = true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
opts.on(
|
|
31
|
+
'-u', '--[no-]assign [<user>]', 'assign to specified user'
|
|
32
|
+
) do |assignee|
|
|
33
|
+
assigns[:assignee] = assignee
|
|
34
|
+
end
|
|
35
|
+
opts.on '--claim', 'assign to yourself' do
|
|
36
|
+
assigns[:assignee] = Authorization.username
|
|
37
|
+
end
|
|
38
|
+
opts.on(
|
|
39
|
+
'-M', '--milestone <n>', 'associate with milestone'
|
|
40
|
+
) do |milestone|
|
|
41
|
+
assigns[:milestone] = milestone
|
|
42
|
+
end
|
|
43
|
+
opts.on(
|
|
44
|
+
'-L', '--label <labelname>...', Array, 'associate with label(s)'
|
|
45
|
+
) do |labels|
|
|
46
|
+
(assigns[:labels] ||= []).concat labels
|
|
47
|
+
end
|
|
48
|
+
opts.separator ''
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def execute
|
|
53
|
+
require_repo
|
|
54
|
+
self.action = 'create'
|
|
55
|
+
|
|
56
|
+
options.parse! args
|
|
57
|
+
|
|
58
|
+
if GHI.config('ghi.infer-issue') != 'false' && extract_issue
|
|
59
|
+
Edit.execute args.push('-so', issue, '--', repo)
|
|
60
|
+
exit
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
case action
|
|
64
|
+
when 'index'
|
|
65
|
+
if assigns.key? :assignee
|
|
66
|
+
args.unshift assigns[:assignee] if assigns[:assignee]
|
|
67
|
+
args.unshift '-u'
|
|
68
|
+
end
|
|
69
|
+
args.unshift '-w' if web
|
|
70
|
+
List.execute args.push('--', repo)
|
|
71
|
+
when 'create'
|
|
72
|
+
if web
|
|
73
|
+
Web.new(repo).open 'issues/new'
|
|
74
|
+
else
|
|
75
|
+
unless args.empty?
|
|
76
|
+
assigns[:title], assigns[:body] = args.join(' '), assigns[:title]
|
|
77
|
+
end
|
|
78
|
+
assigns[:title] = args.join ' ' unless args.empty?
|
|
79
|
+
if assigns[:title].nil? || editor
|
|
80
|
+
e = Editor.new 'GHI_ISSUE'
|
|
81
|
+
message = e.gets format_editor(assigns)
|
|
82
|
+
e.unlink "There's no issue?" if message.nil? || message.empty?
|
|
83
|
+
assigns[:title], assigns[:body] = message.split(/\n+/, 2)
|
|
84
|
+
end
|
|
85
|
+
i = throb { api.post "/repos/#{repo}/issues", assigns }.body
|
|
86
|
+
e.unlink if e
|
|
87
|
+
puts format_issue(i)
|
|
88
|
+
puts "Opened on #{repo}."
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
rescue Client::Error => e
|
|
92
|
+
raise unless error = e.errors.first
|
|
93
|
+
abort "%s %s %s %s." % [
|
|
94
|
+
error['resource'],
|
|
95
|
+
error['field'],
|
|
96
|
+
[*error['value']].join(', '),
|
|
97
|
+
error['code']
|
|
98
|
+
]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module GHI
|
|
2
|
+
module Commands
|
|
3
|
+
class Show < Command
|
|
4
|
+
attr_accessor :patch, :web
|
|
5
|
+
|
|
6
|
+
def options
|
|
7
|
+
OptionParser.new do |opts|
|
|
8
|
+
opts.banner = 'usage: ghi show <issueno>'
|
|
9
|
+
opts.separator ''
|
|
10
|
+
opts.on('-p', '--patch') { self.patch = true }
|
|
11
|
+
opts.on('-w', '--web') { self.web = true }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def execute
|
|
16
|
+
require_issue
|
|
17
|
+
require_repo
|
|
18
|
+
options.parse! args
|
|
19
|
+
patch_path = "pull/#{issue}.patch" if patch # URI also in API...
|
|
20
|
+
if web
|
|
21
|
+
Web.new(repo).open patch_path || "issues/#{issue}"
|
|
22
|
+
else
|
|
23
|
+
if patch_path
|
|
24
|
+
i = throb { Web.new(repo).curl patch_path }
|
|
25
|
+
unless i.start_with? 'From'
|
|
26
|
+
warn 'Patch not found'
|
|
27
|
+
abort
|
|
28
|
+
end
|
|
29
|
+
page do
|
|
30
|
+
no_color { puts i }
|
|
31
|
+
break
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
i = throb { api.get "/repos/#{repo}/issues/#{issue}" }.body
|
|
35
|
+
determine_merge_status(i) if pull_request?(i)
|
|
36
|
+
page do
|
|
37
|
+
puts format_issue(i)
|
|
38
|
+
n = i['comments']
|
|
39
|
+
if n > 0
|
|
40
|
+
puts "#{n} comment#{'s' unless n == 1}:\n\n"
|
|
41
|
+
Comment.execute %W(-l #{issue} -- #{repo})
|
|
42
|
+
end
|
|
43
|
+
break
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def pull_request?(issue)
|
|
52
|
+
issue['pull_request']['html_url']
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def determine_merge_status(pr)
|
|
56
|
+
pr['merged'] = true if pr['state'] == 'closed' && merged?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def merged?
|
|
60
|
+
# API returns with a Not Found error when the PR is not merged
|
|
61
|
+
api.get "/repos/#{repo}/pulls/#{issue}/merge" rescue false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module GHI
|
|
2
|
+
module Commands
|
|
3
|
+
class Status < Command
|
|
4
|
+
|
|
5
|
+
def options
|
|
6
|
+
OptionParser.new do |opts|
|
|
7
|
+
opts.banner = 'usage: ghi status'
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def execute
|
|
12
|
+
begin
|
|
13
|
+
options.parse! args
|
|
14
|
+
@repo ||= ARGV[0] if ARGV.one?
|
|
15
|
+
rescue OptionParser::InvalidOption => e
|
|
16
|
+
fallback.parse! e.args
|
|
17
|
+
retry
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
require_repo
|
|
21
|
+
res = throb { api.get "/repos/#{repo}" }.body
|
|
22
|
+
|
|
23
|
+
if res['has_issues']
|
|
24
|
+
puts "Issues are enabled for this repo"
|
|
25
|
+
else
|
|
26
|
+
puts "Issues are not enabled for this repo"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/ghi/commands.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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 :Disable, 'ghi/commands/disable'
|
|
12
|
+
autoload :Edit, 'ghi/commands/edit'
|
|
13
|
+
autoload :Enable, 'ghi/commands/enable'
|
|
14
|
+
autoload :Help, 'ghi/commands/help'
|
|
15
|
+
autoload :Label, 'ghi/commands/label'
|
|
16
|
+
autoload :Milestone, 'ghi/commands/milestone'
|
|
17
|
+
autoload :Reopen, 'ghi/commands/reopen'
|
|
18
|
+
autoload :Show, 'ghi/commands/show'
|
|
19
|
+
autoload :Status, 'ghi/commands/status'
|
|
20
|
+
autoload :Unassign, 'ghi/commands/unassign'
|
|
21
|
+
autoload :Version, 'ghi/commands/version'
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/ghi/editor.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'tmpdir'
|
|
2
|
+
|
|
3
|
+
module GHI
|
|
4
|
+
class Editor
|
|
5
|
+
attr_reader :filename
|
|
6
|
+
def initialize filename
|
|
7
|
+
@filename = filename
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def gets prefill
|
|
11
|
+
File.open path, 'a+' do |f|
|
|
12
|
+
f << prefill if File.zero? path
|
|
13
|
+
f.rewind
|
|
14
|
+
system "#{editor} #{f.path}"
|
|
15
|
+
return File.read(f.path).gsub(/(?:^#.*$\n?)+\s*\z/, '').strip
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def unlink message = nil
|
|
20
|
+
File.delete path
|
|
21
|
+
abort message if message
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def editor
|
|
27
|
+
editor = GHI.config 'ghi.editor'
|
|
28
|
+
editor ||= GHI.config 'core.editor'
|
|
29
|
+
editor ||= ENV['VISUAL']
|
|
30
|
+
editor ||= ENV['EDITOR']
|
|
31
|
+
editor ||= 'vi'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def path
|
|
35
|
+
File.join dir, filename
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def dir
|
|
39
|
+
@dir ||= git_dir || Dir.tmpdir
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def git_dir
|
|
43
|
+
return unless Commands::Command.detected_repo
|
|
44
|
+
dir = `git rev-parse --git-dir 2>/dev/null`.chomp
|
|
45
|
+
dir unless dir.empty?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|