jscruggs-metric_fu 0.7.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.
- data/History.txt +30 -0
- data/MIT-LICENSE +22 -0
- data/Manifest.txt +28 -0
- data/README +100 -0
- data/TODO.txt +4 -0
- data/lib/metric_fu.rb +7 -0
- data/lib/metric_fu/flog_reporter.rb +5 -0
- data/lib/metric_fu/flog_reporter/base.rb +59 -0
- data/lib/metric_fu/flog_reporter/flog_reporter.css +39 -0
- data/lib/metric_fu/flog_reporter/generator.rb +71 -0
- data/lib/metric_fu/flog_reporter/operator.rb +10 -0
- data/lib/metric_fu/flog_reporter/page.rb +36 -0
- data/lib/metric_fu/flog_reporter/scanned_method.rb +28 -0
- data/lib/metric_fu/md5_tracker.rb +52 -0
- data/lib/metric_fu/saikuro/SAIKURO_README +142 -0
- data/lib/metric_fu/saikuro/saikuro.rb +1216 -0
- data/lib/tasks/churn.rake +95 -0
- data/lib/tasks/coverage.rake +38 -0
- data/lib/tasks/flog.rake +58 -0
- data/lib/tasks/metric_fu.rake +11 -0
- data/lib/tasks/metric_fu.rb +6 -0
- data/lib/tasks/saikuro.rake +33 -0
- data/lib/tasks/stats.rake +14 -0
- data/metric_fu.gemspec +17 -0
- data/test/test_helper.rb +6 -0
- data/test/test_md5_tracker.rb +59 -0
- metadata +103 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
namespace :metrics do
|
2
|
+
CHURN_DIR = File.join(MetricFu::BASE_DIRECTORY, 'churn')
|
3
|
+
|
4
|
+
desc "Which files change the most"
|
5
|
+
task :churn do
|
6
|
+
date_range, minimum_churn_count = churn_options()
|
7
|
+
svn_logs = `svn log #{date_range} --verbose`.split(/\n/).select {|line| line.strip =~ /^[A,M]/}
|
8
|
+
|
9
|
+
changes = {}
|
10
|
+
svn_logs.each do |line|
|
11
|
+
line.strip =~ /^[A,M] (.*)/
|
12
|
+
changes[$1] ? changes[$1] += 1 : changes[$1] = 1
|
13
|
+
end
|
14
|
+
write_churn_file(changes.reject {|file, change_count| change_count < minimum_churn_count})
|
15
|
+
system("open #{CHURN_DIR}/index.html") if PLATFORM['darwin']
|
16
|
+
end
|
17
|
+
|
18
|
+
def churn_options
|
19
|
+
options = defined?(CHURN_OPTIONS) ? CHURN_OPTIONS : {}
|
20
|
+
if options[:start_date]
|
21
|
+
require File.dirname(__FILE__) + '/../../../../config/environment'
|
22
|
+
date_range = "--revision {#{options[:start_date].call.strftime('%Y-%m-%d')}}:{#{Time.now.strftime('%Y-%m-%d')}}"
|
23
|
+
else
|
24
|
+
date_range = ""
|
25
|
+
end
|
26
|
+
minimum_churn_count = options[:minimum_churn_count] ? options[:minimum_churn_count] : 5
|
27
|
+
return date_range, minimum_churn_count
|
28
|
+
end
|
29
|
+
|
30
|
+
def write_churn_file changes
|
31
|
+
FileUtils.mkdir_p(CHURN_DIR, :verbose => false) unless File.directory?(CHURN_DIR)
|
32
|
+
File.open("#{CHURN_DIR}/index.html", "w+") do |file|
|
33
|
+
file << CHURN_FILE_BEGINING
|
34
|
+
changes.to_a.sort {|x,y| y[1] <=> x[1]}.each do |change|
|
35
|
+
file << "<tr><td>#{change[0]}</td><td class='warning'>#{change[1]}</td></tr>\n"
|
36
|
+
end
|
37
|
+
file << CHURN_FILE_END
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
CHURN_FILE_BEGINING = <<-EOS
|
42
|
+
<html><head><title>Source Control Churn Results</title></head>
|
43
|
+
<style>
|
44
|
+
body {
|
45
|
+
margin: 20px;
|
46
|
+
padding: 0;
|
47
|
+
font-size: 12px;
|
48
|
+
font-family: bitstream vera sans, verdana, arial, sans serif;
|
49
|
+
background-color: #efefef;
|
50
|
+
}
|
51
|
+
|
52
|
+
table {
|
53
|
+
border-collapse: collapse;
|
54
|
+
/*border-spacing: 0;*/
|
55
|
+
border: 1px solid #666;
|
56
|
+
background-color: #fff;
|
57
|
+
margin-bottom: 20px;
|
58
|
+
}
|
59
|
+
|
60
|
+
table, th, th+th, td, td+td {
|
61
|
+
border: 1px solid #ccc;
|
62
|
+
}
|
63
|
+
|
64
|
+
table th {
|
65
|
+
font-size: 12px;
|
66
|
+
color: #fc0;
|
67
|
+
padding: 4px 0;
|
68
|
+
background-color: #336;
|
69
|
+
}
|
70
|
+
|
71
|
+
th, td {
|
72
|
+
padding: 4px 10px;
|
73
|
+
}
|
74
|
+
|
75
|
+
td {
|
76
|
+
font-size: 13px;
|
77
|
+
}
|
78
|
+
|
79
|
+
.warning {
|
80
|
+
background-color: yellow;
|
81
|
+
}
|
82
|
+
</style>
|
83
|
+
|
84
|
+
<body>
|
85
|
+
<h1>Source Control Churn Results</h1>
|
86
|
+
<table width="100%" border="1">
|
87
|
+
<tr><th>File Path</th><th>Times Changed</th></tr>
|
88
|
+
EOS
|
89
|
+
|
90
|
+
CHURN_FILE_END = <<-EOS
|
91
|
+
</table>
|
92
|
+
</body>
|
93
|
+
</html>
|
94
|
+
EOS
|
95
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rcov'
|
5
|
+
require 'rcov/rcovtask'
|
6
|
+
|
7
|
+
namespace :metrics do
|
8
|
+
|
9
|
+
COVERAGE_DIR = File.join(MetricFu::BASE_DIRECTORY, 'coverage')
|
10
|
+
COVERAGE_DATA_FILE = File.join(MetricFu::BASE_DIRECTORY, 'coverage.data')
|
11
|
+
|
12
|
+
namespace :coverage do
|
13
|
+
rcov_output = COVERAGE_DIR
|
14
|
+
|
15
|
+
desc "Delete aggregate coverage data."
|
16
|
+
task(:clean) { rm_f("rcov_tmp", :verbose => false) }
|
17
|
+
|
18
|
+
desc "RCov task to generate report"
|
19
|
+
Rcov::RcovTask.new(:do => :clean) do |t|
|
20
|
+
FileUtils.mkdir_p(MetricFu::BASE_DIRECTORY) unless File.directory?(MetricFu::BASE_DIRECTORY)
|
21
|
+
t.test_files = FileList['test/**/*_test.rb']
|
22
|
+
t.rcov_opts = ["--sort coverage", "--aggregate '#{COVERAGE_DATA_FILE}'", "--html", "--rails"]
|
23
|
+
t.output_dir = COVERAGE_DIR
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "Generate and open coverage report"
|
28
|
+
task :coverage => ['coverage:do'] do
|
29
|
+
system("open #{COVERAGE_DIR}/index.html") if PLATFORM['darwin']
|
30
|
+
end
|
31
|
+
end
|
32
|
+
rescue LoadError
|
33
|
+
if RUBY_PLATFORM =~ /java/
|
34
|
+
puts 'running in jruby - rcov tasks not available'
|
35
|
+
else
|
36
|
+
puts 'sudo gem install rcov # if you want the rcov tasks'
|
37
|
+
end
|
38
|
+
end
|
data/lib/tasks/flog.rake
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'metricks', 'md5_tracker')
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'metricks', 'flog_reporter')
|
3
|
+
|
4
|
+
begin
|
5
|
+
FLOG_DIR = File.join(MetricFu::BASE_DIRECTORY, 'flog')
|
6
|
+
|
7
|
+
def flog(output, directory)
|
8
|
+
Dir.glob("#{directory}/**/*.rb").each do |filename|
|
9
|
+
output_dir = "#{FLOG_DIR}/#{filename.split("/")[0..-2].join("/")}"
|
10
|
+
mkdir_p(output_dir, :verbose => false) unless File.directory?(output_dir)
|
11
|
+
`flog #{filename} > #{FLOG_DIR}/#{filename.split('.')[0]}.txt` if MetricFu::MD5Tracker.file_changed?(filename, FLOG_DIR)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
namespace :metrics do
|
16
|
+
|
17
|
+
task :flog => ['flog:all'] do
|
18
|
+
end
|
19
|
+
|
20
|
+
namespace :flog do
|
21
|
+
desc "Delete aggregate flog data."
|
22
|
+
task(:clean) { rm_rf(FLOG_DIR, :verbose => false) }
|
23
|
+
|
24
|
+
desc "Flog code in app/models"
|
25
|
+
task :models do
|
26
|
+
flog "models", "app/models"
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Flog code in app/controllers"
|
30
|
+
task :controllers do
|
31
|
+
flog "controllers", "app/controllers"
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "Flog code in app/helpers"
|
35
|
+
task :helpers do
|
36
|
+
flog "helpers", "app/helpers"
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "Flog code in lib"
|
40
|
+
task :lib do
|
41
|
+
flog "lib", "lib"
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "Generate and open flog report"
|
45
|
+
task :all => [:models, :controllers, :helpers, :lib] do
|
46
|
+
MetricFu::FlogReporter::Generator.generate_report(FLOG_DIR)
|
47
|
+
system("open #{FLOG_DIR}/index.html") if PLATFORM['darwin']
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
rescue LoadError
|
53
|
+
if RUBY_PLATFORM =~ /java/
|
54
|
+
puts 'running in jruby - flog tasks not available'
|
55
|
+
else
|
56
|
+
puts 'sudo gem install flog # if you want the flog tasks'
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
namespace :metrics do
|
2
|
+
task :prepare do
|
3
|
+
RAILS_ENV = 'test'
|
4
|
+
end
|
5
|
+
|
6
|
+
desc "Useful for continuous integration"
|
7
|
+
task :all_with_migrate => [:prepare, "db:migrate", :all]
|
8
|
+
|
9
|
+
desc "Generate coverage, cyclomatic complexity (saikuro), flog, stats, and churn reports"
|
10
|
+
task :all => [:coverage, :saikuro, :flog, :churn, :stats]
|
11
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
namespace :metrics do
|
2
|
+
|
3
|
+
SAIKURO_DIR = File.join(MetricFu::BASE_DIRECTORY, 'saikuro')
|
4
|
+
|
5
|
+
desc "A cyclomatic complexity report using Saikuro"
|
6
|
+
task :saikuro do
|
7
|
+
default_options = {"--output_directory" => SAIKURO_DIR,
|
8
|
+
"--input_directory" => "app",
|
9
|
+
"--cyclo" => "",
|
10
|
+
"--filter_cyclo" => "0",
|
11
|
+
"--warn_cyclo" => "5",
|
12
|
+
"--error_cyclo" => "7"}
|
13
|
+
|
14
|
+
default_options.merge!(SAIKURO_OPTIONS) if defined?(SAIKURO_OPTIONS)
|
15
|
+
options = ""
|
16
|
+
default_options.each_pair { |key, value| options << "#{key} #{value} " }
|
17
|
+
|
18
|
+
sh "ruby \"#{File.expand_path(File.join(File.dirname(__FILE__), '..', 'metricks', 'saikuro'))}/saikuro.rb\" " +
|
19
|
+
"#{options}" do |ok, response|
|
20
|
+
unless ok
|
21
|
+
puts "Saikuro failed with exit status: #{response.exitstatus}"
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if File.exist? "#{SAIKURO_DIR}/index_cyclo.html"
|
27
|
+
mv "#{SAIKURO_DIR}/index_cyclo.html",
|
28
|
+
"#{SAIKURO_DIR}/index.html"
|
29
|
+
end
|
30
|
+
|
31
|
+
system("open #{SAIKURO_DIR}/index.html") if PLATFORM['darwin']
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
namespace :metrics do
|
2
|
+
|
3
|
+
STATS_DIR = File.join(MetricFu::BASE_DIRECTORY, 'stats')
|
4
|
+
STATS_FILE = File.join(STATS_DIR, 'index.html')
|
5
|
+
|
6
|
+
desc "A stats report"
|
7
|
+
task :stats do
|
8
|
+
mkdir_p(STATS_DIR) unless File.directory?(STATS_DIR)
|
9
|
+
`echo '<pre>' > #{STATS_FILE}`
|
10
|
+
`rake stats >> #{STATS_FILE}`
|
11
|
+
`echo '</pre>' >> #{STATS_FILE}`
|
12
|
+
system("open #{STATS_FILE}") if PLATFORM['darwin']
|
13
|
+
end
|
14
|
+
end
|
data/metric_fu.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "metric_fu"
|
3
|
+
s.version = "0.7.0"
|
4
|
+
s.date = "2008-09-11"
|
5
|
+
s.summary = "Generates project metrics using Flog, RCov, Saikuro and more"
|
6
|
+
s.email = "jake.scruggs@gmail.com"
|
7
|
+
s.homepage = "http://metric-fu.rubyforge.org/"
|
8
|
+
s.description = "Gives you a fist full of code metrics"
|
9
|
+
s.has_rdoc = true
|
10
|
+
s.authors = ["Jake Scruggs", "Sean Soper"]
|
11
|
+
s.files = ["History.txt", "Manifest.txt", "metric_fu.gemspec", "MIT-LICENSE", "README", "TODO.txt", "lib/metric_fu", "lib/metric_fu/flog_reporter", "lib/metric_fu/flog_reporter/base.rb", "lib/metric_fu/flog_reporter/flog_reporter.css", "lib/metric_fu/flog_reporter/generator.rb", "lib/metric_fu/flog_reporter/operator.rb", "lib/metric_fu/flog_reporter/page.rb", "lib/metric_fu/flog_reporter/scanned_method.rb", "lib/metric_fu/flog_reporter.rb", "lib/metric_fu/md5_tracker.rb", "lib/metric_fu/saikuro", "lib/metric_fu/saikuro/saikuro.rb", "lib/metric_fu/saikuro/SAIKURO_README", "lib/metric_fu.rb", "lib/tasks", "lib/tasks/churn.rake", "lib/tasks/coverage.rake", "lib/tasks/flog.rake", "lib/tasks/metric_fu.rake", "lib/tasks/metric_fu.rb", "lib/tasks/saikuro.rake", "lib/tasks/stats.rake", "test/test_helper.rb", "test/test_md5_tracker.rb"]
|
12
|
+
s.test_files = ["test/test_md5_tracker.rb"]
|
13
|
+
s.rdoc_options = ["--main", "README"]
|
14
|
+
s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README"]
|
15
|
+
s.add_dependency("flog", ["> 0.0.0"])
|
16
|
+
s.add_dependency("rcov", ["> 0.0.0"])
|
17
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
class TestMD5Tracker < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@tmp_dir = File.join(File.dirname(__FILE__), 'tmp')
|
7
|
+
FileUtils.mkdir_p(@tmp_dir, :verbose => false) unless File.directory?(@tmp_dir)
|
8
|
+
@file1 = File.new(File.join(@tmp_dir, 'file1.txt'), 'w')
|
9
|
+
@file2 = File.new(File.join(@tmp_dir, 'file2.txt'), 'w')
|
10
|
+
end
|
11
|
+
|
12
|
+
def teardown
|
13
|
+
FileUtils.rm_rf(@tmp_dir, :verbose => false)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_identical_files_match
|
17
|
+
@file1.puts("Hello World")
|
18
|
+
@file1.close
|
19
|
+
file1_md5 = MetricFu::MD5Tracker.track(@file1.path, @tmp_dir)
|
20
|
+
|
21
|
+
@file2.puts("Hello World")
|
22
|
+
@file2.close
|
23
|
+
file2_md5 = MetricFu::MD5Tracker.track(@file2.path, @tmp_dir)
|
24
|
+
|
25
|
+
assert file1_md5 == file2_md5
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_different_files_dont_match
|
29
|
+
@file1.puts("Hello World")
|
30
|
+
@file1.close
|
31
|
+
file1_md5 = MetricFu::MD5Tracker.track(@file1.path, @tmp_dir)
|
32
|
+
|
33
|
+
@file2.puts("Goodbye World")
|
34
|
+
@file2.close
|
35
|
+
file2_md5 = MetricFu::MD5Tracker.track(@file2.path, @tmp_dir)
|
36
|
+
|
37
|
+
assert file1_md5 != file2_md5
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_file_changed
|
41
|
+
@file2.close
|
42
|
+
|
43
|
+
@file1.puts("Hello World")
|
44
|
+
@file1.close
|
45
|
+
file1_md5 = MetricFu::MD5Tracker.track(@file1.path, @tmp_dir)
|
46
|
+
|
47
|
+
@file1 = File.new(File.join(@tmp_dir, 'file1.txt'), 'w')
|
48
|
+
@file1.puts("Goodbye World")
|
49
|
+
@file1.close
|
50
|
+
assert MetricFu::MD5Tracker.file_changed?(@file1.path, @tmp_dir)
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_file_changed_if_not_tracking
|
54
|
+
@file2.close
|
55
|
+
|
56
|
+
assert MetricFu::MD5Tracker.file_changed?(@file1.path, @tmp_dir)
|
57
|
+
assert File.exist?(MetricFu::MD5Tracker.md5_file(@file1.path, @tmp_dir))
|
58
|
+
end
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jscruggs-metric_fu
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.7.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jake Scruggs
|
8
|
+
- Sean Soper
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2008-09-11 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: flog
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rcov
|
27
|
+
version_requirement:
|
28
|
+
version_requirements: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.0.0
|
33
|
+
version:
|
34
|
+
description: Gives you a fist full of code metrics
|
35
|
+
email: jake.scruggs@gmail.com
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files:
|
41
|
+
- History.txt
|
42
|
+
- Manifest.txt
|
43
|
+
- README
|
44
|
+
files:
|
45
|
+
- History.txt
|
46
|
+
- Manifest.txt
|
47
|
+
- metric_fu.gemspec
|
48
|
+
- MIT-LICENSE
|
49
|
+
- README
|
50
|
+
- TODO.txt
|
51
|
+
- lib/metric_fu
|
52
|
+
- lib/metric_fu/flog_reporter
|
53
|
+
- lib/metric_fu/flog_reporter/base.rb
|
54
|
+
- lib/metric_fu/flog_reporter/flog_reporter.css
|
55
|
+
- lib/metric_fu/flog_reporter/generator.rb
|
56
|
+
- lib/metric_fu/flog_reporter/operator.rb
|
57
|
+
- lib/metric_fu/flog_reporter/page.rb
|
58
|
+
- lib/metric_fu/flog_reporter/scanned_method.rb
|
59
|
+
- lib/metric_fu/flog_reporter.rb
|
60
|
+
- lib/metric_fu/md5_tracker.rb
|
61
|
+
- lib/metric_fu/saikuro
|
62
|
+
- lib/metric_fu/saikuro/saikuro.rb
|
63
|
+
- lib/metric_fu/saikuro/SAIKURO_README
|
64
|
+
- lib/metric_fu.rb
|
65
|
+
- lib/tasks
|
66
|
+
- lib/tasks/churn.rake
|
67
|
+
- lib/tasks/coverage.rake
|
68
|
+
- lib/tasks/flog.rake
|
69
|
+
- lib/tasks/metric_fu.rake
|
70
|
+
- lib/tasks/metric_fu.rb
|
71
|
+
- lib/tasks/saikuro.rake
|
72
|
+
- lib/tasks/stats.rake
|
73
|
+
- test/test_helper.rb
|
74
|
+
- test/test_md5_tracker.rb
|
75
|
+
has_rdoc: true
|
76
|
+
homepage: http://metric-fu.rubyforge.org/
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options:
|
79
|
+
- --main
|
80
|
+
- README
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: "0"
|
88
|
+
version:
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: "0"
|
94
|
+
version:
|
95
|
+
requirements: []
|
96
|
+
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 1.2.0
|
99
|
+
signing_key:
|
100
|
+
specification_version: 2
|
101
|
+
summary: Generates project metrics using Flog, RCov, Saikuro and more
|
102
|
+
test_files:
|
103
|
+
- test/test_md5_tracker.rb
|