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,523 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
require 'date'
|
|
3
|
+
require 'erb'
|
|
4
|
+
|
|
5
|
+
module GHI
|
|
6
|
+
module Formatting
|
|
7
|
+
class << self
|
|
8
|
+
attr_accessor :paginate
|
|
9
|
+
end
|
|
10
|
+
self.paginate = true # Default.
|
|
11
|
+
|
|
12
|
+
attr_accessor :paging
|
|
13
|
+
|
|
14
|
+
autoload :Colors, 'ghi/formatting/colors'
|
|
15
|
+
include Colors
|
|
16
|
+
|
|
17
|
+
CURSOR = {
|
|
18
|
+
:up => lambda { |n| "\e[#{n}A" },
|
|
19
|
+
:column => lambda { |n| "\e[#{n}G" },
|
|
20
|
+
:hide => "\e[?25l",
|
|
21
|
+
:show => "\e[?25h"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
THROBBERS = [
|
|
25
|
+
%w(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏),
|
|
26
|
+
%w(⠋ ⠙ ⠚ ⠞ ⠖ ⠦ ⠴ ⠲ ⠳ ⠓),
|
|
27
|
+
%w(⠄ ⠆ ⠇ ⠋ ⠙ ⠸ ⠰ ⠠ ⠰ ⠸ ⠙ ⠋ ⠇ ⠆ ),
|
|
28
|
+
%w(⠋ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋),
|
|
29
|
+
%w(⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠴ ⠲ ⠒ ⠂ ⠂ ⠒ ⠚ ⠙ ⠉ ⠁),
|
|
30
|
+
%w(⠈ ⠉ ⠋ ⠓ ⠒ ⠐ ⠐ ⠒ ⠖ ⠦ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈),
|
|
31
|
+
%w(⠁ ⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈ ⠈ ⠉)
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
def puts *strings
|
|
35
|
+
strings = strings.flatten.map { |s|
|
|
36
|
+
s.gsub(/(^| *)@(\w+)/) {
|
|
37
|
+
if $2 == Authorization.username
|
|
38
|
+
bright { fg(:yellow) { "#$1@#$2" } }
|
|
39
|
+
else
|
|
40
|
+
bright { "#$1@#$2" }
|
|
41
|
+
end
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
super strings
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def page header = nil, throttle = 0
|
|
48
|
+
if paginate?
|
|
49
|
+
pager = GHI.config('ghi.pager') || GHI.config('core.pager')
|
|
50
|
+
pager ||= ENV['PAGER']
|
|
51
|
+
pager ||= 'less'
|
|
52
|
+
pager += ' -EKRX -b1' if pager =~ /^less( -[EKRX]+)?$/
|
|
53
|
+
|
|
54
|
+
if pager && !pager.empty? && pager != 'cat'
|
|
55
|
+
$stdout = IO.popen pager, 'w'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts header if header
|
|
59
|
+
self.paging = true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
loop do
|
|
63
|
+
yield
|
|
64
|
+
sleep throttle
|
|
65
|
+
end
|
|
66
|
+
rescue Errno::EPIPE
|
|
67
|
+
exit
|
|
68
|
+
ensure
|
|
69
|
+
unless $stdout == STDOUT
|
|
70
|
+
$stdout.close_write
|
|
71
|
+
$stdout = STDOUT
|
|
72
|
+
print CURSOR[:show]
|
|
73
|
+
exit
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def paginate?
|
|
78
|
+
($stdout.tty? && $stdout == STDOUT && Formatting.paginate) || paging?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def paging?
|
|
82
|
+
!!paging
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def truncate string, reserved
|
|
86
|
+
return string unless paginate?
|
|
87
|
+
space=columns - reserved
|
|
88
|
+
space=5 if space < 5
|
|
89
|
+
result = string.scan(/.{0,#{space}}(?:\s|\Z)/).first.strip
|
|
90
|
+
result << "..." if result != string
|
|
91
|
+
result
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def indent string, level = 4, maxwidth = columns
|
|
95
|
+
string = string.gsub(/\r/, '')
|
|
96
|
+
string.gsub!(/[\t ]+$/, '')
|
|
97
|
+
string.gsub!(/\n{3,}/, "\n\n")
|
|
98
|
+
width = maxwidth - level - 1
|
|
99
|
+
lines = string.scan(
|
|
100
|
+
/.{0,#{width}}(?:\s|\Z)|[\S]{#{width},}/ # TODO: Test long lines.
|
|
101
|
+
).map { |line| " " * level + line.chomp }
|
|
102
|
+
format_markdown lines.join("\n").rstrip, level
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def columns
|
|
106
|
+
dimensions[1] || 80
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def dimensions
|
|
110
|
+
`stty size 2>/dev/null`.chomp.split(' ').map { |n| n.to_i }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
#--
|
|
114
|
+
# Specific formatters:
|
|
115
|
+
#++
|
|
116
|
+
|
|
117
|
+
def format_username username
|
|
118
|
+
username == Authorization.username ? 'you' : username
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def format_issues_header
|
|
122
|
+
state = assigns[:state] ||= 'open'
|
|
123
|
+
org = assigns[:org] ||= nil
|
|
124
|
+
header = "# #{repo || org || 'Global,'} #{state} issues"
|
|
125
|
+
if repo
|
|
126
|
+
if milestone = assigns[:milestone]
|
|
127
|
+
case milestone
|
|
128
|
+
when '*' then header << ' with a milestone'
|
|
129
|
+
when 'none' then header << ' without a milestone'
|
|
130
|
+
else
|
|
131
|
+
header.sub! repo, "#{repo} milestone ##{milestone}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
if assignee = assigns[:assignee]
|
|
135
|
+
header << case assignee
|
|
136
|
+
when '*' then ', assigned'
|
|
137
|
+
when 'none' then ', unassigned'
|
|
138
|
+
else
|
|
139
|
+
", assigned to #{format_username assignee}"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
if mentioned = assigns[:mentioned]
|
|
143
|
+
header << ", mentioning #{format_username mentioned}"
|
|
144
|
+
end
|
|
145
|
+
else
|
|
146
|
+
header << case assigns[:filter]
|
|
147
|
+
when 'created' then ' you created'
|
|
148
|
+
when 'mentioned' then ' that mention you'
|
|
149
|
+
when 'subscribed' then " you're subscribed to"
|
|
150
|
+
when 'all' then ' that you can see'
|
|
151
|
+
else
|
|
152
|
+
' assigned to you'
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
if creator = assigns[:creator]
|
|
156
|
+
header << " #{format_username creator} created"
|
|
157
|
+
end
|
|
158
|
+
if labels = assigns[:labels]
|
|
159
|
+
header << ", labeled #{labels.gsub ',', ', '}"
|
|
160
|
+
end
|
|
161
|
+
if excluded_labels = assigns[:exclude_labels]
|
|
162
|
+
header << ", excluding those labeled #{excluded_labels.gsub ',', ', '}"
|
|
163
|
+
end
|
|
164
|
+
if sort = assigns[:sort]
|
|
165
|
+
header << ", by #{sort} #{reverse ? 'ascending' : 'descending'}"
|
|
166
|
+
end
|
|
167
|
+
format_state assigns[:state], header
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def format_issues issues, include_repo
|
|
171
|
+
return 'None.' if issues.empty?
|
|
172
|
+
|
|
173
|
+
include_repo and issues.each do |i|
|
|
174
|
+
%r{/repos/[^/]+/([^/]+)} === i['url'] and i['repo'] = $1
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
nmax, rmax = %w(number repo).map { |f|
|
|
178
|
+
issues.sort_by { |i| i[f].to_s.size }.last[f].to_s.size
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
issues.map { |i|
|
|
182
|
+
n, title, labels = i['number'], i['title'], i['labels']
|
|
183
|
+
l = 9 + nmax + rmax + no_color { format_labels labels }.to_s.length
|
|
184
|
+
a = i['assignee']
|
|
185
|
+
a_is_me = a && a['login'] == Authorization.username
|
|
186
|
+
l += a['login'].to_s.length + 2 if a
|
|
187
|
+
p = i['pull_request']['html_url'] and l += 2 if i['pull_request']
|
|
188
|
+
c = i['comments']
|
|
189
|
+
l += c.to_s.length + 1 unless c == 0
|
|
190
|
+
m = i['milestone']
|
|
191
|
+
[
|
|
192
|
+
" ",
|
|
193
|
+
(i['repo'].to_s.rjust(rmax) if i['repo']),
|
|
194
|
+
format_number(n.to_s.rjust(nmax)),
|
|
195
|
+
truncate(title, l),
|
|
196
|
+
(format_labels(labels) unless assigns[:dont_print_labels]),
|
|
197
|
+
(fg(:green) { m['title'] } if m),
|
|
198
|
+
(fg('aaaaaa') { c } unless c == 0),
|
|
199
|
+
(fg('aaaaaa') { '↑' } if p),
|
|
200
|
+
(fg(a_is_me ? :yellow : :gray) { "@#{a['login']}" } if a),
|
|
201
|
+
(fg('aaaaaa') { '‡' } if m)
|
|
202
|
+
].compact.join ' '
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def format_number n
|
|
207
|
+
colorize? ? "#{bright { n }}:" : "#{n} "
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# TODO: Show milestone, number of comments, pull request attached.
|
|
211
|
+
def format_issue i, width = columns
|
|
212
|
+
return unless i['created_at']
|
|
213
|
+
ERB.new(<<EOF).result binding
|
|
214
|
+
<% p = i['pull_request']['html_url'] %>\
|
|
215
|
+
<%= bright { no_color { indent '%s%s: %s' % [p ? '↑' : '#', \
|
|
216
|
+
*i.values_at('number', 'title')], 0, width } } %>
|
|
217
|
+
@<%= i['user']['login'] %> opened this <%= p ? 'pull request' : 'issue' %> \
|
|
218
|
+
<%= format_date DateTime.parse(i['created_at']) %>. \
|
|
219
|
+
<% if i['merged'] %><%= format_state 'merged', format_tag('merged'), :bg %><% end %> \
|
|
220
|
+
<%= format_state i['state'], format_tag(i['state']), :bg %> \
|
|
221
|
+
<% unless i['comments'] == 0 %>\
|
|
222
|
+
<%= fg('aaaaaa'){
|
|
223
|
+
template = "%d comment"
|
|
224
|
+
template << "s" unless i['comments'] == 1
|
|
225
|
+
'(' << template % i['comments'] << ')'
|
|
226
|
+
} %>\
|
|
227
|
+
<% end %>\
|
|
228
|
+
<% if i['assignee'] || !i['labels'].empty? %>
|
|
229
|
+
<% if i['assignee'] %>@<%= i['assignee']['login'] %> is assigned. <% end %>\
|
|
230
|
+
<% unless i['labels'].empty? %><%= format_labels(i['labels']) %><% end %>\
|
|
231
|
+
<% end %>\
|
|
232
|
+
<% if i['milestone'] %>
|
|
233
|
+
Milestone #<%= i['milestone']['number'] %>: <%= i['milestone']['title'] %>\
|
|
234
|
+
<%= " \#{bright{fg(:yellow){'⚠'}}}" if past_due? i['milestone'] %>\
|
|
235
|
+
<% end %>
|
|
236
|
+
<% if i['body'] && !i['body'].empty? %>
|
|
237
|
+
<%= indent i['body'], 4, width %>
|
|
238
|
+
<% end %>
|
|
239
|
+
|
|
240
|
+
EOF
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def format_comments_and_events elements
|
|
244
|
+
return 'None.' if elements.empty?
|
|
245
|
+
elements.map do |element|
|
|
246
|
+
if event = element['event']
|
|
247
|
+
format_event(element) unless unimportant_event?(event)
|
|
248
|
+
else
|
|
249
|
+
format_comment(element)
|
|
250
|
+
end
|
|
251
|
+
end.compact
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def format_comment c, width = columns
|
|
255
|
+
<<EOF
|
|
256
|
+
@#{c['user']['login']} commented \
|
|
257
|
+
#{format_date DateTime.parse(c['created_at'])}:
|
|
258
|
+
#{indent c['body'], 4, width}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
EOF
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def format_event e, width = columns
|
|
265
|
+
reference = e['commit_id']
|
|
266
|
+
<<EOF
|
|
267
|
+
#{bright { '⁕' }} #{format_event_type(e['event'])} by @#{e['actor']['login']}\
|
|
268
|
+
#{" through #{underline { reference[0..6] }}" if reference} \
|
|
269
|
+
#{format_date DateTime.parse(e['created_at'])}
|
|
270
|
+
|
|
271
|
+
EOF
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def format_milestones milestones
|
|
275
|
+
return 'None.' if milestones.empty?
|
|
276
|
+
|
|
277
|
+
max = milestones.sort_by { |m|
|
|
278
|
+
m['number'].to_s.size
|
|
279
|
+
}.last['number'].to_s.size
|
|
280
|
+
|
|
281
|
+
milestones.map { |m|
|
|
282
|
+
line = [" #{m['number'].to_s.rjust max }:"]
|
|
283
|
+
space = past_due?(m) ? 6 : 4
|
|
284
|
+
line << truncate(m['title'], max + space)
|
|
285
|
+
line << '⚠' if past_due? m
|
|
286
|
+
percent m, line.join(' ')
|
|
287
|
+
}
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def format_milestone m, width = columns
|
|
291
|
+
ERB.new(<<EOF).result binding
|
|
292
|
+
<%= bright { no_color { \
|
|
293
|
+
indent '#%s: %s' % m.values_at('number', 'title'), 0, width } } %>
|
|
294
|
+
@<%= m['creator']['login'] %> created this milestone \
|
|
295
|
+
<%= format_date DateTime.parse(m['created_at']) %>. \
|
|
296
|
+
<%= format_state m['state'], format_tag(m['state']), :bg %>
|
|
297
|
+
<% if m['due_on'] %>\
|
|
298
|
+
<% due_on = DateTime.parse m['due_on'] %>\
|
|
299
|
+
<% if past_due? m %>\
|
|
300
|
+
<%= bright{fg(:yellow){"⚠"}} %> \
|
|
301
|
+
<%= bright{fg(:red){"Past due by \#{format_date due_on, false}."}} %>
|
|
302
|
+
<% else %>\
|
|
303
|
+
Due in <%= format_date due_on, false %>.
|
|
304
|
+
<% end %>\
|
|
305
|
+
<% end %>\
|
|
306
|
+
<%= percent m %>
|
|
307
|
+
<% if m['description'] && !m['description'].empty? %>
|
|
308
|
+
<%= indent m['description'], 4, width %>
|
|
309
|
+
<% end %>
|
|
310
|
+
|
|
311
|
+
EOF
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def past_due? milestone
|
|
315
|
+
return false unless milestone['due_on']
|
|
316
|
+
DateTime.parse(milestone['due_on']) <= DateTime.now
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def percent milestone, string = nil
|
|
320
|
+
open, closed = milestone.values_at('open_issues', 'closed_issues')
|
|
321
|
+
complete = closed.to_f / (open + closed)
|
|
322
|
+
complete = 0 if complete.nan?
|
|
323
|
+
i = (columns * complete).round
|
|
324
|
+
if string.nil?
|
|
325
|
+
string = ' %d%% (%d closed, %d open)' % [complete * 100, closed, open]
|
|
326
|
+
end
|
|
327
|
+
string = string.ljust columns
|
|
328
|
+
[bg('2cc200'){string[0, i]}, string[i, columns - i]].join
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def format_state state, string = state, layer = :fg
|
|
332
|
+
color_codes = {
|
|
333
|
+
'closed' => 'ff0000',
|
|
334
|
+
'open' => '2cc200',
|
|
335
|
+
'merged' => '511c7d',
|
|
336
|
+
}
|
|
337
|
+
send(layer, color_codes[state]) { string }
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def format_labels labels
|
|
341
|
+
return if labels.empty?
|
|
342
|
+
[*labels].map { |l| bg(l['color']) { format_tag l['name'] } }.join ' '
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def format_tag tag
|
|
346
|
+
(colorize? ? ' %s ' : '[%s]') % tag
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def format_event_type(event)
|
|
350
|
+
color_codes = {
|
|
351
|
+
'reopened' => '2cc200',
|
|
352
|
+
'closed' => 'ff0000',
|
|
353
|
+
'merged' => '9677b1',
|
|
354
|
+
'assigned' => 'e1811d',
|
|
355
|
+
'referenced' => 'aaaaaa'
|
|
356
|
+
}
|
|
357
|
+
fg(color_codes[event]) { event }
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
#--
|
|
361
|
+
# Helpers:
|
|
362
|
+
#++
|
|
363
|
+
|
|
364
|
+
#--
|
|
365
|
+
# TODO: DRY up editor formatters.
|
|
366
|
+
#++
|
|
367
|
+
def format_editor issue = nil
|
|
368
|
+
message = ERB.new(<<EOF).result binding
|
|
369
|
+
|
|
370
|
+
Please explain the issue. The first line will become the title. Trailing
|
|
371
|
+
lines starting with '#' (like these) will be ignored, and empty messages will
|
|
372
|
+
not be submitted. Issues are formatted with GitHub Flavored Markdown (GFM):
|
|
373
|
+
|
|
374
|
+
http://github.github.com/github-flavored-markdown
|
|
375
|
+
|
|
376
|
+
On <%= repo %>
|
|
377
|
+
|
|
378
|
+
<%= no_color { format_issue issue, columns - 2 if issue } %>
|
|
379
|
+
EOF
|
|
380
|
+
message.rstrip!
|
|
381
|
+
message.gsub!(/(?!\A)^.*$/) { |line|
|
|
382
|
+
"# #{line}".rstrip
|
|
383
|
+
}
|
|
384
|
+
# Adding an extra newline for formatting
|
|
385
|
+
message.insert 0, "\n"
|
|
386
|
+
message.insert 0, [
|
|
387
|
+
issue['title'] || issue[:title], issue['body'] || issue[:body]
|
|
388
|
+
].compact.join("\n\n") if issue
|
|
389
|
+
message
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def format_milestone_editor milestone = nil
|
|
393
|
+
message = ERB.new(<<EOF).result binding
|
|
394
|
+
|
|
395
|
+
Describe the milestone. The first line will become the title. Trailing lines
|
|
396
|
+
starting with '#' (like these) will be ignored, and empty messages will not be
|
|
397
|
+
submitted. Milestones are formatted with GitHub Flavored Markdown (GFM):
|
|
398
|
+
|
|
399
|
+
http://github.github.com/github-flavored-markdown
|
|
400
|
+
|
|
401
|
+
On <%= repo %>
|
|
402
|
+
|
|
403
|
+
<%= no_color { format_milestone milestone, columns - 2 } if milestone %>
|
|
404
|
+
EOF
|
|
405
|
+
message.rstrip!
|
|
406
|
+
message.gsub!(/(?!\A)^.*$/) { |line| "# #{line}".rstrip }
|
|
407
|
+
message.insert 0, [
|
|
408
|
+
milestone['title'], milestone['description']
|
|
409
|
+
].join("\n\n") if milestone
|
|
410
|
+
message
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def format_comment_editor issue, comment = nil
|
|
414
|
+
message = ERB.new(<<EOF).result binding
|
|
415
|
+
|
|
416
|
+
Leave a comment. Trailing lines starting with '#' (like these) will be ignored,
|
|
417
|
+
and empty messages will not be submitted. Comments are formatted with GitHub
|
|
418
|
+
Flavored Markdown (GFM):
|
|
419
|
+
|
|
420
|
+
http://github.github.com/github-flavored-markdown
|
|
421
|
+
|
|
422
|
+
On <%= repo %> issue #<%= issue['number'] %>
|
|
423
|
+
|
|
424
|
+
<%= no_color { format_issue issue } if verbose %>\
|
|
425
|
+
<%= no_color { format_comment comment, columns - 2 } if comment %>
|
|
426
|
+
EOF
|
|
427
|
+
message.rstrip!
|
|
428
|
+
message.gsub!(/(?!\A)^.*$/) { |line| "# #{line}".rstrip }
|
|
429
|
+
message.insert 0, comment['body'] if comment
|
|
430
|
+
message
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def format_markdown string, indent = 4
|
|
434
|
+
c = '268bd2'
|
|
435
|
+
|
|
436
|
+
# Headers.
|
|
437
|
+
string.gsub!(/^( {#{indent}}\#{1,6} .+)$/, bright{'\1'})
|
|
438
|
+
string.gsub!(
|
|
439
|
+
/(^ {#{indent}}.+$\n^ {#{indent}}[-=]+$)/, bright{'\1'}
|
|
440
|
+
)
|
|
441
|
+
# Strong.
|
|
442
|
+
string.gsub!(
|
|
443
|
+
/(^|\s)(\*{2}\w(?:[^*]*\w)?\*{2})(\s|$)/m, '\1' + bright{'\2'} + '\3'
|
|
444
|
+
)
|
|
445
|
+
string.gsub!(
|
|
446
|
+
/(^|\s)(_{2}\w(?:[^_]*\w)?_{2})(\s|$)/m, '\1' + bright {'\2'} + '\3'
|
|
447
|
+
)
|
|
448
|
+
# Emphasis.
|
|
449
|
+
string.gsub!(
|
|
450
|
+
/(^|\s)(\*\w(?:[^*]*\w)?\*)(\s|$)/m, '\1' + underline{'\2'} + '\3'
|
|
451
|
+
)
|
|
452
|
+
string.gsub!(
|
|
453
|
+
/(^|\s)(_\w(?:[^_]*\w)?_)(\s|$)/m, '\1' + underline{'\2'} + '\3'
|
|
454
|
+
)
|
|
455
|
+
# Bullets/Blockquotes.
|
|
456
|
+
string.gsub!(/(^ {#{indent}}(?:[*>-]|\d+\.) )/, fg(c){'\1'})
|
|
457
|
+
# URIs.
|
|
458
|
+
string.gsub!(
|
|
459
|
+
%r{\b(<)?(https?://\S+|[^@\s]+@[^@\s]+)(>)?\b},
|
|
460
|
+
fg(c){'\1' + underline{'\2'} + '\3'}
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# Inline code
|
|
464
|
+
string.gsub!(/`([^`].+?)`(?=[^`])/, inverse { ' \1 ' })
|
|
465
|
+
|
|
466
|
+
# Code blocks
|
|
467
|
+
string.gsub!(/(?<indent>^\ {#{indent}})(```)\s*(?<lang>\w*$)(\n)(?<code>.+?)(\n)(^\ {#{indent}}```$)/m) do |m|
|
|
468
|
+
highlight(Regexp.last_match)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
string
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def format_date date, suffix = true
|
|
475
|
+
days = (interval = DateTime.now - date).to_i.abs
|
|
476
|
+
string = if days.zero?
|
|
477
|
+
seconds, _ = interval.divmod Rational(1, 86400)
|
|
478
|
+
hours, seconds = seconds.divmod 3600
|
|
479
|
+
minutes, seconds = seconds.divmod 60
|
|
480
|
+
if hours > 0
|
|
481
|
+
"#{hours} hour#{'s' unless hours == 1}"
|
|
482
|
+
elsif minutes > 0
|
|
483
|
+
"#{minutes} minute#{'s' unless minutes == 1}"
|
|
484
|
+
else
|
|
485
|
+
"#{seconds} second#{'s' unless seconds == 1}"
|
|
486
|
+
end
|
|
487
|
+
else
|
|
488
|
+
"#{days} day#{'s' unless days == 1}"
|
|
489
|
+
end
|
|
490
|
+
ago = interval < 0 ? 'from now' : 'ago' if suffix
|
|
491
|
+
[string, ago].compact.join ' '
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def throb position = 0, redraw = CURSOR[:up][1]
|
|
495
|
+
return yield unless paginate?
|
|
496
|
+
|
|
497
|
+
throb = THROBBERS[rand(THROBBERS.length)]
|
|
498
|
+
throb.reverse! if rand > 0.5
|
|
499
|
+
i = rand throb.length
|
|
500
|
+
|
|
501
|
+
thread = Thread.new do
|
|
502
|
+
dot = lambda do
|
|
503
|
+
print "\r#{CURSOR[:column][position]}#{throb[i]}#{CURSOR[:hide]}"
|
|
504
|
+
i = (i + 1) % throb.length
|
|
505
|
+
sleep 0.1 and dot.call
|
|
506
|
+
end
|
|
507
|
+
dot.call
|
|
508
|
+
end
|
|
509
|
+
yield
|
|
510
|
+
ensure
|
|
511
|
+
if thread
|
|
512
|
+
thread.kill
|
|
513
|
+
puts "\r#{CURSOR[:column][position]}#{redraw}#{CURSOR[:show]}"
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
private
|
|
518
|
+
|
|
519
|
+
def unimportant_event?(event)
|
|
520
|
+
%w{ subscribed unsubscribed mentioned }.include?(event)
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
end
|
data/lib/ghi/web.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'open-uri'
|
|
2
|
+
require 'uri'
|
|
3
|
+
|
|
4
|
+
module GHI
|
|
5
|
+
class Web
|
|
6
|
+
HOST = GHI.config('github.host') || 'github.com'
|
|
7
|
+
BASE_URI = "https://#{HOST}/"
|
|
8
|
+
|
|
9
|
+
attr_reader :base
|
|
10
|
+
def initialize base
|
|
11
|
+
@base = base
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def open path = '', params = {}
|
|
15
|
+
path = uri_for path, params
|
|
16
|
+
$stdout.puts path
|
|
17
|
+
return unless $stdout.tty?
|
|
18
|
+
launcher = 'open'
|
|
19
|
+
launcher = 'xdg-open' if /linux/ =~ RUBY_PLATFORM
|
|
20
|
+
system "#{launcher} '#{path}'"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def curl path = '', params = {}
|
|
24
|
+
uri_for(path, params).open.read
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def uri_for path, params
|
|
30
|
+
unless params.empty?
|
|
31
|
+
q = params.map { |k, v| "#{CGI.escape k.to_s}=#{CGI.escape v.to_s}" }
|
|
32
|
+
path += "?#{q.join '&'}"
|
|
33
|
+
end
|
|
34
|
+
URI(BASE_URI) + "#{base}/" + path
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|