locat 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.ruby +48 -0
- data/COPYING.rdoc +33 -0
- data/README.rdoc +55 -0
- data/bin/locat +3 -0
- data/lib/locat.rb +14 -0
- data/lib/locat/command.rb +129 -0
- data/lib/locat/counter.rb +155 -0
- data/lib/locat/gitloc.rb +139 -0
- data/lib/locat/matcher.rb +39 -0
- data/lib/locat/template.rb +104 -0
- data/lib/locat/template/graphael.rhtml +257 -0
- data/lib/locat/template/highchart.rhtml +249 -0
- data/lib/locat/template/javascript.js +55 -0
- metadata +102 -0
data/.ruby
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
---
|
2
|
+
name: locat
|
3
|
+
version: 0.1.0
|
4
|
+
title: LOCat
|
5
|
+
summary: Lines of Code Attache
|
6
|
+
description: LOCat is a customizable Line-Of-Code metric system. LOC might not be the most useful metric in the universe but it still provide useful inforamtion and can be a lot of fun.
|
7
|
+
loadpath:
|
8
|
+
- lib
|
9
|
+
manifest: MANIFEST
|
10
|
+
requires:
|
11
|
+
- name: ansi
|
12
|
+
version: 0+
|
13
|
+
group: []
|
14
|
+
|
15
|
+
- name: detroit
|
16
|
+
version: 0+
|
17
|
+
group:
|
18
|
+
- build
|
19
|
+
- name: qed
|
20
|
+
version: 0+
|
21
|
+
group:
|
22
|
+
- test
|
23
|
+
conflicts: []
|
24
|
+
|
25
|
+
replaces: []
|
26
|
+
|
27
|
+
engine_check: []
|
28
|
+
|
29
|
+
organization: Rubyworks
|
30
|
+
contact: trans <transfire@gmail.com>
|
31
|
+
created: 2011-07-07
|
32
|
+
copyright: Copyright (c) 2011 Thomas Sawyer
|
33
|
+
licenses:
|
34
|
+
- BSD-2-Clause
|
35
|
+
authors:
|
36
|
+
- Thomas Sawyer
|
37
|
+
maintainers: []
|
38
|
+
|
39
|
+
resources:
|
40
|
+
home: http://rubyworks.github.com/locat
|
41
|
+
code: http://github.com/rubyworks/locat
|
42
|
+
docs: http://wiki.github.com/rubyworks/locat/docs/qed
|
43
|
+
wiki: http://wiki.github.com/rubyworks/locat
|
44
|
+
bugs: http://github.com/rubyworks/locat/issues
|
45
|
+
mail: http://groups.google.com/group/rubyworks-mailinglist
|
46
|
+
repositories:
|
47
|
+
public: git://github.com/rubyworks/locat.git
|
48
|
+
spec_version: 1.0.0
|
data/COPYING.rdoc
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
= COPYRIGHT NOTICES
|
2
|
+
|
3
|
+
== LOCat
|
4
|
+
|
5
|
+
Copyright:: (c) 2011 Thomas Sawyer, Rubyworks
|
6
|
+
License:: BSD-2-Clause
|
7
|
+
|
8
|
+
Copyright (c) 2011 Thomas Sawyer. All rights reserved.
|
9
|
+
|
10
|
+
Redistribution and use in source and binary forms, with or without modification, are
|
11
|
+
permitted provided that the following conditions are met:
|
12
|
+
|
13
|
+
1. Redistributions of source code must retain the above copyright notice, this list of
|
14
|
+
conditions and the following disclaimer.
|
15
|
+
|
16
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list
|
17
|
+
of conditions and the following disclaimer in the documentation and/or other materials
|
18
|
+
provided with the distribution.
|
19
|
+
|
20
|
+
THIS SOFTWARE IS PROVIDED BY Thomas Sawyer ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
21
|
+
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Thomas Sawyer OR
|
23
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
24
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
26
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
27
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
28
|
+
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
|
+
|
30
|
+
The views and conclusions contained in the software and documentation are those of the
|
31
|
+
authors and should not be interpreted as representing official policies, either expressed
|
32
|
+
or implied, of Thomas Sawyer.
|
33
|
+
|
data/README.rdoc
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
= LOCAT
|
2
|
+
|
3
|
+
|
4
|
+
== DESCRIPTION
|
5
|
+
|
6
|
+
LOCat is a fancy LOC code analysis tool.
|
7
|
+
|
8
|
+
|
9
|
+
== RESOURCES
|
10
|
+
|
11
|
+
* {Homepage}[http://rubyworks.github.com/locat]
|
12
|
+
* {Development}[http://github.com/rubyworks/locat]
|
13
|
+
* {Issue Tracker}[http://github.com/rubyworks/locat/issues]
|
14
|
+
* {Mailing List}[http://groups.google.com/group/rubyworks-mailinglist]
|
15
|
+
|
16
|
+
|
17
|
+
== SYNOPSIS
|
18
|
+
|
19
|
+
Define a `.locat` Ruby script in your project, e.g.
|
20
|
+
|
21
|
+
match 'lib/**.rb' do |file, line|
|
22
|
+
case line
|
23
|
+
when /^\s*#/
|
24
|
+
'Comment'
|
25
|
+
when /^\s*$/
|
26
|
+
'Blank'
|
27
|
+
else
|
28
|
+
'Code'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
match 'test/**.rb' do |file, line|
|
33
|
+
case line
|
34
|
+
when /^\s*#/
|
35
|
+
'Comment'
|
36
|
+
when /^\s*$/
|
37
|
+
'Blank'
|
38
|
+
else
|
39
|
+
'Test'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
Then run `locat`, e.g.
|
44
|
+
|
45
|
+
$ locat -o locat.html
|
46
|
+
|
47
|
+
|
48
|
+
== COPYRIGHT
|
49
|
+
|
50
|
+
Copyright (c) 2011 Thomas Sawyer, Rubyworks
|
51
|
+
|
52
|
+
BSD 2-Clause License
|
53
|
+
|
54
|
+
See COPYING.rdoc for details.
|
55
|
+
|
data/bin/locat
ADDED
data/lib/locat.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
module LOCat
|
2
|
+
|
3
|
+
#
|
4
|
+
def self.cli(*argv)
|
5
|
+
options = {}
|
6
|
+
|
7
|
+
OptionParser.new do |opt|
|
8
|
+
opt.on('-o', '--output FILE', 'output file') do |output|
|
9
|
+
options[:output] = output
|
10
|
+
end
|
11
|
+
opt.on('-j', '--json', 'output JSON formmated data') do
|
12
|
+
options[:format] = 'json'
|
13
|
+
end
|
14
|
+
opt.on('-y', '--yaml', 'output YAML formmated data') do
|
15
|
+
options[:format] = 'yaml'
|
16
|
+
end
|
17
|
+
opt.on('-c', '--config NAME', 'matcher configuraton') do |name|
|
18
|
+
options[:config] ||= []
|
19
|
+
options[:config] << name
|
20
|
+
end
|
21
|
+
opt.on('-D', '--debug', 'run in debug mode') do
|
22
|
+
$DEBUG = true
|
23
|
+
end
|
24
|
+
opt.on('-h', '--help', 'display this help message') do
|
25
|
+
puts opt
|
26
|
+
exit 0
|
27
|
+
end
|
28
|
+
end.parse!(argv)
|
29
|
+
|
30
|
+
options[:files] = argv
|
31
|
+
|
32
|
+
command = Command.new(options)
|
33
|
+
command.run
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
class Command
|
38
|
+
|
39
|
+
#
|
40
|
+
def initialize(options)
|
41
|
+
@files = nil
|
42
|
+
@output = nil
|
43
|
+
@format = 'highchart'
|
44
|
+
@config = default_config_files
|
45
|
+
|
46
|
+
options.each do |k,v|
|
47
|
+
send("#{k}=", v)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Files to include in analysis.
|
52
|
+
attr_accessor :files
|
53
|
+
|
54
|
+
# The output format (json, yaml, highchart).
|
55
|
+
attr_accessor :format
|
56
|
+
|
57
|
+
# List of configuration files.
|
58
|
+
attr_accessor :config
|
59
|
+
|
60
|
+
# Output file.
|
61
|
+
attr_reader :output
|
62
|
+
|
63
|
+
# Set output file name.
|
64
|
+
def output=(file)
|
65
|
+
@output = file
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
def run
|
70
|
+
save
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
#
|
76
|
+
def default_config_files
|
77
|
+
Dir['.locat{,/*.rb}'].select{ |f| File.file?(f) }
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
def matcher
|
82
|
+
@matcher ||= Matcher.new(*config)
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
def counter
|
87
|
+
@counter ||= Counter.new(matcher, :files=>files)
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
def template
|
92
|
+
@template ||= Template.new(counter)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Save.
|
96
|
+
def save
|
97
|
+
case format.downcase
|
98
|
+
when 'json'
|
99
|
+
json = counter.to_h.to_json
|
100
|
+
save_file(json)
|
101
|
+
when 'yaml'
|
102
|
+
yaml = counter.to_h.to_yaml
|
103
|
+
save_file(yaml)
|
104
|
+
else
|
105
|
+
html = template.render(format)
|
106
|
+
save_file(html)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
def save_file(text)
|
112
|
+
if output
|
113
|
+
FileUtils.mkdir_p(File.dirname(output))
|
114
|
+
File.open(File.join(output), 'w') do |f|
|
115
|
+
f << text
|
116
|
+
end
|
117
|
+
else
|
118
|
+
puts text
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
#
|
123
|
+
def template_dir
|
124
|
+
File.dirname(__FILE__) + '/locat/template'
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module LOCat
|
2
|
+
|
3
|
+
#
|
4
|
+
class Counter
|
5
|
+
|
6
|
+
#
|
7
|
+
def initialize(matcher, options)
|
8
|
+
@matcher = matcher
|
9
|
+
|
10
|
+
options.each do |k,v|
|
11
|
+
send("#{k}=", v)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
attr :matcher
|
17
|
+
|
18
|
+
#
|
19
|
+
attr :files
|
20
|
+
|
21
|
+
#
|
22
|
+
def files=(files)
|
23
|
+
@files = files.map{ |f| Dir[f] }.flatten
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
def counts
|
28
|
+
@counts ||= (
|
29
|
+
table = Hash.new{ |h,k| h[k] = 0 }
|
30
|
+
matcher.each do |pattern, block|
|
31
|
+
files = resolve_files(pattern)
|
32
|
+
files.each do |file|
|
33
|
+
File.readlines(file).each do |line|
|
34
|
+
line = line.chomp("\n")
|
35
|
+
group = block.call(file, line)
|
36
|
+
if group
|
37
|
+
table[group.to_s] += 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
table
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
def total
|
48
|
+
@total = counts.values.inject(0){ |s,v| s+=v; s }
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
def loc
|
53
|
+
@loc ||= (
|
54
|
+
a = []
|
55
|
+
a << [nil , *counts.keys]
|
56
|
+
a << ['loc', *counts.values]
|
57
|
+
a
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Compute the ratios between each group,
|
62
|
+
# returning a two-dimensional array.
|
63
|
+
def ratio
|
64
|
+
@ratio ||= (
|
65
|
+
rcounts = counts.dup
|
66
|
+
rcounts['Total'] = total
|
67
|
+
r = Array.new(rcounts.size+1){ [] }
|
68
|
+
x = 1
|
69
|
+
rcounts.each do |type1, count1|
|
70
|
+
r[x][0] = type1
|
71
|
+
y = 1
|
72
|
+
rcounts.each do |type2, count2|
|
73
|
+
r[0][y] = type2
|
74
|
+
if count2 == 0 or count2 == 0
|
75
|
+
r[x][y] = 0
|
76
|
+
else
|
77
|
+
r[x][y] = (1000 * (count2.to_f / count1.to_f)).round.to_f / 1000.0
|
78
|
+
end
|
79
|
+
y += 1
|
80
|
+
end
|
81
|
+
x += 1
|
82
|
+
end
|
83
|
+
r
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Calculate the percentages.
|
88
|
+
def percent
|
89
|
+
@percent ||= (
|
90
|
+
a = []
|
91
|
+
a << [nil , *counts.keys]
|
92
|
+
a << ['%', *counts.values.map{ |f| 100 * f / total }]
|
93
|
+
a
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
def to_h
|
99
|
+
h = {}
|
100
|
+
h['loc'] = loc
|
101
|
+
h['pcnt'] = percent
|
102
|
+
h['ratio'] = ratio
|
103
|
+
h['scm'] = scm if scm?
|
104
|
+
h
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
#def to_json
|
109
|
+
# to_h.to_json
|
110
|
+
#end
|
111
|
+
|
112
|
+
#
|
113
|
+
def gitloc
|
114
|
+
@gitloc ||= GitLOC.new(matcher)
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
def scm?
|
119
|
+
File.directory?('.git')
|
120
|
+
end
|
121
|
+
|
122
|
+
#
|
123
|
+
def scm
|
124
|
+
if scm?
|
125
|
+
gitloc.timeline_table
|
126
|
+
else
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
# Resolve file glob.
|
134
|
+
def resolve_files(pattern)
|
135
|
+
case pattern
|
136
|
+
when String
|
137
|
+
Dir['**/*'].select{ |path| File.fnmatch(pattern, path) }
|
138
|
+
when Regexp
|
139
|
+
Dir['**/*'].select{ |path| pattern =~ path }
|
140
|
+
else
|
141
|
+
[]
|
142
|
+
end
|
143
|
+
#if File.directory?(glob)
|
144
|
+
# glob = File.join(glob, '**', '*')
|
145
|
+
#end
|
146
|
+
#if files.nil? or files.empty?
|
147
|
+
# Dir[glob].flatten
|
148
|
+
#else
|
149
|
+
# Dir[glob].flatten & files
|
150
|
+
#end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
data/lib/locat/gitloc.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
#require 'active_support'
|
3
|
+
require 'grit'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module LOCat
|
8
|
+
|
9
|
+
# Based on `git-line-count.rb` by Tieg Zaharia
|
10
|
+
class GitLOC
|
11
|
+
|
12
|
+
include Grit
|
13
|
+
|
14
|
+
MAX_COMMITS = 1_000_000
|
15
|
+
OUTPUT_FILE = "gitloc.html"
|
16
|
+
#FILE_EXTENSION = /\.(rb|js)$/
|
17
|
+
#EXCLUDED_FILES = %w(files.js to.js exclude.js).map{ |str| Regexp.escape(str) }.join('|')
|
18
|
+
#EXCLUDED = /#{EXCLUDED_FILES}/i
|
19
|
+
PER_DAY = false # true = show 1-commit-per-day, false = show all commits
|
20
|
+
DATA_POINTS = 25
|
21
|
+
|
22
|
+
attr :matcher
|
23
|
+
|
24
|
+
attr :repo
|
25
|
+
|
26
|
+
#
|
27
|
+
def initialize(matcher)
|
28
|
+
@matcher = matcher
|
29
|
+
@repo = Repo.new(".")
|
30
|
+
|
31
|
+
@data_points = DATA_POINTS
|
32
|
+
@output_file = OUTPUT_FILE
|
33
|
+
#@file_extension = FILE_EXTENSION
|
34
|
+
#@exclude = EXCLUDED
|
35
|
+
@per_day = PER_DAY
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
def title
|
40
|
+
"'#{repo.head.name}' branch JavaScript LOC #{@per_day ? 'per day' : 'per commit'}"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Count lines by groups.
|
44
|
+
def recursive_loc_count(dir, tree_or_blob, total_count=nil)
|
45
|
+
total_count ||= Hash.new{|h,k| h[k]=0 }
|
46
|
+
if tree_or_blob.is_a?(Grit::Tree) # directory
|
47
|
+
tree_or_blob.contents.each do |tob|
|
48
|
+
dname = dir ? File.join(dir, tree_or_blob.name) : tree_or_blob.name
|
49
|
+
recursive_loc_count(dname, tob, total_count)
|
50
|
+
end
|
51
|
+
elsif tree_or_blob.is_a?(Grit::Blob) # file
|
52
|
+
file = dir ? File.join(dir, tree_or_blob.name) : tree_or_blob.name
|
53
|
+
matcher.each do |glob, block|
|
54
|
+
if File.fnmatch?(glob, file)
|
55
|
+
tree_or_blob.data.lines.each do |line|
|
56
|
+
group = block.call(file, line)
|
57
|
+
if group
|
58
|
+
total_count[group] += 1
|
59
|
+
end
|
60
|
+
#total_count['Total'] += 1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
else
|
65
|
+
# what is it then?
|
66
|
+
end
|
67
|
+
total_count
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
def commits_with_loc
|
72
|
+
@commits_with_loc ||= (
|
73
|
+
table = []
|
74
|
+
total_count = Hash.new{|h,k|h[k]=0} # gets reset every commit
|
75
|
+
current_date = nil # gets reset every commit
|
76
|
+
|
77
|
+
size = repo.commits('master', MAX_COMMITS).size
|
78
|
+
mod = size < @data_points ? 1 : (size / @data_points).round
|
79
|
+
|
80
|
+
# puts repo.commits[0].methods.sort.join(', ')
|
81
|
+
repo.commits('master', MAX_COMMITS).each_with_index do |commit, index|
|
82
|
+
next unless index % mod == 0
|
83
|
+
|
84
|
+
total_count = Hash.new{|h,k|h[k]=0}
|
85
|
+
this_date = commit.committed_date.to_date
|
86
|
+
if !@per_day || (@per_day && this_date != current_date)
|
87
|
+
# Record this commit as end-of-day commit
|
88
|
+
current_date = this_date
|
89
|
+
commit.tree.contents.each do |tob|
|
90
|
+
recursive_loc_count(nil, tob, total_count)
|
91
|
+
end
|
92
|
+
table << {
|
93
|
+
:date => commit.committed_date.to_datetime,
|
94
|
+
:id => commit.id,
|
95
|
+
:loc => total_count
|
96
|
+
}
|
97
|
+
else
|
98
|
+
# The day this commits falls on has already been recorded
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
table.reverse
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
def timeline_table
|
108
|
+
#mod = 7
|
109
|
+
th = [nil]
|
110
|
+
commits_with_loc.each_with_index do |commit, index|
|
111
|
+
#if index % mod == 0 || index == commits_with_loc.size - 1
|
112
|
+
# th << commit[:date].strftime("%-y %-m %-d")
|
113
|
+
#else
|
114
|
+
th << commit[:date].strftime("%y %m %d")
|
115
|
+
#end
|
116
|
+
end
|
117
|
+
tg = []
|
118
|
+
groups = []
|
119
|
+
commits_with_loc.each do |commit|
|
120
|
+
groups = groups | commit[:loc].keys
|
121
|
+
end
|
122
|
+
groups.each_with_index do |g, i|
|
123
|
+
tg[i] = [g]
|
124
|
+
end
|
125
|
+
tt = ['Total']
|
126
|
+
commits_with_loc.each do |commit|
|
127
|
+
sum = 0
|
128
|
+
groups.each_with_index do |g, i|
|
129
|
+
tg[i] << commit[:loc][g]
|
130
|
+
sum += commit[:loc][g]
|
131
|
+
end
|
132
|
+
tt << sum
|
133
|
+
end
|
134
|
+
[th] + tg + [tt]
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|