ghi 0.9.0.dev1 → 0.9.0.20120627
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/ghi +1 -1
- data/lib/ghi.rb +46 -19
- data/lib/ghi/authorization.rb +11 -15
- data/lib/ghi/client.rb +2 -2
- data/lib/ghi/commands/close.rb +6 -1
- data/lib/ghi/commands/command.rb +24 -15
- data/lib/ghi/commands/comment.rb +39 -15
- data/lib/ghi/commands/config.rb +24 -4
- data/lib/ghi/commands/edit.rb +89 -22
- data/lib/ghi/commands/label.rb +1 -1
- data/lib/ghi/commands/list.rb +33 -22
- data/lib/ghi/commands/milestone.rb +78 -21
- data/lib/ghi/commands/open.rb +39 -14
- data/lib/ghi/commands/show.rb +16 -8
- data/lib/ghi/commands/version.rb +1 -1
- data/lib/ghi/editor.rb +39 -17
- data/lib/ghi/formatting.rb +125 -40
- data/lib/ghi/formatting/colors.rb +1 -1
- data/lib/ghi/web.rb +26 -0
- metadata +57 -48
data/lib/ghi/commands/edit.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module GHI
|
2
2
|
module Commands
|
3
3
|
class Edit < Command
|
4
|
-
attr_accessor :
|
4
|
+
attr_accessor :editor
|
5
5
|
|
6
6
|
def options
|
7
7
|
OptionParser.new do |opts|
|
@@ -12,7 +12,7 @@ EOF
|
|
12
12
|
opts.on(
|
13
13
|
'-m', '--message [<text>]', 'change issue description'
|
14
14
|
) do |text|
|
15
|
-
next self.
|
15
|
+
next self.editor = true if text.nil?
|
16
16
|
assigns[:title], assigns[:body] = text.split(/\n+/, 2)
|
17
17
|
end
|
18
18
|
opts.on(
|
@@ -20,6 +20,9 @@ EOF
|
|
20
20
|
) do |assignee|
|
21
21
|
assigns[:assignee] = assignee
|
22
22
|
end
|
23
|
+
opts.on '--claim', 'assign to yourself' do
|
24
|
+
assigns[:assignee] = Authorization.username
|
25
|
+
end
|
23
26
|
opts.on(
|
24
27
|
'-s', '--state <in>', %w(open closed),
|
25
28
|
{'o'=>'open', 'c'=>'closed'}, "'open' or 'closed'"
|
@@ -34,36 +37,100 @@ EOF
|
|
34
37
|
opts.on(
|
35
38
|
'-L', '--label <labelname>...', Array, 'associate with label(s)'
|
36
39
|
) do |labels|
|
37
|
-
assigns[: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
|
38
58
|
end
|
39
59
|
opts.separator ''
|
40
60
|
end
|
41
61
|
end
|
42
62
|
|
43
63
|
def execute
|
44
|
-
|
64
|
+
self.action = 'edit'
|
45
65
|
require_repo
|
66
|
+
require_issue
|
46
67
|
options.parse! args
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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 assigns[:title] && i
|
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
|
+
i = throb {
|
90
|
+
api.patch "/repos/#{repo}/issues/#{issue}", assigns
|
91
|
+
}.body
|
92
|
+
puts format_issue(i)
|
93
|
+
puts 'Updated.'
|
94
|
+
end
|
95
|
+
e.unlink if e
|
96
|
+
rescue Client::Error => e
|
97
|
+
raise unless error = e.errors.first
|
98
|
+
abort "%s %s %s %s." % [
|
99
|
+
error['resource'],
|
100
|
+
error['field'],
|
101
|
+
[*error['value']].join(', '),
|
102
|
+
error['code']
|
103
|
+
]
|
104
|
+
end
|
105
|
+
when 'pull'
|
106
|
+
begin
|
107
|
+
assigns[:issue] = issue
|
108
|
+
assigns[:base] ||= 'master'
|
109
|
+
head = begin
|
110
|
+
if ref = %x{
|
111
|
+
git rev-parse --abbrev-ref HEAD@{upstream} 2>/dev/null
|
112
|
+
}.chomp!
|
113
|
+
ref.split('/').last if $? == 0
|
114
|
+
end
|
115
|
+
end
|
116
|
+
assigns[:head] ||= head
|
117
|
+
if assigns[:head]
|
118
|
+
assigns[:head].sub!(/:$/, ":#{head}")
|
119
|
+
else
|
120
|
+
abort <<EOF.chomp
|
121
|
+
fatal: HEAD can't be null. (Is your current branch being tracked upstream?)
|
122
|
+
EOF
|
123
|
+
end
|
124
|
+
throb { api.post "/repos/#{repo}/pulls", assigns }
|
125
|
+
base = [repo.split('/').first, assigns[:base]].join ':'
|
126
|
+
puts 'Issue #%d set up to track remote branch %s against %s.' % [
|
127
|
+
issue, assigns[:head], base
|
128
|
+
]
|
129
|
+
rescue Client::Error => e
|
130
|
+
raise unless error = e.errors.last
|
131
|
+
abort error['message'].sub /^base /, ''
|
57
132
|
end
|
58
133
|
end
|
59
|
-
if titles_match && bodies_match
|
60
|
-
abort 'No change.' if assigns.dup.delete_if { |k, v|
|
61
|
-
[:title, :body].include? k
|
62
|
-
}
|
63
|
-
end
|
64
|
-
i = throb { api.patch "/repos/#{repo}/issues/#{issue}", assigns }.body
|
65
|
-
puts format_issue(i)
|
66
|
-
puts 'Updated.'
|
67
134
|
end
|
68
135
|
end
|
69
136
|
end
|
data/lib/ghi/commands/label.rb
CHANGED
data/lib/ghi/commands/list.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
require 'curses'
|
2
1
|
require 'date'
|
3
2
|
|
4
3
|
module GHI
|
5
4
|
module Commands
|
6
5
|
class List < Command
|
6
|
+
attr_accessor :web
|
7
7
|
attr_accessor :reverse
|
8
8
|
attr_accessor :quiet
|
9
9
|
|
@@ -23,7 +23,7 @@ module GHI
|
|
23
23
|
opts.on(
|
24
24
|
'-L', '--label <labelname>...', Array, 'by label(s)'
|
25
25
|
) do |labels|
|
26
|
-
assigns[:labels]
|
26
|
+
(assigns[:labels] ||= []).concat labels
|
27
27
|
end
|
28
28
|
opts.on(
|
29
29
|
'-S', '--sort <by>', %w(created updated comments),
|
@@ -45,9 +45,8 @@ module GHI
|
|
45
45
|
raise OptionParser::InvalidArgument, e.message
|
46
46
|
end
|
47
47
|
end
|
48
|
-
opts.on
|
49
|
-
|
50
|
-
end
|
48
|
+
opts.on('-v', '--verbose') { self.verbose = true }
|
49
|
+
opts.on('-w', '--web') { self.web = true }
|
51
50
|
opts.separator ''
|
52
51
|
opts.separator 'Global options'
|
53
52
|
opts.on(
|
@@ -69,13 +68,14 @@ module GHI
|
|
69
68
|
opts.on(
|
70
69
|
'-u', '--[no-]assignee [<user>]', 'assigned to specified user'
|
71
70
|
) do |assignee|
|
71
|
+
assignee = assignee.sub /^@/, ''
|
72
72
|
assigns[:assignee] = any_or_none_or assignee
|
73
73
|
end
|
74
74
|
opts.on '--mine', 'assigned to you' do
|
75
75
|
assigns[:assignee] = Authorization.username
|
76
76
|
end
|
77
77
|
opts.on(
|
78
|
-
'-U', '--mentioned [<user>]', 'mentioning you or specified user'
|
78
|
+
'-U', '--mentioned [<user>]', 'mentioning you or specified user'
|
79
79
|
) do |mentioned|
|
80
80
|
assigns[:mentioned] = mentioned || Authorization.username
|
81
81
|
end
|
@@ -84,32 +84,43 @@ module GHI
|
|
84
84
|
end
|
85
85
|
|
86
86
|
def execute
|
87
|
+
if index = args.index { |arg| /^@/ === arg }
|
88
|
+
assigns[:assignee] = args.delete_at(index)[1..-1]
|
89
|
+
end
|
90
|
+
|
87
91
|
begin
|
88
92
|
options.parse! args
|
89
93
|
rescue OptionParser::InvalidOption => e
|
90
94
|
fallback.parse! e.args
|
91
95
|
retry
|
92
96
|
end
|
97
|
+
assigns[:labels] = assigns[:labels].join ',' if assigns[:labels]
|
93
98
|
if reverse
|
94
99
|
assigns[:sort] ||= 'created'
|
95
100
|
assigns[:direction] = 'asc'
|
96
101
|
end
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
102
|
+
if web
|
103
|
+
Web.new(repo || 'dashboard').open 'issues', assigns
|
104
|
+
else
|
105
|
+
assigns[:per_page] = 100
|
106
|
+
unless quiet
|
107
|
+
print header = format_issues_header
|
108
|
+
print "\n" unless paginate?
|
109
|
+
end
|
110
|
+
res = throb(
|
111
|
+
0, format_state(assigns[:state], quiet ? CURSOR[:up][1] : '#')
|
112
|
+
) { api.get uri, assigns }
|
113
|
+
print "\r#{CURSOR[:up][1]}" if header && paginate?
|
114
|
+
page header do
|
115
|
+
issues = res.body
|
116
|
+
if verbose
|
117
|
+
puts issues.map { |i| format_issue i }
|
118
|
+
else
|
119
|
+
puts format_issues(issues, repo.nil?)
|
120
|
+
end
|
121
|
+
break unless res.next_page
|
122
|
+
res = throb { api.get res.next_page }
|
110
123
|
end
|
111
|
-
break unless res.next_page
|
112
|
-
res = throb { api.get res.next_page }
|
113
124
|
end
|
114
125
|
rescue Client::Error => e
|
115
126
|
if e.response.code == '422'
|
@@ -124,7 +135,7 @@ module GHI
|
|
124
135
|
private
|
125
136
|
|
126
137
|
def uri
|
127
|
-
repo ? "/repos/#{repo}
|
138
|
+
(repo ? "/repos/#{repo}" : '') << '/issues'
|
128
139
|
end
|
129
140
|
|
130
141
|
def fallback
|
@@ -3,7 +3,9 @@ require 'date'
|
|
3
3
|
module GHI
|
4
4
|
module Commands
|
5
5
|
class Milestone < Command
|
6
|
+
attr_accessor :edit
|
6
7
|
attr_accessor :reverse
|
8
|
+
attr_accessor :web
|
7
9
|
|
8
10
|
#--
|
9
11
|
# FIXME: Opt for better interface, e.g.,
|
@@ -39,12 +41,15 @@ EOF
|
|
39
41
|
opts.on '-v', '--verbose', 'list milestones verbosely' do
|
40
42
|
self.verbose = true
|
41
43
|
end
|
44
|
+
opts.on('-w', '--web') { self.web = true }
|
42
45
|
opts.separator ''
|
43
46
|
opts.separator 'Milestone modification options'
|
44
47
|
opts.on(
|
45
|
-
'-m', '--message <text>', 'change milestone description'
|
48
|
+
'-m', '--message [<text>]', 'change milestone description'
|
46
49
|
) do |text|
|
47
50
|
self.action = 'create'
|
51
|
+
self.edit = true
|
52
|
+
next unless text
|
48
53
|
assigns[:title], assigns[:description] = text.split(/\n+/, 2)
|
49
54
|
end
|
50
55
|
# FIXME: We already describe --[no-]closed; describe this, too?
|
@@ -76,6 +81,7 @@ EOF
|
|
76
81
|
|
77
82
|
def execute
|
78
83
|
self.action = 'index'
|
84
|
+
require_repo
|
79
85
|
extract_milestone
|
80
86
|
|
81
87
|
begin
|
@@ -96,32 +102,83 @@ EOF
|
|
96
102
|
|
97
103
|
case action
|
98
104
|
when 'index'
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
105
|
+
if web
|
106
|
+
Web.new(repo).open 'issues/milestones', assigns
|
107
|
+
else
|
108
|
+
assigns[:per_page] = 100
|
109
|
+
state = assigns[:state] || 'open'
|
110
|
+
print format_state state, "# #{repo} #{state} milestones"
|
111
|
+
print "\n" unless paginate?
|
112
|
+
res = throb(0, format_state(state, '#')) { api.get uri, assigns }
|
113
|
+
page do
|
114
|
+
milestones = res.body
|
115
|
+
if verbose
|
116
|
+
puts milestones.map { |m| format_milestone m }
|
117
|
+
else
|
118
|
+
puts format_milestones(milestones)
|
119
|
+
end
|
120
|
+
break unless res.next_page
|
121
|
+
res = throb { api.get res.next_page }
|
109
122
|
end
|
110
|
-
break unless res.next_page
|
111
|
-
res = throb { api.get res.next_page }
|
112
123
|
end
|
113
124
|
when 'show'
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
125
|
+
if web
|
126
|
+
List.execute %W(-w -M #{milestone} -- #{repo})
|
127
|
+
else
|
128
|
+
m = throb { api.get uri }.body
|
129
|
+
page do
|
130
|
+
puts format_milestone(m)
|
131
|
+
puts 'Issues:'
|
132
|
+
args.unshift(*%W(-q -M #{milestone} -- #{repo}))
|
133
|
+
args.unshift '-v' if verbose
|
134
|
+
List.execute args
|
135
|
+
break
|
136
|
+
end
|
137
|
+
end
|
118
138
|
when 'create'
|
119
|
-
|
120
|
-
|
139
|
+
if web
|
140
|
+
Web.new(repo).open 'issues/milestones/new'
|
141
|
+
else
|
142
|
+
if assigns[:title].nil?
|
143
|
+
e = Editor.new 'GHI_MILESTONE'
|
144
|
+
message = e.gets format_milestone_editor
|
145
|
+
e.unlink 'Empty milestone.' if message.nil? || message.empty?
|
146
|
+
assigns[:title], assigns[:description] = message.split(/\n+/, 2)
|
147
|
+
end
|
148
|
+
m = throb { api.post uri, assigns }.body
|
149
|
+
puts 'Milestone #%d created.' % m['number']
|
150
|
+
e.unlink if e
|
151
|
+
end
|
121
152
|
when 'update'
|
122
|
-
|
123
|
-
|
153
|
+
if web
|
154
|
+
Web.new(repo).open "issues/milestones/#{milestone}/edit"
|
155
|
+
else
|
156
|
+
if edit || assigns.empty?
|
157
|
+
m = throb { api.get "/repos/#{repo}/milestones/#{milestone}" }.body
|
158
|
+
e = Editor.new "GHI_MILESTONE_#{milestone}"
|
159
|
+
message = e.gets format_milestone_editor(m)
|
160
|
+
e.unlink 'Empty milestone.' if message.nil? || message.empty?
|
161
|
+
assigns[:title], assigns[:description] = message.split(/\n+/, 2)
|
162
|
+
end
|
163
|
+
if assigns[:title] && m
|
164
|
+
t_match = assigns[:title].strip == m['title'].strip
|
165
|
+
if assigns[:description]
|
166
|
+
b_match = assigns[:description].strip == m['description'].strip
|
167
|
+
end
|
168
|
+
if t_match && b_match
|
169
|
+
e.unlink if e
|
170
|
+
abort 'No change.' if assigns.dup.delete_if { |k, v|
|
171
|
+
[:title, :description].include? k
|
172
|
+
}
|
173
|
+
end
|
174
|
+
end
|
175
|
+
m = throb { api.patch uri, assigns }.body
|
176
|
+
puts format_milestone(m)
|
177
|
+
puts 'Updated.'
|
178
|
+
e.unlink if e
|
179
|
+
end
|
124
180
|
when 'destroy'
|
181
|
+
require_milestone
|
125
182
|
throb { api.delete uri }
|
126
183
|
puts 'Milestone deleted.'
|
127
184
|
end
|
data/lib/ghi/commands/open.rb
CHANGED
@@ -1,12 +1,10 @@
|
|
1
1
|
module GHI
|
2
2
|
module Commands
|
3
3
|
class Open < Command
|
4
|
+
attr_accessor :editor
|
5
|
+
attr_accessor :web
|
6
|
+
|
4
7
|
def options
|
5
|
-
#--
|
6
|
-
# TODO: Support shortcuts, e.g,
|
7
|
-
#
|
8
|
-
# ghi open "Issue Title"
|
9
|
-
#++
|
10
8
|
OptionParser.new do |opts|
|
11
9
|
opts.banner = <<EOF
|
12
10
|
usage: ghi open [options]
|
@@ -16,16 +14,24 @@ EOF
|
|
16
14
|
opts.on '-l', '--list', 'list open tickets' do
|
17
15
|
self.action = 'index'
|
18
16
|
end
|
17
|
+
opts.on('-w', '--web') { self.web = true }
|
19
18
|
opts.separator ''
|
20
19
|
opts.separator 'Issue modification options'
|
21
20
|
opts.on '-m', '--message [<text>]', 'describe issue' do |text|
|
22
|
-
|
21
|
+
if text
|
22
|
+
assigns[:title], assigns[:body] = text.split(/\n+/, 2)
|
23
|
+
else
|
24
|
+
self.editor = true
|
25
|
+
end
|
23
26
|
end
|
24
27
|
opts.on(
|
25
28
|
'-u', '--[no-]assign [<user>]', 'assign to specified user'
|
26
29
|
) do |assignee|
|
27
30
|
assigns[:assignee] = assignee
|
28
31
|
end
|
32
|
+
opts.on '--claim', 'assign to yourself' do
|
33
|
+
assigns[:assignee] = Authorization.username
|
34
|
+
end
|
29
35
|
opts.on(
|
30
36
|
'-M', '--milestone <n>', 'associate with milestone'
|
31
37
|
) do |milestone|
|
@@ -34,7 +40,7 @@ EOF
|
|
34
40
|
opts.on(
|
35
41
|
'-L', '--label <labelname>...', Array, 'associate with label(s)'
|
36
42
|
) do |labels|
|
37
|
-
assigns[:labels]
|
43
|
+
(assigns[:labels] ||= []).concat labels
|
38
44
|
end
|
39
45
|
opts.separator ''
|
40
46
|
end
|
@@ -57,17 +63,36 @@ EOF
|
|
57
63
|
args.unshift assigns[:assignee] if assigns[:assignee]
|
58
64
|
args.unshift '-u'
|
59
65
|
end
|
66
|
+
args.unshift '-w' if web
|
60
67
|
List.execute args.push('--', repo)
|
61
68
|
when 'create'
|
62
|
-
if
|
63
|
-
|
64
|
-
|
65
|
-
|
69
|
+
if web
|
70
|
+
Web.new(repo).open 'issues/new'
|
71
|
+
else
|
72
|
+
unless args.empty?
|
73
|
+
assigns[:title], assigns[:body] = args.join(' '), assigns[:title]
|
74
|
+
end
|
75
|
+
assigns[:title] = args.join ' ' unless args.empty?
|
76
|
+
if assigns[:title].nil? || editor
|
77
|
+
e = Editor.new 'GHI_ISSUE'
|
78
|
+
message = e.gets format_editor(assigns)
|
79
|
+
e.unlink "There's no issue?" if message.nil? || message.empty?
|
80
|
+
assigns[:title], assigns[:body] = message.split(/\n+/, 2)
|
81
|
+
end
|
82
|
+
i = throb { api.post "/repos/#{repo}/issues", assigns }.body
|
83
|
+
e.unlink if e
|
84
|
+
puts format_issue(i)
|
85
|
+
puts 'Opened.'
|
66
86
|
end
|
67
|
-
i = throb { api.post "/repos/#{repo}/issues", assigns }.body
|
68
|
-
puts format_issue(i)
|
69
|
-
puts 'Opened.'
|
70
87
|
end
|
88
|
+
rescue Client::Error => e
|
89
|
+
raise unless error = e.errors.first
|
90
|
+
abort "%s %s %s %s." % [
|
91
|
+
error['resource'],
|
92
|
+
error['field'],
|
93
|
+
[*error['value']].join(', '),
|
94
|
+
error['code']
|
95
|
+
]
|
71
96
|
end
|
72
97
|
end
|
73
98
|
end
|