ruby-bugzilla 0.3.2 → 0.3.3

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