bzconsole 0.1.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d1c3740839611c195fc87c3e101d90c48673a5ab
4
+ data.tar.gz: 560eb412d950c11df54e09da47997ed3728b6475
5
+ SHA512:
6
+ metadata.gz: b49b1db604b77034d6c119be9d59e371614d47849259c5bf9e2cdb6c726f8ece0142c1ef1ea6b5effd761c5a6cc9416f66daebf869f18ae426e420bab3cd0285
7
+ data.tar.gz: ecc94454418d35c59acc64c6878852085c5a29b05f85cc2a633a48bd52a04c2ce6cfbf007f7d7df1084428123c85cd4547aecdd0aef15693af638c3b31235dfa
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.0
5
+ before_install: gem install bundler -v 1.16.1
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at vpereirabr@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in bzconsole.gemspec
6
+ gemspec
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ bzconsole (0.1.0)
5
+ bugzilla
6
+ gruff (~> 0)
7
+ highline
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ bugzilla (0.10.0)
13
+ gruff (~> 0)
14
+ highline
15
+ xmlrpc (~> 0.3.0)
16
+ gruff (0.7.0)
17
+ rmagick (~> 2.13, >= 2.13.4)
18
+ highline (1.7.10)
19
+ minitest (5.11.3)
20
+ rake (10.5.0)
21
+ rmagick (2.16.0)
22
+ xmlrpc (0.3.0)
23
+
24
+ PLATFORMS
25
+ ruby
26
+
27
+ DEPENDENCIES
28
+ bundler (~> 1.16)
29
+ bzconsole!
30
+ minitest (~> 5.0)
31
+ rake (~> 10.0)
32
+
33
+ BUNDLED WITH
34
+ 1.16.1
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Victor Pereira
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,43 @@
1
+ # Bzconsole
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/bzconsole`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'bzconsole'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install bzconsole
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/bzconsole. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
40
+
41
+ ## Code of Conduct
42
+
43
+ Everyone interacting in the Bzconsole project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/bzconsole/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,969 @@
1
+ #! /usr/bin/env ruby
2
+ # bzconsole
3
+ # Copyright (C) 2010-2014 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 3 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
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
+
21
+ require 'optparse'
22
+ require 'time'
23
+ require 'yaml'
24
+ require_relative '../lib/bzconsole'
25
+
26
+
27
+ module BzConsole
28
+ class Setup < CommandTemplate
29
+ def initialize(plugin)
30
+ super
31
+ @n_args = 0
32
+ end # def initialize
33
+
34
+ def parse(parser, argv, opts)
35
+ parser.banner = format('Usage: %s [global options] setup', File.basename(__FILE__))
36
+ parser.separator ''
37
+ parser.separator 'Command options:'
38
+
39
+ super
40
+ end # def parse
41
+
42
+ def do(_argv, opts)
43
+ template = {
44
+ 'rhbz' => {
45
+ Name: 'Red Hat Bugzilla',
46
+ URL: 'https://bugzilla.redhat.com',
47
+ User: 'account@example.com',
48
+ Password: 'blahblahblah',
49
+ ProductAliases: {
50
+ 'RHEL3' => 'Red Hat Enterprise Linux 3',
51
+ 'RHEL4' => 'Red Hat Enterprise Linux 4',
52
+ 'RHEL5' => 'Red Hat Enterprise Linux 5',
53
+ 'RHEL6' => 'Red Hat Enterprise Linux 6',
54
+ 'Security' => 'Security Response'
55
+ },
56
+ Plugin: 'plugins/rhbugzilla.rb'
57
+ },
58
+ 'gnbz' => {
59
+ Name: 'GNOME Bugzilla',
60
+ URL: 'https://bugzilla.gnome.org',
61
+ User: 'account@example.com',
62
+ Password: 'blahblahblah'
63
+ },
64
+ 'fdobz' => {
65
+ Name: 'FreeDesktop Bugzilla',
66
+ URL: 'https://bugs.freedesktop.org',
67
+ User: 'account@example.com',
68
+ Password: 'blahblahblah'
69
+ },
70
+ 'mzbz' => {
71
+ Name: 'Mozilla Bugzilla',
72
+ URL: 'https://bugzilla.mozilla.org',
73
+ User: 'account@example.com',
74
+ Password: 'blahblahblah'
75
+ },
76
+ 'susebz' => {
77
+ Name: 'SUSE Bugzilla',
78
+ URL: 'https://bugzilla.suse.com',
79
+ User: 'account@example.com',
80
+ Password: 'blahblahblah',
81
+ ProductAliases: {
82
+ 'Security' => 'SUSE Security Incidents'
83
+ },
84
+ Plugin: 'plugins/nvbugzilla.rb'
85
+ }
86
+ }
87
+
88
+ template.each do |_k, v|
89
+ @plugin.run(:pre, v[:URL].sub(/\Ahttps?:\/\//, '').sub(/\/\Z/, ''), :setup, v)
90
+ end
91
+
92
+ fname = opts[:config].nil? ? @defaultyamlfile : opts[:config]
93
+ if File.exist?(fname)
94
+ raise '.bzconsole.yml already exists. please remove it first to proceed to the next step.'
95
+ end
96
+ File.open(fname, File::CREAT | File::WRONLY, 0o600) do |f|
97
+ f.write(template.to_yaml)
98
+ end
99
+ printf("%s has been created. please modify your account information before operating.\n", fname)
100
+ end # def do
101
+ end # class Setup
102
+
103
+ class Login < CommandTemplate
104
+ def initialize(plugin)
105
+ super
106
+ @n_args = 1
107
+ end # def initialize
108
+
109
+ def parse(parser, argv, opts)
110
+ opts[:output] = File.join(ENV['HOME'], '.ruby-bugzilla-cookie.yml')
111
+ parser.banner = format('Usage: %s [global options] login [command options] <prefix> ...', File.basename(__FILE__))
112
+ parser.separator ''
113
+ parser.separator 'Command options:'
114
+ parser.on('-o', '--output=FILE', 'FILE to store the cookie') { |v| opts[:output] = v }
115
+
116
+ super
117
+ end # def parse
118
+
119
+ def do(argv, opts)
120
+ conf = read_config(opts)
121
+ conf.freeze
122
+ cconf = read_config(config: opts[:command][:output])
123
+ argv.each do |prefix|
124
+ raise ArgumentError, 'No prefix to log in' if prefix.nil?
125
+ unless conf.include?(prefix)
126
+ raise format('No host information for %s', prefix)
127
+ end
128
+
129
+ info = conf[prefix]
130
+ login = info[:User].nil? ? ask('Bugzilla ID: ') : info[:User]
131
+ pass = info[:Password].nil? ? ask('Bugzilla password: ') { |q| q.echo = false } : info[:Password]
132
+
133
+ xmlrpc, host = get_xmlrpc(conf[prefix], opts)
134
+ user = Bugzilla::User.new(xmlrpc)
135
+
136
+ begin
137
+ result = user.login('login' => login, 'password' => pass, 'remember' => true)
138
+ cconf[host] = xmlrpc.cookie
139
+ save_config({config: opts[:command][:output] }, cconf)
140
+ rescue XMLRPC::FaultException => e
141
+ printf("E: %s\n", e.message)
142
+ end
143
+ end
144
+ end # def do
145
+ end # class Login
146
+
147
+ class Getbug < CommandTemplate
148
+ def initialize(plugin)
149
+ super
150
+
151
+ @n_args = 1
152
+ end # def initialize
153
+
154
+ def parse(parser, argv, opts)
155
+ parser.banner = format('Usage: %s [global options] getbug [command options] <prefix:bug number> ...', File.basename(__FILE__))
156
+ parser.separator ''
157
+ parser.separator 'Command options:'
158
+ parser.on('-s', '--summary', 'Displays bugs summary only') { opts[:summary] = true }
159
+ parser.on('-d', '--details', 'Displays detailed bugs information') { opts[:details] = true }
160
+ parser.on('-a', '--all', 'Displays the whole data in bugs') { opts[:all] = true }
161
+ parser.on('--anonymous', 'Access to Bugzilla anonymously') { opts[:anonymous] = true }
162
+
163
+ super
164
+ end # def parse
165
+
166
+ def do(argv, opts)
167
+ real_do(argv, opts) do |result|
168
+ if opts[:command][:summary] == true
169
+ printf("Bug#%s, %s, %s[%s, %s] %s\n",
170
+ result['id'],
171
+ result['product'],
172
+ result['component'],
173
+ result['status'],
174
+ result['severity'],
175
+ result['summary'])
176
+ elsif opts[:command][:details] == true
177
+ printf("Bug#%s, %s, %s, %s[%s, %s, %s, %s] %s\n",
178
+ result['id'],
179
+ result['product'],
180
+ result['assigned_to'],
181
+ result['component'],
182
+ result['status'],
183
+ result['resolution'],
184
+ result['priority'],
185
+ result['severity'],
186
+ result['summary'])
187
+ else
188
+ printf("Bug#%s - %s\n", result['id'], result['summary'])
189
+ printf("Status:\t\t%s%s\n", result['status'], !result['resolution'].empty? ? format('[%s]', result['resolution']) : '')
190
+ printf("Product:\t%s\n", result['product'])
191
+ printf("Version:\t%s\n", result['version'])
192
+ printf("Component:\t%s\n", result['component'])
193
+ printf("Assinged To:\t%s\n", result['assigned_to'])
194
+ printf("Priority:\t%s\n", result['priority'])
195
+ printf("Severity:\t%s\n", result['severity'])
196
+ result.keys.reject { |x| %w[id summary status resolution product version component assigned_to priority severity comments].include?(x) }.each do |x|
197
+ printf("%s:\t%s\n", x.capitalize, result[x].respond_to?(:to_time) ? result[x].to_time : result[x])
198
+ end
199
+ printf("Comments:\t%d\n\n", result['comments'].length)
200
+ i = 0
201
+ result['comments'].each do |c|
202
+ printf("Comment#%d%s %s %s\n", i, c['is_private'] == true ? '[private]' : '', c['creator'], c['creation_time'].to_time)
203
+ printf("\n %s\n\n", c['text'].split("\n").join("\n "))
204
+ i += 1
205
+ end
206
+ end
207
+ end
208
+ end # def do
209
+
210
+ private
211
+
212
+ def real_do(argv, opts)
213
+ conf = read_config(opts)
214
+ conf.freeze
215
+ argv.each do |bugn|
216
+ bugn =~ /(.*):(.*)/
217
+ prefix = Regexp.last_match(1)
218
+ nnn = Regexp.last_match(2)
219
+ if prefix.nil?
220
+ raise ArgumentError, format('No prefix specified for Bug#%s', bugn)
221
+ end
222
+ unless conf.include?(prefix)
223
+ raise format('No host information for %s', prefix)
224
+ end
225
+
226
+ info = conf[prefix]
227
+ if opts[:command][:anonymous] == true
228
+ login = nil
229
+ pass = nil
230
+ else
231
+ login = info[:User].nil? ? ask('Bugzilla ID: ') : info[:User]
232
+ pass = info[:Password].nil? ? ask('Bugzilla password: ') { |q| q.echo = false } : info[:Password]
233
+ end
234
+
235
+ xmlrpc, host = get_xmlrpc(conf[prefix], opts) do |h|
236
+ @plugin.run(:pre, h, :getbug, opts)
237
+ end
238
+
239
+ user = Bugzilla::User.new(xmlrpc)
240
+ user.session(login, pass) do
241
+ bug = Bugzilla::Bug.new(xmlrpc)
242
+
243
+ result = nil
244
+ result = if opts[:command][:summary] == true
245
+ bug.get_bugs(nnn.split(','))
246
+ elsif opts[:command][:details] == true
247
+ bug.get_bugs(nnn.split(','), ::Bugzilla::Bug::FIELDS_DETAILS)
248
+ else
249
+ 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
+ end # class Getbug
261
+
262
+ class Search < CommandTemplate
263
+ def initialize(plugin)
264
+ super
265
+
266
+ @n_args = 1
267
+ end # def initialize
268
+
269
+ def parse(parser, argv, opts)
270
+ opts[:query] ||= {}
271
+ parser.banner = format('Usage: %s [global options] search [command options] <prefix> ...', File.basename(__FILE__))
272
+ parser.separator ''
273
+ parser.separator 'Search options:'
274
+ parser.on('--alias=ALIASES', 'filter out the result by the alias') { |v| opts[:query][:alias] ||= []; opts[:query][:alias].push(*v.split(',')) }
275
+ 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(',')) }
276
+ parser.on('--bug=BUGS', 'filter out the result by the specific bug number') { |v| opts[:query][:id] ||= []; opts[:query][:id].push(*v.split(',')) }
277
+ parser.on('-c', '--component=COMPONENTS', 'filter out the result by the specific components') { |v| opts[:query][:component] ||= []; opts[:query][:component].push(*v.split(',')) }
278
+ parser.on('--create-time=TIME', 'Searches for bugs that were created at this time or later') { |v| opts[:query][:creation_time] = Time.parse(v) }
279
+ 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(',')) }
280
+ 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) }
281
+ 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(',')) }
282
+ parser.on('--platform=PLATFORMS', 'filter out the result by the platform') { |v| opts[:query][:platform] ||= []; opts[:query][:platform].push(*v.split(',')) }
283
+ parser.on('--priority=PRIORITY', 'filter out the result by the priority') { |v| opts[:query][:priority] ||= []; opts[:query][:priority].push(*v.split(',')) }
284
+ parser.on('-p', '--product=PRODUCTS', 'filter out the result by the specific products') { |v| opts[:query][:product] ||= []; opts[:query][:product].push(*v.split(',')) }
285
+ parser.on('--resolution=RESOLUTIONS', 'filter out the result by the resolutions') { |v| opts[:query][:resolution] ||= []; opts[:query][:resolution].push(*v.split(',')) }
286
+ parser.on('--severity=SEVERITY', 'filter out the result by the severity') { |v| opts[:query][:severity] ||= []; opts[:query][:severity].push(*v.split(',')) }
287
+ parser.on('-s', '--status=STATUSES', 'filter out the result by the status') { |v| opts[:query][:status] ||= []; opts[:query][:status].push(*v.split(',')) }
288
+ parser.on('--summary=SUMMARY', 'filter out the result by the summary') { |v| opts[:query][:summary] ||= []; opts[:query][:summary] << v }
289
+ parser.on('--milestone=MILESTONE', 'filter out the result by the target milestone') { |v| opts[:query][:target_milestone] ||= []; opts[:query][:target_milestone].push(*v.split(',')) }
290
+ parser.on('--whiteboard=STRING', 'filter out the result by the specific words in the status whiteboard') { |v| opts[:query][:whiteboard] ||= []; opts[:query][:whiteboard] << v }
291
+ parser.separator ''
292
+ parser.separator 'Command options:'
293
+ parser.on('--short-list', 'Displays bugs summary only') { opts[:summary] = true }
294
+ parser.on('--detailed-list', 'Displays detailed bugs information') { opts[:details] = true }
295
+ parser.on('--anonymous', 'Access to Bugzilla anonymously') { opts[:anonymous] = true }
296
+
297
+ super
298
+ end # def parse
299
+
300
+ def do(argv, opts)
301
+ c = 0
302
+ real_do(argv, opts) do |result|
303
+ if opts[:command][:summary] == true
304
+ printf("Bug#%s, %s, %s[%s, %s] %s\n",
305
+ result['id'] || result['bug_id'],
306
+ result['product'],
307
+ result['component'],
308
+ result['status'],
309
+ result['severity'],
310
+ result['summary'])
311
+ elsif opts[:command][:details] == true
312
+ printf("Bug#%s, %s, %s, %s[%s, %s, %s, %s] %s\n",
313
+ result['id'],
314
+ result['product'],
315
+ result['assigned_to'],
316
+ result['component'],
317
+ result['status'],
318
+ result['resolution'],
319
+ result['priority'],
320
+ result['severity'],
321
+ result['summary'])
322
+ end
323
+ c += 1
324
+ end
325
+ printf("\n%d bug(s) found\n", c)
326
+ end # def do
327
+
328
+ private
329
+
330
+ def real_do(argv, opts)
331
+ conf = read_config(opts)
332
+ conf.freeze
333
+ argv.each do |prefix|
334
+ unless conf.include?(prefix)
335
+ raise format('No host information for %s', prefix)
336
+ end
337
+
338
+ info = conf[prefix]
339
+ uri = URI.parse(info[:URL])
340
+ host = uri.host
341
+ port = uri.port
342
+ path = uri.path.empty? ? nil : uri.path
343
+ if opts[:command][:anonymous] == true
344
+ login = nil
345
+ pass = nil
346
+ else
347
+ login = info[:User].nil? ? ask('Bugzilla ID: ') : info[:User]
348
+ pass = info[:Password].nil? ? ask('Bugzilla password: ') { |q| q.echo = false } : info[:Password]
349
+ end
350
+ proxy_host, proxy_port = get_proxy(info)
351
+ timeout = opts[:timeout].nil? ? 60 : opts[:timeout]
352
+
353
+ @plugin.run(:pre, host, :search, opts[:command][:query])
354
+ xmlrpc = Bugzilla::XMLRPC.new(host, port:port, path: path, proxy_host:
355
+ proxy_host, proxy_port: proxy_port, timeout:
356
+ timeout, http_basic_auth_user: uri.user, http_basic_auth_pass: uri.password, debug: opts[:debug])
357
+ user = Bugzilla::User.new(xmlrpc)
358
+ user.session(login, pass) do
359
+ bug = Bugzilla::Bug.new(xmlrpc)
360
+ opts[:command][:query][:product].map! do |x|
361
+ info.include?(:ProductAliases) &&
362
+ info[:ProductAliases].include?(x) ? info[:ProductAliases][x] : x end if opts[:command][:query].include?(:product)
363
+
364
+ result = bug.search(opts[:command][:query])
365
+
366
+ @plugin.run(:post, host, :search, result)
367
+
368
+ if result.include?('bugs')
369
+ result['bugs'].each do |r|
370
+ yield r
371
+ end
372
+ end
373
+ end
374
+ end
375
+ end # def real_do
376
+ end # class Search
377
+
378
+ class Show < CommandTemplate
379
+ def initialize(plugin)
380
+ super
381
+
382
+ @n_args = 1
383
+ end # def initialize
384
+
385
+ def parse(parser, argv, opts)
386
+ opts[:show] ||= {}
387
+ opts[:show][:mode] = :component
388
+ opts[:show][:field] = []
389
+ parser.banner = format('Usage: %s [global options] show [command options] <prefix> ...', File.basename(__FILE__))
390
+ parser.separator ''
391
+ parser.separator 'Command options:'
392
+ parser.on('-f', '--field', 'Displays available field informations') { |_v| opts[:show][:mode] = :field }
393
+ parser.on('--field-name=NAME', 'Displays NAME field information') { |v| opts[:show][:field] << v.split(',') }
394
+ parser.on('-p', '--product', 'Displays available product names') { |_v| opts[:show][:mode] = :product }
395
+ parser.on('-c', '--component', 'Displays available component names (default)') { |_v| opts[:show][:mode] = :component }
396
+ parser.on('--anonymous', 'Access to Bugzilla anonymously') { opts[:anonymous] = true }
397
+
398
+ super
399
+ end # def parse
400
+
401
+ def do(argv, opts)
402
+ real_do(argv, opts) do |*result|
403
+ if opts[:command][:show][:mode] == :product
404
+ printf("%s\n", result[0])
405
+ elsif opts[:command][:show][:mode] == :component
406
+ printf("%s:\n", result[0])
407
+ printf(" %s\n", result[1].join("\n "))
408
+ end
409
+ end
410
+ end # def do
411
+
412
+ private
413
+
414
+ def real_do(argv, opts)
415
+ conf = read_config(opts)
416
+ conf.freeze
417
+ argv.each do |prefix|
418
+ unless conf.include?(prefix)
419
+ raise format('No host information for %s', prefix)
420
+ end
421
+
422
+ info = conf[prefix]
423
+ uri = URI.parse(info[:URL])
424
+ host = uri.host
425
+ port = uri.port
426
+ path = uri.path.empty? ? nil : uri.path
427
+ if opts[:command][:anonymous] == true
428
+ login = nil
429
+ pass = nil
430
+ else
431
+ login = info[:User].nil? ? ask('Bugzilla ID: ') : info[:User]
432
+ pass = info[:Password].nil? ? ask('Bugzilla password: ') { |q| q.echo = false } : info[:Password]
433
+ end
434
+ proxy_host, proxy_port = get_proxy(info)
435
+ timeout = opts[:timeout].nil? ? 60 : opts[:timeout]
436
+
437
+ @plugin.run(:pre, host, :show, opts)
438
+
439
+ xmlrpc = Bugzilla::XMLRPC.new(host, port:port, path: path, proxy_host:
440
+ proxy_host, proxy_port: proxy_port, timeout:
441
+ timeout, http_basic_auth_user: uri.user, http_basic_auth_pass: uri.password, debug: opts[:debug])
442
+ user = Bugzilla::User.new(xmlrpc)
443
+ user.session(login, pass) do
444
+ if opts[:command][:show][:mode] == :field
445
+ bug = Bugzilla::Bug.new(xmlrpc)
446
+
447
+ result = bug.fields(opts[:command][:show][:field].flatten)
448
+
449
+ @plugin.run(:post, host, :show, result)
450
+
451
+ else
452
+ product = Bugzilla::Product.new(xmlrpc)
453
+
454
+ result = product.selectable_products
455
+
456
+ @plugin.run(:post, host, :show, result)
457
+
458
+ products = result.keys.sort
459
+ products.each do |p|
460
+ if opts[:command][:show][:mode] == :product
461
+ yield p
462
+ elsif opts[:command][:show][:mode] == :component
463
+ yield p, result[p][0].sort
464
+ end
465
+ end
466
+ end
467
+ end
468
+ end
469
+ end # def real_do
470
+ end # class Show
471
+
472
+ class Metrics < CommandTemplate
473
+ def initialize(plugin)
474
+ super
475
+
476
+ @n_args = 1
477
+ end # def initialize
478
+
479
+ def parse(parser, argv, opts)
480
+ opts[:metrics] = {}
481
+ opts[:query] = {}
482
+ opts[:metrics][:output] = 'bz-metrics.png'
483
+ opts[:metrics][:x_coordinate] = :monthly
484
+
485
+ parser.banner = format('Usage: %s [global options] metrics [command options] <prefix> ...', File.basename(__FILE__))
486
+ parser.separator ''
487
+ parser.separator 'Search options:'
488
+ parser.on('--alias=ALIASES', 'filter out the result by the alias') { |v| opts[:query][:alias] ||= []; opts[:query][:alias].push(*v.split(',')) }
489
+ 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(',')) }
490
+ parser.on('--bug=BUGS', 'filter out the result by the specific bug number') { |v| opts[:query][:id] ||= []; opts[:query][:id].push(*v.split(',')) }
491
+ parser.on('-c', '--component=COMPONENTS', 'filter out the result by the specific components') { |v| opts[:query][:component] ||= []; opts[:query][:component].push(*v.split(',')) }
492
+ 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(',')) }
493
+ 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(',')) }
494
+ parser.on('--platform=PLATFORMS', 'filter out the result by the platform') { |v| opts[:query][:platform] ||= []; opts[:query][:platform].push(*v.split(',')) }
495
+ parser.on('--priority=PRIORITY', 'filter out the result by the priority') { |v| opts[:query][:priority] ||= []; opts[:query][:priority].push(*v.split(',')) }
496
+ parser.on('-p', '--product=PRODUCTS', 'filter out the result by the specific products') { |v| opts[:query][:product] ||= []; opts[:query][:product].push(*v.split(',')) }
497
+ parser.on('--resolution=RESOLUSIONS', 'filter out the result by the resolusions') { |v| opts[:query][:resolution] ||= []; opts[:query][:resolusion].push(*v.split(',')) }
498
+ parser.on('--severity=SEVERITY', 'filter out the result by the severity') { |v| opts[:query][:severity] ||= []; opts[:query][:severity].push(*v.split(',')) }
499
+ parser.on('--summary=SUMMARY', 'filter out the result by the summary') { |v| opts[:query][:summary] ||= []; opts[:query][:summary] << v }
500
+ parser.on('--milestone=MILESTONE', 'filter out the result by the target milestone') { |v| opts[:query][:target_milestone] ||= []; opts[:query][:target_milestone].push(*v.split(',')) }
501
+ parser.on('--whiteboard=STRING', 'filter out the result by the specific words in the status whiteboard') { |v| opts[:query][:whiteboard] ||= []; opts[:query][:whiteboard] << v }
502
+ parser.separator ''
503
+ parser.separator 'Command options:'
504
+ parser.on('-t', '--title=TITLE', 'add TITLE to the graph') { |v| opts[:metrics][:title] = v }
505
+ 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) }
506
+ 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 }
507
+ parser.on('-o', '--output=FILE', 'generate the graph to FILE') { |v| opts[:metrics][:output] = v }
508
+ parser.on('--anonymous', 'access to Bugzilla anonymously') { |_v| opts[:anonymous] = true }
509
+ parser.on('--weekly', 'genereate the graph with weekly X-coordinate') { |_v| opts[:metrics][:x_coordinate] = :weekly }
510
+ parser.on('--monthly', 'genereate the graph with monthly X-coordinate (default)') { |_v| opts[:metrics][:x_coordinate] = :monthly }
511
+
512
+ super
513
+ end # def parse
514
+
515
+ def do(argv, opts)
516
+ data = {
517
+ :label => [],
518
+ 'NEW' => [],
519
+ 'ASSIGNED' => [],
520
+ 'MODIFIED' => [],
521
+ 'ON_QA' => [],
522
+ 'CLOSED' => [],
523
+ 'OPEN' => []
524
+ }
525
+ last_label = nil
526
+ real_do(argv, opts) do |t, new, assigned, modified, on_qa, closed, open|
527
+ printf("%s, new: %d, assigned: %d, modified %d, on_qa %d, closed %d / open %d\n",
528
+ opts[:command][:metrics][:x_coordinate] == :weekly ? format('week %d', Date.new(t.year, t.month, t.day).cweek) : t.strftime('%Y-%m'), new, assigned, modified, on_qa, closed, open)
529
+ data['NEW'] << new
530
+ data['ASSIGNED'] << assigned
531
+ data['MODIFIED'] << modified
532
+ data['ON_QA'] << on_qa
533
+ data['CLOSED'] << closed
534
+ data['OPEN'] << open
535
+ label = t.strftime('%Y/%m')
536
+ if last_label != label
537
+ data[:label] << label
538
+ last_label = label
539
+ else
540
+ data[:label] << nil
541
+ end
542
+ end
543
+
544
+ timeline = data[:label]
545
+ data.delete(:label)
546
+ def timeline.to_hash
547
+ ret = {}
548
+ (0..length - 1).each do |i|
549
+ ret[i] = self[i] unless self[i].nil?
550
+ end
551
+ ret
552
+ end # def timeline.to_hash
553
+
554
+ # output the trend graph
555
+ g = Gruff::Line.new
556
+ g.title = format('Trend: %s', opts[:command][:metrics][:title])
557
+ g.labels = timeline.to_hash
558
+ data.each do |k, v|
559
+ next unless k == 'NEW' || k == 'OPEN' || k == 'CLOSED'
560
+ g.data(k, v)
561
+ end
562
+ g.write(format('trend-%s', opts[:command][:metrics][:output]))
563
+
564
+ # output the activity graph
565
+ g = Gruff::StackedBar.new
566
+ g.title = format('Activity: %s', opts[:command][:metrics][:title])
567
+ g.labels = timeline.to_hash
568
+ g.data('Resolved', data['CLOSED'])
569
+ x = []
570
+ (0..data['ASSIGNED'].length - 1).each do |i|
571
+ x[i] = data['ASSIGNED'][i] + data['MODIFIED'][i] + data['ON_QA'][i]
572
+ end
573
+ g.data('Unresolved', x)
574
+ a = []
575
+ (0..data['OPEN'].length - 1).each do |i|
576
+ a[i] = data['OPEN'][i] - x[i]
577
+ end
578
+ g.data('non-activity bugs', a)
579
+ g.write(format('activity-%s', opts[:command][:metrics][:output]))
580
+ end # def do
581
+
582
+ private
583
+
584
+ def real_do(argv, opts)
585
+ conf = read_config(opts)
586
+ conf.freeze
587
+ argv.each do |prefix|
588
+ unless conf.include?(prefix)
589
+ raise format('No host information for %s', prefix)
590
+ end
591
+
592
+ info = conf[prefix]
593
+ if opts[:command][:anonymous] == true
594
+ login = nil
595
+ pass = nil
596
+ else
597
+ login = info[:User].nil? ? ask('Bugzilla ID: ') : info[:User]
598
+ pass = info[:Password].nil? ? ask('Bugzilla password: ') { |q| q.echo = false } : info[:Password]
599
+ end
600
+
601
+ xmlrpc, host = get_xmlrpc(conf[prefix], opts)
602
+ user = Bugzilla::User.new(xmlrpc)
603
+ user.session(login, pass) do
604
+ bug = Bugzilla::Bug.new(xmlrpc)
605
+
606
+ opts[:command][:query][:product].map! { |x| info.include?(:ProductAliases) && info[:ProductAliases].include?(x) ? info[:ProductAliases][x] : x } if opts[:command][:query].include?(:product)
607
+
608
+ ts = opts[:command][:metrics][:begin_date] || Time.utc(Time.new.year, 1, 1)
609
+ te = opts[:command][:metrics][:end_date] || Time.utc(Time.new.year + 1, 1, 1) - 1
610
+ if opts[:command][:metrics][:x_coordinate] == :weekly
611
+ # align to the week
612
+ d = Date.new(ts.year, ts.month, ts.day)
613
+ ds = Date.commercial(d.year, d.cweek, 1)
614
+ d = Date.new(te.year, te.month, te.day)
615
+ de = Date.commercial(d.year, d.cweek, 7)
616
+ ts = Time.utc(ds.year, ds.month, ds.day)
617
+ te = Time.utc(de.year, de.month, de.day)
618
+ end
619
+
620
+ searchopts = opts[:command][:query].clone
621
+
622
+ @plugin.run(:pre, host, :metrics, searchopts, opts[:metrics])
623
+
624
+ raise NoMethodError, 'No method to deal with the query' if searchopts == opts[:command][:query]
625
+
626
+ while ts < te
627
+ searchopts = opts[:command][:query].clone
628
+
629
+ # don't rely on the status to deal with NEW bugs.
630
+ # unable to estimate the case bugs closed quickly
631
+ if opts[:command][:metrics][:x_coordinate] == :weekly
632
+ d = Date.new(ts.year, ts.month, ts.day)
633
+ de = Date.commercial(d.year, d.cweek, 7)
634
+ drange = [ts, Time.utc(de.year, de.month, de.day, 23, 59, 59)]
635
+ else
636
+ drange = [ts, Time.utc(ts.year, ts.month + 1, 1) - 1]
637
+ end
638
+
639
+ searchopts[:creation_time] = drange
640
+
641
+ @plugin.run(:pre, host, :metrics, searchopts)
642
+
643
+ result = bug.search(searchopts)
644
+
645
+ @plugin.run(:post, host, :search, result)
646
+
647
+ new = result.include?('bugs') ? result['bugs'].length : 0
648
+
649
+ # for open bugs
650
+ # what we are interested in here would be how many bugs still keeps open.
651
+ searchopts = opts[:command][:query].clone
652
+ searchopts[:last_change_time] = drange
653
+ searchopts[:status] = '__open__'
654
+
655
+ @plugin.run(:pre, host, :metrics, searchopts)
656
+
657
+ result = bug.search(searchopts)
658
+
659
+ @plugin.run(:post, host, :search, result)
660
+
661
+ assigned = result.include?('bugs') ? result['bugs'].map { |x| x['status'] == 'ASSIGNED' ? x : nil }.compact.length : 0
662
+ modified = result.include?('bugs') ? result['bugs'].map { |x| x['status'] == 'MODIFIED' ? x : nil }.compact.length : 0
663
+ on_qa = result.include?('bugs') ? result['bugs'].map { |x| x['status'] == 'ON_QA' ? x : nil }.compact.length : 0
664
+
665
+ # send a separate query for closed.
666
+ # just counting CLOSED the above is meaningless.
667
+ # what we are interested in here would be how much bugs are
668
+ # actually closed, but not how many closed bugs one worked on.
669
+ searchopts = opts[:command][:query].clone
670
+ searchopts[:last_change_time] = drange
671
+ searchopts[:status] = 'CLOSED'
672
+
673
+ @plugin.run(:pre, host, :metrics, searchopts)
674
+
675
+ result = bug.search(searchopts)
676
+
677
+ @plugin.run(:post, host, :search, result)
678
+
679
+ closed = result.include?('bugs') ? result['bugs'].length : 0
680
+
681
+ # obtain the information for open bugs that closed now
682
+ searchopts = opts[:command][:query].clone
683
+ searchopts[:status] = 'CLOSED'
684
+ searchopts[:metrics_closed_after] = drange[1] + 1
685
+
686
+ @plugin.run(:pre, host, :metrics, searchopts)
687
+
688
+ result = bug.search(searchopts)
689
+
690
+ @plugin.run(:post, host, :search, result)
691
+
692
+ open_bugs = result.include?('bugs') ? result['bugs'].length : 0
693
+
694
+ # obtain the information for open bugs
695
+ searchopts = opts[:command][:query].clone
696
+ searchopts[:metrics_not_closed] = drange[1]
697
+
698
+ @plugin.run(:pre, host, :metrics, searchopts)
699
+
700
+ result = bug.search(searchopts)
701
+
702
+ @plugin.run(:post, host, :search, result)
703
+
704
+ open_bugs += result.include?('bugs') ? result['bugs'].length : 0
705
+
706
+ yield ts, new, assigned, modified, on_qa, closed, open_bugs
707
+
708
+ ts = drange[1] + 1
709
+ end # while
710
+ end
711
+ end
712
+ end # def real_do
713
+ end # class Metrics
714
+
715
+ class Newbug < CommandTemplate
716
+ def initialize(plugin)
717
+ super
718
+
719
+ @n_args = 1
720
+ end # def initialize
721
+
722
+ def parse(parser, argv, opts)
723
+ opts[:newbug] = {}
724
+
725
+ parser.banner = format('Usage: %s [global options] newbug [command options] <prefix>', File.basename(__FILE__))
726
+ parser.separator ''
727
+ parser.separator 'Options:'
728
+ parser.on('-p', '--product=PRODUCT', 'The name of the product the bug is being filed against') { |v| opts[:newbug][:product] = v }
729
+ parser.on('-c', '--component=COMPONENT', 'The name of the component in PRODUCT') { |v| opts[:newbug][:component] = v }
730
+ parser.on('-s', '--summary=SUMMARY', 'A brief description of the bug being filed') { |v| opts[:newbug][:summary] = v }
731
+ parser.on('-v', '--version=VERSION', 'A version of PRODUCT that the bug was found in') { |v| opts[:newbug][:version] = v }
732
+ parser.on('-d', '--description=DESCRIPTION', 'The initial description for bug') { |v| opts[:newbug][:description] = v }
733
+ parser.on('--opsys=OPSYS', 'The operating system the bug was discovered on') { |v| opts[:newbug][:op_sys] = v }
734
+ parser.on('--platform=PLATFORM', 'What type of hardware the bug was experienced on') { |v| opts[:newbug][:platform] = v }
735
+ parser.on('--priority=PRIORITY', 'What order the bug will be fixed in by the developer') { |v| opts[:newbug][:priority] = v }
736
+ parser.on('--severity=SEVERITY', 'How severe the bug is') { |v| opts[:newbug][:severity] = v }
737
+ parser.on('--alias=ALIAS', 'A brief alias for the bug that can be used instead of a bug number') { |v| opts[:newbug][:alias] = v }
738
+ parser.on('--assigned_to=ASSGINEE', 'A user to assign the bug to') { |v| opts[:newbug][:assigned_to] = v }
739
+ parser.on('--comment_is_private', 'Make the description to private') { |_v| opts[:newbug][:comment_is_private] = true }
740
+ parser.on('--groups=GROUPS', 'The list of group names to put this bug into') { |v| opts[:newbug][:groups] = v.split(/,/) }
741
+ parser.on('--qacontact=USER', 'The QA concact to assign the bug to') { |v| opts[:newbug][:qa_contact] = v }
742
+ parser.on('--status=STATUS', 'The status that the bug should start out as') { |v| opts[:newbug][:status] = v }
743
+ parser.on('--resolution=RESOLUTION', 'Set the resolution if filing a closed bug') { |v| opts[:newbug][:resolution] = v }
744
+ parser.on('--targetmilestone=MILESTONE', 'A valid target milestone for PRODUCT') { |v| opts[:newbug][:target_milestone] = v }
745
+
746
+ super
747
+ end # def parse
748
+
749
+ def do(argv, opts)
750
+ real_do(argv, opts) do |res|
751
+ if res.include?('id')
752
+ printf("A bug has been filed as Bug#%s\n", res['id'])
753
+ else
754
+ p res
755
+ end
756
+ end
757
+ end # def do
758
+
759
+ private
760
+
761
+ def real_do(argv, opts)
762
+ conf = read_config(opts)
763
+ conf.freeze
764
+ # not supporting filing a bug to multiple bugzilla
765
+ prefix = argv[0]
766
+ unless conf.include?(prefix)
767
+ raise format('No host information for %s', prefix)
768
+ end
769
+
770
+ info = conf[prefix]
771
+ uri = URI.parse(info[:URL])
772
+ host = uri.host
773
+ port = uri.port
774
+ path = uri.path.empty? ? nil : uri.path
775
+ login = info[:User].nil? ? ask('Bugzilla ID: ') : info[:User]
776
+ pass = info[:Password].nil? ? ask('Bugzilla password: ') { |q| q.echo = false } : info[:Password]
777
+ proxy_host, proxy_port = get_proxy(info)
778
+ timeout = opts[:timeout].nil? ? 60 : opts[:timeout]
779
+
780
+ @plugin.run(:pre, host, :newbug, opts)
781
+
782
+ xmlrpc = Bugzilla::XMLRPC.new(host, port:port, path: path, proxy_host:
783
+ proxy_host, proxy_port: proxy_port, timeout:
784
+ timeout, http_basic_auth_user: uri.user, http_basic_auth_pass: uri.password, debug: opts[:debug])
785
+ user = Bugzilla::User.new(xmlrpc)
786
+ user.session(login, pass) do
787
+ bug = Bugzilla::Bug.new(xmlrpc)
788
+
789
+ result = bug.create(opts[:command][:newbug])
790
+
791
+ @plugin.run(:post, host, :newbug, result)
792
+
793
+ yield result
794
+ end
795
+ end # def real_do
796
+ end # class Newbug
797
+
798
+ class Responsetime < CommandTemplate
799
+ def initialize(plugin)
800
+ super
801
+ @n_args = 1
802
+ end # def initialize
803
+
804
+ def parse(parser, argv, opts)
805
+ opts[:responsetime] = {}
806
+ opts[:query] = {}
807
+ parser.banner = format('Usage: %s [global options] responsetime [command options] <prefix:bug number>...', File.basename(__FILE__))
808
+ parser.separator ''
809
+ parser.separator 'Search options:'
810
+ parser.on('--alias=ALIASES', 'filter out the result by the alias') { |v| opts[:query][:alias] ||= []; opts[:query][:alias].push(*v.split(',')) }
811
+ 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(',')) }
812
+ parser.on('--bug=BUGS', 'filter out the result by the specific bug number') { |v| opts[:query][:id] ||= []; opts[:query][:id].push(*v.split(',')) }
813
+ parser.on('-c', '--component=COMPONENTS', 'filter out the result by the specific components') { |v| opts[:query][:component] ||= []; opts[:query][:component].push(*v.split(',')) }
814
+ 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(',')) }
815
+ 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(',')) }
816
+ parser.on('--platform=PLATFORMS', 'filter out the result by the platform') { |v| opts[:query][:platform] ||= []; opts[:query][:platform].push(*v.split(',')) }
817
+ parser.on('--priority=PRIORITY', 'filter out the result by the priority') { |v| opts[:query][:priority] ||= []; opts[:query][:priority].push(*v.split(',')) }
818
+ parser.on('-p', '--product=PRODUCTS', 'filter out the result by the specific products') { |v| opts[:query][:product] ||= []; opts[:query][:product].push(*v.split(',')) }
819
+ parser.on('--resolution=RESOLUSIONS', 'filter out the result by the resolusions') { |v| opts[:query][:resolution] ||= []; opts[:query][:resolusion].push(*v.split(',')) }
820
+ parser.on('--severity=SEVERITY', 'filter out the result by the severity') { |v| opts[:query][:severity] ||= []; opts[:query][:severity].push(*v.split(',')) }
821
+ parser.on('--summary=SUMMARY', 'filter out the result by the summary') { |v| opts[:query][:summary] ||= []; opts[:query][:summary] << v }
822
+ parser.on('--milestone=MILESTONE', 'filter out the result by the target milestone') { |v| opts[:query][:target_milestone] ||= []; opts[:query][:target_milestone].push(*v.split(',')) }
823
+ parser.on('--whiteboard=STRING', 'filter out the result by the specific words in the status whiteboard') { |v| opts[:query][:whiteboard] ||= []; opts[:query][:whiteboard] << v }
824
+ parser.separator ''
825
+ parser.separator 'Command Options:'
826
+ parser.on('--begin-date=DATE', 'Analyse the response time since DATE') { |v| x = Time.parse(v); opts[:responsetime][:begin_date] = Time.utc(x.year, x.month, x.day, 0, 0, 0) }
827
+ parser.on('--end-date=DATE', 'Analyse the response time until DATE') { |v| x = Time.parse(v); opts[:responsetime][:end_date] = Time.utc(x.year, x.month, x.day, 23, 59, 59) }
828
+ parser.on('--anonymous', 'access to Bugzilla anonymously') { |_v| opts[:anonymous] = true }
829
+
830
+ super
831
+ end # def parse
832
+
833
+ def do(argv, opts)
834
+ real_do(argv, opts) do |_ts, te, login, user, bug|
835
+ printf("Bug#%s: [%s] - [%s] [%s] %s - %d comments\n", bug['id'], bug['product'], bug['component'], bug['status'], bug['summary'], bug['comments'].length)
836
+
837
+ ucache = {}
838
+ st = nil
839
+ total = 0
840
+ notyetrespond = false
841
+ ncomment = 0
842
+ n = 0
843
+ over = ''
844
+ bug['comments'].each do |comment|
845
+ u = nil
846
+ if ucache.include?(comment['creator'])
847
+ u = ucache[comment['creator']]
848
+ else
849
+ u = user.get_userinfo(comment['creator'])
850
+ u = u[0] # FIXME
851
+ ucache[comment['creator']] = u
852
+ end
853
+ printf(" #%d. On %s, %s wrote\n", comment['count'], comment['creation_time'].to_time, comment['creator'].include?('@') ? format('%s <%s>', u['real_name'], comment['creator']) : comment['creator'])
854
+ if comment['creator'] != login
855
+ unless notyetrespond
856
+ st = comment['creation_time'].to_time
857
+ notyetrespond = true
858
+ end
859
+ else
860
+ ncomment += 1
861
+ et = comment['creation_time'].to_time
862
+ total += (et - st) unless st.nil?
863
+ st = et
864
+ notyetrespond = false
865
+ end
866
+ n += 1
867
+ end
868
+ x = ncomment
869
+ if notyetrespond && bug['bug_status'] != 'CLOSED'
870
+ total += (te - st)
871
+ over = '>'
872
+ x = 1 if x == 0
873
+ end
874
+ printf(" Own comment#: %d - avg. response time: %s%.f days\n", ncomment, over, total.to_f / x / 86_400.0)
875
+ end
876
+ end # def do
877
+
878
+ def real_do(argv, opts)
879
+ conf = read_config(opts)
880
+ conf.freeze
881
+ argv.each do |prefix|
882
+ unless conf.include?(prefix)
883
+ raise format('No host information for %s', prefix)
884
+ end
885
+
886
+ info = conf[prefix]
887
+ if opts[:command][:anonymous] == true
888
+ login = nil
889
+ pass = nil
890
+ else
891
+ login = info[:User].nil? ? ask('Bugzilla ID: ') : info[:User]
892
+ pass = info[:Password].nil? ? ask('Bugzilla password: ') { |q| q.echo = false } : info[:Password]
893
+ end
894
+
895
+ xmlrpc, host = get_xmlrpc(conf[prefix], opts)
896
+ user = Bugzilla::User.new(xmlrpc)
897
+ user.session(login, pass) do
898
+ bug = Bugzilla::Bug.new(xmlrpc)
899
+
900
+ opts[:command][:query][:product].map! { |x| info.include?(:ProductAliases) && info[:ProductAliases].include?(x) ? info[:ProductAliases][x] : x } if opts[:command][:query].include?(:product)
901
+ ts = opts[:command][:responsetime][:begin_date] || Time.utc(Time.new.year, 1, 1)
902
+ te = opts[:command][:responsetime][:end_date] || Time.utc(Time.new.year + 1, 1, 1) - 1
903
+ searchopts = opts[:command][:query].clone
904
+ searchopts[:last_change_time] = [ts, te]
905
+
906
+ @plugin.run(:pre, host, :metrics, searchopts)
907
+
908
+ result = bug.search(searchopts)
909
+
910
+ @plugin.run(:post, host, :search, result)
911
+
912
+ if result.include?('bugs')
913
+ ids = result['bugs'].map { |x| x['id'] }
914
+ res = bug.get_bugs(ids, nil)
915
+ res.each do |r|
916
+ yield ts, te, login, user, r
917
+ end
918
+ end
919
+ end
920
+ end
921
+ end # def real_do
922
+ end # class
923
+ end # module BzConsole
924
+
925
+ if $0 == __FILE__
926
+ opts = {}
927
+ opts[:command] = {}
928
+ subargv = []
929
+
930
+ o = ARGV.options do |opt|
931
+ opt.banner = format('Usage: %s [global options] <command> ...', File.basename(__FILE__))
932
+ opt.separator ''
933
+ opt.separator 'Global options:'
934
+ opt.on('-c', '--config=FILE', 'read FILE as the configuration file.') { |v| opts[:config] = v }
935
+ opt.on('-t', '--timeout=SEC', 'Set XMLRPC timeout in a second.') { |v| opts[:timeout] = v.to_i }
936
+ opt.on('-d', '--debug') { |v| opts[:debug] = true }
937
+ opt.on('-h', '--help', 'show this message') { |v| opts[:help] = true }
938
+
939
+ cmds = BzConsole.constants.sort.map { |x| (k = eval("BzConsole::#{x}")).class == Class && x != :CommandTemplate ? x.downcase.to_sym : nil }.compact
940
+
941
+ subargv = opt.order(ARGV)
942
+
943
+ command = subargv[0]
944
+
945
+ if !subargv.empty?
946
+ n = cmds.index(command.to_sym)
947
+ if n.nil?
948
+ STDERR.printf("E: Unknown command: %s\n", subargv[0])
949
+ STDERR.printf(" Available commands: %s\n", cmds.join(' '))
950
+ exit 1
951
+ else
952
+ opts[:instance] = eval("BzConsole::#{cmds[n].to_s.capitalize}.new(Bugzilla::Plugin::Template.new)")
953
+ subargv = opts[:instance].parse(opt, subargv[1..-1], opts[:command])
954
+ end
955
+ else
956
+ opt.separator ''
957
+ opt.separator 'Available commands:'
958
+ opt.separator format(' %s', cmds.join(' '))
959
+ end
960
+
961
+ if opts[:instance].nil? && subargv.empty? ||
962
+ opts[:help] == true ||
963
+ subargv.length < opts[:instance].n_args
964
+ puts opt.help
965
+ exit
966
+ end
967
+ end
968
+ opts[:instance].do(subargv, opts)
969
+ end