ghi 0.9.2 → 0.9.3

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 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