bzconsole 0.1.0

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