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,168 @@
|
|
|
1
|
+
module GHI
|
|
2
|
+
module Commands
|
|
3
|
+
class Comment < Command
|
|
4
|
+
attr_accessor :comment
|
|
5
|
+
attr_accessor :verbose
|
|
6
|
+
attr_accessor :web
|
|
7
|
+
|
|
8
|
+
def options
|
|
9
|
+
OptionParser.new do |opts|
|
|
10
|
+
opts.banner = <<EOF
|
|
11
|
+
usage: ghi comment [options] <issueno>
|
|
12
|
+
EOF
|
|
13
|
+
opts.separator ''
|
|
14
|
+
opts.on '-l', '--list', 'list comments' do
|
|
15
|
+
self.action = 'list'
|
|
16
|
+
end
|
|
17
|
+
opts.on('-w', '--web') { self.web = true }
|
|
18
|
+
# opts.on '-v', '--verbose', 'list events, too'
|
|
19
|
+
opts.separator ''
|
|
20
|
+
opts.separator 'Comment modification options'
|
|
21
|
+
opts.on '-m', '--message [<text>]', 'comment body' do |text|
|
|
22
|
+
assigns[:body] = text
|
|
23
|
+
end
|
|
24
|
+
opts.on '--amend', 'amend previous comment' do
|
|
25
|
+
self.action = 'update'
|
|
26
|
+
end
|
|
27
|
+
opts.on '-D', '--delete', 'delete previous comment' do
|
|
28
|
+
self.action = 'destroy'
|
|
29
|
+
end
|
|
30
|
+
opts.on '--close', 'close associated issue' do
|
|
31
|
+
self.action = 'close'
|
|
32
|
+
end
|
|
33
|
+
opts.on '-v', '--verbose' do
|
|
34
|
+
self.verbose = true
|
|
35
|
+
end
|
|
36
|
+
opts.separator ''
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def execute
|
|
41
|
+
require_issue
|
|
42
|
+
require_repo
|
|
43
|
+
self.action ||= 'create'
|
|
44
|
+
options.parse! args
|
|
45
|
+
|
|
46
|
+
case action
|
|
47
|
+
when 'list'
|
|
48
|
+
get_requests(:index, :events)
|
|
49
|
+
res = index
|
|
50
|
+
page do
|
|
51
|
+
elements = sort_by_creation(res.body + paged_events(events, res))
|
|
52
|
+
puts format_comments_and_events(elements)
|
|
53
|
+
break unless res.next_page
|
|
54
|
+
res = throb { api.get res.next_page }
|
|
55
|
+
end
|
|
56
|
+
when 'create'
|
|
57
|
+
if web
|
|
58
|
+
Web.new(repo).open "issues/#{issue}#issue_comment_form"
|
|
59
|
+
else
|
|
60
|
+
create
|
|
61
|
+
end
|
|
62
|
+
when 'update', 'destroy'
|
|
63
|
+
res = index
|
|
64
|
+
res = throb { api.get res.last_page } if res.last_page
|
|
65
|
+
self.comment = res.body.reverse.find { |c|
|
|
66
|
+
c['user']['login'] == Authorization.username
|
|
67
|
+
}
|
|
68
|
+
if comment
|
|
69
|
+
send action
|
|
70
|
+
else
|
|
71
|
+
abort 'No recent comment found.'
|
|
72
|
+
end
|
|
73
|
+
when 'close'
|
|
74
|
+
Close.execute [issue, '-m', assigns[:body], '--', repo].compact
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
protected
|
|
79
|
+
|
|
80
|
+
def index
|
|
81
|
+
@index ||= throb { api.get uri, :per_page => 100 }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def create message = 'Commented.'
|
|
85
|
+
e = require_body
|
|
86
|
+
c = throb { api.post uri, assigns }.body
|
|
87
|
+
puts format_comment(c)
|
|
88
|
+
puts message
|
|
89
|
+
e.unlink if e
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def update
|
|
93
|
+
create 'Comment updated.'
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def destroy
|
|
97
|
+
throb { api.delete uri }
|
|
98
|
+
puts 'Comment deleted.'
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def events
|
|
102
|
+
@events ||= begin
|
|
103
|
+
events = []
|
|
104
|
+
res = api.get(event_uri, :per_page => 100)
|
|
105
|
+
loop do
|
|
106
|
+
events += res.body
|
|
107
|
+
break unless res.next_page
|
|
108
|
+
res = api.get res.next_page
|
|
109
|
+
end
|
|
110
|
+
events
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def get_requests(*methods)
|
|
117
|
+
threads = methods.map do |method|
|
|
118
|
+
Thread.new { send(method) }
|
|
119
|
+
end
|
|
120
|
+
threads.each { |t| t.join }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def uri
|
|
124
|
+
if comment
|
|
125
|
+
comment['url']
|
|
126
|
+
else
|
|
127
|
+
"/repos/#{repo}/issues/#{issue}/comments"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def event_uri
|
|
132
|
+
"/repos/#{repo}/issues/#{issue}/events"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def require_body
|
|
136
|
+
assigns[:body] = args.join ' ' unless args.empty?
|
|
137
|
+
return if assigns[:body]
|
|
138
|
+
if issue && verbose
|
|
139
|
+
i = throb { api.get "/repos/#{repo}/issues/#{issue}" }.body
|
|
140
|
+
else
|
|
141
|
+
i = {'number'=>issue}
|
|
142
|
+
end
|
|
143
|
+
filename = "GHI_COMMENT_#{issue}"
|
|
144
|
+
filename << "_#{comment['id']}" if comment
|
|
145
|
+
e = Editor.new filename
|
|
146
|
+
message = e.gets format_comment_editor(i, comment)
|
|
147
|
+
e.unlink 'No comment.' if message.nil? || message.empty?
|
|
148
|
+
if comment && message.strip == comment['body'].strip
|
|
149
|
+
e.unlink 'No change.'
|
|
150
|
+
end
|
|
151
|
+
assigns[:body] = message if message
|
|
152
|
+
e
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def paged_events(events, comments_res)
|
|
156
|
+
if comments_res.next_page
|
|
157
|
+
last_comment_creation = comments_res.body.last['created_at']
|
|
158
|
+
events_for_this_page, @events = events.partition do |event|
|
|
159
|
+
event['created_at'] < last_comment_creation
|
|
160
|
+
end
|
|
161
|
+
events_for_this_page
|
|
162
|
+
else
|
|
163
|
+
events
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module GHI
|
|
2
|
+
module Commands
|
|
3
|
+
class Config < Command
|
|
4
|
+
def options
|
|
5
|
+
OptionParser.new do |opts|
|
|
6
|
+
opts.banner = <<EOF
|
|
7
|
+
usage: ghi config [options]
|
|
8
|
+
EOF
|
|
9
|
+
opts.separator ''
|
|
10
|
+
opts.on '--local', 'set for local repo only' do
|
|
11
|
+
assigns[:local] = true
|
|
12
|
+
end
|
|
13
|
+
opts.on '--auth [<username>]' do |username|
|
|
14
|
+
self.action = 'auth'
|
|
15
|
+
assigns[:username] = username || Authorization.username
|
|
16
|
+
end
|
|
17
|
+
opts.separator ''
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def execute
|
|
22
|
+
# TODO: Investigate whether or not this variable is needed
|
|
23
|
+
global = true
|
|
24
|
+
|
|
25
|
+
options.parse! args.empty? ? %w(-h) : args
|
|
26
|
+
|
|
27
|
+
if action == 'auth'
|
|
28
|
+
assigns[:password] = Authorization.password || get_password
|
|
29
|
+
Authorization.authorize!(
|
|
30
|
+
assigns[:username], assigns[:password], assigns[:local]
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def get_password
|
|
38
|
+
print "Enter #{assigns[:username]}'s GitHub password (never stored): "
|
|
39
|
+
current_tty = `stty -g`
|
|
40
|
+
system 'stty raw -echo -icanon isig' if $?.success?
|
|
41
|
+
input = ''
|
|
42
|
+
while char = $stdin.getbyte and not (char == 13 or char == 10)
|
|
43
|
+
if char == 127 or char == 8
|
|
44
|
+
input[-1, 1] = '' unless input.empty?
|
|
45
|
+
else
|
|
46
|
+
input << char.chr
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
input
|
|
50
|
+
rescue Interrupt
|
|
51
|
+
print '^C'
|
|
52
|
+
ensure
|
|
53
|
+
system "stty #{current_tty}" unless current_tty.empty?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module GHI
|
|
2
|
+
module Commands
|
|
3
|
+
class Disable < Command
|
|
4
|
+
|
|
5
|
+
def options
|
|
6
|
+
OptionParser.new do |opts|
|
|
7
|
+
opts.banner = 'usage: ghi disable'
|
|
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
|
+
repo_name = require_repo_name
|
|
20
|
+
unless repo_name.nil?
|
|
21
|
+
patch_data = {}
|
|
22
|
+
patch_data[:name] = repo_name
|
|
23
|
+
patch_data[:has_issues] = false
|
|
24
|
+
res = throb { api.patch "/repos/#{repo}", patch_data }.body
|
|
25
|
+
if !res['has_issues']
|
|
26
|
+
puts "Issues are now disabled for this repo"
|
|
27
|
+
else
|
|
28
|
+
puts "Something went wrong disabling issues for this repo"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
module GHI
|
|
2
|
+
module Commands
|
|
3
|
+
class Edit < Command
|
|
4
|
+
attr_accessor :editor
|
|
5
|
+
|
|
6
|
+
def options
|
|
7
|
+
OptionParser.new do |opts|
|
|
8
|
+
opts.banner = <<EOF
|
|
9
|
+
usage: ghi edit <issueno> [options]
|
|
10
|
+
EOF
|
|
11
|
+
opts.separator ''
|
|
12
|
+
opts.on(
|
|
13
|
+
'-m', '--message [<text>]', 'change issue description'
|
|
14
|
+
) do |text|
|
|
15
|
+
next self.editor = true if text.nil?
|
|
16
|
+
assigns[:title], assigns[:body] = text.split(/\n+/, 2)
|
|
17
|
+
end
|
|
18
|
+
opts.on(
|
|
19
|
+
'-u', '--[no-]assign [<user>]', 'assign to specified user'
|
|
20
|
+
) do |assignee|
|
|
21
|
+
assigns[:assignee] = assignee || nil
|
|
22
|
+
end
|
|
23
|
+
opts.on '--claim', 'assign to yourself' do
|
|
24
|
+
assigns[:assignee] = Authorization.username
|
|
25
|
+
end
|
|
26
|
+
opts.on(
|
|
27
|
+
'-s', '--state <in>', %w(open closed),
|
|
28
|
+
{'o'=>'open', 'c'=>'closed'}, "'open' or 'closed'"
|
|
29
|
+
) do |state|
|
|
30
|
+
assigns[:state] = state
|
|
31
|
+
end
|
|
32
|
+
opts.on(
|
|
33
|
+
'-M', '--[no-]milestone [<n>]', Integer, 'associate with milestone'
|
|
34
|
+
) do |milestone|
|
|
35
|
+
assigns[:milestone] = milestone
|
|
36
|
+
end
|
|
37
|
+
opts.on(
|
|
38
|
+
'-L', '--label <labelname>...', Array, 'associate with label(s)'
|
|
39
|
+
) do |labels|
|
|
40
|
+
(assigns[:labels] ||= []).concat labels
|
|
41
|
+
end
|
|
42
|
+
opts.separator ''
|
|
43
|
+
opts.separator 'Pull request options'
|
|
44
|
+
opts.on(
|
|
45
|
+
'-H', '--head [[<user>:]<branch>]',
|
|
46
|
+
'branch where your changes are implemented',
|
|
47
|
+
'(defaults to current branch)'
|
|
48
|
+
) do |head|
|
|
49
|
+
self.action = 'pull'
|
|
50
|
+
assigns[:head] = head
|
|
51
|
+
end
|
|
52
|
+
opts.on(
|
|
53
|
+
'-b', '--base [<branch>]',
|
|
54
|
+
'branch you want your changes pulled into', '(defaults to master)'
|
|
55
|
+
) do |base|
|
|
56
|
+
self.action = 'pull'
|
|
57
|
+
assigns[:base] = base
|
|
58
|
+
end
|
|
59
|
+
opts.separator ''
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def execute
|
|
64
|
+
self.action = 'edit'
|
|
65
|
+
require_repo
|
|
66
|
+
require_issue
|
|
67
|
+
options.parse! args
|
|
68
|
+
case action
|
|
69
|
+
when 'edit'
|
|
70
|
+
begin
|
|
71
|
+
if editor || assigns.empty?
|
|
72
|
+
i = throb { api.get "/repos/#{repo}/issues/#{issue}" }.body
|
|
73
|
+
e = Editor.new "GHI_ISSUE_#{issue}"
|
|
74
|
+
message = e.gets format_editor(i)
|
|
75
|
+
e.unlink "There's no issue." if message.nil? || message.empty?
|
|
76
|
+
assigns[:title], assigns[:body] = message.split(/\n+/, 2)
|
|
77
|
+
end
|
|
78
|
+
if i && assigns.keys.map { |k| k.to_s }.sort == %w[body title]
|
|
79
|
+
titles_match = assigns[:title].strip == i['title'].strip
|
|
80
|
+
if assigns[:body]
|
|
81
|
+
bodies_match = assigns[:body].to_s.strip == i['body'].to_s.strip
|
|
82
|
+
end
|
|
83
|
+
if titles_match && bodies_match
|
|
84
|
+
e.unlink if e
|
|
85
|
+
abort 'No change.' if assigns.dup.delete_if { |k, v|
|
|
86
|
+
[:title, :body].include? k
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
unless assigns.empty?
|
|
91
|
+
i = throb {
|
|
92
|
+
api.patch "/repos/#{repo}/issues/#{issue}", assigns
|
|
93
|
+
}.body
|
|
94
|
+
puts format_issue(i)
|
|
95
|
+
puts 'Updated.'
|
|
96
|
+
end
|
|
97
|
+
e.unlink if e
|
|
98
|
+
rescue Client::Error => e
|
|
99
|
+
raise unless error = e.errors.first
|
|
100
|
+
abort "%s %s %s %s." % [
|
|
101
|
+
error['resource'],
|
|
102
|
+
error['field'],
|
|
103
|
+
[*error['value']].join(', '),
|
|
104
|
+
error['code']
|
|
105
|
+
]
|
|
106
|
+
end
|
|
107
|
+
when 'pull'
|
|
108
|
+
begin
|
|
109
|
+
assigns[:issue] = issue
|
|
110
|
+
assigns[:base] ||= 'master'
|
|
111
|
+
head = begin
|
|
112
|
+
if ref = %x{
|
|
113
|
+
git rev-parse --abbrev-ref HEAD@{upstream} 2>/dev/null
|
|
114
|
+
}.chomp!
|
|
115
|
+
ref.split('/', 2).last if $? == 0
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
assigns[:head] ||= head
|
|
119
|
+
if assigns[:head]
|
|
120
|
+
assigns[:head].sub!(/:$/, ":#{head}")
|
|
121
|
+
else
|
|
122
|
+
abort <<EOF.chomp
|
|
123
|
+
fatal: HEAD can't be null. (Is your current branch being tracked upstream?)
|
|
124
|
+
EOF
|
|
125
|
+
end
|
|
126
|
+
throb { api.post "/repos/#{repo}/pulls", assigns }
|
|
127
|
+
base = [repo.split('/').first, assigns[:base]].join ':'
|
|
128
|
+
puts 'Issue #%d set up to track remote branch %s against %s.' % [
|
|
129
|
+
issue, assigns[:head], base
|
|
130
|
+
]
|
|
131
|
+
rescue Client::Error => e
|
|
132
|
+
raise unless error = e.errors.last
|
|
133
|
+
abort error['message'].sub(/^base /, '')
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module GHI
|
|
2
|
+
module Commands
|
|
3
|
+
class Enable < Command
|
|
4
|
+
|
|
5
|
+
def options
|
|
6
|
+
OptionParser.new do |opts|
|
|
7
|
+
opts.banner = 'usage: ghi enable'
|
|
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
|
+
repo_name = require_repo_name
|
|
20
|
+
unless repo_name.nil?
|
|
21
|
+
patch_data = {}
|
|
22
|
+
patch_data[:name] = repo_name
|
|
23
|
+
patch_data[:has_issues] = true
|
|
24
|
+
res = throb { api.patch "/repos/#{repo}", patch_data }.body
|
|
25
|
+
if res['has_issues']
|
|
26
|
+
puts "Issues are now enabled for this repo"
|
|
27
|
+
else
|
|
28
|
+
puts "Something went wrong enabling issues for this repo"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module GHI
|
|
2
|
+
module Commands
|
|
3
|
+
class Help < Command
|
|
4
|
+
def self.execute args, message = nil
|
|
5
|
+
new(args).execute message
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
attr_accessor :command
|
|
9
|
+
|
|
10
|
+
def options
|
|
11
|
+
OptionParser.new do |opts|
|
|
12
|
+
opts.banner = 'usage: ghi help [--all] [--man|--web] <command>'
|
|
13
|
+
opts.separator ''
|
|
14
|
+
opts.on('-a', '--all', 'print all available commands') { all }
|
|
15
|
+
opts.on('-m', '--man', 'show man page') { man }
|
|
16
|
+
opts.on('-w', '--web', 'show manual in web browser') { web }
|
|
17
|
+
opts.separator ''
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def execute message = nil
|
|
22
|
+
self.command = args.shift if args.first !~ /^-/
|
|
23
|
+
|
|
24
|
+
if command.nil? && args.empty?
|
|
25
|
+
puts message if message
|
|
26
|
+
puts <<EOF
|
|
27
|
+
|
|
28
|
+
The most commonly used ghi commands are:
|
|
29
|
+
list List your issues (or a repository's)
|
|
30
|
+
show Show an issue's details
|
|
31
|
+
open Open (or reopen) an issue
|
|
32
|
+
close Close an issue
|
|
33
|
+
edit Modify an existing issue
|
|
34
|
+
comment Leave a comment on an issue
|
|
35
|
+
label Create, list, modify, or delete labels
|
|
36
|
+
assign Assign an issue to yourself (or someone else)
|
|
37
|
+
milestone Manage project milestones
|
|
38
|
+
status Determine whether or not issues are enabled for this repo
|
|
39
|
+
enable Enable issues for the current repo
|
|
40
|
+
disable Disable issues for the current repo
|
|
41
|
+
|
|
42
|
+
See 'ghi help <command>' for more information on a specific command.
|
|
43
|
+
EOF
|
|
44
|
+
exit
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
options.parse! args.empty? ? %w(-m) : args
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def all
|
|
51
|
+
raise 'TODO'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def man
|
|
55
|
+
GHI.execute [command, '-h']
|
|
56
|
+
# TODO:
|
|
57
|
+
# exec "man #{['ghi', command].compact.join '-'}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def web
|
|
61
|
+
raise 'TODO'
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
module GHI
|
|
2
|
+
module Commands
|
|
3
|
+
class Label < Command
|
|
4
|
+
attr_accessor :name
|
|
5
|
+
|
|
6
|
+
#--
|
|
7
|
+
# FIXME: This does too much. Opt for a secondary command, e.g.,
|
|
8
|
+
#
|
|
9
|
+
# ghi label add <labelname>
|
|
10
|
+
# ghi label rm <labelname>
|
|
11
|
+
# ghi label <issueno> <labelname>...
|
|
12
|
+
#++
|
|
13
|
+
def options
|
|
14
|
+
OptionParser.new do |opts|
|
|
15
|
+
opts.banner = <<EOF
|
|
16
|
+
usage: ghi label <labelname> [-c <color>] [-r <newname>]
|
|
17
|
+
or: ghi label -D <labelname>
|
|
18
|
+
or: ghi label <issueno(s)> [-a] [-d] [-f] <label>
|
|
19
|
+
or: ghi label -l [<issueno>]
|
|
20
|
+
EOF
|
|
21
|
+
opts.separator ''
|
|
22
|
+
opts.on '-l', '--list [<issueno>]', 'list label names' do |n|
|
|
23
|
+
self.action = 'index'
|
|
24
|
+
@issue ||= n
|
|
25
|
+
end
|
|
26
|
+
opts.on '-D', '--delete', 'delete label' do
|
|
27
|
+
self.action = 'destroy'
|
|
28
|
+
end
|
|
29
|
+
opts.separator ''
|
|
30
|
+
opts.separator 'Label modification options'
|
|
31
|
+
opts.on(
|
|
32
|
+
'-c', '--color <color>', 'color name or 6-character hex code'
|
|
33
|
+
) do |color|
|
|
34
|
+
assigns[:color] = to_hex color
|
|
35
|
+
self.action ||= 'create'
|
|
36
|
+
end
|
|
37
|
+
opts.on '-r', '--rename <labelname>', 'new label name' do |name|
|
|
38
|
+
assigns[:name] = name
|
|
39
|
+
self.action = 'update'
|
|
40
|
+
end
|
|
41
|
+
opts.separator ''
|
|
42
|
+
opts.separator 'Issue modification options'
|
|
43
|
+
opts.on '-a', '--add', 'add labels to issue' do
|
|
44
|
+
self.action = issues_present? ? 'add' : 'create'
|
|
45
|
+
end
|
|
46
|
+
opts.on '-d', '--delete', 'remove labels from issue' do
|
|
47
|
+
self.action = issues_present? ? 'remove' : 'destroy'
|
|
48
|
+
end
|
|
49
|
+
opts.on '-f', '--force', 'replace existing labels' do
|
|
50
|
+
self.action = issues_present? ? 'replace' : 'update'
|
|
51
|
+
end
|
|
52
|
+
opts.separator ''
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def execute
|
|
57
|
+
extract_issue
|
|
58
|
+
require_repo
|
|
59
|
+
options.parse! args.empty? ? %w(-l) : args
|
|
60
|
+
|
|
61
|
+
if issues_present?
|
|
62
|
+
self.action ||= 'add'
|
|
63
|
+
self.name = args.shift.to_s.split ','
|
|
64
|
+
self.name.concat args
|
|
65
|
+
multi_action(action)
|
|
66
|
+
else
|
|
67
|
+
self.action ||= 'create'
|
|
68
|
+
self.name ||= args.shift
|
|
69
|
+
send action
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
protected
|
|
74
|
+
|
|
75
|
+
def index
|
|
76
|
+
if issue
|
|
77
|
+
uri = "/repos/#{repo}/issues/#{issue}/labels"
|
|
78
|
+
else
|
|
79
|
+
uri = "/repos/#{repo}/labels"
|
|
80
|
+
end
|
|
81
|
+
labels = throb { api.get uri }.body
|
|
82
|
+
if labels.empty?
|
|
83
|
+
puts 'None.'
|
|
84
|
+
else
|
|
85
|
+
puts labels.map { |label|
|
|
86
|
+
name = label['name']
|
|
87
|
+
colorize? ? bg(label['color']) { " #{name} " } : name
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def create
|
|
93
|
+
label = throb {
|
|
94
|
+
api.post "/repos/#{repo}/labels", assigns.merge(:name => name)
|
|
95
|
+
}.body
|
|
96
|
+
return update if label.nil?
|
|
97
|
+
puts "%s created." % bg(label['color']) { " #{label['name']} "}
|
|
98
|
+
rescue Client::Error => e
|
|
99
|
+
if e.errors.find { |error| error['code'] == 'already_exists' }
|
|
100
|
+
return update
|
|
101
|
+
end
|
|
102
|
+
raise
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def update
|
|
106
|
+
label = throb {
|
|
107
|
+
api.patch "/repos/#{repo}/labels/#{name}", assigns
|
|
108
|
+
}.body
|
|
109
|
+
puts "%s updated." % bg(label['color']) { " #{label['name']} "}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def destroy
|
|
113
|
+
throb { api.delete "/repos/#{repo}/labels/#{name}" }
|
|
114
|
+
puts "[#{name}] deleted."
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def add
|
|
118
|
+
labels = throb {
|
|
119
|
+
api.post "/repos/#{repo}/issues/#{issue}/labels", name
|
|
120
|
+
}.body
|
|
121
|
+
puts "Issue #%d labeled %s." % [issue, format_labels(labels)]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def remove
|
|
125
|
+
case name.length
|
|
126
|
+
when 0
|
|
127
|
+
throb { api.delete base_uri }
|
|
128
|
+
puts "Labels removed."
|
|
129
|
+
when 1
|
|
130
|
+
labels = throb { api.delete "#{base_uri}/#{name.join}" }.body
|
|
131
|
+
if labels.empty?
|
|
132
|
+
puts "Issue #%d unlabeled." % issue
|
|
133
|
+
else
|
|
134
|
+
puts "Issue #%d labeled %s." % [issue, format_labels(labels)]
|
|
135
|
+
end
|
|
136
|
+
else
|
|
137
|
+
labels = throb {
|
|
138
|
+
api.get "/repos/#{repo}/issues/#{issue}/labels"
|
|
139
|
+
}.body
|
|
140
|
+
self.name = labels.map { |l| l['name'] } - name
|
|
141
|
+
replace
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def replace
|
|
146
|
+
labels = throb { api.put base_uri, name }.body
|
|
147
|
+
if labels.empty?
|
|
148
|
+
puts "Issue #%d unlabeled." % issue
|
|
149
|
+
else
|
|
150
|
+
puts "Issue #%d labeled %s." % [issue, format_labels(labels)]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def base_uri
|
|
157
|
+
"/repos/#{repo}/#{issue ? "issues/#{issue}/labels" : 'labels'}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# This method is usually inherited from Command and extracts a single issue
|
|
161
|
+
# from args - we override it to handle multiple issues at once.
|
|
162
|
+
def extract_issue
|
|
163
|
+
@issues = []
|
|
164
|
+
args.delete_if do |arg|
|
|
165
|
+
arg.match(/^\d+$/) ? @issues << arg : break
|
|
166
|
+
end
|
|
167
|
+
infer_issue_from_branch_prefix unless @issues.any?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def issues_present?
|
|
171
|
+
@issues.any? || @issue
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def multi_action(action)
|
|
175
|
+
if @issues.any?
|
|
176
|
+
override_issue_reader
|
|
177
|
+
threads = @issues.map do |issue|
|
|
178
|
+
Thread.new do
|
|
179
|
+
Thread.current[:issue] = issue
|
|
180
|
+
send action
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
threads.each(&:join)
|
|
184
|
+
else
|
|
185
|
+
send action
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def override_issue_reader
|
|
190
|
+
def issue
|
|
191
|
+
Thread.current[:issue]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|