ruby-bugzilla 0.1

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