ruby-bugzilla 0.3.2 → 0.3.3

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/README.rdoc CHANGED
@@ -11,7 +11,7 @@ APIs are available:
11
11
 
12
12
  == Copyright
13
13
 
14
- Copyright (c) 2010 Red Hat, Inc. See COPYING for details.
14
+ Copyright (c) 2010-2012 Red Hat, Inc. See COPYING for details.
15
15
 
16
16
  == Authors
17
17
 
data/bin/bzconsole CHANGED
@@ -1,6 +1,6 @@
1
1
  #! /usr/bin/env ruby
2
2
  # bzconsole.rb
3
- # Copyright (C) 2010 Red Hat, Inc.
3
+ # Copyright (C) 2010-2012 Red Hat, Inc.
4
4
  #
5
5
  # Authors:
6
6
  # Akira TAGOH <tagoh@redhat.com>
@@ -29,16 +29,15 @@ require 'gruff'
29
29
  require 'uri'
30
30
  require 'highline/import'
31
31
 
32
-
33
- $KCODE = 'u'
34
-
35
32
  begin
36
33
  require 'bugzilla'
37
34
  rescue LoadError
38
- require File.join(File.dirname(__FILE__), "..", "lib", "bugzilla.rb")
39
- $:.push(File.join(File.dirname(__FILE__), "..", "lib"))
35
+ $:.push(File.join(File.dirname(__FILE__), '..', 'lib'))
36
+ require 'bugzilla'
40
37
  end
41
38
 
39
+ $KCODE = 'u'
40
+
42
41
  module BzConsole
43
42
 
44
43
  class CommandTemplate
@@ -94,6 +93,16 @@ module BzConsole
94
93
  File.open(fname, "w") {|f| f.chmod(0600); f.write(conf.to_yaml)}
95
94
  end # def save_config
96
95
 
96
+ private
97
+
98
+ def get_proxy(info)
99
+ uri = info[:Proxy] || ENV["http_proxy"]
100
+ proxy_uri = uri.nil? ? nil : URI.parse(uri)
101
+ proxy_host = proxy_uri.nil? ? nil : proxy_uri.host
102
+ proxy_port = proxy_uri.nil? ? nil : proxy_uri.port
103
+ return proxy_host, proxy_port
104
+ end
105
+
97
106
  end # class CommandTemplate
98
107
 
99
108
  class Setup < CommandTemplate
@@ -200,8 +209,9 @@ module BzConsole
200
209
  path = uri.path.empty? ? nil : uri.path
201
210
  login = info[:User].nil? ? ask("Bugzilla ID: ") : info[:User]
202
211
  pass = info[:Password].nil? ? ask("Bugzilla password: ") {|q| q.echo = false} : info[:Password]
212
+ proxy_host, proxy_port = get_proxy(info)
203
213
 
204
- xmlrpc = Bugzilla::XMLRPC.new(host, port, path)
214
+ xmlrpc = Bugzilla::XMLRPC.new(host, port, path, proxy_host, proxy_port)
205
215
  user = Bugzilla::User.new(xmlrpc)
206
216
  begin
207
217
  result = user.login({'login'=>login, 'password'=>pass, 'remember'=>true})
@@ -298,10 +308,11 @@ module BzConsole
298
308
  path = uri.path.empty? ? nil : uri.path
299
309
  login = opts[:command][:anonymous] == true ? nil : info[:User]
300
310
  pass = opts[:command][:anonymous] == true ? nil : info[:Password]
311
+ proxy_host, proxy_port = get_proxy(info)
301
312
 
302
313
  @plugin.run(:pre, host, :getbug, opts)
303
314
 
304
- xmlrpc = Bugzilla::XMLRPC.new(host, port, path)
315
+ xmlrpc = Bugzilla::XMLRPC.new(host, port, path, proxy_host, proxy_port)
305
316
  user = Bugzilla::User.new(xmlrpc)
306
317
  user.session(login, pass) do
307
318
  bug = Bugzilla::Bug.new(xmlrpc)
@@ -410,10 +421,11 @@ module BzConsole
410
421
  path = uri.path.empty? ? nil : uri.path
411
422
  login = opts[:command][:anonymous] == true ? nil : info[:User]
412
423
  pass = opts[:command][:anonymous] == true ? nil : info[:Password]
424
+ proxy_host, proxy_port = get_proxy(info)
413
425
 
414
426
  @plugin.run(:pre, host, :search, opts[:command][:query])
415
427
 
416
- xmlrpc = Bugzilla::XMLRPC.new(host, port, path)
428
+ xmlrpc = Bugzilla::XMLRPC.new(host, port, path, proxy_host, proxy_port)
417
429
  user = Bugzilla::User.new(xmlrpc)
418
430
  user.session(login, pass) do
419
431
  bug = Bugzilla::Bug.new(xmlrpc)
@@ -487,10 +499,11 @@ module BzConsole
487
499
  path = uri.path.empty? ? nil : uri.path
488
500
  login = opts[:command][:anonymous] == true ? nil : info[:User]
489
501
  pass = opts[:command][:anonymous] == true ? nil : info[:Password]
502
+ proxy_host, proxy_port = get_proxy(info)
490
503
 
491
504
  @plugin.run(:pre, host, :show, opts)
492
505
 
493
- xmlrpc = Bugzilla::XMLRPC.new(host, port, path)
506
+ xmlrpc = Bugzilla::XMLRPC.new(host, port, path, proxy_host, proxy_port)
494
507
  user = Bugzilla::User.new(xmlrpc)
495
508
  user.session(login, pass) do
496
509
  if opts[:command][:show][:mode] == :field then
@@ -651,8 +664,9 @@ module BzConsole
651
664
  path = uri.path.empty? ? nil : uri.path
652
665
  login = opts[:command][:anonymous] == true ? nil : info[:User]
653
666
  pass = opts[:command][:anonymous] == true ? nil : info[:Password]
667
+ proxy_host, proxy_port = get_proxy(info)
654
668
 
655
- xmlrpc = Bugzilla::XMLRPC.new(host, port, path)
669
+ xmlrpc = Bugzilla::XMLRPC.new(host, port, path, proxy_host, proxy_port)
656
670
  user = Bugzilla::User.new(xmlrpc)
657
671
  user.session(login, pass) do
658
672
  bug = Bugzilla::Bug.new(xmlrpc)
data/bin/bzconsole~ ADDED
@@ -0,0 +1,750 @@
1
+ #! /usr/bin/env ruby
2
+ # bzconsole.rb
3
+ # Copyright (C) 2010 Red Hat, Inc.
4
+ #
5
+ # Authors:
6
+ # Akira TAGOH <tagoh@redhat.com>
7
+ #
8
+ # This library is free software; you can redistribute it and/or
9
+ # modify it under the terms of the GNU Lesser General Public
10
+ # License as published by the Free Software Foundation; either
11
+ # version 2 of the License, or (at your option) any later version.
12
+ #
13
+ # This library is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16
+ # Lesser General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU Lesser General Public
19
+ # License along with this library; if not, write to the
20
+ # Free Software Foundation, Inc., 59 Temple Place - Suite 330,
21
+ # Boston, MA 02111-1307, USA.
22
+
23
+ require 'optparse'
24
+ require 'time'
25
+ require 'yaml'
26
+ require 'rubygems'
27
+ require 'pp'
28
+ require 'gruff'
29
+ require 'uri'
30
+
31
+
32
+ $KCODE = 'u'
33
+
34
+ begin
35
+ require 'bugzilla'
36
+ rescue LoadError
37
+ require File.join(File.dirname(__FILE__), "..", "lib", "bugzilla.rb")
38
+ $:.push(File.join(File.dirname(__FILE__), "..", "lib"))
39
+ end
40
+
41
+ module BzConsole
42
+
43
+ class CommandTemplate
44
+
45
+ def initialize(plugin)
46
+ @n_args = 0
47
+ @defaultyamlfile = File.join(ENV['HOME'], ".bzconsole.yml")
48
+ @plugin = plugin
49
+ end # def initialize
50
+
51
+ attr_reader :n_args
52
+
53
+ def parse(parser, argv, opts)
54
+ raise RuntimeError, sprintf("No implementation for %s", self.class) if self.class =~ /CommandTemplate/
55
+
56
+ parser.on('-h', '--help', 'show this message') {|x| opts[:help] = true}
57
+
58
+ read_config(opts)
59
+ @plugin.run(:parser, nil, parser, argv, opts)
60
+
61
+ parser.order(argv)
62
+ end # def parse
63
+
64
+ def read_config(opts)
65
+ fname = opts[:config].nil? ? @defaultyamlfile : opts[:config]
66
+ begin
67
+ conf = YAML.load(File.open(fname).read)
68
+ rescue Errno::ENOENT
69
+ conf = {}
70
+ end
71
+ conf.each do |k, v|
72
+ if v.include?(:Plugin) then
73
+ load(v[:Plugin])
74
+ end
75
+ end
76
+ conf
77
+ end # def read_config
78
+
79
+ def do(argv)
80
+ raise RuntimeError, sprintf("No implementation for %s", self.class)
81
+ end # def do
82
+
83
+ end # class CommandTemplate
84
+
85
+ class Setup < CommandTemplate
86
+
87
+ def initialize(plugin)
88
+ super
89
+
90
+ @n_args = 0
91
+ end # def initialize
92
+
93
+ def parse(parser, argv, opts)
94
+ parser.banner = sprintf("Usage: %s [global options] setup", File.basename(__FILE__))
95
+ parser.separator ""
96
+ parser.separator "Command options:"
97
+
98
+ super
99
+ end # def parse
100
+
101
+ def do(argv, opts)
102
+ template = {
103
+ "rhbz"=>{
104
+ :Name=>"Red Hat Bugzilla",
105
+ :URL=>"https://bugzilla.redhat.com",
106
+ :User=>"account@example.com",
107
+ :Password=>"blahblahblah",
108
+ :ProductAliases=>{
109
+ 'RHEL3'=>'Red Hat Enterprise Linux 3',
110
+ 'RHEL4'=>'Red Hat Enterprise Linux 4',
111
+ 'RHEL5'=>'Red Hat Enterprise Linux 5',
112
+ 'RHEL6'=>'Red Hat Enterprise Linux 6'
113
+ },
114
+ :Plugin=>"ruby-bugzilla/rhbugzilla.rb"
115
+ },
116
+ "gnbz"=>{
117
+ :Name=>"GNOME Bugzilla",
118
+ :URL=>"https://bugzilla.gnome.org",
119
+ :User=>"account@example.com",
120
+ :Password=>"blahblahblah",
121
+ },
122
+ "fdobz"=>{
123
+ :Name=>"FreeDesktop Bugzilla",
124
+ :URL=>"https://bugs.freedesktop.org",
125
+ :User=>"account@example.com",
126
+ :Password=>"blahblahblah",
127
+ },
128
+ "mzbz"=>{
129
+ :Name=>"Mozilla Bugzilla",
130
+ :URL=>"https://bugzilla.mozilla.org",
131
+ :User=>"account@example.com",
132
+ :Password=>"blahblahblah",
133
+ }
134
+ }
135
+
136
+ template.each do |k, v|
137
+ @plugin.run(:pre, v[:URL].sub(/\Ahttps?:\/\//, '').sub(/\/\Z/, ''), :setup, v)
138
+ end
139
+
140
+ fname = opts[:config].nil? ? @defaultyamlfile : opts[:config]
141
+ if File.exist?(fname) then
142
+ raise RuntimeError, ".bzconsole.yml already exists. please remove it first to proceed to the next step."
143
+ end
144
+ File.open(fname, File::CREAT|File::WRONLY, 0600) do |f|
145
+ f.write(template.to_yaml)
146
+ end
147
+ printf("%s has been created. please modify your account information before operating.\n", fname)
148
+ end # def do
149
+
150
+ end # class Setup
151
+
152
+ class Getbug < CommandTemplate
153
+
154
+ def initialize(plugin)
155
+ super
156
+
157
+ @n_args = 1
158
+ end # def initialize
159
+
160
+ def parse(parser, argv, opts)
161
+ parser.banner = sprintf("Usage: %s [global options] getbug [command options] <prefix:bug number> ...", File.basename(__FILE__))
162
+ parser.separator ""
163
+ parser.separator "Command options:"
164
+ parser.on('-s', '--summary', 'Displays bugs summary only') {opts[:summary] = true}
165
+ parser.on('-d', '--details', 'Displays detailed bugs information') {opts[:details] = true}
166
+ parser.on('-a', '--all', 'Displays the whole data in bugs') {opts[:all] = true}
167
+ parser.on('--anonymous', 'Access to Bugzilla anonymously') {opts[:anonymous] = true}
168
+
169
+ super
170
+ end # def parse
171
+
172
+ def do(argv, opts)
173
+ real_do(argv, opts) do |result|
174
+ if opts[:command][:summary] == true then
175
+ printf("Bug#%s, %s, %s[%s, %s] %s\n",
176
+ result['id'],
177
+ result['product'],
178
+ result['component'],
179
+ result['status'],
180
+ result['severity'],
181
+ result['summary'])
182
+ elsif opts[:command][:details] == true then
183
+ printf("Bug#%s, %s, %s, %s[%s, %s, %s, %s] %s\n",
184
+ result['id'],
185
+ result['product'],
186
+ result['assigned_to'],
187
+ result['component'],
188
+ result['status'],
189
+ result['resolution'],
190
+ result['priority'],
191
+ result['severity'],
192
+ result['summary'])
193
+ else
194
+ printf("Bug#%s - %s\n", result['id'], result['summary'])
195
+ printf("Status:\t\t%s%s\n", result['status'], !result['resolution'].empty? ? sprintf("[%s]", result['resolution']) : "")
196
+ printf("Product:\t%s\n", result['product'])
197
+ printf("Component:\t%s\n", result['component'])
198
+ printf("Assinged To:\t%s\n", result['assigned_to'])
199
+ printf("Priority:\t%s\n", result['priority'])
200
+ printf("Severity:\t%s\n", result['severity'])
201
+ printf("Comments:\t%d\n\n", result['comments'].length)
202
+ i = 0
203
+ result['comments'].each do |c|
204
+ printf("Comment#%d%s %s %s\n", i, c['is_private'] == true ? "[private]" : "", c['author'], c['time'].to_time)
205
+ printf("\n %s\n\n", c['text'].split("\n").join("\n "))
206
+ i += 1
207
+ end
208
+ end
209
+ end
210
+ end # def do
211
+
212
+ private
213
+
214
+ def real_do(argv, opts)
215
+ conf = read_config(opts)
216
+ conf.freeze
217
+ argv.each do |bugn|
218
+ bugn =~ /(.*):(.*)/
219
+ prefix = $1
220
+ nnn = $2
221
+ if prefix.nil? then
222
+ raise ArgumentError, sprintf("No prefix specified for Bug#%s", bugn)
223
+ end
224
+ unless conf.include?(prefix) then
225
+ raise RuntimeError, sprintf("No host information for %s", prefix)
226
+ end
227
+
228
+ info = conf[prefix]
229
+ uri = URI.parse(info[:URL])
230
+ host = uri.host
231
+ port = uri.port
232
+ path = uri.path.empty? ? nil : uri.path
233
+ login = opts[:command][:anonymous] == true ? nil : info[:User]
234
+ pass = opts[:command][:anonymous] == true ? nil : info[:Password]
235
+
236
+ @plugin.run(:pre, host, :getbug, opts)
237
+
238
+ xmlrpc = Bugzilla::XMLRPC.new(host, port, path)
239
+ user = Bugzilla::User.new(xmlrpc)
240
+ user.session(login, pass) do
241
+ bug = Bugzilla::Bug.new(xmlrpc)
242
+
243
+ result = nil
244
+ if opts[:command][:summary] == true then
245
+ result = bug.get_bugs(nnn.split(','))
246
+ elsif opts[:command][:details] == true then
247
+ result = bug.get_bugs(nnn.split(','), ::Bugzilla::Bug::FIELDS_DETAILS)
248
+ else
249
+ result = bug.get_bugs(nnn.split(','), nil)
250
+ end
251
+
252
+ @plugin.run(:post, host, :getbug, result)
253
+
254
+ result.each do |r|
255
+ yield r
256
+ end
257
+ end
258
+ end
259
+ end # def real_do
260
+
261
+ end # class Getbug
262
+
263
+ class Search < CommandTemplate
264
+
265
+ def initialize(plugin)
266
+ super
267
+
268
+ @n_args = 1
269
+ end # def initialize
270
+
271
+ def parse(parser, argv, opts)
272
+ opts[:query] ||= {}
273
+ parser.banner = sprintf("Usage: %s [global options] search [command options] <prefix> ...", File.basename(__FILE__))
274
+ parser.separator ""
275
+ parser.separator "Search options:"
276
+ parser.on('--alias=ALIASES', 'filter out the result by the alias') {|v| opts[:query][:alias] ||= []; opts[:query][:alias].push(*v.split(','))}
277
+ parser.on('-a', '--assignee=ASSIGNEES', 'filter out the result by the specific assignees') {|v| opts[:query][:assigned_to] ||= []; opts[:query][:assigned_to].push(*v.split(','))}
278
+ parser.on('--bug=BUGS', 'filter out the result by the specific bug number') {|v| opts[:query][:id] ||= []; opts[:query][:id].push(*v.split(','))}
279
+ parser.on('-c', '--component=COMPONENTS', 'filter out the result by the specific components') {|v| opts[:query][:component] ||= []; opts[:query][:component].push(*v.split(','))}
280
+ parser.on('--create-time=TIME', 'Searches for bugs that were created at this time or later') {|v| opts[:query][:creation_time] = Time.parse(v)}
281
+ parser.on('--creator=CREATER', 'filter out the result by the specific user who reported bugs') {|v| opts[:query][:creator] ||= []; opts[:query][:creator].push(*v.split(','))}
282
+ parser.on('--last-change-time=TIME', 'Searches for bugs that were modified at this time or later') {|v| opts[:query][:last_change_time] = Time.parse(v)}
283
+ parser.on('--op-sys=NAMES', 'filter out the result by the operating system') {|v| opts[:query][:op_sys] ||= []; opts[:query][:op_sys].push(*v.split(','))}
284
+ parser.on('--platform=PLATFORMS', 'filter out the result by the platform') {|v| opts[:query][:platform] ||= []; opts[:query][:platform].push(*v.split(','))}
285
+ parser.on('--priority=PRIORITY', 'filter out the result by the priority') {|v| opts[:query][:priority] ||= []; opts[:query][:priority].push(*v.split(','))}
286
+ parser.on('-p', '--product=PRODUCTS', 'filter out the result by the specific products') {|v| opts[:query][:product] ||= []; opts[:query][:product].push(*v.split(','))}
287
+ parser.on('--resolution=RESOLUSIONS', 'filter out the result by the resolusions') {|v| opts[:query][:resolution] ||= []; opts[:query][:resolusion].push(*v.split(','))}
288
+ parser.on('--severity=SEVERITY', 'filter out the result by the severity') {|v| opts[:query][:severity] ||= []; opts[:query][:severity].push(*v.split(','))}
289
+ parser.on('-s', '--status=STATUSES', 'filter out the result by the status') {|v| opts[:query][:status] ||= []; opts[:query][:status].push(*v.split(','))}
290
+ parser.on('--summary=SUMMARY', 'filter out the result by the summary') {|v| opts[:query][:summary] ||= []; opts[:query][:summary] << v}
291
+ parser.on('--milestone=MILESTONE', 'filter out the result by the target milestone') {|v| opts[:query][:target_milestone] ||= []; opts[:query][:target_milestone].push(*v.split(','))}
292
+ parser.on('--whiteboard=STRING', 'filter out the result by the specific words in the status whiteboard') {|v| opts[:query][:whiteboard] ||= []; opts[:query][:whiteboard] << v}
293
+ parser.separator ""
294
+ parser.separator "Command options:"
295
+ parser.on('--short-list', 'Displays bugs summary only') {opts[:summary] = true}
296
+ parser.on('--detailed-list', 'Displays detailed bugs information') {opts[:details] = true}
297
+ parser.on('--anonymous', 'Access to Bugzilla anonymously') {opts[:anonymous] = true}
298
+
299
+ super
300
+ end # def parse
301
+
302
+ def do(argv, opts)
303
+ c = 0
304
+ real_do(argv, opts) do |result|
305
+ if opts[:command][:summary] == true then
306
+ printf("Bug#%s, %s, %s[%s, %s] %s\n",
307
+ result['id'] || result['bug_id'],
308
+ result['product'],
309
+ result['component'],
310
+ result['status'],
311
+ result['severity'],
312
+ result['summary'])
313
+ elsif opts[:command][:details] == true then
314
+ printf("Bug#%s, %s, %s, %s[%s, %s, %s, %s] %s\n",
315
+ result['id'],
316
+ result['product'],
317
+ result['assigned_to'],
318
+ result['component'],
319
+ result['status'],
320
+ result['resolution'],
321
+ result['priority'],
322
+ result['severity'],
323
+ result['summary'])
324
+ end
325
+ c += 1
326
+ end
327
+ printf("\n%d bug(s) found\n", c)
328
+ end # def do
329
+
330
+ private
331
+
332
+ def real_do(argv, opts)
333
+ conf = read_config(opts)
334
+ conf.freeze
335
+ argv.each do |prefix|
336
+ unless conf.include?(prefix) then
337
+ raise RuntimeError, sprintf("No host information for %s", prefix)
338
+ end
339
+
340
+ info = conf[prefix]
341
+ uri = URI.parse(info[:URL])
342
+ host = uri.host
343
+ port = uri.port
344
+ path = uri.path.empty? ? nil : uri.path
345
+ login = opts[:command][:anonymous] == true ? nil : info[:User]
346
+ pass = opts[:command][:anonymous] == true ? nil : info[:Password]
347
+
348
+ @plugin.run(:pre, host, :search, opts[:command][:query])
349
+
350
+ xmlrpc = Bugzilla::XMLRPC.new(host, port, path)
351
+ user = Bugzilla::User.new(xmlrpc)
352
+ user.session(login, pass) do
353
+ bug = Bugzilla::Bug.new(xmlrpc)
354
+
355
+ opts[:command][:query][:product].map! {|x| info.include?(:ProductAliases) && info[:ProductAliases].include?(x) ? info[:ProductAliases][x] : x} if opts[:command][:query].include?(:product)
356
+
357
+ result = bug.search(opts[:command][:query])
358
+
359
+ @plugin.run(:post, host, :search, result)
360
+
361
+ if result.include?('bugs') then
362
+ result['bugs'].each do |r|
363
+ yield r
364
+ end
365
+ end
366
+ end
367
+ end
368
+ end # def real_do
369
+
370
+ end # class Search
371
+
372
+ class Show < CommandTemplate
373
+
374
+ def initialize(plugin)
375
+ super
376
+
377
+ @n_args = 1
378
+ end # def initialize
379
+
380
+ def parse(parser, argv, opts)
381
+ opts[:show] ||= {}
382
+ opts[:show][:mode] = :component
383
+ opts[:show][:field] = []
384
+ parser.banner = sprintf("Usage: %s [global options] show [command options] <prefix> ...", File.basename(__FILE__))
385
+ parser.separator ""
386
+ parser.separator "Command options:"
387
+ parser.on('-f', '--field', 'Displays available field informations') {|v| opts[:show][:mode] = :field}
388
+ parser.on('--field-name=NAME', 'Displays NAME field information') {|v| opts[:show][:field] << v.split(',')}
389
+ parser.on('-p', '--product', 'Displays available product names') {|v| opts[:show][:mode] = :product}
390
+ parser.on('-c', '--component', 'Displays available component names (default)') {|v| opts[:show][:mode] = :component}
391
+ parser.on('--anonymous', 'Access to Bugzilla anonymously') {opts[:anonymous] = true}
392
+
393
+ super
394
+ end # def parse
395
+
396
+ def do(argv, opts)
397
+ real_do(argv, opts) do |*result|
398
+ if opts[:command][:show][:mode] == :product then
399
+ printf("%s\n", result[0])
400
+ elsif opts[:command][:show][:mode] == :component then
401
+ printf("%s:\n", result[0])
402
+ printf(" %s\n", result[1].join("\n "))
403
+ end
404
+ end
405
+ end # def do
406
+
407
+ private
408
+
409
+ def real_do(argv, opts)
410
+ conf = read_config(opts)
411
+ conf.freeze
412
+ argv.each do |prefix|
413
+ unless conf.include?(prefix) then
414
+ raise RuntimeError, sprintf("No host information for %s", prefix)
415
+ end
416
+
417
+ info = conf[prefix]
418
+ uri = URI.parse(info[:URL])
419
+ host = uri.host
420
+ port = uri.port
421
+ path = uri.path.empty? ? nil : uri.path
422
+ login = opts[:command][:anonymous] == true ? nil : info[:User]
423
+ pass = opts[:command][:anonymous] == true ? nil : info[:Password]
424
+
425
+ @plugin.run(:pre, host, :show, opts)
426
+
427
+ xmlrpc = Bugzilla::XMLRPC.new(host, port, path)
428
+ user = Bugzilla::User.new(xmlrpc)
429
+ user.session(login, pass) do
430
+ if opts[:command][:show][:mode] == :field then
431
+ bug = Bugzilla::Bug.new(xmlrpc)
432
+
433
+ result = bug.fields(opts[:command][:show][:field].flatten)
434
+
435
+ @plugin.run(:post, host, :show, result)
436
+
437
+ pp result
438
+ else
439
+ product = Bugzilla::Product.new(xmlrpc)
440
+
441
+ result = product.selectable_products
442
+
443
+ @plugin.run(:post, host, :show, result)
444
+
445
+ products = result.keys.sort
446
+ products.each do |p|
447
+ if opts[:command][:show][:mode] == :product then
448
+ yield p
449
+ elsif opts[:command][:show][:mode] == :component then
450
+ yield p, result[p][0].sort
451
+ end
452
+ end
453
+ end
454
+ end
455
+ end
456
+ end # def real_do
457
+
458
+ end # class Show
459
+
460
+ class Metrics < CommandTemplate
461
+
462
+ def initialize(plugin)
463
+ super
464
+
465
+ @n_args = 1
466
+ end # def initialize
467
+
468
+ def parse(parser, argv, opts)
469
+ opts[:metrics] = {}
470
+ opts[:query] = {}
471
+ opts[:metrics][:output] = 'bz-metrics.png'
472
+ opts[:metrics][:x_coordinate] = :monthly
473
+
474
+ parser.banner = sprintf("Usage: %s [global options] metrics [command options] <prefix> ...", File.basename(__FILE__))
475
+ parser.separator ""
476
+ parser.separator "Search options:"
477
+ parser.on('--alias=ALIASES', 'filter out the result by the alias') {|v| opts[:query][:alias] ||= []; opts[:query][:alias].push(*v.split(','))}
478
+ parser.on('-a', '--assignee=ASSIGNEES', 'filter out the result by the specific assignees') {|v| opts[:query][:assigned_to] ||= []; opts[:query][:assigned_to].push(*v.split(','))}
479
+ parser.on('--bug=BUGS', 'filter out the result by the specific bug number') {|v| opts[:query][:id] ||= []; opts[:query][:id].push(*v.split(','))}
480
+ parser.on('-c', '--component=COMPONENTS', 'filter out the result by the specific components') {|v| opts[:query][:component] ||= []; opts[:query][:component].push(*v.split(','))}
481
+ parser.on('--creator=CREATER', 'filter out the result by the specific user who reported bugs') {|v| opts[:query][:creator] ||= []; opts[:query][:creator].push(*v.split(','))}
482
+ parser.on('--op-sys=NAMES', 'filter out the result by the operating system') {|v| opts[:query][:op_sys] ||= []; opts[:query][:op_sys].push(*v.split(','))}
483
+ parser.on('--platform=PLATFORMS', 'filter out the result by the platform') {|v| opts[:query][:platform] ||= []; opts[:query][:platform].push(*v.split(','))}
484
+ parser.on('--priority=PRIORITY', 'filter out the result by the priority') {|v| opts[:query][:priority] ||= []; opts[:query][:priority].push(*v.split(','))}
485
+ parser.on('-p', '--product=PRODUCTS', 'filter out the result by the specific products') {|v| opts[:query][:product] ||= []; opts[:query][:product].push(*v.split(','))}
486
+ parser.on('--resolution=RESOLUSIONS', 'filter out the result by the resolusions') {|v| opts[:query][:resolution] ||= []; opts[:query][:resolusion].push(*v.split(','))}
487
+ parser.on('--severity=SEVERITY', 'filter out the result by the severity') {|v| opts[:query][:severity] ||= []; opts[:query][:severity].push(*v.split(','))}
488
+ parser.on('--summary=SUMMARY', 'filter out the result by the summary') {|v| opts[:query][:summary] ||= []; opts[:query][:summary] << v}
489
+ parser.on('--milestone=MILESTONE', 'filter out the result by the target milestone') {|v| opts[:query][:target_milestone] ||= []; opts[:query][:target_milestone].push(*v.split(','))}
490
+ parser.on('--whiteboard=STRING', 'filter out the result by the specific words in the status whiteboard') {|v| opts[:query][:whiteboard] ||= []; opts[:query][:whiteboard] << v}
491
+ parser.separator ""
492
+ parser.separator "Command options:"
493
+ parser.on('-t', '--title=TITLE', 'add TITLE to the graph') {|v| opts[:metrics][:title] = v}
494
+ parser.on('--begin-date=DATE', 'generate the graph since the beginning of month of DATE.') {|v| x = Time.parse(v); opts[:metrics][:begin_date] = Time.utc(x.year, x.month, 1, 0, 0, 0)}
495
+ parser.on('--end-date=DATE', 'generate the graph until the end of month of DATE.') {|v| x = Time.parse(v); opts[:metrics][:end_date] = Time.utc(x.year, x.month + 1, 1, 0, 0, 0) - 1}
496
+ parser.on('-o', '--output=FILE', 'generate the graph to FILE') {|v| opts[:metrics][:output] = v}
497
+ parser.on('--anonymous', 'access to Bugzilla anonymously') {|v| opts[:anonymous] = true}
498
+ parser.on('--weekly', 'genereate the graph with weekly X-coordinate') {|v| opts[:metrics][:x_coordinate] = :weekly}
499
+ parser.on('--monthly', 'genereate the graph with monthly X-coordinate (default)') {|v| opts[:metrics][:x_coordinate] = :monthly}
500
+
501
+ super
502
+ end # def parse
503
+
504
+ def do(argv, opts)
505
+ data = {
506
+ :label=>[],
507
+ 'NEW'=>[],
508
+ 'ASSIGNED'=>[],
509
+ 'MODIFIED'=>[],
510
+ 'ON_QA'=>[],
511
+ 'CLOSED'=>[],
512
+ 'OPEN'=>[]
513
+ }
514
+ last_label = nil
515
+ real_do(argv, opts) do |t, new, assigned, modified, on_qa, closed, open|
516
+ printf("%s, new: %d, assigned: %d, modified %d, on_qa %d, closed %d / open %d\n",
517
+ opts[:command][:metrics][:x_coordinate] == :weekly ? sprintf("week %d", Date.new(t.year, t.month, t.day).cweek) : t.strftime("%Y-%m"), new, assigned, modified, on_qa, closed, open)
518
+ data['NEW'] << new
519
+ data['ASSIGNED'] << assigned
520
+ data['MODIFIED'] << modified
521
+ data['ON_QA'] << on_qa
522
+ data['CLOSED'] << closed
523
+ data['OPEN'] << open
524
+ label = t.strftime("%Y/%m")
525
+ if last_label != label then
526
+ data[:label] << label
527
+ last_label = label
528
+ else
529
+ data[:label] << nil
530
+ end
531
+ end
532
+
533
+ timeline = data[:label]
534
+ data.delete(:label)
535
+ def timeline.to_hash
536
+ ret = {}
537
+ (0..self.length-1).each do |i|
538
+ ret[i] = self[i] unless self[i].nil?
539
+ end
540
+ ret
541
+ end # def timeline.to_hash
542
+
543
+ # output the trend graph
544
+ g = Gruff::Line.new
545
+ g.title = sprintf("Trend: %s", opts[:command][:metrics][:title])
546
+ g.labels = timeline.to_hash
547
+ data.each do |k, v|
548
+ next unless k == 'NEW' || k == 'OPEN' || k == 'CLOSED'
549
+ g.data(k, v)
550
+ end
551
+ g.write(sprintf("trend-%s", opts[:command][:metrics][:output]))
552
+
553
+ # output the activity graph
554
+ g = Gruff::StackedBar.new
555
+ g.title = sprintf("Activity: %s", opts[:command][:metrics][:title])
556
+ g.labels = timeline.to_hash
557
+ g.data('Resolved', data['CLOSED'])
558
+ x = []
559
+ (0..data['ASSIGNED'].length-1).each do |i|
560
+ x[i] = data['ASSIGNED'][i] + data['MODIFIED'][i] + data['ON_QA'][i]
561
+ end
562
+ g.data('Unresolved', x)
563
+ a = []
564
+ (0..data['OPEN'].length-1).each do |i|
565
+ a[i] = data['OPEN'][i] - x[i]
566
+ end
567
+ g.data('non-activity bugs', a)
568
+ g.write(sprintf("activity-%s", opts[:command][:metrics][:output]))
569
+ end # def do
570
+
571
+ private
572
+
573
+ def real_do(argv, opts)
574
+ conf = read_config(opts)
575
+ conf.freeze
576
+ argv.each do |prefix|
577
+ unless conf.include?(prefix) then
578
+ raise RuntimeError, sprintf("No host information for %s", prefix)
579
+ end
580
+
581
+ info = conf[prefix]
582
+ uri = URI.parse(info[:URL])
583
+ host = uri.host
584
+ port = uri.port
585
+ path = uri.path.empty? ? nil : uri.path
586
+ login = opts[:command][:anonymous] == true ? nil : info[:User]
587
+ pass = opts[:command][:anonymous] == true ? nil : info[:Password]
588
+
589
+ xmlrpc = Bugzilla::XMLRPC.new(host, port, path)
590
+ user = Bugzilla::User.new(xmlrpc)
591
+ user.session(login, pass) do
592
+ bug = Bugzilla::Bug.new(xmlrpc)
593
+
594
+ opts[:command][:query][:product].map! {|x| info.include?(:ProductAliases) && info[:ProductAliases].include?(x) ? info[:ProductAliases][x] : x} if opts[:command][:query].include?(:product)
595
+
596
+ ts = opts[:command][:metrics][:begin_date] || Time.utc(Time.new.year, 1, 1)
597
+ te = opts[:command][:metrics][:end_date] || Time.utc(Time.new.year+1, 1, 1) - 1
598
+ if opts[:command][:metrics][:x_coordinate] == :weekly then
599
+ # align to the week
600
+ d = Date.new(ts.year, ts.month, ts.day)
601
+ ds = Date.commercial(d.year, d.cweek, 1)
602
+ d = Date.new(te.year, te.month, te.day)
603
+ de = Date.commercial(d.year, d.cweek, 7)
604
+ ts = Time.utc(ds.year, ds.month, ds.day)
605
+ te = Time.utc(de.year, de.month, de.day)
606
+ end
607
+
608
+ searchopts = opts[:command][:query].clone
609
+
610
+ @plugin.run(:pre, host, :metrics, searchopts, opts[:metrics])
611
+
612
+ if searchopts == opts[:command][:query] then
613
+ raise NoMethodError, "No method to deal with the query"
614
+ end
615
+
616
+ while ts < te do
617
+ searchopts = opts[:command][:query].clone
618
+
619
+ # don't rely on the status to deal with NEW bugs.
620
+ # unable to estimate the case bugs closed quickly
621
+ if opts[:command][:metrics][:x_coordinate] == :weekly then
622
+ d = Date.new(ts.year, ts.month, ts.day)
623
+ de = Date.commercial(d.year, d.cweek, 7)
624
+ drange = [ts, Time.utc(de.year, de.month, de.day, 23, 59, 59)]
625
+ else
626
+ drange = [ts, Time.utc(ts.year, ts.month + 1, 1) - 1]
627
+ end
628
+ searchopts[:creation_time] = drange
629
+
630
+ @plugin.run(:pre, host, :metrics, searchopts)
631
+
632
+ result = bug.search(searchopts)
633
+
634
+ @plugin.run(:post, host, :search, result)
635
+
636
+ new = result.include?('bugs') ? result['bugs'].length : 0
637
+
638
+ # for open bugs
639
+ # what we are interested in here would be how many bugs still keeps open.
640
+ searchopts = opts[:command][:query].clone
641
+ searchopts[:last_change_time] = drange
642
+ searchopts[:status] = '__open__'
643
+
644
+ @plugin.run(:pre, host, :metrics, searchopts)
645
+
646
+ result = bug.search(searchopts)
647
+
648
+ @plugin.run(:post, host, :search, result)
649
+
650
+ assigned = result.include?('bugs') ? result['bugs'].map{|x| x['status'] == 'ASSIGNED' ? x : nil}.compact.length : 0
651
+ modified = result.include?('bugs') ? result['bugs'].map{|x| x['status'] == 'MODIFIED' ? x : nil}.compact.length : 0
652
+ on_qa = result.include?('bugs') ? result['bugs'].map{|x| x['status'] == 'ON_QA' ? x : nil}.compact.length : 0
653
+
654
+ # send a separate query for closed.
655
+ # just counting CLOSED the above is meaningless.
656
+ # what we are interested in here would be how much bugs are
657
+ # actually closed, but not how many closed bugs one worked on.
658
+ searchopts = opts[:command][:query].clone
659
+ searchopts[:last_change_time] = drange
660
+ searchopts[:status] = 'CLOSED'
661
+
662
+ @plugin.run(:pre, host, :metrics, searchopts)
663
+
664
+ result = bug.search(searchopts)
665
+
666
+ @plugin.run(:post, host, :search, result)
667
+
668
+ closed = result.include?('bugs') ? result['bugs'].length : 0
669
+
670
+ # obtain the information for open bugs that closed now
671
+ searchopts = opts[:command][:query].clone
672
+ searchopts[:status] = 'CLOSED'
673
+ searchopts[:metrics_closed_after] = drange[1] + 1
674
+
675
+ @plugin.run(:pre, host, :metrics, searchopts)
676
+
677
+ result = bug.search(searchopts)
678
+
679
+ @plugin.run(:post, host, :search, result)
680
+
681
+ open_bugs = result.include?('bugs') ? result['bugs'].length : 0
682
+
683
+ # obtain the information for open bugs
684
+ searchopts = opts[:command][:query].clone
685
+ searchopts[:metrics_not_closed] = drange[1]
686
+
687
+ @plugin.run(:pre, host, :metrics, searchopts)
688
+
689
+ result = bug.search(searchopts)
690
+
691
+ @plugin.run(:post, host, :search, result)
692
+
693
+ open_bugs += result.include?('bugs') ? result['bugs'].length : 0
694
+
695
+ yield ts, new, assigned, modified, on_qa, closed, open_bugs
696
+
697
+ ts = drange[1] + 1
698
+ end
699
+ end
700
+ end
701
+ end # def real_do
702
+
703
+ end # class Metrics
704
+
705
+ end # module BzConsole
706
+
707
+ begin
708
+ opts = {}
709
+ opts[:command] = {}
710
+ subargv = []
711
+
712
+ o = ARGV.options do |opt|
713
+ opt.banner = sprintf("Usage: %s [global options] <command> ...", File.basename(__FILE__))
714
+ opt.separator ""
715
+ opt.separator "Global options:"
716
+ opt.on('-c', '--config=FILE', 'read FILE as the configuration file.') {|v| opts[:config] = v}
717
+ opt.on('-h', '--help', 'show this message') {|x| opts[:help] = true}
718
+
719
+ cmds = BzConsole.constants.sort.map {|x| (k = eval("BzConsole::#{x}")).class == Class && x != "CommandTemplate" ? x.downcase : nil}.compact
720
+
721
+ subargv = opt.order(ARGV);
722
+
723
+ command = subargv[0]
724
+
725
+ if subargv.length > 0 then
726
+ n = cmds.index(command)
727
+ unless n.nil? then
728
+ opts[:instance] = eval("BzConsole::#{cmds[n].capitalize}.new(Bugzilla::Plugin::Template.new)")
729
+ subargv = opts[:instance].parse(opt, subargv[1..-1], opts[:command])
730
+ else
731
+ STDERR.printf("E: Unknown command: %s\n", subargv[0])
732
+ STDERR.printf(" Available commands: %s\n", cmds.join(' '))
733
+ exit 1
734
+ end
735
+ else
736
+ opt.separator ""
737
+ opt.separator "Available commands:"
738
+ opt.separator sprintf(" %s", cmds.join(' '))
739
+ end
740
+
741
+ if opts[:instance].nil? && subargv.length == 0 ||
742
+ opts[:help] == true ||
743
+ subargv.length < opts[:instance].n_args then
744
+ puts opt.help
745
+ exit
746
+ end
747
+ end
748
+
749
+ opts[:instance].do(subargv, opts)
750
+ end