git-issue 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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