ruby-bugzilla 0.1

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 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