ghi 0.3.1 → 0.9.0.dev
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.
- data/bin/ghi +2 -4
- data/lib/ghi.rb +112 -42
- data/lib/ghi/authorization.rb +71 -0
- data/lib/ghi/client.rb +126 -0
- data/lib/ghi/commands.rb +20 -0
- data/lib/ghi/commands/assign.rb +53 -0
- data/lib/ghi/commands/close.rb +45 -0
- data/lib/ghi/commands/command.rb +114 -0
- data/lib/ghi/commands/comment.rb +104 -0
- data/lib/ghi/commands/config.rb +35 -0
- data/lib/ghi/commands/edit.rb +49 -0
- data/lib/ghi/commands/help.rb +62 -0
- data/lib/ghi/commands/label.rb +153 -0
- data/lib/ghi/commands/list.rb +133 -0
- data/lib/ghi/commands/milestone.rb +150 -0
- data/lib/ghi/commands/open.rb +69 -0
- data/lib/ghi/commands/show.rb +21 -0
- data/lib/ghi/commands/version.rb +16 -0
- data/lib/ghi/formatting.rb +301 -0
- data/lib/ghi/formatting/colors.rb +295 -0
- data/lib/ghi/json.rb +1304 -0
- metadata +71 -49
- data/README.rdoc +0 -126
- data/lib/ghi/api.rb +0 -145
- data/lib/ghi/cli.rb +0 -657
- data/lib/ghi/issue.rb +0 -30
- data/spec/ghi/api_spec.rb +0 -218
- data/spec/ghi/cli_spec.rb +0 -267
- data/spec/ghi/issue_spec.rb +0 -26
- data/spec/ghi_spec.rb +0 -62
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'curses'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module GHI
|
5
|
+
module Commands
|
6
|
+
class List < Command
|
7
|
+
attr_accessor :reverse
|
8
|
+
attr_accessor :quiet
|
9
|
+
|
10
|
+
def options
|
11
|
+
OptionParser.new do |opts|
|
12
|
+
opts.banner = 'usage: ghi list [options]'
|
13
|
+
opts.separator ''
|
14
|
+
opts.on '-a', '--global', '--all', 'all of your issues on GitHub' do
|
15
|
+
@repo = nil
|
16
|
+
end
|
17
|
+
opts.on(
|
18
|
+
'-s', '--state <in>', %w(open closed),
|
19
|
+
{'o'=>'open', 'c'=>'closed'}, "'open' or 'closed'"
|
20
|
+
) do |state|
|
21
|
+
assigns[:state] = state
|
22
|
+
end
|
23
|
+
opts.on(
|
24
|
+
'-L', '--label <labelname>...', Array, 'by label(s)'
|
25
|
+
) do |labels|
|
26
|
+
assigns[:labels] = labels.join ','
|
27
|
+
end
|
28
|
+
opts.on(
|
29
|
+
'-S', '--sort <by>', %w(created updated comments),
|
30
|
+
{'c'=>'created','u'=>'updated','m'=>'comments'},
|
31
|
+
"'created', 'updated', or 'comments'"
|
32
|
+
) do |sort|
|
33
|
+
assigns[:sort] = sort
|
34
|
+
end
|
35
|
+
opts.on '--reverse', 'reverse (ascending) sort order' do
|
36
|
+
self.reverse = !reverse
|
37
|
+
end
|
38
|
+
opts.on(
|
39
|
+
'--since <date>', 'issues more recent than',
|
40
|
+
"e.g., '2011-04-30'"
|
41
|
+
) do |date|
|
42
|
+
begin
|
43
|
+
assigns[:since] = DateTime.parse date # TODO: Better parsing.
|
44
|
+
rescue ArgumentError => e
|
45
|
+
raise OptionParser::InvalidArgument, e.message
|
46
|
+
end
|
47
|
+
end
|
48
|
+
opts.on '-v', '--verbose' do
|
49
|
+
self.verbose = true
|
50
|
+
end
|
51
|
+
opts.separator ''
|
52
|
+
opts.separator 'Global options'
|
53
|
+
opts.on(
|
54
|
+
'-f', '--filter <by>',
|
55
|
+
filters = %w(assigned created mentioned subscribed),
|
56
|
+
Hash[filters.map { |f| [f[0, 1], f] }],
|
57
|
+
"'assigned', 'created', 'mentioned', or", "'subscribed'"
|
58
|
+
) do |filter|
|
59
|
+
assigns[:filter] = filter
|
60
|
+
end
|
61
|
+
opts.separator ''
|
62
|
+
opts.separator 'Project options'
|
63
|
+
opts.on(
|
64
|
+
'-M', '--[no-]milestone [<n>]', Integer,
|
65
|
+
'with (specified) milestone'
|
66
|
+
) do |milestone|
|
67
|
+
assigns[:milestone] = any_or_none_or milestone
|
68
|
+
end
|
69
|
+
opts.on(
|
70
|
+
'-u', '--[no-]assignee [<user>]', 'assigned to specified user'
|
71
|
+
) do |assignee|
|
72
|
+
assigns[:assignee] = any_or_none_or assignee
|
73
|
+
end
|
74
|
+
opts.on '--mine', 'assigned to you' do
|
75
|
+
assigns[:assignee] = Authorization.username
|
76
|
+
end
|
77
|
+
opts.on(
|
78
|
+
'-U', '--mentioned [<user>]', 'mentioning you or specified user'
|
79
|
+
) do |mentioned|
|
80
|
+
assigns[:mentioned] = mentioned || Authorization.username
|
81
|
+
end
|
82
|
+
opts.separator ''
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def execute
|
87
|
+
begin
|
88
|
+
options.parse! args
|
89
|
+
rescue OptionParser::InvalidOption => e
|
90
|
+
fallback.parse! e.args
|
91
|
+
retry
|
92
|
+
end
|
93
|
+
|
94
|
+
if reverse
|
95
|
+
assigns[:sort] ||= 'created'
|
96
|
+
assigns[:direction] = 'asc'
|
97
|
+
end
|
98
|
+
|
99
|
+
unless quiet
|
100
|
+
print format_issues_header
|
101
|
+
print "\n" unless STDOUT.tty?
|
102
|
+
end
|
103
|
+
res = throb(
|
104
|
+
0, format_state(assigns[:state], quiet ? CURSOR[:up][1] : '#')
|
105
|
+
) { api.get uri, assigns }
|
106
|
+
loop do
|
107
|
+
issues = res.body
|
108
|
+
if verbose
|
109
|
+
puts issues.map { |i| format_issue i }
|
110
|
+
else
|
111
|
+
puts format_issues(issues, repo.nil?)
|
112
|
+
end
|
113
|
+
break unless res.next_page
|
114
|
+
page?
|
115
|
+
res = throb { api.get res.next_page }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def uri
|
122
|
+
repo ? "/repos/#{repo}/issues" : '/issues'
|
123
|
+
end
|
124
|
+
|
125
|
+
def fallback
|
126
|
+
OptionParser.new do |opts|
|
127
|
+
opts.on('-c', '--closed') { assigns[:state] = 'closed' }
|
128
|
+
opts.on('-q', '--quiet') { self.quiet = true }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module GHI
|
4
|
+
module Commands
|
5
|
+
class Milestone < Command
|
6
|
+
attr_accessor :reverse
|
7
|
+
|
8
|
+
#--
|
9
|
+
# FIXME: Opt for better interface, e.g.,
|
10
|
+
#
|
11
|
+
# ghi milestone [-v | --verbose] [--[no-]closed]
|
12
|
+
# ghi milestone add <name> <description>
|
13
|
+
# ghi milestone rm <milestoneno>
|
14
|
+
#++
|
15
|
+
def options
|
16
|
+
OptionParser.new do |opts|
|
17
|
+
opts.banner = <<EOF
|
18
|
+
usage: ghi milestone [<modification options>] [<milestoneno>]
|
19
|
+
or: ghi milestone -D <milestoneno>
|
20
|
+
or: ghi milestone -l [-c] [-v]
|
21
|
+
EOF
|
22
|
+
opts.separator ''
|
23
|
+
opts.on '-l', '--list', 'list milestones' do
|
24
|
+
self.action = 'index'
|
25
|
+
end
|
26
|
+
opts.on '-c', '--[no-]closed', 'show closed milestones' do |closed|
|
27
|
+
assigns[:state] = closed ? 'closed' : 'open'
|
28
|
+
end
|
29
|
+
opts.on(
|
30
|
+
'-S', '--sort <on>', %w(due_date completeness),
|
31
|
+
{'d'=>'due_date', 'due'=>'due_date', 'c'=>'completeness'},
|
32
|
+
"'due_date' or 'completeness'"
|
33
|
+
) do |sort|
|
34
|
+
assigns[:sort] = sort
|
35
|
+
end
|
36
|
+
opts.on '--reverse', 'reverse (ascending) sort order' do
|
37
|
+
self.reverse = !reverse
|
38
|
+
end
|
39
|
+
opts.on '-v', '--verbose', 'list milestones verbosely' do
|
40
|
+
self.verbose = true
|
41
|
+
end
|
42
|
+
opts.separator ''
|
43
|
+
opts.separator 'Milestone modification options'
|
44
|
+
opts.on(
|
45
|
+
'-m', '--message <text>', 'change milestone description'
|
46
|
+
) do |text|
|
47
|
+
self.action = 'create'
|
48
|
+
assigns[:title], assigns[:description] = text.split(/\n+/, 2)
|
49
|
+
end
|
50
|
+
# FIXME: We already describe --[no-]closed; describe this, too?
|
51
|
+
opts.on(
|
52
|
+
'-s', '--state <in>', %w(open closed),
|
53
|
+
{'o'=>'open', 'c'=>'closed'}, "'open' or 'closed'"
|
54
|
+
) do |state|
|
55
|
+
self.action = 'create'
|
56
|
+
assigns[:state] = state
|
57
|
+
end
|
58
|
+
opts.on(
|
59
|
+
'--due <on>', 'when milestone should be complete',
|
60
|
+
"e.g., '2012-04-30'"
|
61
|
+
) do |date|
|
62
|
+
self.action = 'create'
|
63
|
+
begin
|
64
|
+
# TODO: Better parsing.
|
65
|
+
assigns[:due_on] = DateTime.parse(date).strftime
|
66
|
+
rescue ArgumentError => e
|
67
|
+
raise OptionParser::InvalidArgument, e.message
|
68
|
+
end
|
69
|
+
end
|
70
|
+
opts.on '-D', '--delete', 'delete milestone' do
|
71
|
+
self.action = 'destroy'
|
72
|
+
end
|
73
|
+
opts.separator ''
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def execute
|
78
|
+
self.action = 'index'
|
79
|
+
extract_milestone
|
80
|
+
|
81
|
+
begin
|
82
|
+
options.parse! args
|
83
|
+
rescue OptionParser::AmbiguousOption => e
|
84
|
+
fallback.parse! e.args
|
85
|
+
end
|
86
|
+
|
87
|
+
milestone and case action
|
88
|
+
when 'create' then self.action = 'update'
|
89
|
+
when 'index' then self.action = 'show'
|
90
|
+
end
|
91
|
+
|
92
|
+
if reverse
|
93
|
+
assigns[:sort] ||= 'created'
|
94
|
+
assigns[:direction] = 'asc'
|
95
|
+
end
|
96
|
+
|
97
|
+
case action
|
98
|
+
when 'index'
|
99
|
+
state = assigns[:state] || 'open'
|
100
|
+
print format_state state, "# #{repo} #{state} milestones"
|
101
|
+
print "\n" unless STDOUT.tty?
|
102
|
+
res = throb(0, format_state(state, '#')) { api.get uri }
|
103
|
+
loop do
|
104
|
+
milestones = res.body
|
105
|
+
if verbose
|
106
|
+
puts milestones.map { |m| format_milestone m }
|
107
|
+
else
|
108
|
+
puts format_milestones(milestones)
|
109
|
+
end
|
110
|
+
break unless res.next_page
|
111
|
+
page?
|
112
|
+
res = throb { api.get res.next_page }
|
113
|
+
end
|
114
|
+
when 'show'
|
115
|
+
m = throb { api.get uri }.body
|
116
|
+
print format_milestone(m)
|
117
|
+
puts 'Issues:'
|
118
|
+
List.execute %W(-q -M #{milestone} -- #{repo})
|
119
|
+
when 'create'
|
120
|
+
m = throb { api.post uri, assigns }.body
|
121
|
+
puts 'Milestone #%d created.' % m['number']
|
122
|
+
when 'update'
|
123
|
+
throb { api.patch uri, assigns }
|
124
|
+
puts 'Milestone updated.'
|
125
|
+
when 'destroy'
|
126
|
+
throb { api.delete uri }
|
127
|
+
puts 'Milestone deleted.'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def uri
|
134
|
+
if milestone
|
135
|
+
"/repos/#{repo}/milestones/#{milestone}"
|
136
|
+
else
|
137
|
+
"/repos/#{repo}/milestones"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def fallback
|
142
|
+
OptionParser.new do |opts|
|
143
|
+
opts.on '-d' do
|
144
|
+
self.action = 'destroy'
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module GHI
|
2
|
+
module Commands
|
3
|
+
class Open < Command
|
4
|
+
def options
|
5
|
+
#--
|
6
|
+
# TODO: Support shortcuts, e.g,
|
7
|
+
#
|
8
|
+
# ghi open "Issue Title"
|
9
|
+
#++
|
10
|
+
OptionParser.new do |opts|
|
11
|
+
opts.banner = <<EOF
|
12
|
+
usage: ghi open [options]
|
13
|
+
or: ghi reopen [options] <issueno>
|
14
|
+
EOF
|
15
|
+
opts.separator ''
|
16
|
+
opts.on '-l', '--list', 'list open tickets' do
|
17
|
+
self.action = 'index'
|
18
|
+
end
|
19
|
+
opts.separator ''
|
20
|
+
opts.separator 'Issue modification options'
|
21
|
+
opts.on '-m', '--message <text>', 'describe issue' do |text|
|
22
|
+
assigns[:title], assigns[:body] = text.split(/\n+/, 2)
|
23
|
+
end
|
24
|
+
opts.on(
|
25
|
+
'-u', '--[no-]assign <user>', 'assign to specified user'
|
26
|
+
) do |assignee|
|
27
|
+
assigns[:assignee] = assignee
|
28
|
+
end
|
29
|
+
opts.on(
|
30
|
+
'-M', '--milestone <n>', 'associate with milestone'
|
31
|
+
) do |milestone|
|
32
|
+
assigns[:milestone] = milestone
|
33
|
+
end
|
34
|
+
opts.on(
|
35
|
+
'-L', '--label <labelname>...', Array, 'associate with label(s)'
|
36
|
+
) do |labels|
|
37
|
+
assigns[:labels] = labels
|
38
|
+
end
|
39
|
+
opts.separator ''
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def execute
|
44
|
+
self.action = 'create'
|
45
|
+
|
46
|
+
if extract_issue
|
47
|
+
Edit.new(args.unshift('-so', issue)).execute
|
48
|
+
puts 'Reopened.'
|
49
|
+
exit
|
50
|
+
end
|
51
|
+
|
52
|
+
options.parse! args
|
53
|
+
|
54
|
+
case action
|
55
|
+
when 'index'
|
56
|
+
List.new(args).execute
|
57
|
+
when 'create'
|
58
|
+
if assigns[:title].nil? # FIXME: Open $EDITOR
|
59
|
+
warn "Missing argument: -m"
|
60
|
+
abort options.to_s
|
61
|
+
end
|
62
|
+
i = throb { api.post "/repos/#{repo}/issues", assigns }.body
|
63
|
+
puts format_issue(i)
|
64
|
+
puts 'Opened.'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module GHI
|
2
|
+
module Commands
|
3
|
+
class Show < Command
|
4
|
+
def options
|
5
|
+
OptionParser.new do |opts|
|
6
|
+
opts.banner = 'usage: ghi show <issueno> [[<user>/]<repo>]'
|
7
|
+
opts.separator ''
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute
|
12
|
+
require_issue
|
13
|
+
require_repo
|
14
|
+
i = throb { api.get "/repos/#{repo}/issues/#{issue}" }.body
|
15
|
+
puts format_issue(i)
|
16
|
+
page? 'Load comments?'
|
17
|
+
Comment.execute %W(-l #{issue} -- #{repo})
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,301 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'date'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
module GHI
|
6
|
+
module Formatting
|
7
|
+
autoload :Colors, 'ghi/formatting/colors'
|
8
|
+
include Colors
|
9
|
+
|
10
|
+
CURSOR = {
|
11
|
+
:up => lambda { |n| "\e[#{n}A" },
|
12
|
+
:column => lambda { |n| "\e[#{n}G" },
|
13
|
+
:hide => "\e[?25l",
|
14
|
+
:show => "\e[?25h"
|
15
|
+
}
|
16
|
+
|
17
|
+
THROBBERS = [
|
18
|
+
%w(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏),
|
19
|
+
%w(⠋ ⠙ ⠚ ⠞ ⠖ ⠦ ⠴ ⠲ ⠳ ⠓),
|
20
|
+
%w(⠄ ⠆ ⠇ ⠋ ⠙ ⠸ ⠰ ⠠ ⠰ ⠸ ⠙ ⠋ ⠇ ⠆ ),
|
21
|
+
%w(⠋ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋),
|
22
|
+
%w(⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠴ ⠲ ⠒ ⠂ ⠂ ⠒ ⠚ ⠙ ⠉ ⠁),
|
23
|
+
%w(⠈ ⠉ ⠋ ⠓ ⠒ ⠐ ⠐ ⠒ ⠖ ⠦ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈),
|
24
|
+
%w(⠁ ⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈ ⠈ ⠉)
|
25
|
+
]
|
26
|
+
|
27
|
+
def puts *strings
|
28
|
+
strings = strings.flatten.map { |s|
|
29
|
+
s.gsub(/@([^@\s]+)/) {
|
30
|
+
if $1 == Authorization.username
|
31
|
+
bright { fg(:yellow) { "@#$1" } }
|
32
|
+
else
|
33
|
+
bright { "@#$1" }
|
34
|
+
end
|
35
|
+
}
|
36
|
+
}
|
37
|
+
stdout, $stdout = $stdout, IO.popen('less -ErX', 'w')
|
38
|
+
super strings
|
39
|
+
rescue Errno::EPIPE
|
40
|
+
ensure
|
41
|
+
$stdout.close_write
|
42
|
+
$stdout = stdout
|
43
|
+
print CURSOR[:show]
|
44
|
+
end
|
45
|
+
|
46
|
+
def truncate string, reserved
|
47
|
+
result = string.scan(/.{0,#{columns - reserved}}(?:\s|\Z)/).first.strip
|
48
|
+
result << "..." if result != string
|
49
|
+
result
|
50
|
+
end
|
51
|
+
|
52
|
+
def indent string, level = 4
|
53
|
+
string = string.gsub(/\r/, '')
|
54
|
+
string.gsub!(/[\t ]+$/, '')
|
55
|
+
string.gsub!(/\n{3,}/, "\n\n")
|
56
|
+
width = columns - level - 1
|
57
|
+
lines = string.scan(
|
58
|
+
/.{0,#{width}}(?:\s|\Z)|[\S]{#{width},}/ # TODO: Test long lines.
|
59
|
+
).map { |line| " " * level + line.chomp }
|
60
|
+
format_markdown lines.join("\n").rstrip, level
|
61
|
+
end
|
62
|
+
|
63
|
+
def columns
|
64
|
+
dimensions[1] || 80
|
65
|
+
end
|
66
|
+
|
67
|
+
def dimensions
|
68
|
+
`stty size`.chomp.split(' ').map { |n| n.to_i }
|
69
|
+
end
|
70
|
+
|
71
|
+
#--
|
72
|
+
# Specific formatters:
|
73
|
+
#++
|
74
|
+
|
75
|
+
def format_issues_header
|
76
|
+
state = assigns[:state] || 'open'
|
77
|
+
header = "# #{repo || 'Global,'} #{state} issues"
|
78
|
+
if repo
|
79
|
+
if milestone = assigns[:milestone]
|
80
|
+
header.sub! repo, "#{repo} milestone ##{milestone}"
|
81
|
+
end
|
82
|
+
if assignee = assigns[:assignee]
|
83
|
+
header << case assignee
|
84
|
+
when '*' then ', assigned'
|
85
|
+
when 'none' then ', unassigned'
|
86
|
+
else
|
87
|
+
assignee = 'you' if Authorization.username == assignee
|
88
|
+
", assigned to #{assignee}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
if mentioned = assigns[:mentioned]
|
92
|
+
mentioned = 'you' if Authorization.username == mentioned
|
93
|
+
header << ", mentioning #{mentioned}"
|
94
|
+
end
|
95
|
+
else
|
96
|
+
header << case assigns[:filter]
|
97
|
+
when 'created' then ' you created'
|
98
|
+
when 'mentioned' then ' that mention you'
|
99
|
+
when 'subscribed' then " you're subscribed to"
|
100
|
+
else
|
101
|
+
' assigned to you'
|
102
|
+
end
|
103
|
+
end
|
104
|
+
if labels = assigns[:labels]
|
105
|
+
header << ", labeled #{assigns[:labels].gsub ',', ', '}"
|
106
|
+
end
|
107
|
+
if sort = assigns[:sort]
|
108
|
+
header << ", by #{sort} #{reverse ? 'ascending' : 'descending'}"
|
109
|
+
end
|
110
|
+
format_state assigns[:state], header
|
111
|
+
end
|
112
|
+
|
113
|
+
# TODO: Show milestones.
|
114
|
+
def format_issues issues, include_repo
|
115
|
+
return 'None.' if issues.empty?
|
116
|
+
|
117
|
+
include_repo and issues.each do |i|
|
118
|
+
%r{/repos/[^/]+/([^/]+)} === i['url'] and i['repo'] = $1
|
119
|
+
end
|
120
|
+
|
121
|
+
nmax, rmax = %w(number repo).map { |f|
|
122
|
+
issues.sort_by { |i| i[f].to_s.size }.last[f].to_s.size
|
123
|
+
}
|
124
|
+
|
125
|
+
issues.map { |i|
|
126
|
+
n, title, labels = i['number'], i['title'], i['labels']
|
127
|
+
l = 8 + nmax + rmax + no_color { format_labels labels }.to_s.length
|
128
|
+
a = i['assignee'] && i['assignee']['login'] == Authorization.username
|
129
|
+
l += 2 if a
|
130
|
+
p = i['pull_request']['html_url'] and l += 2
|
131
|
+
c = i['comments'] if i['comments'] > 0 and l += 2
|
132
|
+
[
|
133
|
+
" ",
|
134
|
+
(i['repo'].to_s.rjust(rmax) if i['repo']),
|
135
|
+
"#{bright { n.to_s.rjust nmax }}:",
|
136
|
+
truncate(title, l),
|
137
|
+
format_labels(labels),
|
138
|
+
(fg('aaaaaa') { c } if c),
|
139
|
+
(fg('aaaaaa') { '↑' } if p),
|
140
|
+
(fg(:yellow) { '@' } if a)
|
141
|
+
].compact.join ' '
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
# TODO: Show milestone, number of comments, pull request attached.
|
146
|
+
def format_issue i
|
147
|
+
ERB.new(<<EOF).result binding
|
148
|
+
<% p = i['pull_request'].key? 'html_url' %>\
|
149
|
+
<%= bright { \
|
150
|
+
indent '%s%s: %s' % [p ? '↑' : '#', *i.values_at('number', 'title')], 0 } %>
|
151
|
+
@<%= i['user']['login'] %> opened this <%= p ? 'pull request' : 'issue' %> \
|
152
|
+
<%= format_date DateTime.parse(i['created_at']) %>. \
|
153
|
+
<%= format_state i['state'], format_tag(i['state']), :bg %>\
|
154
|
+
<% if i['assignee'] || !i['labels'].empty? %>
|
155
|
+
<% if i['assignee'] %>@<%= i['assignee']['login'] %> is assigned. <% end %>\
|
156
|
+
<% unless i['labels'].empty? %><%= format_labels(i['labels']) %><% end %>\
|
157
|
+
<% end %>
|
158
|
+
<% if i['body'] && !i['body'].empty? %>\n<%= indent i['body'] %>
|
159
|
+
<% end %>
|
160
|
+
|
161
|
+
EOF
|
162
|
+
end
|
163
|
+
|
164
|
+
def format_comments comments
|
165
|
+
return 'None.' if comments.empty?
|
166
|
+
comments.map { |comment| format_comment comment }
|
167
|
+
end
|
168
|
+
|
169
|
+
def format_comment c
|
170
|
+
<<EOF
|
171
|
+
@#{c['user']['login']} commented \
|
172
|
+
#{format_date DateTime.parse(c['created_at'])}:
|
173
|
+
#{indent c['body']}
|
174
|
+
|
175
|
+
|
176
|
+
EOF
|
177
|
+
end
|
178
|
+
|
179
|
+
def format_milestones milestones
|
180
|
+
return 'None.' if milestones.empty?
|
181
|
+
|
182
|
+
max = milestones.sort_by { |m|
|
183
|
+
m['number'].to_s.size
|
184
|
+
}.last['number'].to_s.size
|
185
|
+
|
186
|
+
milestones.map { |m|
|
187
|
+
due_on = m['due_on'] && DateTime.parse(m['due_on'])
|
188
|
+
[
|
189
|
+
" #{bright { m['number'].to_s.rjust max }}:",
|
190
|
+
fg((:red if due_on && due_on <= DateTime.now)) {
|
191
|
+
truncate(m['title'], max + 4)
|
192
|
+
}
|
193
|
+
].compact.join ' '
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
def format_milestone m
|
198
|
+
ERB.new(<<EOF).result binding
|
199
|
+
<%= bright { indent '#%s: %s' % m.values_at('number', 'title'), 0 } %>
|
200
|
+
@<%= m['creator']['login'] %> created this milestone <%= m['created_at'] %>. \
|
201
|
+
<%= format_state m['state'], format_tag(m['state']), :bg %>
|
202
|
+
<% if m['due_on'] %>\
|
203
|
+
<% due_on = DateTime.parse m['due_on'] %>\
|
204
|
+
Due <%= fg((:red if due_on <= DateTime.now)) { format_date due_on } %>.
|
205
|
+
<% end %>\
|
206
|
+
<% if m['description'] && !m['description'].empty? %>
|
207
|
+
<%= indent m['description'] %>\
|
208
|
+
<% end %>
|
209
|
+
EOF
|
210
|
+
end
|
211
|
+
|
212
|
+
def format_state state, string = state, layer = :fg
|
213
|
+
send(layer, state == 'closed' ? :red : :green) { string }
|
214
|
+
end
|
215
|
+
|
216
|
+
def format_labels labels
|
217
|
+
return if labels.empty?
|
218
|
+
[*labels].map { |l| bg(l['color']) { format_tag l['name'] } }.join ' '
|
219
|
+
end
|
220
|
+
|
221
|
+
def format_tag tag
|
222
|
+
(colorize? ? ' %s ' : '[%s]') % tag
|
223
|
+
end
|
224
|
+
|
225
|
+
def format_markdown string, indent = 4
|
226
|
+
# Headers.
|
227
|
+
string.gsub!(/^( {#{indent}}\#{1,6} .+)$/, bright{'\1'})
|
228
|
+
string.gsub!(
|
229
|
+
/(^ {#{indent}}.+$\n^ {#{indent}}[-=]+$)/, bright{'\1'}
|
230
|
+
)
|
231
|
+
# Emphasis.
|
232
|
+
string.gsub!(
|
233
|
+
/(^|\s)(\*\w(?:[^*]*\w)?\*)(\s|$)/m, '\1' + underline{'\2'} + '\3'
|
234
|
+
)
|
235
|
+
string.gsub!(
|
236
|
+
/(^|\s)(_\w(?:[^_]*\w)?_)(\s|$)/m, '\1' + underline{'\2'} + '\3'
|
237
|
+
)
|
238
|
+
# Strong.
|
239
|
+
string.gsub!(
|
240
|
+
/(^|\s)(\*{2}\w(?:[^*]*\w)?\*{2})(\s|$)/m, '\1' + bright{'\2'} + '\3'
|
241
|
+
)
|
242
|
+
string.gsub!(
|
243
|
+
/(^|\s)(_{2}\w(?:[^_]*\w)?_{2})(\s|$)/m, '\1' + bright {'\2'} + '\3'
|
244
|
+
)
|
245
|
+
# Code.
|
246
|
+
string.gsub!(
|
247
|
+
/
|
248
|
+
(^\ {#{indent}}```.*?$)(.+?^\ {#{indent}}```$)|
|
249
|
+
(^|[^`])(`[^`]+`)([^`]|$)
|
250
|
+
/mx,
|
251
|
+
fg(c = '268bd2'){'\1'} + bg(c){'\2'} + '\3' + fg(c){'\4'} + '\5'
|
252
|
+
)
|
253
|
+
# URI.
|
254
|
+
string.gsub!(
|
255
|
+
%r{\b(<)?(https?://[\s]+|\w+@\w+)(>)?\b},
|
256
|
+
'\1' + underline{'\2'} + '\3'
|
257
|
+
)
|
258
|
+
string
|
259
|
+
end
|
260
|
+
|
261
|
+
def format_date date
|
262
|
+
days = (interval = DateTime.now - date).to_i.abs
|
263
|
+
string = if days.zero?
|
264
|
+
hours, minutes, seconds = DateTime.day_fraction_to_time interval.abs
|
265
|
+
if hours > 0
|
266
|
+
"#{hours} hour#{'s' unless hours == 1}"
|
267
|
+
elsif minutes > 0
|
268
|
+
"#{minutes} minute#{'s' unless minutes == 1}"
|
269
|
+
else
|
270
|
+
"#{seconds} second#{'s' unless seconds == 1}"
|
271
|
+
end
|
272
|
+
else
|
273
|
+
"#{days} day#{'s' unless days == 1}"
|
274
|
+
end
|
275
|
+
[string, interval < 0 ? 'from now' : 'ago'].join ' '
|
276
|
+
end
|
277
|
+
|
278
|
+
def throb position = 0, redraw = CURSOR[:up][1]
|
279
|
+
return yield unless STDOUT.tty?
|
280
|
+
|
281
|
+
throb = THROBBERS[rand(THROBBERS.length)]
|
282
|
+
throb.reverse! if rand > 0.5
|
283
|
+
i = rand throb.length
|
284
|
+
|
285
|
+
thread = Thread.new do
|
286
|
+
dot = lambda do
|
287
|
+
print("\r#{CURSOR[:column][position]}#{throb[i]}#{CURSOR[:hide]}")
|
288
|
+
i = (i + 1) % throb.length
|
289
|
+
sleep 0.1 and dot.call
|
290
|
+
end
|
291
|
+
dot.call
|
292
|
+
end
|
293
|
+
yield
|
294
|
+
ensure
|
295
|
+
if thread
|
296
|
+
thread.kill
|
297
|
+
puts "\r#{CURSOR[:column][position]}#{redraw}#{CURSOR[:show]}"
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|