git-issue 0.7.0

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.
@@ -0,0 +1,362 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module GitIssue
4
+ class GitIssue::Github < GitIssue::Base
5
+ def initialize(args, options = {})
6
+ super(args, options)
7
+
8
+ @apikey = options[:apikey] || configured_value('apikey')
9
+ @apikey = global_configured_value('github.token') if @apikey.blank?
10
+ configure_error('apikey', "git config issue.apikey some_api_key") if @apikey.blank?
11
+
12
+ @repo = options[:repo] || configured_value('repo')
13
+ configure_error('repo', "git config issue.repo git-issue") if @repo.blank?
14
+
15
+ @user = options[:user] || configured_value('user')
16
+ @user = global_configured_value('github.user') if @user.blank?
17
+ configure_error('user', "git config issue.user yuroyoro") if @user.blank?
18
+ end
19
+
20
+ def commands
21
+ cl = super
22
+ cl << GitIssue::Command.new(:mention, :men, 'create a comment to given issue')
23
+ end
24
+
25
+ def show(options = {})
26
+ ticket = options[:ticket_id]
27
+ raise 'ticket_id is required.' unless ticket
28
+ issue = fetch_issue(ticket, options)
29
+
30
+ if options[:oneline]
31
+ puts oneline_issue(issue, options)
32
+ else
33
+ comments = []
34
+
35
+ if issue['comments'].to_i > 0
36
+ comments = fetch_comments(ticket) unless options[:supperss_comments]
37
+ end
38
+ puts ""
39
+ puts format_issue(issue, comments, options)
40
+ end
41
+ end
42
+
43
+ def list(options = {})
44
+ state = options[:state] || "open"
45
+
46
+ query_names = [:state, :milestone, :assignee, :mentioned, :labels, :sort, :direction]
47
+ params = query_names.inject({}){|h,k| h[k] = options[k] if options[k];h}
48
+ params[:state] ||= "open"
49
+
50
+ url = to_url("repos",@user, @repo, 'issues')
51
+
52
+ issues = fetch_json(url, params)
53
+ issues = issues.sort_by{|i| i['number'].to_i} unless params[:sort] || params[:direction]
54
+
55
+ t_max = issues.map{|i| mlength(i['title'])}.max
56
+ l_max = issues.map{|i| mlength(i['labels'].map{|l| l['name']}.join(","))}.max
57
+ u_max = issues.map{|i| mlength(i['user']['login'])}.max
58
+
59
+ or_zero = lambda{|v| v.blank? ? "0" : v }
60
+
61
+ issues.each do |i|
62
+ puts sprintf("#%-4d %s %s %s %s c:%s v:%s p:%s %s %s",
63
+ i['number'].to_i,
64
+ i['state'],
65
+ mljust(i['title'], t_max),
66
+ mljust(i['user']['login'], u_max),
67
+ mljust(i['labels'].map{|l| l['name']}.join(','), l_max),
68
+ or_zero.call(i['comments']),
69
+ or_zero.call(i['votes']),
70
+ or_zero.call(i['position']),
71
+ to_date(i['created_at']),
72
+ to_date(i['updated_at'])
73
+ )
74
+ end
75
+
76
+ end
77
+
78
+ def mine(options = {})
79
+ list(options.merge(:assignee => @user))
80
+ end
81
+
82
+ def add(options = {})
83
+ property_names = [:title, :body, :assignee, :milestone, :labels]
84
+
85
+ json = build_issue_json(options, property_names)
86
+ url = to_url("repos", @user, @repo, 'issues')
87
+
88
+ issue = post_json(url, json, options)
89
+ puts "created issue #{oneline_issue(issue)}"
90
+ end
91
+
92
+ def update(options = {})
93
+ ticket = options[:ticket_id]
94
+ raise 'ticket_id is required.' unless ticket
95
+
96
+ property_names = [:title, :body, :assignee, :milestone, :labels, :state]
97
+
98
+ json = build_issue_json(options, property_names)
99
+ url = to_url("repos", @user, @repo, 'issues', ticket)
100
+
101
+ issue = post_json(url, json, options) # use POST instead of PATCH.
102
+ puts "updated issue #{oneline_issue(issue)}"
103
+ end
104
+
105
+
106
+ def mention(options = {})
107
+ ticket = options[:ticket_id]
108
+ raise 'ticket_id is required.' unless ticket
109
+
110
+ body = options[:body]
111
+ raise 'commnet body is required.' unless body
112
+
113
+ json = { :body => body }
114
+ url = to_url("repos", @user, @repo, 'issues', ticket, 'comments')
115
+
116
+ issue = post_json(url, json, options)
117
+
118
+ issue = fetch_issue(ticket)
119
+ puts "commented issue #{oneline_issue(issue)}"
120
+ end
121
+
122
+ def branch(options = {})
123
+ ticket = options[:ticket_id]
124
+ raise 'ticket_id is required.' unless ticket
125
+
126
+ branch_name = ticket_branch(ticket)
127
+
128
+ if options[:force]
129
+ system "git branch -D #{branch_name}" if options[:force]
130
+ system "git checkout -b #{branch_name}"
131
+ else
132
+ if %x(git branch -l | grep "#{branch_name}").strip.empty?
133
+ system "git checkout -b #{branch_name}"
134
+ else
135
+ system "git checkout #{branch_name}"
136
+ end
137
+ end
138
+
139
+ show(options)
140
+ end
141
+
142
+ private
143
+
144
+ ROOT = 'https://api.github.com/'
145
+ def to_url(*path_list)
146
+ URI.join(ROOT, path_list.join("/"))
147
+ end
148
+
149
+ def fetch_json(url, params = {})
150
+ url += "?" + params.map{|k,v| "#{k}=#{v}"}.join("&") unless params.empty?
151
+
152
+ if @debug
153
+ puts url
154
+ end
155
+ json = open(url, {"Authorizaion" => "#{@user}/token:#{@apikey}"}) {|io|
156
+ JSON.parse(io.read)
157
+ }
158
+
159
+ if @debug
160
+ puts '-' * 80
161
+ puts url
162
+ pp json
163
+ puts '-' * 80
164
+ end
165
+
166
+ json
167
+ end
168
+
169
+ def fetch_issue(ticket_id, params = {})
170
+ url = to_url("repos",@user, @repo, 'issues', ticket_id)
171
+ url += "?" + params.map{|k,v| "#{k}=#{v}"}.join("&") unless params.empty?
172
+ json = fetch_json(url)
173
+
174
+ issue = json['issue'] || json
175
+ raise "no such issue #{ticket} : #{base}" unless issue
176
+
177
+ issue
178
+ end
179
+
180
+ def fetch_comments(ticket_id)
181
+ url = to_url("repos",@user, @repo, 'issues', ticket_id, 'comments')
182
+ json = fetch_json(url) || []
183
+ end
184
+
185
+ def build_issue_json(options, property_names)
186
+ json = property_names.inject({}){|h,k| h[k] = options[k] if options[k]; h}
187
+ json[:labels] = json[:labels].split(",") if json[:labels]
188
+ json
189
+ end
190
+
191
+ def post_json(url, json, options, params = {})
192
+ response = send_json(url, json, options, params, :post)
193
+ json = JSON.parse(response.body)
194
+
195
+ raise error_message(json) unless response_success?(response)
196
+ json
197
+ end
198
+
199
+ def put_json(url, json, options, params = {})
200
+ response = send_json(url, json, options, params, :put)
201
+ json = JSON.parse(response.body)
202
+
203
+ raise error_message(json) unless response_success?(response)
204
+ json
205
+ end
206
+
207
+ def error_message(json)
208
+ msg = [json['message']]
209
+ msg += json['errors'].map(&:pretty_inspect) if json['errors']
210
+ msg.join("\n ")
211
+ end
212
+
213
+ def send_json(url, json, options, params = {}, method = :post)
214
+ url = "#{url}"
215
+ uri = URI.parse(url)
216
+
217
+ if @debug
218
+ puts '-' * 80
219
+ puts url
220
+ pp json
221
+ puts '-' * 80
222
+ end
223
+
224
+ https = Net::HTTP.new(uri.host, uri.port)
225
+ https.use_ssl = true
226
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE
227
+ https.set_debug_output $stderr if @debug && https.respond_to?(:set_debug_output)
228
+ https.start{|http|
229
+
230
+ path = "#{uri.path}"
231
+ path += "?" + params.map{|k,v| "#{k}=#{v}"}.join("&") unless params.empty?
232
+
233
+ request = case method
234
+ when :post then Net::HTTP::Post.new(path)
235
+ when :put then Net::HTTP::Put.new(path)
236
+ else raise "unknown method #{method}"
237
+ end
238
+
239
+ # request["Authorizaion"] = "#{@user}/token: #{@apikey}"
240
+ #
241
+ # Github API v3 does'nt supports API token base authorization for now.
242
+ # For Authentication, this method use Basic Authorizaion instead token.
243
+ password = options[:password] || get_password(@user)
244
+
245
+ request.basic_auth @user, password
246
+
247
+ request.set_content_type("application/json")
248
+ request.body = json.to_json
249
+
250
+ response = http.request(request)
251
+ if @debug
252
+ puts "#{response.code}: #{response.msg}"
253
+ puts response.body
254
+ end
255
+ response
256
+ }
257
+ end
258
+
259
+ def get_password(user)
260
+ print "password(#{user}): "
261
+ system "stty -echo"
262
+ password = $stdin.gets.chop
263
+ system "stty echo"
264
+ password
265
+ end
266
+
267
+ def oneline_issue(issue, options = {})
268
+ issue_title(issue)
269
+ end
270
+
271
+ def format_issue(issue, comments, options)
272
+ msg = [""]
273
+
274
+ msg << issue_title(issue)
275
+ msg << "-" * 80
276
+ msg << issue_author(issue)
277
+ msg << ""
278
+
279
+ props = []
280
+ props << ['comments', issue['comments']]
281
+ props << ['votes', issue['votes']]
282
+ props << ['position', issue['position']]
283
+ props << ['milestone', issue['milestone']['title']] unless issue['milestone'].blank?
284
+
285
+ props.each_with_index do |p,n|
286
+ row = sprintf("%s : %s", mljust(p.first, 18), mljust(p.last.to_s, 24))
287
+ if n % 2 == 0
288
+ msg << row
289
+ else
290
+ msg[-1] = "#{msg.last} #{row}"
291
+ end
292
+ end
293
+
294
+ msg << sprintf("%s : %s", mljust('labels', 18), issue['labels'].map{|l| l['name']}.join(","))
295
+ msg << sprintf("%s : %s", mljust('html_url', 18), issue['html_url'])
296
+ msg << sprintf("%s : %s", mljust('updated_at', 18), Time.parse(issue['updated_at']))
297
+
298
+ # display description
299
+ msg << "-" * 80
300
+ msg << "#{issue['body']}"
301
+ msg << ""
302
+
303
+ # display comments
304
+ if comments && !comments.empty?
305
+ msg << "-" * 80
306
+ msg << ""
307
+ cmts = format_comments(comments)
308
+ msg += cmts.map{|s| " #{s}"}
309
+ end
310
+
311
+ msg.join("\n")
312
+ end
313
+
314
+ def issue_title(issue)
315
+ "[#{issue['state']}] ##{issue['number']} #{issue['title']}"
316
+ end
317
+
318
+ def issue_author(issue)
319
+ author = issue['user']['login']
320
+ created_at = issue['created_at']
321
+
322
+ msg = "#{author} opened this issue #{Time.parse(created_at)}"
323
+ msg
324
+ end
325
+
326
+ def format_comments(comments)
327
+ cmts = []
328
+ comments.sort_by{|c| c['created_at']}.each_with_index do |c,n|
329
+ cmts += format_comment(c,n)
330
+ end
331
+ cmts
332
+ end
333
+
334
+ def format_comment(c, n)
335
+ cmts = []
336
+
337
+ cmts << "##{n + 1} - #{c['user']['login']}が#{time_ago_in_words(c['created_at'])}に更新"
338
+ cmts << "-" * 78
339
+ cmts += c['body'].split("\n").to_a if c['body']
340
+ cmts << ""
341
+ end
342
+
343
+ def opt_parser
344
+ opts = super
345
+ opts.on("--supperss_comments", "-sc", "show issue journals"){|v| @options[:supperss_comments] = true}
346
+ opts.on("--title=VALUE", "Title of issue.Use the given value to create/update issue."){|v| @options[:title] = v}
347
+ opts.on("--body=VALUE", "Body content of issue.Use the given value to create/update issue."){|v| @options[:body] = v}
348
+ opts.on("--state=VALUE", "Use the given value to create/update issue. or query of listing issues.Where 'state' is either 'open' or 'closed'"){|v| @options[:state] = v}
349
+ opts.on("--milestone=VALUE", "Use the given value to create/update issue. or query of listing issues, (Integer Milestone number)"){|v| @options[:milestone] = v }
350
+ opts.on("--assignee=VALUE", "Use the given value to create/update issue. or query of listing issues, (String User login)"){|v| @options[:assignee] = v }
351
+ opts.on("--mentioned=VALUE", "Query of listing issues, (String User login)"){|v| @options[:mentioned] = v }
352
+ opts.on("--labels=VALUE", "Use the given value to create/update issue. or query of listing issues, (String list of comma separated Label names)"){|v| @options[:labels] = v }
353
+ opts.on("--sort=VALUE", "Query of listing issues, (created, updated, comments, default: created)"){|v| @options[:sort] = v }
354
+ opts.on("--direction=VALUE", "Query of listing issues, (asc or desc, default: desc.)"){|v| @options[:direction] = v }
355
+ opts.on("--since=VALUE", "Query of listing issue, (Optional string of a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ)"){|v| @options[:since] = v }
356
+
357
+ opts.on("--password=VALUE", "For Authorizaion of create/update issue. Github API v3 does'nt supports API token base authorization for now. then, use Basic Authorizaion instead token." ){|v| @options[:password]}
358
+ opts
359
+ end
360
+
361
+ end
362
+ end
@@ -0,0 +1,450 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module GitIssue
4
+ class Redmine < GitIssue::Base
5
+
6
+ def initialize(args, options = {})
7
+ super(args, options)
8
+
9
+ @apikey = options[:apikey] || configured_value('apikey')
10
+ configure_error('apikey', "git config issue.apikey some_api_key") if @apikey.blank?
11
+
12
+ @url = options[:url] || configured_value('url')
13
+ configure_error('url', "git config issue.url http://example.com/redmine") if @url.blank?
14
+ end
15
+
16
+ def default_cmd
17
+ Helper.configured_value('project').blank? ? :list : :project
18
+ end
19
+
20
+ def commands
21
+ cl = super
22
+ cl << GitIssue::Command.new(:local, :loc, 'listing local branches tickets')
23
+ cl << GitIssue::Command.new(:project, :prj, 'listing ticket belongs to sspecified project ')
24
+ end
25
+
26
+ def show(options = {})
27
+ ticket = options[:ticket_id]
28
+ raise 'ticket_id is required.' unless ticket
29
+
30
+ issue = fetch_issue(ticket, options)
31
+
32
+ if options[:oneline]
33
+ puts oneline_issue(issue, options)
34
+ else
35
+ puts ""
36
+ puts format_issue(issue, options)
37
+ end
38
+ end
39
+
40
+ def list(options = {})
41
+ url = to_url('issues')
42
+ params = {"limit" => options[:max_count] || 100 }
43
+ params.merge!("assigned_to_id" => "me") if options[:mine]
44
+ params.merge!(Hash[*(options[:query].split("&").map{|s| s.split("=") }.flatten)]) if options[:query]
45
+
46
+ json = fetch_json(url, params)
47
+
48
+ output_issues(json['issues'])
49
+ end
50
+
51
+ def mine(options = {})
52
+ list(options.merge(:mine => true))
53
+ end
54
+
55
+ def commit(options = {})
56
+ ticket = options[:ticket_id]
57
+ raise 'ticket_id is required.' unless ticket
58
+
59
+ issue = fetch_issue(ticket)
60
+
61
+ f = File.open("./commit_msg_#{ticket}", 'w')
62
+ f.write("refs ##{ticket} #{issue['subject']}")
63
+ f.close
64
+
65
+ cmd = "git commit --edit #{options[:all] ? '-a' : ''} --file #{f.path}"
66
+ system(cmd)
67
+
68
+ File.unlink f.path if f.path
69
+ end
70
+
71
+ def add(options = {})
72
+ property_names = [:project_id, :subject, :description, :done_ratio, :status_id, :priority_id, :tracker_id, :assigned_to_id, :category_id, :fixed_version_id, :notes]
73
+
74
+ json = build_issue_json(options, property_names)
75
+ json["issue"][:project_id] ||= Helper.configured_value('project')
76
+
77
+ url = to_url('issues')
78
+
79
+ json = post_json(url, json, options)
80
+ puts "created issue #{oneline_issue(json["issue"])}"
81
+ end
82
+
83
+ def update(options = {})
84
+ ticket = options[:ticket_id]
85
+ raise 'ticket_id is required.' unless ticket
86
+
87
+ property_names = [:subject, :done_ratio, :status_id, :priority_id, :tracker_id, :assigned_to_id, :category_id, :fixed_version_id, :notes]
88
+ json = build_issue_json(options, property_names)
89
+
90
+ url = to_url('issues', ticket)
91
+ put_json(url, json, options)
92
+ issue = fetch_issue(ticket)
93
+ puts "updated issue #{oneline_issue(issue)}"
94
+ end
95
+
96
+ def branch(options = {})
97
+ ticket = options[:ticket_id]
98
+ raise 'ticket_id is required.' unless ticket
99
+
100
+ branch_name = ticket_branch(ticket)
101
+
102
+ if options[:force]
103
+ system "git branch -D #{branch_name}" if options[:force]
104
+ system "git checkout -b #{branch_name}"
105
+ else
106
+ if %x(git branch -l | grep "#{branch_name}").strip.empty?
107
+ system "git checkout -b #{branch_name}"
108
+ else
109
+ system "git checkout #{branch_name}"
110
+ end
111
+ end
112
+
113
+ show(options)
114
+ end
115
+
116
+ def local(option = {})
117
+
118
+ brances = %x(git branch).split(/\n/).map{|b| b.scan(/.*ticket\D*(\d+).*/).first }.reject{|r| r.nil?}.map{|r| r.first }
119
+
120
+ issues = brances.map{|ticket_id| fetch_issue(ticket_id) }
121
+
122
+ output_issues(issues)
123
+ end
124
+
125
+ def project(options = {})
126
+ project_id = Helper.configured_value('project')
127
+ project_id = options[:ticket_id] if project_id.blank?
128
+ raise 'project_id is required.' unless project_id
129
+ list(options.merge(:query => "project_id=#{project_id}"))
130
+ end
131
+
132
+ private
133
+
134
+ def to_url(*path_list)
135
+ URI.join(@url, path_list.join("/"))
136
+ end
137
+
138
+ def fetch_json(url, params = {})
139
+ url = "#{url}.json?key=#{@apikey}"
140
+ url += "&" + params.map{|k,v| "#{k}=#{v}"}.join("&") unless params.empty?
141
+ json = open(url) {|io| JSON.parse(io.read) }
142
+
143
+ if @debug
144
+ puts '-' * 80
145
+ puts url
146
+ pp json
147
+ puts '-' * 80
148
+ end
149
+
150
+ json
151
+ end
152
+
153
+ def fetch_issue(ticket_id, options = {})
154
+ url = to_url("issues", ticket_id)
155
+ includes = issue_includes(options)
156
+ params = includes.empty? ? {} : {"include" => includes }
157
+ json = fetch_json(url, params)
158
+
159
+ issue = json['issue'] || json
160
+ raise "no such issue #{ticket} : #{base}" unless issue
161
+
162
+ issue
163
+ end
164
+
165
+ def post_json(url, json, options, params = {})
166
+ response = send_json(url, json, options, params, :post)
167
+ JSON.parse(response.body) if response_success?(response)
168
+ end
169
+
170
+ def put_json(url, json, options, params = {})
171
+ send_json(url, json, options, params, :put)
172
+ end
173
+
174
+ def send_json(url, json, options, params = {}, method = :post)
175
+ url = "#{url}.json"
176
+ uri = URI.parse(url)
177
+
178
+ if @debug
179
+ puts '-' * 80
180
+ puts url
181
+ pp json
182
+ puts '-' * 80
183
+ end
184
+
185
+ http = Net::HTTP.new(uri.host, uri.port)
186
+ http.set_debug_output $stderr if @debug && http.respond_to?(:set_debug_output)
187
+ http.start(uri.host, uri.port){|http|
188
+
189
+ path = "#{uri.path}?key=#{@apikey}"
190
+ path += "&" + params.map{|k,v| "#{k}=#{v}"}.join("&") unless params.empty?
191
+
192
+ request = case method
193
+ when :post then Net::HTTP::Post.new(path)
194
+ when :put then Net::HTTP::Put.new(path)
195
+ else raise "unknown method #{method}"
196
+ end
197
+
198
+ request.set_content_type("application/json")
199
+ request.body = json.to_json
200
+
201
+ response = http.request(request)
202
+ if @debug
203
+ puts "#{response.code}: #{response.msg}"
204
+ puts response.body
205
+ end
206
+ response
207
+ }
208
+ end
209
+
210
+ def issue_includes(options)
211
+ includes = []
212
+ includes << "journals" if ! options[:supperss_journals] || options[:verbose]
213
+ includes << "changesets" if ! options[:supperss_changesets] || options[:verbose]
214
+ includes << "relations" if ! options[:supperss_relations] || options[:verbose]
215
+ includes.join(",")
216
+ end
217
+
218
+
219
+ def issue_title(issue)
220
+ "[#{issue['project']['name']}] #{issue['tracker']['name']} ##{issue['id']} #{issue['subject']}"
221
+ end
222
+
223
+ def issue_author(issue)
224
+ author = issue['author']['name']
225
+ created_on = issue['created_on']
226
+ updated_on = issue['updated_on']
227
+
228
+ msg = "#{author}が#{time_ago_in_words(created_on)}に追加"
229
+ msg += ", #{time_ago_in_words(updated_on)}に更新" unless created_on == updated_on
230
+ msg
231
+ end
232
+
233
+ PROPERTY_TITLES= {"status"=>"ステータス", "start_date"=>"開始日", "category"=>"カテゴリ", "assigned_to"=>"担当者", "estimated_hours"=>"予定工数", "priority"=>"優先度", "fixed_version"=>"対象バージョン", "due_date"=>"期日", "done_ratio"=>"進捗"}
234
+
235
+ def property_title(name)
236
+ PROPERTY_TITLES[name] || name
237
+ end
238
+
239
+ def oneline_issue(issue, options = {})
240
+ "##{issue['id']} #{issue['subject']}"
241
+ end
242
+
243
+ def format_issue(issue, options)
244
+ msg = [""]
245
+
246
+ msg << issue_title(issue)
247
+ msg << "-" * 80
248
+ msg << issue_author(issue)
249
+ msg << ""
250
+
251
+ props = []
252
+ add_prop = Proc.new{|name|
253
+ title = property_title(name)
254
+ value = issue[name] || ""
255
+ props << [title, value]
256
+ }
257
+ add_prop_name = Proc.new{|name|
258
+ title = property_title(name)
259
+ value = ''
260
+ value = issue[name]['name'] if issue[name] && issue[name]['name']
261
+ props << [title, value]
262
+ }
263
+
264
+ add_prop_name.call('status')
265
+ add_prop.call("start_date")
266
+ add_prop_name.call('priority')
267
+ add_prop.call('due_date')
268
+ add_prop_name.call('assigned_to')
269
+ add_prop.call('done_ratio')
270
+ add_prop_name.call('category')
271
+ add_prop.call('estimated_hours')
272
+ add_prop_name.call('fixed_version')
273
+
274
+ # acd custom_fields if it have value.
275
+ if custom_fields = issue[:custom_fields] && custom_fields.reject{|cf| cf['value'].nil? || cf['value'].empty? }
276
+ custom_fields.each do |cf|
277
+ props << [cf['name'], cf['value']]
278
+ end
279
+ end
280
+
281
+ props.each_with_index do |p,n|
282
+ row = sprintf("%s : %s", mljust(p.first, 18), mljust(p.last.to_s, 24))
283
+ if n % 2 == 0
284
+ msg << row
285
+ else
286
+ msg[-1] = "#{msg.last} #{row}"
287
+ end
288
+ end
289
+
290
+ # display relations tickets
291
+ if ! options[:supperss_relations] || options[:verbose]
292
+ relations = issue['relations']
293
+ if relations && !relations.empty?
294
+ msg << "関連するチケット"
295
+ msg << "-" * 80
296
+ rels = format_relations(relations)
297
+ msg += rels
298
+ end
299
+ end
300
+
301
+ # display description
302
+ msg << "-" * 80
303
+ msg << "#{issue['description']}"
304
+ msg << ""
305
+
306
+ # display journals
307
+ if ! options[:supperss_journals] || options[:verbose]
308
+ journals = issue['journals']
309
+ if journals && !journals.empty?
310
+ msg << "履歴"
311
+ msg << "-" * 80
312
+ msg << ""
313
+ jnl = format_jounals(journals)
314
+ msg += jnl.map{|s| " #{s}"}
315
+ end
316
+ end
317
+
318
+ # display changesets
319
+ if ! options[:supperss_changesets] || options[:verbose]
320
+ changesets = issue['changesets']
321
+ if changesets && !changesets.empty?
322
+ msg << "関係しているリビジョン"
323
+ msg << "-" * 80
324
+ msg << ""
325
+ cs = format_changesets(changesets)
326
+ msg += cs.map{|s| " #{s}"}
327
+ end
328
+ end
329
+
330
+ msg.join("\n")
331
+
332
+ end
333
+
334
+ def format_jounals(journals)
335
+ jnl = []
336
+ journals.sort_by{|j| j['created_on']}.each_with_index do |j,n|
337
+ jnl += format_jounal(j,n)
338
+ end
339
+ jnl
340
+ end
341
+
342
+ def format_jounal(j, n)
343
+ jnl = []
344
+
345
+ jnl << "##{n + 1} - #{j['user']['name']}が#{time_ago_in_words(j['created_on'])}に更新"
346
+ jnl << "-" * 78
347
+ j['details'].each do |d|
348
+ log = "#{property_title(d['name'])}を"
349
+ if d['old_value']
350
+ log += "\"#{d['old_value']}\"から\"#{d['new_value']}\"へ変更"
351
+ else
352
+ log += "\"#{d['new_value']}\"にセット"
353
+ end
354
+ jnl << log
355
+ end
356
+ jnl += j['notes'].split("\n").to_a if j['notes']
357
+ jnl << ""
358
+ end
359
+
360
+ def format_changesets(changesets)
361
+ cs = []
362
+ changesets.sort_by{|c| c['committed_on'] }.each do |c|
363
+ cs << "リビジョン: #{c['revision'][0..10]} #{c['user']['name']}が#{time_ago_in_words(c['committed_on'])}に追加"
364
+ cs += c['comments'].split("\n").to_a
365
+ cs << ""
366
+ end
367
+ cs
368
+ end
369
+
370
+ def format_relations(relations)
371
+ relations.map{|r|
372
+ issue = fetch_issue(r['issue_id'])
373
+ "#{relations_label(r['relation_type'])} #{issue_title(issue)} #{issue['status']['name']} #{issue['start_date']} "
374
+ }
375
+ end
376
+
377
+ def format_issue_tables(issues_json)
378
+ issues = issues_json.map{ |issue|
379
+ project = issue['project']['name'] rescue ""
380
+ tracker = issue['tracker']['name'] rescue ""
381
+ status = issue['status']['name'] rescue ""
382
+ assigned_to = issue['assigned_to']['name'] rescue ""
383
+ [issue['id'], project, tracker, status, issue['subject'], assigned_to, issue['updated_on']]
384
+ }
385
+
386
+ p_max = issues.map{|i| mlength(i[1])}.max
387
+ t_max = issues.map{|i| mlength(i[2])}.max
388
+ s_max = issues.map{|i| mlength(i[3])}.max
389
+ a_max = issues.map{|i| mlength(i[5])}.max
390
+
391
+ issues.map {|i|
392
+ sprintf("#%-4d %s %s %s %s %s %s", i[0].to_i, mljust(i[1], p_max), mljust(i[2], t_max), mljust(i[3], s_max), mljust(i[4], 80), mljust(i[5], a_max), to_date(i[6]))
393
+ }
394
+ end
395
+
396
+ def output_issues(issues)
397
+ if options[:raw_id]
398
+ issues.each do |i|
399
+ puts i['id']
400
+ end
401
+ else
402
+ format_issue_tables(issues).each do |i|
403
+ puts i
404
+ end
405
+ end
406
+ end
407
+
408
+ RELATIONS_LABEL = { "relates" => "関係している", "duplicates" => "重複している",
409
+ "duplicated" => "重複されている", "blocks" => "ブロックしている",
410
+ "blocked" => "ブロックされている", "precedes" => "先行する", "follows" => "後続する",
411
+ }
412
+
413
+ def relations_label(rel)
414
+ RELATIONS_LABEL[rel] || rel
415
+ end
416
+
417
+ def build_issue_json(options, property_names)
418
+ json = {"issue" => property_names.inject({}){|h,k| h[k] = options[k] if options[k]; h} }
419
+
420
+ if custom_fields = options[:custom_fields]
421
+ json['custom_fields'] = custom_fields.split(",").map{|s| k,*v = s.split(":");{'id' => k.to_i, 'value' => v.join }}
422
+ end
423
+ json
424
+ end
425
+
426
+ def opt_parser
427
+ opts = super
428
+ opts.on("--supperss_journals", "-j", "do not show issue journals"){|v| @options[:supperss_journals] = true}
429
+ opts.on("--supperss_relations", "-r", "do not show issue relations tickets"){|v| @options[:supperss_relations] = true}
430
+ opts.on("--supperss_changesets", "-c", "do not show issue changesets"){|v| @options[:supperss_changesets] = true}
431
+ opts.on("--query=VALUE",'-q=VALUE', "filter query of listing tickets") {|v| @options[:query] = v}
432
+
433
+ opts.on("--project_id=VALUE", "use the given value to create subject"){|v| @options[:project_id] = v}
434
+ opts.on("--description=VALUE", "use the given value to create subject"){|v| @options[:description] = v}
435
+ opts.on("--subject=VALUE", "use the given value to create/update subject"){|v| @options[:subject] = v}
436
+ opts.on("--ratio=VALUE", "use the given value to create/update done-ratio(%)"){|v| @options[:done_ratio] = v.to_i}
437
+ opts.on("--status=VALUE", "use the given value to create/update issue statues id"){|v| @options[:status_id] = v }
438
+ opts.on("--priority=VALUE", "use the given value to create/update issue priority id"){|v| @options[:priority_id] = v }
439
+ opts.on("--tracker=VALUE", "use the given value to create/update tracker id"){|v| @options[:tracker_id] = v }
440
+ opts.on("--assigned_to_id=VALUE", "use the given value to create/update assigned_to id"){|v| @options[:assigned_to_id] = v }
441
+ opts.on("--category=VALUE", "use the given value to create/update category id"){|v| @options[:category_id] = v }
442
+ opts.on("--fixed_version=VALUE", "use the given value to create/update fixed_version id"){|v| @options[:fixed_version_id] = v }
443
+ opts.on("--custom_fields=VALUE", "value should be specifies '<custom_fields_id1>:<value2>,<custom_fields_id2>:<value2>, ...' "){|v| @options[:custom_fields] = v }
444
+
445
+ opts.on("--notes=VALUE", "add notes to issue"){|v| @options[:notes] = v}
446
+
447
+ opts
448
+ end
449
+ end
450
+ end