ghi 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c253937537d9abb1e9a721c469220a8e2fb95050
4
- data.tar.gz: 20c3911c61cbec263eb0347f8a5ce66de3290004
3
+ metadata.gz: 542fd93fc503609bf5d4cc5a258356d35ee0313c
4
+ data.tar.gz: 03e5dc1531dd7a57fe02e36e265f54d94c799546
5
5
  SHA512:
6
- metadata.gz: 39bd93ab895a37ca157602f80183978b7c27895a5b6e0af4962ee7dd076a957575854b46aece1fe8b9e1d87a22a574aecd30b5c03c563855144b1870cebdf60a
7
- data.tar.gz: 4d136961fe3a859f1c9958d4e0d9a067d68074e0e48361074a0f84e00331516c5069cf759505d5676631f398c72c82eb2491df91f921792cb43ecd046146a23b
6
+ metadata.gz: f2984c9e741d600f119935ff9afb7b7a228839987d56caf40311aaaa43358ca000b5fdab2eba6cdea1f551ac0ce5b1d71e8c1cda7d2f43dea3e26b23b126cd30
7
+ data.tar.gz: 7e0e12147f1c5fc7f6a2cf6563076fa01e4e9648e0e0b9f0a0403627d95fcf405c399b3ce75400d14f489e747126617cf24ad820f56e31f373ae4348ab9999e7
@@ -18,12 +18,15 @@ module GHI
18
18
  return false unless user && pass
19
19
  code ||= nil # 2fa
20
20
  args = code ? [] : [54, "✔\r"]
21
+ note = %w[ghi]
22
+ note << "(#{GHI.repo})" if local
23
+ note << "on #{Socket.gethostname}"
21
24
  res = throb(*args) {
22
25
  headers = {}
23
26
  headers['X-GitHub-OTP'] = code if code
24
27
  body = {
25
28
  :scopes => %w(public_repo repo),
26
- :note => 'ghi',
29
+ :note => note.join(' '),
27
30
  :note_url => 'https://github.com/stephencelis/ghi'
28
31
  }
29
32
  Client.new(user, pass).post(
@@ -47,7 +50,18 @@ module GHI
47
50
  retry
48
51
  end
49
52
 
50
- abort "#{e.message}#{CURSOR[:column][0]}"
53
+ if e.errors.any? { |err| err['code'] == 'already_exists' }
54
+ message = <<EOF.chomp
55
+ A ghi token already exists!
56
+
57
+ Please revoke all previously-generated ghi personal access tokens here:
58
+
59
+ https://github.com/settings/applications
60
+ EOF
61
+ else
62
+ message = e.message
63
+ end
64
+ abort "#{message}#{CURSOR[:column][0]}"
51
65
  end
52
66
 
53
67
  def username
data/lib/ghi/client.rb CHANGED
@@ -45,7 +45,7 @@ module GHI
45
45
  end
46
46
  end
47
47
 
48
- CONTENT_TYPE = 'application/vnd.github+json'
48
+ CONTENT_TYPE = 'application/vnd.github.beta+json'
49
49
  USER_AGENT = 'ghi/%s (%s; +%s)' % [
50
50
  GHI::Commands::Version::VERSION,
51
51
  RUBY_DESCRIPTION,
@@ -88,8 +88,7 @@ module GHI
88
88
  if index = args.index { |arg| /^\d+$/ === arg }
89
89
  @issue = args.delete_at index
90
90
  else
91
- @issue = `git symbolic-ref --short HEAD 2>/dev/null`[/^\d+/];
92
- warn "(Inferring issue from branch prefix: ##@issue)" if @issue
91
+ infer_issue_from_branch_prefix
93
92
  end
94
93
  @issue
95
94
  end
@@ -97,6 +96,11 @@ module GHI
97
96
  alias milestone issue
98
97
  alias extract_milestone issue
99
98
 
99
+ def infer_issue_from_branch_prefix
100
+ @issue = `git symbolic-ref --short HEAD 2>/dev/null`[/^\d+/];
101
+ warn "(Inferring issue from branch prefix: ##@issue)" if @issue
102
+ end
103
+
100
104
  def require_issue
101
105
  raise MissingArgument, 'Issue required.' unless issue
102
106
  end
@@ -109,6 +113,10 @@ module GHI
109
113
  def any_or_none_or input
110
114
  input ? input : { nil => '*', false => 'none' }[input]
111
115
  end
116
+
117
+ def sort_by_creation(arr)
118
+ arr.sort_by { |el| el['created_at'] }
119
+ end
112
120
  end
113
121
  end
114
122
  end
@@ -45,9 +45,11 @@ EOF
45
45
 
46
46
  case action
47
47
  when 'list'
48
+ get_requests(:index, :events)
48
49
  res = index
49
50
  page do
50
- puts format_comments(res.body)
51
+ elements = sort_by_creation(res.body + paged_events(events, res))
52
+ puts format_comments_and_events(elements)
51
53
  break unless res.next_page
52
54
  res = throb { api.get res.next_page }
53
55
  end
@@ -76,7 +78,7 @@ EOF
76
78
  protected
77
79
 
78
80
  def index
79
- throb { api.get uri, :per_page => 100 }
81
+ @index ||= throb { api.get uri, :per_page => 100 }
80
82
  end
81
83
 
82
84
  def create message = 'Commented.'
@@ -96,8 +98,28 @@ EOF
96
98
  puts 'Comment deleted.'
97
99
  end
98
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
+
99
114
  private
100
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
+
101
123
  def uri
102
124
  if comment
103
125
  comment['url']
@@ -106,6 +128,10 @@ EOF
106
128
  end
107
129
  end
108
130
 
131
+ def event_uri
132
+ "/repos/#{repo}/issues/#{issue}/events"
133
+ end
134
+
109
135
  def require_body
110
136
  assigns[:body] = args.join ' ' unless args.empty?
111
137
  return if assigns[:body]
@@ -125,6 +151,18 @@ EOF
125
151
  assigns[:body] = message if message
126
152
  e
127
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
128
166
  end
129
167
  end
130
168
  end
@@ -112,7 +112,7 @@ EOF
112
112
  if ref = %x{
113
113
  git rev-parse --abbrev-ref HEAD@{upstream} 2>/dev/null
114
114
  }.chomp!
115
- ref.split('/').last if $? == 0
115
+ ref.split('/', 2).last if $? == 0
116
116
  end
117
117
  end
118
118
  assigns[:head] ||= head
@@ -15,7 +15,7 @@ module GHI
15
15
  opts.banner = <<EOF
16
16
  usage: ghi label <labelname> [-c <color>] [-r <newname>]
17
17
  or: ghi label -D <labelname>
18
- or: ghi label <issueno> [-a] [-d] [-f]
18
+ or: ghi label <issueno(s)> [-a] [-d] [-f] <label>
19
19
  or: ghi label -l [<issueno>]
20
20
  EOF
21
21
  opts.separator ''
@@ -41,13 +41,13 @@ EOF
41
41
  opts.separator ''
42
42
  opts.separator 'Issue modification options'
43
43
  opts.on '-a', '--add', 'add labels to issue' do
44
- self.action = issue ? 'add' : 'create'
44
+ self.action = issues_present? ? 'add' : 'create'
45
45
  end
46
46
  opts.on '-d', '--delete', 'remove labels from issue' do
47
- self.action = issue ? 'remove' : 'destroy'
47
+ self.action = issues_present? ? 'remove' : 'destroy'
48
48
  end
49
49
  opts.on '-f', '--force', 'replace existing labels' do
50
- self.action = issue ? 'replace' : 'update'
50
+ self.action = issues_present? ? 'replace' : 'update'
51
51
  end
52
52
  opts.separator ''
53
53
  end
@@ -58,16 +58,16 @@ EOF
58
58
  require_repo
59
59
  options.parse! args.empty? ? %w(-l) : args
60
60
 
61
- if issue
61
+ if issues_present?
62
62
  self.action ||= 'add'
63
63
  self.name = args.shift.to_s.split ','
64
64
  self.name.concat args
65
+ multi_action(action)
65
66
  else
66
67
  self.action ||= 'create'
67
68
  self.name ||= args.shift
69
+ send action
68
70
  end
69
-
70
- send action
71
71
  end
72
72
 
73
73
  protected
@@ -156,6 +156,41 @@ EOF
156
156
  def base_uri
157
157
  "/repos/#{repo}/#{issue ? "issues/#{issue}/labels" : 'labels'}"
158
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
159
194
  end
160
195
  end
161
196
  end
@@ -7,6 +7,7 @@ module GHI
7
7
  attr_accessor :reverse
8
8
  attr_accessor :quiet
9
9
  attr_accessor :exclude_pull_requests
10
+ attr_accessor :pull_requests_only
10
11
 
11
12
  def options
12
13
  OptionParser.new do |opts|
@@ -42,7 +43,8 @@ module GHI
42
43
  opts.on '--reverse', 'reverse (ascending) sort order' do
43
44
  self.reverse = !reverse
44
45
  end
45
- opts.on('-p', '--no-pulls','exclude pull requests') { self.exclude_pull_requests = true }
46
+ opts.on('-p', '--pulls','list only pull requests') { self.pull_requests_only = true }
47
+ opts.on('-P', '--no-pulls','exclude pull requests') { self.exclude_pull_requests = true }
46
48
  opts.on(
47
49
  '--since <date>', 'issues more recent than',
48
50
  "e.g., '2011-04-30'"
@@ -136,8 +138,10 @@ module GHI
136
138
  print "\r#{CURSOR[:up][1]}" if header && paginate?
137
139
  page header do
138
140
  issues = res.body
139
- if exclude_pull_requests
140
- issues = issues.reject {|i| i["pull_request"].any? {|k,v| !v.nil? } }
141
+
142
+ if exclude_pull_requests || pull_requests_only
143
+ prs, issues = issues.partition { |i| i['pull_request'].values.any? }
144
+ issues = prs if pull_requests_only
141
145
  end
142
146
  if assigns[:exclude_labels]
143
147
  issues = issues.reject do |i|
@@ -82,7 +82,7 @@ EOF
82
82
  i = throb { api.post "/repos/#{repo}/issues", assigns }.body
83
83
  e.unlink if e
84
84
  puts format_issue(i)
85
- puts 'Opened.'
85
+ puts "Opened on #{repo}."
86
86
  end
87
87
  end
88
88
  rescue Client::Error => e
@@ -32,6 +32,7 @@ module GHI
32
32
  end
33
33
  else
34
34
  i = throb { api.get "/repos/#{repo}/issues/#{issue}" }.body
35
+ determine_merge_status(i) if pull_request?(i)
35
36
  page do
36
37
  puts format_issue(i)
37
38
  n = i['comments']
@@ -44,6 +45,21 @@ module GHI
44
45
  end
45
46
  end
46
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
47
63
  end
48
64
  end
49
65
  end
@@ -3,7 +3,7 @@ module GHI
3
3
  module Version
4
4
  MAJOR = 0
5
5
  MINOR = 9
6
- PATCH = 2
6
+ PATCH = 3
7
7
  PRE = nil
8
8
 
9
9
  VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join '.'
@@ -33,7 +33,7 @@ module GHI
33
33
 
34
34
  def puts *strings
35
35
  strings = strings.flatten.map { |s|
36
- s.gsub(/(^| )*@([^@\s]+)/) {
36
+ s.gsub(/(^| *)@(\w+)/) {
37
37
  if $2 == Authorization.username
38
38
  bright { fg(:yellow) { "#$1@#$2" } }
39
39
  else
@@ -117,7 +117,7 @@ module GHI
117
117
  end
118
118
 
119
119
  def format_issues_header
120
- state = assigns[:state] || 'open'
120
+ state = assigns[:state] ||= 'open'
121
121
  header = "# #{repo || 'Global,'} #{state} issues"
122
122
  if repo
123
123
  if milestone = assigns[:milestone]
@@ -210,6 +210,7 @@ module GHI
210
210
  *i.values_at('number', 'title')], 0, width } } %>
211
211
  @<%= i['user']['login'] %> opened this <%= p ? 'pull request' : 'issue' %> \
212
212
  <%= format_date DateTime.parse(i['created_at']) %>. \
213
+ <% if i['merged'] %><%= format_state 'merged', format_tag('merged'), :bg %><% end %> \
213
214
  <%= format_state i['state'], format_tag(i['state']), :bg %> \
214
215
  <% unless i['comments'] == 0 %>\
215
216
  <%= fg('aaaaaa'){
@@ -233,9 +234,15 @@ Milestone #<%= i['milestone']['number'] %>: <%= i['milestone']['title'] %>\
233
234
  EOF
234
235
  end
235
236
 
236
- def format_comments comments
237
- return 'None.' if comments.empty?
238
- comments.map { |comment| format_comment comment }
237
+ def format_comments_and_events elements
238
+ return 'None.' if elements.empty?
239
+ elements.map do |element|
240
+ if event = element['event']
241
+ format_event(element) unless unimportant_event?(event)
242
+ else
243
+ format_comment(element)
244
+ end
245
+ end.compact
239
246
  end
240
247
 
241
248
  def format_comment c, width = columns
@@ -245,6 +252,16 @@ EOF
245
252
  #{indent c['body'], 4, width}
246
253
 
247
254
 
255
+ EOF
256
+ end
257
+
258
+ def format_event e, width = columns
259
+ reference = e['commit_id']
260
+ <<EOF
261
+ #{bright { '⁕' }} #{format_event_type(e['event'])} by @#{e['actor']['login']}\
262
+ #{" through #{underline { reference[0..6] }}" if reference} \
263
+ #{format_date DateTime.parse(e['created_at'])}
264
+
248
265
  EOF
249
266
  end
250
267
 
@@ -306,7 +323,12 @@ EOF
306
323
  end
307
324
 
308
325
  def format_state state, string = state, layer = :fg
309
- send(layer, state == 'closed' ? 'ff0000' : '2cc200') { string }
326
+ color_codes = {
327
+ 'closed' => 'ff0000',
328
+ 'open' => '2cc200',
329
+ 'merged' => '511c7d',
330
+ }
331
+ send(layer, color_codes[state]) { string }
310
332
  end
311
333
 
312
334
  def format_labels labels
@@ -318,6 +340,17 @@ EOF
318
340
  (colorize? ? ' %s ' : '[%s]') % tag
319
341
  end
320
342
 
343
+ def format_event_type(event)
344
+ color_codes = {
345
+ 'reopened' => '2cc200',
346
+ 'closed' => 'ff0000',
347
+ 'merged' => '9677b1',
348
+ 'assigned' => 'e1811d',
349
+ 'referenced' => 'aaaaaa'
350
+ }
351
+ fg(color_codes[event]) { event }
352
+ end
353
+
321
354
  #--
322
355
  # Helpers:
323
356
  #++
@@ -416,16 +449,15 @@ EOF
416
449
  %r{\b(<)?(https?://\S+|[^@\s]+@[^@\s]+)(>)?\b},
417
450
  fg(c){'\1' + underline{'\2'} + '\3'}
418
451
  )
419
- # Code.
420
- # string.gsub!(
421
- # /
422
- # (^\ {#{indent}}```.*?$)(.+?^\ {#{indent}}```$)|
423
- # (^|[^`])(`[^`]+`)([^`]|$)
424
- # /mx
425
- # ) {
426
- # post = $5
427
- # fg(c){"#$1#$2#$3#$4".gsub(/\e\[[\d;]+m/, '')} + "#{post}"
428
- # }
452
+
453
+ # Inline code
454
+ string.gsub!(/`([^`].+?)`(?=[^`])/, inverse { ' \1 ' })
455
+
456
+ # Code blocks
457
+ string.gsub!(/(?<indent>^\ {#{indent}})(```)\s*(?<lang>\w*$)(\n)(?<code>.+?)(\n)(^\ {#{indent}}```$)/m) do |m|
458
+ highlight(Regexp.last_match)
459
+ end
460
+
429
461
  string
430
462
  end
431
463
 
@@ -471,5 +503,11 @@ EOF
471
503
  puts "\r#{CURSOR[:column][position]}#{redraw}#{CURSOR[:show]}"
472
504
  end
473
505
  end
506
+
507
+ private
508
+
509
+ def unimportant_event?(event)
510
+ %w{ subscribed unsubscribed mentioned }.include?(event)
511
+ end
474
512
  end
475
513
  end
@@ -37,6 +37,11 @@ module GHI
37
37
  escape :inverse, &block
38
38
  end
39
39
 
40
+ def highlight(code_block)
41
+ return code_block unless colorize?
42
+ highlighter.highlight(code_block)
43
+ end
44
+
40
45
  def no_color
41
46
  old_colorize, Colors.colorize = colorize?, false
42
47
  yield
@@ -228,7 +233,11 @@ module GHI
228
233
  end
229
234
 
230
235
  def escape_256 color
231
- "8;5;#{to_256(*to_rgb(color))}" if `tput colors` =~ /256/
236
+ "8;5;#{to_256(*to_rgb(color))}" if supports_256_colors?
237
+ end
238
+
239
+ def supports_256_colors?
240
+ `tput colors` =~ /256/
232
241
  end
233
242
 
234
243
  def to_256 r, g, b
@@ -294,6 +303,58 @@ module GHI
294
303
  return m1 + (m2 - m1) * (2.0/3 - h) * 6 if h * 3 < 2
295
304
  return m1
296
305
  end
306
+
307
+ def highlighter
308
+ @highlighter ||= begin
309
+ raise unless supports_256_colors?
310
+ require 'pygments'
311
+ Pygmentizer.new
312
+ rescue
313
+ FakePygmentizer.new
314
+ end
315
+ end
316
+
317
+ class FakePygmentizer
318
+ def highlight(code_block)
319
+ code_block
320
+ end
321
+ end
322
+
323
+ class Pygmentizer
324
+ def initialize
325
+ @style = GHI.config('ghi.highlight.style') || 'monokai'
326
+ end
327
+
328
+ def highlight(code_block)
329
+ begin
330
+ indent = code_block['indent']
331
+ lang = code_block['lang']
332
+ code = code_block['code']
333
+
334
+ output = pygmentize(lang, code)
335
+ with_indentation(output, indent)
336
+ rescue
337
+ code_block
338
+ end
339
+ end
340
+
341
+ private
342
+
343
+ def pygmentize(lang, code)
344
+ Pygments.highlight(unescape(code), :formatter => '256', :lexer => lang,
345
+ :options => { :style => @style })
346
+ end
347
+
348
+ def unescape(str)
349
+ str.gsub(/\e\[[^m]*m/, '')
350
+ end
351
+
352
+ def with_indentation(string, indent)
353
+ string.each_line.map do |line|
354
+ "#{indent}#{line}"
355
+ end.join
356
+ end
357
+ end
297
358
  end
298
359
  end
299
360
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ghi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Celis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-02-17 00:00:00.000000000 Z
11
+ date: 2014-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -88,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
88
  version: '0'
89
89
  requirements: []
90
90
  rubyforge_project:
91
- rubygems_version: 2.2.0
91
+ rubygems_version: 2.2.2
92
92
  signing_key:
93
93
  specification_version: 4
94
94
  summary: GitHub Issues command line interface