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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +34 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +10 -0
- data/bin/bzconsole +969 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bzconsole.gemspec +31 -0
- data/lib/bzconsole.rb +12 -0
- data/lib/bzconsole/command_template.rb +27 -0
- data/lib/bzconsole/version.rb +3 -0
- metadata +143 -0
checksums.yaml
ADDED
|
@@ -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
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -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
data/Gemfile.lock
ADDED
|
@@ -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
|
data/LICENSE.txt
ADDED
|
@@ -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.
|
data/README.md
ADDED
|
@@ -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).
|
data/Rakefile
ADDED
data/bin/bzconsole
ADDED
|
@@ -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
|