stephencelis-ghi 0.0.3 → 0.0.4
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/History.rdoc +12 -0
- data/bin/ghi +1 -1
- data/lib/ghi/api.rb +3 -0
- data/lib/ghi/cli.rb +237 -199
- data/lib/ghi/issue.rb +14 -0
- data/lib/ghi.rb +1 -1
- metadata +1 -1
data/History.rdoc
CHANGED
data/bin/ghi
CHANGED
data/lib/ghi/api.rb
CHANGED
data/lib/ghi/cli.rb
CHANGED
@@ -4,234 +4,272 @@ require "ghi"
|
|
4
4
|
require "ghi/api"
|
5
5
|
require "ghi/issue"
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
option_parser.parse!(ARGV)
|
13
|
-
|
14
|
-
`git config --get remote.origin.url`.match %r{([^:/]+)/([^/]+).git$}
|
15
|
-
@user ||= $1
|
16
|
-
@repo ||= $2
|
17
|
-
@api = GHI::API.new user, repo
|
18
|
-
|
19
|
-
case action
|
20
|
-
when :search then search search_term, state
|
21
|
-
when :list then list state
|
22
|
-
when :show then show number
|
23
|
-
when :open then open title
|
24
|
-
when :edit then edit number
|
25
|
-
when :close then close number
|
26
|
-
when :reopen then reopen number
|
27
|
-
else puts option_parser
|
28
|
-
end
|
29
|
-
rescue GHI::API::InvalidConnection
|
30
|
-
warn "#{File.basename $0}: not a GitHub repo"
|
31
|
-
rescue GHI::API::ResponseError => e
|
32
|
-
warn "#{File.basename $0}: #{e.message} (#{user}/#{repo})"
|
33
|
-
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
34
|
-
warn "#{File.basename $0}: #{e.message}"
|
35
|
-
end
|
7
|
+
module GHI::CLI #:nodoc:
|
8
|
+
module FileHelper
|
9
|
+
def launch_editor(file)
|
10
|
+
system "#{editor} #{file.path}"
|
11
|
+
end
|
36
12
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
opts.on("-l", "--list", "--search", "--show [state|term|number]") do |v|
|
44
|
-
@action = :list
|
45
|
-
case v
|
46
|
-
when nil, /^o$/
|
47
|
-
@state = :open
|
48
|
-
when /^\d+$/
|
49
|
-
@action = :show
|
50
|
-
@number = v.to_i
|
51
|
-
when /^c$/
|
52
|
-
@state = :closed
|
53
|
-
else
|
54
|
-
@action = :search
|
55
|
-
@state ||= :open
|
56
|
-
@search_term = v
|
57
|
-
end
|
13
|
+
def gets_from_editor(issue)
|
14
|
+
if gitdir
|
15
|
+
File.open message_path, "a+", &file_proc(issue)
|
16
|
+
else
|
17
|
+
Tempfile.new message_filename, &file_proc(issue)
|
58
18
|
end
|
19
|
+
return @message.shift.strip, @message.join.sub(/\b\n\b/, " ").strip
|
20
|
+
end
|
59
21
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
@number = v.to_i
|
66
|
-
when /^l$/
|
67
|
-
@action = :list
|
68
|
-
@state = :open
|
69
|
-
else
|
70
|
-
@title = v
|
71
|
-
end
|
72
|
-
end
|
22
|
+
def delete_message
|
23
|
+
File.delete message_path
|
24
|
+
rescue TypeError
|
25
|
+
nil
|
26
|
+
end
|
73
27
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
28
|
+
def message_path
|
29
|
+
File.join gitdir, message_filename
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def editor
|
35
|
+
ENV["VISUAL"] || ENV["EDITOR"] || "vi"
|
36
|
+
end
|
37
|
+
|
38
|
+
def gitdir
|
39
|
+
@gitdir ||= begin
|
40
|
+
dirs = []
|
41
|
+
Dir.pwd.count("/").times { |n| dirs << ([".."] * n << ".git") * "/" }
|
42
|
+
Dir[*dirs].first
|
85
43
|
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def message_filename
|
47
|
+
@message_filename ||= "GHI_#{action.to_s.upcase}#{number}_MESSAGE"
|
48
|
+
end
|
49
|
+
|
50
|
+
def file_proc(issue)
|
51
|
+
lambda do |file|
|
52
|
+
file << edit_format(issue).join("\n") if File.zero? message_path
|
53
|
+
file.rewind
|
54
|
+
launch_editor file
|
55
|
+
@message = File.readlines(file.path).find_all { |l| !l.match(/^#/) }
|
86
56
|
|
87
|
-
|
88
|
-
|
89
|
-
when /^\d+$/
|
90
|
-
@action = :edit
|
91
|
-
@state = :closed
|
92
|
-
@number = v.to_i
|
93
|
-
else
|
94
|
-
raise OptionParser::MissingArgument
|
57
|
+
if message.to_s =~ /\A\s*\Z/
|
58
|
+
raise GHI::API::InvalidRequest, "can't file empty issue"
|
95
59
|
end
|
60
|
+
raise GHI::API::InvalidRequest, "no change" if issue == message
|
96
61
|
end
|
62
|
+
end
|
63
|
+
end
|
97
64
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
65
|
+
module FormattingHelper
|
66
|
+
def list_format(issues, term = nil)
|
67
|
+
l = if term
|
68
|
+
["# #{state.to_s.capitalize} #{term.inspect} issues on #{user}/#{repo}"]
|
69
|
+
else
|
70
|
+
["# #{state.to_s.capitalize} issues on #{user}/#{repo}"]
|
102
71
|
end
|
103
72
|
|
104
|
-
|
105
|
-
|
106
|
-
|
73
|
+
l += unless issues.empty?
|
74
|
+
issues.map { |i| " #{i.number.to_s.rjust 3}: #{truncate i.title, 72}" }
|
75
|
+
else
|
76
|
+
["none"]
|
107
77
|
end
|
78
|
+
end
|
108
79
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
80
|
+
def edit_format(issue)
|
81
|
+
l = []
|
82
|
+
l << issue.title if issue.title
|
83
|
+
l << ""
|
84
|
+
l << issue.body if issue.body
|
85
|
+
l << "# Please explain the issue. The first line will become the title."
|
86
|
+
l << "# Lines beginning '#' will be ignored. Empty issues won't be filed."
|
87
|
+
l << "# All line breaks will be honored in accordance with GFM:"
|
88
|
+
l << "#"
|
89
|
+
l << "# http://github.github.com/github-flavored-markdown"
|
90
|
+
l << "#"
|
91
|
+
l << "# On #{user}/#{repo}:"
|
92
|
+
l << "#"
|
93
|
+
l += show_format(issue, false).map { |line| "# #{line}" }
|
94
|
+
end
|
115
95
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
96
|
+
def show_format(issue, verbose = true)
|
97
|
+
l = []
|
98
|
+
l << " number: #{issue.number}" if issue.number
|
99
|
+
l << " state: #{issue.state}" if issue.state
|
100
|
+
l << " title: #{issue.title}" if issue.title
|
101
|
+
l << " user: #{issue.user || GHI.login}"
|
102
|
+
l << " votes: #{issue.votes}" if issue.votes
|
103
|
+
l << " created at: #{issue.created_at}" if issue.created_at
|
104
|
+
l << " updated at: #{issue.updated_at}" if issue.updated_at
|
105
|
+
return l unless verbose
|
106
|
+
l << ""
|
107
|
+
l += issue.body.scan(/.{0,75}(?:\s|$)/).map { |line| " #{line}" }
|
123
108
|
end
|
124
|
-
end
|
125
109
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
110
|
+
def action_format(issue)
|
111
|
+
key = "#{action.to_s.capitalize.sub(/e?$/, "ed")} issue #{issue.number}"
|
112
|
+
"#{key}: #{truncate issue.title, 78 - key.length}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def truncate(string, length)
|
116
|
+
result = string.scan(/.{0,#{length}}(?:\s|$)/).first.strip
|
117
|
+
result << "..." if result != string
|
118
|
+
result
|
133
119
|
end
|
134
120
|
end
|
135
121
|
|
136
|
-
|
137
|
-
|
138
|
-
puts <<-BODY
|
139
|
-
#{issue.number}: #{issue.title} [#{issue.state}]
|
122
|
+
class Executable
|
123
|
+
include FileHelper, FormattingHelper
|
140
124
|
|
141
|
-
|
142
|
-
|
143
|
-
updated_at: #{issue.updated_at}
|
125
|
+
attr_reader :message, :user, :repo, :api, :action, :state, :number, :title,
|
126
|
+
:search_term
|
144
127
|
|
145
|
-
|
128
|
+
def initialize
|
129
|
+
option_parser.parse!(ARGV)
|
146
130
|
|
147
|
-
|
148
|
-
|
149
|
-
|
131
|
+
`git config --get remote.origin.url`.match %r{([^:/]+)/([^/]+).git$}
|
132
|
+
@user ||= $1
|
133
|
+
@repo ||= $2
|
134
|
+
@api = GHI::API.new user, repo
|
150
135
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
temp.close!
|
170
|
-
if lines.to_s =~ /\A\s*\Z/
|
171
|
-
warn "can't file empty issue"
|
136
|
+
case action
|
137
|
+
when :search then search search_term, state
|
138
|
+
when :list then list state
|
139
|
+
when :show then show number
|
140
|
+
when :open then open title
|
141
|
+
when :edit then edit number
|
142
|
+
when :close then close number
|
143
|
+
when :reopen then reopen number
|
144
|
+
else puts option_parser
|
145
|
+
end
|
146
|
+
rescue GHI::API::InvalidConnection
|
147
|
+
warn "#{File.basename $0}: not a GitHub repo"
|
148
|
+
exit 1
|
149
|
+
rescue GHI::API::InvalidRequest, GHI::API::ResponseError => e
|
150
|
+
warn "#{File.basename $0}: #{e.message} (#{user}/#{repo})"
|
151
|
+
exit 1
|
152
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
153
|
+
warn "#{File.basename $0}: #{e.message}"
|
172
154
|
exit 1
|
173
|
-
else
|
174
|
-
title = lines.shift.strip
|
175
|
-
body = lines.join.sub(/\b\n\b/, " ").strip
|
176
|
-
issue = api.open title, body
|
177
|
-
puts " Opened issue #{issue.number}: #{issue.title[0,58]}"
|
178
155
|
end
|
179
|
-
end
|
180
156
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
if issue.updated_at > issue.created_at
|
203
|
-
temp.write "# updated at: #{issue.updated_at}"
|
204
|
-
end
|
205
|
-
temp.rewind
|
206
|
-
system "#{edit} #{temp.path}"
|
207
|
-
lines = File.readlines(temp.path)
|
208
|
-
if temp.readlines == lines
|
209
|
-
warn "no change"
|
210
|
-
exit 1
|
211
|
-
else
|
212
|
-
lines.reject! { |l| l.match(/^#/) }
|
213
|
-
if lines.to_s =~ /\A\s*\Z/
|
214
|
-
warn "can't file empty issue"
|
215
|
-
exit 1
|
216
|
-
else
|
217
|
-
title = lines.shift.strip
|
218
|
-
body = lines.join.sub(/\b\n\b/, " ").strip
|
219
|
-
issue = api.edit number, title, body
|
220
|
-
puts " Updated issue #{issue.number}: #{issue.title[0,58]}"
|
157
|
+
private
|
158
|
+
|
159
|
+
def option_parser
|
160
|
+
@option_parser ||= OptionParser.new { |opts|
|
161
|
+
opts.banner = "Usage: #{File.basename $0} [options]"
|
162
|
+
|
163
|
+
opts.on("-l", "--list", "--search", "--show [state|term|number]") do |v|
|
164
|
+
@action = :list
|
165
|
+
case v
|
166
|
+
when nil, /^o$/
|
167
|
+
@state = :open
|
168
|
+
when /^\d+$/
|
169
|
+
@action = :show
|
170
|
+
@number = v.to_i
|
171
|
+
when /^c$/
|
172
|
+
@state = :closed
|
173
|
+
else
|
174
|
+
@action = :search
|
175
|
+
@state ||= :open
|
176
|
+
@search_term = v
|
177
|
+
end
|
221
178
|
end
|
222
|
-
|
223
|
-
|
224
|
-
|
179
|
+
|
180
|
+
opts.on("-o", "--open", "--reopen [number]") do |v|
|
181
|
+
@action = :open
|
182
|
+
case v
|
183
|
+
when /^\d+$/
|
184
|
+
@action = :reopen
|
185
|
+
@number = v.to_i
|
186
|
+
when /^l$/
|
187
|
+
@action = :list
|
188
|
+
@state = :open
|
189
|
+
else
|
190
|
+
@title = v
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
opts.on("-c", "--closed", "--close [number]") do |v|
|
195
|
+
case v
|
196
|
+
when /^\d+$/
|
197
|
+
@action = :close
|
198
|
+
@number = v.to_i
|
199
|
+
when /^l/, nil
|
200
|
+
@action = :list
|
201
|
+
@state = :closed
|
202
|
+
else
|
203
|
+
raise OptionParser::InvalidOption
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
opts.on("-e", "--edit [number]") do |v|
|
208
|
+
case v
|
209
|
+
when /^\d+$/
|
210
|
+
@action = :edit
|
211
|
+
@state = :closed
|
212
|
+
@number = v.to_i
|
213
|
+
else
|
214
|
+
raise OptionParser::MissingArgument
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
opts.on("-r", "--repo", "--repository [name]") do |v|
|
219
|
+
repo = v.split "/"
|
220
|
+
repo.unshift GHI.login if repo.length == 1
|
221
|
+
@user, @repo = repo
|
222
|
+
end
|
223
|
+
|
224
|
+
opts.on("-V", "--version") do
|
225
|
+
puts "#{File.basename($0)}: v#{GHI::VERSION}"
|
226
|
+
exit
|
227
|
+
end
|
228
|
+
|
229
|
+
opts.on("-h", "--help") do
|
230
|
+
puts opts
|
231
|
+
exit
|
232
|
+
end
|
233
|
+
}
|
225
234
|
end
|
226
|
-
end
|
227
235
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
236
|
+
def search(term, state)
|
237
|
+
issues = api.search term, state
|
238
|
+
puts list_format(issues, term)
|
239
|
+
end
|
240
|
+
|
241
|
+
def list(state)
|
242
|
+
issues = api.list state
|
243
|
+
puts list_format(issues)
|
244
|
+
end
|
245
|
+
|
246
|
+
def show(number)
|
247
|
+
issue = api.show number
|
248
|
+
puts show_format(issue)
|
249
|
+
end
|
232
250
|
|
233
|
-
|
234
|
-
|
235
|
-
|
251
|
+
def open(title)
|
252
|
+
title, body = gets_from_editor GHI::Issue.new(:title => title)
|
253
|
+
issue = api.open title, body
|
254
|
+
delete_message
|
255
|
+
puts action_format(issue)
|
256
|
+
end
|
257
|
+
|
258
|
+
def edit(number)
|
259
|
+
title, body = gets_from_editor api.show(number)
|
260
|
+
issue = api.edit number, title, body
|
261
|
+
delete_message
|
262
|
+
puts action_format(issue)
|
263
|
+
end
|
264
|
+
|
265
|
+
def close(number)
|
266
|
+
issue = api.close number
|
267
|
+
puts action_format(issue)
|
268
|
+
end
|
269
|
+
|
270
|
+
def reopen(number)
|
271
|
+
issue = api.reopen number
|
272
|
+
puts action_format(issue)
|
273
|
+
end
|
236
274
|
end
|
237
275
|
end
|
data/lib/ghi/issue.rb
CHANGED
@@ -12,4 +12,18 @@ class GHI::Issue
|
|
12
12
|
@created_at = options["created_at"]
|
13
13
|
@updated_at = options["updated_at"]
|
14
14
|
end
|
15
|
+
|
16
|
+
#-
|
17
|
+
# REFACTOR: This code is duplicated from cli.rb:gets_from_editor.
|
18
|
+
#+
|
19
|
+
def ==(other_issue)
|
20
|
+
case other_issue
|
21
|
+
when Array
|
22
|
+
other_title = other_issue.first.strip
|
23
|
+
other_body = other_issue[1..-1].join.sub(/\b\n\b/, " ").strip
|
24
|
+
title == other_title && body == other_body
|
25
|
+
else
|
26
|
+
super other_issue
|
27
|
+
end
|
28
|
+
end
|
15
29
|
end
|
data/lib/ghi.rb
CHANGED