shiftzilla 0.2.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/Gemfile +3 -0
- data/README.md +20 -0
- data/bin/shiftzilla +110 -0
- data/lib/shiftzilla/bug.rb +67 -0
- data/lib/shiftzilla/config.rb +98 -0
- data/lib/shiftzilla/engine.rb +183 -0
- data/lib/shiftzilla/group.rb +10 -0
- data/lib/shiftzilla/helpers.rb +174 -0
- data/lib/shiftzilla/milestone.rb +20 -0
- data/lib/shiftzilla/milestones.rb +28 -0
- data/lib/shiftzilla/org_data.rb +332 -0
- data/lib/shiftzilla/release.rb +34 -0
- data/lib/shiftzilla/release_data.rb +185 -0
- data/lib/shiftzilla/snap_data.rb +31 -0
- data/lib/shiftzilla/source.rb +51 -0
- data/lib/shiftzilla/team.rb +17 -0
- data/lib/shiftzilla/team_data.rb +35 -0
- data/lib/shiftzilla/version.rb +3 -0
- data/shiftzilla.gemspec +30 -0
- data/shiftzilla.sql.tmpl +14 -0
- data/shiftzilla_cfg.yml.tmpl +42 -0
- data/template.haml +186 -0
- data/vendor/flot/jquery.flot.min.js +8 -0
- metadata +218 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 97d7153e749c49c28648b85cd0e2a9704d938b0a462b36d08d506458943d0441
|
4
|
+
data.tar.gz: 85af213bfdf7b6ae5c8854dbef2e5f6bd99233edc3f91a292397cf4de5e1f494
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2d00e6716bdffab9b6266021b95aafd6f5d38f809500a12a9f46bdbf9a3225ab5ab6d17cfa0ec61be8a2d138b7e527e7eddfbdb31779f346cc29c562050f6770
|
7
|
+
data.tar.gz: ac4499fd1bcf2f048672b528ab422494386f4a2ec18fb2cf0d6e9327d7f5af0ecff3b7bcc7f397b3f7d10ed41104c3207e60777cace213ba71c397ca81b1fedc
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Shiftzilla
|
2
|
+
a.k.a "The tool that we made because Bugzilla lacks any meaingful aggreation reporting."
|
3
|
+
|
4
|
+
This is a specialized tool for aggregating Bugzilla records in a way that is useful for some development teams. In order to use it:
|
5
|
+
|
6
|
+
1. This utility depends on a python-based tool called [python-bugzilla](https://pypi.python.org/pypi/python-bugzilla).
|
7
|
+
* Install it so that the `bugzilla` executable is in your $PATH
|
8
|
+
* Configure it by running `bugzilla login`
|
9
|
+
2. Next grab this utility from RubyGems:
|
10
|
+
* gem install shiftzilla
|
11
|
+
* Run any command (like `shiftzilla summary`) to have the utility set up your local $HOME/.shiftzilla directory
|
12
|
+
3. Edit $HOME/.shiftzilla/shiftzilla_cfg.yml to reflect the right organizational info for your teams and groups, plus the saved reports in Bugzilla that you want to draw data from. The utlity expects three tables:
|
13
|
+
* One for _all_ team bugs
|
14
|
+
* One for bugs filtered by the release that you are tracking
|
15
|
+
* One for bugs identified as test blockers by your QE team
|
16
|
+
|
17
|
+
With all of this done, you can start to run reports (or even set up cron jobs around them):
|
18
|
+
* `shiftzilla load` polls bugzilla and stores info in a local SQLite3 database
|
19
|
+
* `shiftzilla summary` gives you an overview report in your terminal
|
20
|
+
* `shiftzilla build` generates an overall and team-by-team reports that it will push to a web server as static web pages
|
data/bin/shiftzilla
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'shiftzilla/engine'
|
4
|
+
require 'shiftzilla/helpers'
|
5
|
+
require 'shiftzilla/version'
|
6
|
+
require 'trollop'
|
7
|
+
|
8
|
+
include Shiftzilla::Engine
|
9
|
+
include Shiftzilla::Helpers
|
10
|
+
|
11
|
+
SUB_COMMANDS = %w{help version load build purge triage}
|
12
|
+
Trollop::options do
|
13
|
+
version Shiftzilla::VERSION
|
14
|
+
banner <<-EOF
|
15
|
+
Usage:
|
16
|
+
#$0 <command>
|
17
|
+
|
18
|
+
Commands:
|
19
|
+
load
|
20
|
+
Queries Bugzilla and loads records into the local DB
|
21
|
+
build
|
22
|
+
Generates reports based local DB contents
|
23
|
+
purge
|
24
|
+
Removes today's records from the local DB
|
25
|
+
triage
|
26
|
+
Generate a report of teams that need to do bug triage
|
27
|
+
|
28
|
+
Options:
|
29
|
+
EOF
|
30
|
+
stop_on SUB_COMMANDS
|
31
|
+
end
|
32
|
+
|
33
|
+
cmd = ARGV.shift
|
34
|
+
cmd_opts = case cmd
|
35
|
+
when 'load'
|
36
|
+
Trollop::options do
|
37
|
+
banner <<-EOF
|
38
|
+
Usage:
|
39
|
+
#$0 load <options>
|
40
|
+
|
41
|
+
Description:
|
42
|
+
Runs all of the queries in the 'Queries' portion of the
|
43
|
+
config file, and stores info about each retrieved Bugzilla record
|
44
|
+
in a local database
|
45
|
+
|
46
|
+
Options:
|
47
|
+
EOF
|
48
|
+
end
|
49
|
+
when 'build'
|
50
|
+
Trollop::options do
|
51
|
+
banner <<-EOF
|
52
|
+
Usage:
|
53
|
+
#$0 build <options>
|
54
|
+
|
55
|
+
Description:
|
56
|
+
Performs aggregations and generates reports based on those,
|
57
|
+
which are rsynced to a target location.
|
58
|
+
|
59
|
+
Options:
|
60
|
+
EOF
|
61
|
+
opt :local_preview, "Don't publish, just show the report locally", :default => false
|
62
|
+
opt :quiet, "For cron use; don't print anything to STDOUT.", :default => false
|
63
|
+
end
|
64
|
+
when 'purge'
|
65
|
+
Trollop::options do
|
66
|
+
banner <<-EOF
|
67
|
+
Usage:
|
68
|
+
#$0 purge <options>
|
69
|
+
|
70
|
+
Description:
|
71
|
+
Purges today's data from the local database
|
72
|
+
|
73
|
+
Options:
|
74
|
+
EOF
|
75
|
+
end
|
76
|
+
when 'triage'
|
77
|
+
Trollop::options do
|
78
|
+
banner <<-EOF
|
79
|
+
Usage:
|
80
|
+
#$0 triage <options>
|
81
|
+
|
82
|
+
Description:
|
83
|
+
Generate a report of bugs with no target release,
|
84
|
+
ranked by team / component and age.
|
85
|
+
|
86
|
+
Options:
|
87
|
+
EOF
|
88
|
+
end
|
89
|
+
when 'help'
|
90
|
+
Trollop::educate
|
91
|
+
when 'version'
|
92
|
+
puts Shiftzilla::VERSION
|
93
|
+
exit 0
|
94
|
+
end
|
95
|
+
|
96
|
+
# Check for the config info
|
97
|
+
check_config
|
98
|
+
|
99
|
+
case cmd
|
100
|
+
when 'load'
|
101
|
+
load_records
|
102
|
+
when 'build'
|
103
|
+
build_reports(cmd_opts)
|
104
|
+
when 'purge'
|
105
|
+
purge_records
|
106
|
+
when 'triage'
|
107
|
+
triage_report
|
108
|
+
end
|
109
|
+
|
110
|
+
exit
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Shiftzilla
|
2
|
+
class Bug
|
3
|
+
attr_reader :id, :first_seen, :last_seen, :test_blocker, :ops_blocker, :owner, :component, :pm_score, :cust_cases, :tgt_release
|
4
|
+
|
5
|
+
def initialize(bzid,binfo)
|
6
|
+
@id = bzid
|
7
|
+
@first_seen = binfo[:snapdate]
|
8
|
+
@last_seen = binfo[:snapdate]
|
9
|
+
@test_blocker = binfo[:test_blocker]
|
10
|
+
@ops_blocker = binfo[:ops_blocker]
|
11
|
+
@owner = binfo[:owner]
|
12
|
+
@summary = binfo[:summary]
|
13
|
+
@component = binfo[:component]
|
14
|
+
@pm_score = binfo[:pm_score]
|
15
|
+
@cust_cases = binfo[:cust_cases]
|
16
|
+
@tgt_release = binfo[:tgt_release]
|
17
|
+
end
|
18
|
+
|
19
|
+
def update(binfo)
|
20
|
+
@last_seen = binfo[:snapdate]
|
21
|
+
@test_blocker = binfo[:test_blocker]
|
22
|
+
@ops_blocker = binfo[:ops_blocker]
|
23
|
+
@owner = binfo[:owner]
|
24
|
+
@summary = binfo[:summary]
|
25
|
+
@component = binfo[:component]
|
26
|
+
@pm_score = binfo[:pm_score]
|
27
|
+
@cust_cases = binfo[:cust_cases]
|
28
|
+
@tgt_release = binfo[:tgt_release]
|
29
|
+
end
|
30
|
+
|
31
|
+
def summary
|
32
|
+
@summary[0..30].gsub(/\s\w+\s*$/, '...')
|
33
|
+
end
|
34
|
+
|
35
|
+
def age
|
36
|
+
(@last_seen - @first_seen).to_i
|
37
|
+
end
|
38
|
+
|
39
|
+
def semver
|
40
|
+
parts = @tgt_release.split('.')
|
41
|
+
if parts.length == 1
|
42
|
+
return @tgt_release
|
43
|
+
end
|
44
|
+
semver = ''
|
45
|
+
first_part = true
|
46
|
+
parts.each do |part|
|
47
|
+
unless is_number?(part)
|
48
|
+
semver += part
|
49
|
+
else
|
50
|
+
semver += ("%09d" % part).to_s
|
51
|
+
end
|
52
|
+
# A version like '3.z' gets a middle 0 for sort purposes.
|
53
|
+
if first_part and parts.length == 2
|
54
|
+
semver += ("%09d" % 0).to_s
|
55
|
+
end
|
56
|
+
first_part = false
|
57
|
+
end
|
58
|
+
return semver
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def is_number?(val)
|
64
|
+
val.to_f.to_s == val.to_s || val.to_i.to_s == val.to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'shiftzilla/group'
|
3
|
+
require 'shiftzilla/release'
|
4
|
+
require 'shiftzilla/source'
|
5
|
+
require 'shiftzilla/team'
|
6
|
+
|
7
|
+
module Shiftzilla
|
8
|
+
class Config
|
9
|
+
attr_reader :teams, :groups, :sources, :releases, :ssh
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@teams = []
|
13
|
+
@groups = []
|
14
|
+
@sources = []
|
15
|
+
group_map = {}
|
16
|
+
cfg_file['Groups'].each do |group|
|
17
|
+
gobj = Shiftzilla::Group.new(group)
|
18
|
+
@groups << gobj
|
19
|
+
group_map[gobj.id] = gobj
|
20
|
+
end
|
21
|
+
cfg_file['Teams'].each do |team|
|
22
|
+
@teams << Shiftzilla::Team.new(team,group_map)
|
23
|
+
end
|
24
|
+
cfg_file['Sources'].each do |sid,sinfo|
|
25
|
+
@sources << Shiftzilla::Source.new(sid,sinfo)
|
26
|
+
end
|
27
|
+
# Always track a release for bugs with no target release
|
28
|
+
@releases = [Shiftzilla::Release.new({ 'name' => '"---"', 'targets' => ['---'] },true)]
|
29
|
+
cfg_file['Releases'].each do |release|
|
30
|
+
@releases << Shiftzilla::Release.new(release)
|
31
|
+
end
|
32
|
+
@releases << Shiftzilla::Release.new({ 'name' => 'All', 'targets' => [] },true)
|
33
|
+
@ssh = {
|
34
|
+
:host => cfg_file['SSH']['host'],
|
35
|
+
:path => cfg_file['SSH']['path'],
|
36
|
+
:url => cfg_file['SSH']['url'],
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def earliest_milestone
|
41
|
+
milestone_boundaries[:earliest]
|
42
|
+
end
|
43
|
+
|
44
|
+
def latest_milestone
|
45
|
+
milestone_boundaries[:latest]
|
46
|
+
end
|
47
|
+
|
48
|
+
def team(tname)
|
49
|
+
@teams.select{ |t| t.name == tname }[0]
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_ad_hoc_team(tinfo)
|
53
|
+
@teams << Shiftzilla::Team.new(tinfo,{},true)
|
54
|
+
end
|
55
|
+
|
56
|
+
def release(rname)
|
57
|
+
@releases.select{ |r| r.name == rname }[0]
|
58
|
+
end
|
59
|
+
|
60
|
+
def release_by_target(tgt)
|
61
|
+
return target_map[tgt]
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def target_map
|
67
|
+
@target_map ||= begin
|
68
|
+
tmap = {}
|
69
|
+
@releases.each do |release|
|
70
|
+
release.targets.each do |target|
|
71
|
+
tmap[target] = release
|
72
|
+
end
|
73
|
+
end
|
74
|
+
tmap
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def milestone_boundaries
|
79
|
+
@milestone_boundaries ||= begin
|
80
|
+
boundaries = { :earliest => Date.today, :latest => (Date.today - 1800) }
|
81
|
+
@releases.each do |release|
|
82
|
+
next unless release.uses_milestones?
|
83
|
+
ms = release.milestones
|
84
|
+
[ms.start,ms.feature_complete,ms.code_freeze,ms.ga].each do |m|
|
85
|
+
next if m.date.nil?
|
86
|
+
if m.date < boundaries[:earliest]
|
87
|
+
boundaries[:earliest] = m.date
|
88
|
+
end
|
89
|
+
if m.date > boundaries[:latest]
|
90
|
+
boundaries[:latest] = m.date
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
boundaries
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'highline/import'
|
2
|
+
require 'shiftzilla/config'
|
3
|
+
require 'shiftzilla/helpers'
|
4
|
+
require 'shiftzilla/org_data'
|
5
|
+
require 'terminal-table'
|
6
|
+
require 'trollop'
|
7
|
+
|
8
|
+
include Shiftzilla::Helpers
|
9
|
+
|
10
|
+
module Shiftzilla
|
11
|
+
module Engine
|
12
|
+
def check_config
|
13
|
+
if not File.directory?(SZA_DIR)
|
14
|
+
choose do |menu|
|
15
|
+
menu.header = 'You don\'t have a Shiftzilla config directory at $HOME/.shiftzilla. Should I create one?'
|
16
|
+
menu.prompt = 'Choice?'
|
17
|
+
menu.choice(:yes) {
|
18
|
+
say('Okay. Creating config directory.')
|
19
|
+
Dir.mkdir(SZA_DIR)
|
20
|
+
Dir.mkdir(ARCH_DIR)
|
21
|
+
}
|
22
|
+
menu.choice(:no) {
|
23
|
+
say('Okay. Exiting Shiftzilla.')
|
24
|
+
exit
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
if not File.directory?(ARCH_DIR)
|
29
|
+
puts 'You don\'t have an archive directory at $HOME/.shiftzilla/archive. Creating it.'
|
30
|
+
Dir.mkdir(ARCH_DIR)
|
31
|
+
end
|
32
|
+
if not File.exists?(CFG_FILE)
|
33
|
+
choose do |menu|
|
34
|
+
menu.header = "\nYou don't have a shiftzilla_cfg.yml file in $HOME/.shiftzilla. Should I create one?"
|
35
|
+
menu.prompt = 'Choice?'
|
36
|
+
menu.choice(:yes) {
|
37
|
+
say('Okay. Creating shiftzilla_cfg.yml')
|
38
|
+
FileUtils.cp(CFG_TMPL,CFG_FILE)
|
39
|
+
}
|
40
|
+
menu.choice(:no) {
|
41
|
+
say('Okay. Exiting Shiftzilla.')
|
42
|
+
exit
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
if not File.exists?(DB_FPATH)
|
47
|
+
choose do |menu|
|
48
|
+
menu.header = "\nYou don't have a shiftzilla.sqlite file in $HOME/.shiftzilla.\nI can create it for you, but it is very important for you to\nconfigure Shiftzilla by puttng the proper settings in\n$HOME/.shiftzilla/shiftzilla_cfg.yml first. Do you want me to proceed with creating the database?"
|
49
|
+
menu.prompt = 'Choice?'
|
50
|
+
menu.choice(:yes) {
|
51
|
+
say('Okay. Creating shiftzilla.sqlite')
|
52
|
+
sql_tmpl = File.read(SQL_TMPL)
|
53
|
+
tgt_rel_clause = release_clause(releases)
|
54
|
+
sql_tmpl.gsub! '$RELEASE_CLAUSE', tgt_rel_clause
|
55
|
+
dbh.execute_batch(sql_tmpl)
|
56
|
+
dbh.close
|
57
|
+
exit
|
58
|
+
}
|
59
|
+
menu.choice(:no) {
|
60
|
+
say('Okay. Exiting Shiftzilla.')
|
61
|
+
exit
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def load_records
|
68
|
+
sources.each do |s|
|
69
|
+
if s.has_records_for_today?
|
70
|
+
puts "Skipping query for #{s.id}; it already has records for today."
|
71
|
+
else
|
72
|
+
backup_db
|
73
|
+
puts "Querying bugzilla for #{s.id}"
|
74
|
+
added_count = s.load_records
|
75
|
+
puts "Added #{added_count} records to #{s.table}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def purge_records
|
81
|
+
sources.each do |s|
|
82
|
+
s.purge_records
|
83
|
+
end
|
84
|
+
puts "Purged #{sources.length} tables."
|
85
|
+
end
|
86
|
+
|
87
|
+
def triage_report
|
88
|
+
org_data = Shiftzilla::OrgData.new(shiftzilla_config)
|
89
|
+
org_data.populate_releases
|
90
|
+
teams = org_data.get_ordered_teams
|
91
|
+
no_tgt_rel = shiftzilla_config.releases[0]
|
92
|
+
|
93
|
+
teams.each do |tname|
|
94
|
+
next if tname == '_overall'
|
95
|
+
rdata = org_data.get_release_data(tname,no_tgt_rel)
|
96
|
+
next if rdata.nil? or rdata.snaps.empty?
|
97
|
+
recipients = {}
|
98
|
+
team = shiftzilla_config.team(tname)
|
99
|
+
unless team.nil? or team.ad_hoc?
|
100
|
+
recipients[team.lead] = 1
|
101
|
+
recipients[team.group.lead] = 1
|
102
|
+
end
|
103
|
+
bzids = rdata.snaps.has_key?(latest_snapshot) ? rdata.snaps[latest_snapshot].bug_ids : []
|
104
|
+
next if bzids.length == 0
|
105
|
+
bugs = rdata.bugs
|
106
|
+
table = Terminal::Table.new do |t|
|
107
|
+
t << ['URL','Age','Component','Owner','Summary']
|
108
|
+
t << :separator
|
109
|
+
bzids.sort_by{ |b| [bugs[b].first_seen,bugs[b].component] }.each do |bzid|
|
110
|
+
bug = bugs[bzid]
|
111
|
+
if team.nil?
|
112
|
+
recipients[bug.owner] = 1
|
113
|
+
end
|
114
|
+
t << [
|
115
|
+
bug_url(bzid),
|
116
|
+
bug.age,
|
117
|
+
bug.component,
|
118
|
+
bug.owner,
|
119
|
+
bug.summary
|
120
|
+
]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
puts "#{tname}#{team.nil? ? ' Component' : ' Team'} - #{bzids.length} bugs with no Target Release"
|
124
|
+
puts "To: #{recipients.keys.sort.join(',')}"
|
125
|
+
puts "#{table}\n\n"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def build_reports(options)
|
130
|
+
org_data = Shiftzilla::OrgData.new(shiftzilla_config)
|
131
|
+
org_data.populate_releases
|
132
|
+
org_data.build_series
|
133
|
+
org_data.generate_reports
|
134
|
+
if options[:local_preview]
|
135
|
+
org_data.show_local_reports
|
136
|
+
else
|
137
|
+
org_data.publish_reports(ssh)
|
138
|
+
system("rm -rf #{org_data.tmp_dir}")
|
139
|
+
unless options[:quiet]
|
140
|
+
system("open #{ssh[:url]}")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def shiftzilla_config
|
148
|
+
@shiftzilla_config ||= Shiftzilla::Config.new
|
149
|
+
end
|
150
|
+
|
151
|
+
def teams
|
152
|
+
@teams ||= shiftzilla_config.teams
|
153
|
+
end
|
154
|
+
|
155
|
+
def groups
|
156
|
+
@groups ||= shiftzilla_config.groups
|
157
|
+
end
|
158
|
+
|
159
|
+
def sources
|
160
|
+
@sources ||= shiftzilla_config.sources
|
161
|
+
end
|
162
|
+
|
163
|
+
def releases
|
164
|
+
@releases ||= shiftzilla_config.releases
|
165
|
+
end
|
166
|
+
|
167
|
+
def ssh
|
168
|
+
@ssh ||= shiftzilla_config.ssh
|
169
|
+
end
|
170
|
+
|
171
|
+
def component_team_map
|
172
|
+
@component_team_map ||= begin
|
173
|
+
ctm = {}
|
174
|
+
teams.each do |team|
|
175
|
+
team.components.each do |component|
|
176
|
+
ctm[component] = team
|
177
|
+
end
|
178
|
+
end
|
179
|
+
ctm
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'haml'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'sqlite3'
|
6
|
+
require 'tmpdir'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
module Shiftzilla
|
10
|
+
module Helpers
|
11
|
+
BZ_URL = 'https://bugzilla.redhat.com/show_bug.cgi?id='
|
12
|
+
SZA_DIR = File.join(ENV['HOME'],'.shiftzilla')
|
13
|
+
ARCH_DIR = File.join(SZA_DIR,'archive')
|
14
|
+
CFG_FILE = File.join(SZA_DIR,'shiftzilla_cfg.yml')
|
15
|
+
DB_FNAME = 'shiftzilla.sqlite'
|
16
|
+
DB_FPATH = File.join(SZA_DIR,DB_FNAME)
|
17
|
+
THIS_PATH = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
|
18
|
+
HAML_TMPL = File.expand_path(File.join(File.dirname(THIS_PATH), '../../template.haml'))
|
19
|
+
CFG_TMPL = File.expand_path(File.join(File.dirname(THIS_PATH), '../../shiftzilla_cfg.yml.tmpl'))
|
20
|
+
SQL_TMPL = File.expand_path(File.join(File.dirname(THIS_PATH), '../../shiftzilla.sql.tmpl'))
|
21
|
+
VENDOR_DIR = File.expand_path(File.join(File.dirname(THIS_PATH), '../../vendor'))
|
22
|
+
|
23
|
+
GRAPH_DIMENSIONS = '800x400'
|
24
|
+
GRAPH_THEME = {
|
25
|
+
:colors => [
|
26
|
+
'#268bd2', # Blue
|
27
|
+
'#cb4b16', # Orange
|
28
|
+
'#859900', # Green
|
29
|
+
'#2aa198', # Cyan
|
30
|
+
'#d33682', # Magenta
|
31
|
+
'#6c71c4', # Violet
|
32
|
+
'#b58900', # Yellow
|
33
|
+
'#dc322f', # Red
|
34
|
+
],
|
35
|
+
:marker_color => '#93a1a1', # Base1
|
36
|
+
:font_color => '#586e75', # Base01
|
37
|
+
:background_colors => '#fdf6e3', # Base3
|
38
|
+
:background_image => nil,
|
39
|
+
}
|
40
|
+
|
41
|
+
def tmp_dir
|
42
|
+
@tmp_dir ||= Dir.mktmpdir('shiftzilla-reports-')
|
43
|
+
end
|
44
|
+
|
45
|
+
def cfg_file
|
46
|
+
@cfg_file ||= YAML.load_file(CFG_FILE)
|
47
|
+
end
|
48
|
+
|
49
|
+
def dbh
|
50
|
+
@dbh ||= SQLite3::Database.new(DB_FPATH)
|
51
|
+
end
|
52
|
+
|
53
|
+
def haml_engine
|
54
|
+
@haml_engine ||= Haml::Engine.new(File.read(HAML_TMPL))
|
55
|
+
end
|
56
|
+
|
57
|
+
def new_graph(labels,max_y)
|
58
|
+
g = Gruff::Line.new(GRAPH_DIMENSIONS)
|
59
|
+
g.theme = GRAPH_THEME
|
60
|
+
g.line_width = 2
|
61
|
+
g.labels = labels
|
62
|
+
g.y_axis_increment = set_axis_increment(max_y)
|
63
|
+
g.hide_dots = true
|
64
|
+
return g
|
65
|
+
end
|
66
|
+
|
67
|
+
def backup_db
|
68
|
+
unless db_backed_up
|
69
|
+
today = Date.today.strftime('%Y-%m-%d')
|
70
|
+
tpath = File.join(ARCH_DIR,today)
|
71
|
+
unless Dir.exists?(tpath)
|
72
|
+
Dir.mkdir(tpath)
|
73
|
+
end
|
74
|
+
apath = ''
|
75
|
+
copy_idx = 0
|
76
|
+
loop do
|
77
|
+
copynum = "%02d" % copy_idx
|
78
|
+
apath = File.join(tpath,"#{copynum}-#{DB_FNAME}")
|
79
|
+
break unless File.exists?(apath)
|
80
|
+
copy_idx += 1
|
81
|
+
end
|
82
|
+
FileUtils.cp DB_FPATH, apath
|
83
|
+
puts "Backed up the database."
|
84
|
+
@db_backed_up = true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def timestamp
|
89
|
+
DateTime.now.to_s
|
90
|
+
end
|
91
|
+
|
92
|
+
def bug_url(bug_id)
|
93
|
+
return "#{BZ_URL}#{bug_id}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def set_axis_increment(initial_value)
|
97
|
+
case
|
98
|
+
when initial_value < 10
|
99
|
+
return 1
|
100
|
+
when initial_value < 20
|
101
|
+
return 2
|
102
|
+
when initial_value < 50
|
103
|
+
return 5
|
104
|
+
when initial_value < 100
|
105
|
+
return 10
|
106
|
+
when initial_value < 200
|
107
|
+
return 20
|
108
|
+
when initial_value < 400
|
109
|
+
return 25
|
110
|
+
else
|
111
|
+
return 100
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def all_bugs_query(snapshot)
|
116
|
+
return "SELECT AB.'Bug ID', AB.'Component', AB.'Target Release', AB.'Assignee', AB.'Status', AB.'Summary', AB.'Keywords', AB.'PM Score', AB.'External Bugs' FROM ALL_BUGS AB WHERE AB.'Snapshot' = date('#{snapshot}')"
|
117
|
+
end
|
118
|
+
|
119
|
+
def component_bugs_query(components,snapshot)
|
120
|
+
rclause = list_clause(components)
|
121
|
+
rfilter = rclause == '' ? '' : " AND AB.'Component' #{rclause}"
|
122
|
+
return "SELECT AB.'Bug ID', AB.'Component', AB.'Target Release', AB.'Assignee', AB.'Status', AB.'Summary', AB.'Keywords', AB.'PM Score', AB.'External Bugs' FROM ALL_BUGS AB WHERE AB.'Snapshot' = date('#{snapshot}')#{rfilter} ORDER BY AB.'Target Release' DESC"
|
123
|
+
end
|
124
|
+
|
125
|
+
def component_bugs_count(components,snapshot)
|
126
|
+
rclause = list_clause(components)
|
127
|
+
rfilter = rclause == '' ? '' : " AND AB.'Component' #{rclause}"
|
128
|
+
return "SELECT count(*) FROM ALL_BUGS AB WHERE AB.'Snapshot' = date('#{snapshot}')#{rfilter}"
|
129
|
+
end
|
130
|
+
|
131
|
+
def all_snapshots
|
132
|
+
@all_snapshots ||= begin
|
133
|
+
all_snapshots = []
|
134
|
+
dbh.execute('SELECT DISTINCT Snapshot FROM ALL_BUGS ORDER BY Snapshot ASC') do |row|
|
135
|
+
all_snapshots << row[0]
|
136
|
+
end
|
137
|
+
all_snapshots
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def latest_snapshot
|
142
|
+
all_snapshots[-1]
|
143
|
+
end
|
144
|
+
|
145
|
+
def field_map
|
146
|
+
{
|
147
|
+
:assigned_to => 'Assignee',
|
148
|
+
:component => 'Component',
|
149
|
+
:id => 'Bug ID',
|
150
|
+
:keywords => 'Keywords',
|
151
|
+
:cf_pm_score => 'PM Score',
|
152
|
+
:status => 'Status',
|
153
|
+
:summary => 'Summary',
|
154
|
+
:target_release => 'Target Release',
|
155
|
+
:external_bugs => 'External Bugs',
|
156
|
+
}
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def list_clause(list)
|
162
|
+
return '' if list.length == 0
|
163
|
+
if list.length > 1
|
164
|
+
stmt = list.map{ |t| "'#{t}'" }.join(',')
|
165
|
+
return "IN (#{stmt})"
|
166
|
+
end
|
167
|
+
return "== '#{list[0]}'"
|
168
|
+
end
|
169
|
+
|
170
|
+
def db_backed_up
|
171
|
+
@db_backed_up ||= false
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Shiftzilla
|
2
|
+
class Milestone
|
3
|
+
def initialize(mtxt)
|
4
|
+
@date = nil
|
5
|
+
@stamp = ''
|
6
|
+
unless (mtxt.nil? or mtxt == '')
|
7
|
+
@date = Date.parse(mtxt)
|
8
|
+
@stamp = mtxt
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def date
|
13
|
+
@date
|
14
|
+
end
|
15
|
+
|
16
|
+
def stamp
|
17
|
+
@stamp
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|