├── DEMO.md ├── var ├── name ├── title ├── created ├── version ├── organization ├── summary ├── authors ├── copyrights ├── repositories ├── requirements ├── description └── resources ├── Gemfile ├── bin └── locat ├── .gitignore ├── .travis.yml ├── demo ├── fixtures │ ├── lib │ │ └── example.rb │ └── .locat ├── applique │ └── locat.rb └── testrun.rdoc ├── .locat ├── lib ├── locat.rb └── locat │ ├── matcher.rb │ ├── template │ ├── javascript.js │ ├── highchart.rhtml │ └── graphael.rhtml │ ├── template.rb │ ├── counter.rb │ ├── command.rb │ └── gitloc.rb ├── MANIFEST ├── Toolchain ├── README.md ├── LICENSE.txt ├── .index ├── HISTORY.md └── .gemspec /DEMO.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /var/name: -------------------------------------------------------------------------------- 1 | locat -------------------------------------------------------------------------------- /var/title: -------------------------------------------------------------------------------- 1 | LOCat -------------------------------------------------------------------------------- /var/created: -------------------------------------------------------------------------------- 1 | 2011-07-07 -------------------------------------------------------------------------------- /var/version: -------------------------------------------------------------------------------- 1 | 0.2.4 2 | -------------------------------------------------------------------------------- /var/organization: -------------------------------------------------------------------------------- 1 | Rubyworks -------------------------------------------------------------------------------- /var/summary: -------------------------------------------------------------------------------- 1 | Lines of Code Attache -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /var/authors: -------------------------------------------------------------------------------- 1 | --- 2 | - Trans 3 | -------------------------------------------------------------------------------- /var/copyrights: -------------------------------------------------------------------------------- 1 | --- 2 | - (c) 2011 Rubyworks (BSD-2-Clause) 3 | -------------------------------------------------------------------------------- /bin/locat: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'locat' 3 | LOCat.cli(*ARGV) 4 | -------------------------------------------------------------------------------- /var/repositories: -------------------------------------------------------------------------------- 1 | --- 2 | upstream: git://github.com/rubyworks/locat.git 3 | -------------------------------------------------------------------------------- /var/requirements: -------------------------------------------------------------------------------- 1 | --- 2 | - gitlab-grit 3 | - ansi 4 | - json 5 | - detroit (build) 6 | - qed (test) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .reap/digest 2 | .yardoc 3 | doc/ 4 | log/ 5 | pkg/ 6 | tmp/ 7 | web/ 8 | DEMO.* 9 | *.gem 10 | Gemfile.lock 11 | 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | script: "bundle exec qed" 3 | rvm: 4 | - 1.8.7 5 | - 1.9.2 6 | - 1.9.3 7 | - rbx-2.0 8 | - jruby 9 | - ree 10 | 11 | -------------------------------------------------------------------------------- /demo/fixtures/lib/example.rb: -------------------------------------------------------------------------------- 1 | # Just an example 2 | class Example 3 | # example method 4 | def self.example 5 | puts "Surprise! It's an example!" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /var/description: -------------------------------------------------------------------------------- 1 | LOCat is a customizable Lines-Of-Code analysis tool. LOC might not be the most useful metric in the universe, but it still provides useful information and can be a lot of fun. -------------------------------------------------------------------------------- /var/resources: -------------------------------------------------------------------------------- 1 | --- 2 | home: http://rubyworks.github.com/locat 3 | code: http://github.com/rubyworks/locat 4 | docs: http://rubydoc.info/gems/locat/frames 5 | bugs: http://github.com/rubyworks/locat/issues 6 | mail: http://groups.google.com/group/rubyworks-mailinglist 7 | -------------------------------------------------------------------------------- /.locat: -------------------------------------------------------------------------------- 1 | 2 | match 'lib/**.rb' do |file, line| 3 | case line 4 | when /^\s*#/ 5 | 'Comment' 6 | when /^\s*$/ 7 | 'Blank' 8 | else 9 | 'Code' 10 | end 11 | end 12 | 13 | match 'test/**.rb' do |file, line| 14 | unless line.strip.empty? 15 | 'Test' 16 | end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /demo/fixtures/.locat: -------------------------------------------------------------------------------- 1 | 2 | match 'lib/**.rb' do |file, line| 3 | case line 4 | when /^\s*#/ 5 | 'Comment' 6 | when /^\s*$/ 7 | 'Blank' 8 | else 9 | 'Code' 10 | end 11 | end 12 | 13 | match 'test/**.rb' do |file, line| 14 | unless line.strip.empty? 15 | 'Test' 16 | end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /demo/applique/locat.rb: -------------------------------------------------------------------------------- 1 | require 'locat' 2 | require 'fileutils' 3 | 4 | When 'Given an example ruby project' do 5 | FileUtils.rm 'locat.html' if File.exist?('locat.html') 6 | FileUtils.cp_r(File.dirname(__FILE__) + '/../fixtures/.', '.') 7 | end 8 | 9 | When 'with a `.locat` file' do |text| 10 | File.open('.locat', 'w'){ |f| f << text } 11 | end 12 | 13 | -------------------------------------------------------------------------------- /lib/locat.rb: -------------------------------------------------------------------------------- 1 | if defined?(Encoding) 2 | Encoding.default_external = Encoding::UTF_8 3 | end 4 | 5 | module LOCat 6 | 7 | require 'json' 8 | require 'optparse' 9 | require 'fileutils' 10 | require 'erb' 11 | 12 | require 'locat/matcher' 13 | require 'locat/counter' 14 | require 'locat/template' 15 | require 'locat/gitloc' 16 | require 'locat/command' 17 | 18 | end 19 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | #!mast .index .yardopts bin lib man qed spec test *.md *.txt 2 | .index 3 | bin/locat 4 | lib/locat/command.rb 5 | lib/locat/counter.rb 6 | lib/locat/gitloc.rb 7 | lib/locat/matcher.rb 8 | lib/locat/template/graphael.rhtml 9 | lib/locat/template/highchart.rhtml 10 | lib/locat/template/javascript.js 11 | lib/locat/template.rb 12 | lib/locat.rb 13 | README.md 14 | HISTORY.md 15 | DEMO.md 16 | LICENSE.txt 17 | -------------------------------------------------------------------------------- /demo/testrun.rdoc: -------------------------------------------------------------------------------- 1 | == Test Run 2 | 3 | Given an example ruby project with a `.locat` file: 4 | 5 | match 'lib/**.rb' do |file, line| 6 | case line 7 | when /^\s*#/ 8 | 'Comment' 9 | when /^\s*$/ 10 | 'Blank' 11 | else 12 | 'Code' 13 | end 14 | end 15 | 16 | Running the locat command should produce the expected HTML file. 17 | 18 | LOCat.cli('--output', 'locat.html') 19 | 20 | File.assert.exist?('locat.html') 21 | 22 | -------------------------------------------------------------------------------- /Toolchain: -------------------------------------------------------------------------------- 1 | --- 2 | github: 3 | gh_pages: web 4 | 5 | gem: 6 | active: true 7 | 8 | yard: 9 | yardopts: true 10 | priority: 2 11 | 12 | qedoc: 13 | files: demo 14 | output: DEMO.md 15 | title: LOCat Demonstrations 16 | 17 | qed: 18 | active: true 19 | 20 | locat: 21 | output: web/locat.html 22 | 23 | dnote: 24 | title: Developer's Notes 25 | output: log/notes.html 26 | 27 | vclog: 28 | output: 29 | - log/history.html 30 | - log/changes.html 31 | 32 | email: 33 | mailto: 34 | - ruby-talk@ruby-lang.org 35 | - rubyworks-mailinglist@googlegroups.com 36 | 37 | -------------------------------------------------------------------------------- /lib/locat/matcher.rb: -------------------------------------------------------------------------------- 1 | module LOCat 2 | 3 | # 4 | class Matcher 5 | include Enumerable 6 | 7 | # 8 | def initialize(*config_files) 9 | @rules = [] 10 | 11 | if config_files.empty? 12 | default 13 | end 14 | 15 | config_files.each do |f| 16 | instance_eval(File.read(f)) 17 | end 18 | end 19 | 20 | # 21 | attr :rules 22 | 23 | # 24 | def match(files, &block) 25 | @rules << [files, block] 26 | end 27 | 28 | # 29 | def each(&block) 30 | @rules.each(&block) 31 | end 32 | 33 | # 34 | def size 35 | @rules.size 36 | end 37 | 38 | private 39 | 40 | # Default configuration if none is supplied by the project. 41 | def default 42 | match 'lib/**.rb' do |file, line| 43 | case line 44 | when /^\s*#/ 45 | 'Comment' 46 | when /^\s*$/ 47 | 'Blank' 48 | else 49 | 'Code' 50 | end 51 | end 52 | match 'test/**.rb' do |file, line| 53 | case line 54 | when /^\s*#/ 55 | 'Comment' 56 | when /^\s*$/ 57 | 'Blank' 58 | else 59 | 'Test' 60 | end 61 | end 62 | end 63 | 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LOCat 2 | 3 | [Homepage](http://rubyworks.github.com/locat) | 4 | [Development](http://github.com/rubyworks/locat) | 5 | [Issue Tracker](http://github.com/rubyworks/locat/issues) | 6 | [Mailing List](http://groups.google.com/group/rubyworks-mailinglist) 7 | 8 | [](http://travis-ci.org/rubyworks/locat) 9 | 10 | 11 | ## DESCRIPTION 12 | 13 | LOCat is a fancy Lines-Of-Code analysis tool. 14 | 15 | 16 | ## SYNOPSIS 17 | 18 | Define a `.locat` Ruby script in your project, e.g. 19 | 20 | match 'lib/**.rb' do |file, line| 21 | case line 22 | when /^\s*#/ 23 | 'Comment' 24 | when /^\s*$/ 25 | 'Blank' 26 | else 27 | 'Code' 28 | end 29 | end 30 | 31 | match 'test/**.rb' do |file, line| 32 | case line 33 | when /^\s*#/ 34 | 'Comment' 35 | when /^\s*$/ 36 | 'Blank' 37 | else 38 | 'Test' 39 | end 40 | end 41 | 42 | Then run `locat`, e.g. 43 | 44 | $ locat -o locat.html 45 | 46 | The config file can also be located at `etc/locat.rb` or `config/locat.rb`. 47 | 48 | 49 | ## COPYRIGHT 50 | 51 | Copyright (c) 2011 Rubyworks 52 | 53 | BSD 2-Clause License 54 | 55 | See LICENSE.md for details. 56 | 57 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD-2-Clause License 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are 4 | permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of 7 | conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 10 | of conditions and the following disclaimer in the documentation and/or other materials 11 | provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY Thomas Sawyer ``AS IS'' AND ANY EXPRESS OR IMPLIED 14 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 15 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Thomas Sawyer OR 16 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 17 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 18 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 20 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 21 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | -------------------------------------------------------------------------------- /.index: -------------------------------------------------------------------------------- 1 | --- 2 | revision: 2013 3 | type: ruby 4 | sources: 5 | - var 6 | authors: 7 | - name: Trans 8 | email: transfire@gmail.com 9 | organizations: [] 10 | requirements: 11 | - name: gitlab-grit 12 | - name: ansi 13 | - name: json 14 | - groups: 15 | - build 16 | development: true 17 | name: detroit 18 | - groups: 19 | - test 20 | development: true 21 | name: qed 22 | conflicts: [] 23 | alternatives: [] 24 | resources: 25 | - type: home 26 | uri: http://rubyworks.github.com/locat 27 | label: Homepage 28 | - type: code 29 | uri: http://github.com/rubyworks/locat 30 | label: Source Code 31 | - type: docs 32 | uri: http://rubydoc.info/gems/locat/frames 33 | label: Documentation 34 | - type: bugs 35 | uri: http://github.com/rubyworks/locat/issues 36 | label: Issue Tracker 37 | - type: mail 38 | uri: http://groups.google.com/group/rubyworks-mailinglist 39 | label: Mailing List 40 | repositories: 41 | - name: upstream 42 | scm: git 43 | uri: git://github.com/rubyworks/locat.git 44 | categories: [] 45 | copyrights: 46 | - holder: Rubyworks 47 | year: '2011' 48 | license: BSD-2-Clause 49 | customs: [] 50 | paths: 51 | lib: 52 | - lib 53 | name: locat 54 | title: LOCat 55 | summary: Lines of Code Attache 56 | created: '2011-07-07' 57 | description: LOCat is a customizable Lines-Of-Code analysis tool. LOC might not be 58 | the most useful metric in the universe, but it still provides useful information 59 | and can be a lot of fun. 60 | version: 0.2.4 61 | date: '2014-01-14' 62 | -------------------------------------------------------------------------------- /lib/locat/template/javascript.js: -------------------------------------------------------------------------------- 1 | 2 | function create_chart(chart_id, chart_table, chart_type){ 3 | options = chart_options(chart_id, chart_table, chart_type); 4 | return(new Highcharts.Chart(options)); 5 | }; 6 | 7 | function chart_options(chart_id, chart_table, chart_type){ 8 | options = { 9 | chart: { 10 | renderTo: 'container-' + chart_id, 11 | type: chart_type 12 | }, 13 | title: { 14 | text: chart_id 15 | }, 16 | tooltip: { 17 | enable: true 18 | }, 19 | xAxis: { 20 | categories: [] 21 | }, 22 | yAxis: { 23 | title: { 24 | text: '' //chart_id 25 | } 26 | }, 27 | series: [] 28 | }; 29 | 30 | $.each(chart_table, function(lineNo, line) { 31 | // header line containes categories 32 | if (lineNo == 0) { 33 | $.each(line, function(itemNo, item) { 34 | if (itemNo > 0) options.xAxis.categories.push(item); 35 | }); 36 | } 37 | // the rest of the lines contain data with their name in the first position 38 | else { 39 | var series = { 40 | data: [] 41 | }; 42 | $.each(line, function(itemNo, item) { 43 | if (itemNo == 0) { 44 | series.name = item; 45 | //series.type = chart_type; 46 | } else { 47 | series.data.push(parseFloat(item)); 48 | } 49 | }); 50 | options.series.push(series); 51 | } 52 | }); 53 | return(options); 54 | }; 55 | 56 | -------------------------------------------------------------------------------- /lib/locat/template.rb: -------------------------------------------------------------------------------- 1 | module LOCat 2 | 3 | # 4 | class Template 5 | 6 | # 7 | DIRECTORY = File.dirname(__FILE__) + '/template' 8 | 9 | # 10 | def initialize(counter, metadata) 11 | @counter = counter 12 | @metadata = metadata 13 | end 14 | 15 | # 16 | attr :counter 17 | 18 | # 19 | attr :metadata 20 | 21 | # 22 | def total 23 | counter.total 24 | end 25 | 26 | # 27 | def counts 28 | counter.counts 29 | end 30 | 31 | # 32 | def table_loc 33 | counter.loc 34 | end 35 | 36 | # 37 | def table_pcnt 38 | counter.percent 39 | end 40 | 41 | # 42 | alias_method :table_percentages, :table_pcnt 43 | 44 | # 45 | def table_ratio 46 | counter.ratio 47 | end 48 | 49 | # 50 | def table_scm 51 | counter.scm 52 | end 53 | 54 | # 55 | def to_json 56 | h = {} 57 | h[:loc] = table_loc 58 | h[:pcnt] = table_pcnt 59 | h[:ratio] = table_ratio 60 | h[:scm] = table_scm if scm? 61 | h.to_json 62 | end 63 | 64 | # 65 | def scm? 66 | File.directory?('.git') 67 | end 68 | 69 | # 70 | def title 71 | "The LOCat on " + (metadata['title'] || File.basename(Dir.pwd)) 72 | end 73 | 74 | # 75 | def javascript 76 | @javascript ||= ( 77 | File.read(File.join(DIRECTORY, 'javascript.js')) 78 | ) 79 | end 80 | 81 | # 82 | def render(template) 83 | file = File.join(DIRECTORY, template + '.rhtml') 84 | erb = ERB.new(File.read(file)) 85 | erb.result(__binding__) 86 | end 87 | 88 | private 89 | 90 | # 91 | def __binding__ 92 | binding 93 | end 94 | 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # RELEASE HISTORY 2 | 3 | ## 0.2.4 | 2014-01-14 4 | 5 | This release switch use of grit gem to gitlab-grit. This release also 6 | includes a simple correctiotn to the copyright notices at the bottom 7 | of the generated templates. 8 | 9 | Changes: 10 | 11 | * Use gitlab-grit instead of grit gem. 12 | * Fix copyright notices in templates. 13 | 14 | 15 | ## 0.2.3 | 2014-01-13 16 | 17 | This is a minor release that deals with a couple of minor issues, 18 | albeit the issue with a broken grit dependency can cause locat to crash. 19 | 20 | Changes: 21 | 22 | * If Grit fails then gracely skip git timeline analysis. 23 | * Don't try to create logging directory if it already exists. 24 | * Fix missing YAML requirement. 25 | 26 | 27 | ## 0.2.0 | 2011-11-04 28 | 29 | This release upgrades the HighCharts javascript dependency a notch 30 | to v2.1.6, and provides a default configuration when none is provided 31 | by a project --which makes it even easier to get started using LOCat! 32 | 33 | Changes: 34 | 35 | * Use default configuration if none supplied by project. 36 | * Update HighCharts dependency to 2.1.6. 37 | 38 | 39 | ## 0.1.2 | 2011-07-11 40 | 41 | Doh. Can I not write one line of code with a bug creeping 42 | into it! Any how, this release just fixes a typo in the CLI 43 | parsing code, and adds a default encoding for Ruby 1.9. 44 | 45 | Changes: 46 | 47 | * Fix missing bracket in cli options parsing. 48 | * Set default encoding. 49 | 50 | 51 | ## 0.1.1 | 2011-07-10 52 | 53 | This release fixes a few issues reported by friendly cats. 54 | It also adds a new command line option to set the document 55 | title. 56 | 57 | Changes: 58 | 59 | * Title can now be set via cli. 60 | * Fix Time#to_date missing method. #3 61 | * Fix metadata['title'] nil value. #3 62 | * Add grit dependency. #2 63 | 64 | 65 | ## 0.1.0 | 2011-07-08 | The Cat's Meow 66 | 67 | This is the first release of LOCat a 68 | fancy LOC analysis tool. 69 | 70 | Changes: 71 | 72 | * Its all "change" at this point! 73 | 74 | -------------------------------------------------------------------------------- /lib/locat/counter.rb: -------------------------------------------------------------------------------- 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 | return nil unless scm? 125 | begin 126 | gitloc.timeline_table 127 | rescue 128 | warn "Can't generate git timeline. Skipping this portion." 129 | nil 130 | end 131 | end 132 | 133 | private 134 | 135 | # Resolve file glob. 136 | def resolve_files(pattern) 137 | case pattern 138 | when String 139 | Dir['**/*'].select{ |path| File.fnmatch(pattern, path) } 140 | when Regexp 141 | Dir['**/*'].select{ |path| pattern =~ path } 142 | else 143 | [] 144 | end 145 | #if File.directory?(glob) 146 | # glob = File.join(glob, '**', '*') 147 | #end 148 | #if files.nil? or files.empty? 149 | # Dir[glob].flatten 150 | #else 151 | # Dir[glob].flatten & files 152 | #end 153 | end 154 | 155 | end 156 | 157 | end 158 | -------------------------------------------------------------------------------- /lib/locat/command.rb: -------------------------------------------------------------------------------- 1 | module LOCat 2 | 3 | require 'yaml' 4 | 5 | # 6 | def self.cli(*argv) 7 | options = {} 8 | 9 | OptionParser.new do |opt| 10 | opt.on('-o', '--output FILE', 'output file') do |output| 11 | options[:output] = output 12 | end 13 | opt.on('-j', '--json', 'output JSON formmated data') do 14 | options[:format] = 'json' 15 | end 16 | opt.on('-y', '--yaml', 'output YAML formmated data') do 17 | options[:format] = 'yaml' 18 | end 19 | opt.on('-c', '--config NAME', 'matcher configuraton') do |name| 20 | options[:config] ||= [] 21 | options[:config] << name 22 | end 23 | opt.on('-t', '--title TITLE', 'title to put on report') do |title| 24 | options[:title] = title 25 | end 26 | opt.on('-D', '--debug', 'run in debug mode') do 27 | $DEBUG = true 28 | end 29 | opt.on('-h', '--help', 'display this help message') do 30 | puts opt 31 | exit 0 32 | end 33 | end.parse!(argv) 34 | 35 | options[:files] = argv 36 | 37 | command = Command.new(options) 38 | command.run 39 | end 40 | 41 | # 42 | class Command 43 | 44 | # 45 | def initialize(options) 46 | @files = nil 47 | @output = nil 48 | @format = 'highchart' 49 | @config = default_config_files 50 | @title = nil 51 | 52 | options.each do |k,v| 53 | send("#{k}=", v) 54 | end 55 | end 56 | 57 | # Files to include in analysis. 58 | attr_accessor :files 59 | 60 | # The output format (json, yaml, highchart). 61 | attr_accessor :format 62 | 63 | # List of configuration files. 64 | attr_accessor :config 65 | 66 | # List of configuration files. 67 | attr_accessor :title 68 | 69 | # Output file. 70 | attr_reader :output 71 | 72 | # Set output file name. 73 | def output=(file) 74 | @output = file 75 | end 76 | 77 | # 78 | def run 79 | save 80 | end 81 | 82 | private 83 | 84 | # 85 | def default_config_files 86 | files = [] 87 | files.concat Dir['.locat{,.rb}'] 88 | files.concat Dir['{etc/,config/}locat.rb'] 89 | files.select{ |f| File.file?(f) } 90 | end 91 | 92 | # 93 | def matcher 94 | @matcher ||= Matcher.new(*([config].flatten)) 95 | end 96 | 97 | # 98 | def counter 99 | @counter ||= Counter.new(matcher, :files=>files) 100 | end 101 | 102 | # Access to .index metadata. This only really cares about 103 | # the `title` field (at this point). If no `.index` file 104 | # is found, the title is set to the basename of the current 105 | # working directory. 106 | def metadata 107 | @metadata ||= ( 108 | if File.file?('.index') 109 | data = YAML.load(File.new('.index')) 110 | else 111 | data = {} 112 | end 113 | data['title'] = title if title 114 | data 115 | ) 116 | end 117 | 118 | # 119 | def template 120 | @template ||= Template.new(counter, metadata) 121 | end 122 | 123 | # Save. 124 | def save 125 | case format.downcase 126 | when 'json' 127 | json = counter.to_h.to_json 128 | save_file(json) 129 | when 'yaml' 130 | yaml = counter.to_h.to_yaml 131 | save_file(yaml) 132 | else 133 | html = template.render(format) 134 | save_file(html) 135 | end 136 | end 137 | 138 | # 139 | def save_file(text) 140 | if output 141 | dir = File.dirname(output) 142 | FileUtils.mkdir_p(dir) unless File.directory?(dir) 143 | File.open(File.join(output), 'w') do |f| 144 | f << text 145 | end 146 | else 147 | puts text 148 | end 149 | end 150 | 151 | # 152 | def template_dir 153 | File.dirname(__FILE__) + '/locat/template' 154 | end 155 | 156 | end 157 | 158 | end 159 | -------------------------------------------------------------------------------- /lib/locat/gitloc.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'grit' 3 | require 'open-uri' 4 | require 'fileutils' 5 | require 'date' 6 | 7 | # Fix 1.8.7 bug 8 | class Time 9 | public :to_date 10 | end 11 | 12 | module LOCat 13 | 14 | # Based on `git-line-count.rb` by Tieg Zaharia 15 | class GitLOC 16 | 17 | include Grit 18 | 19 | MAX_COMMITS = 1_000_000 20 | OUTPUT_FILE = "gitloc.html" 21 | #FILE_EXTENSION = /\.(rb|js)$/ 22 | #EXCLUDED_FILES = %w(files.js to.js exclude.js).map{ |str| Regexp.escape(str) }.join('|') 23 | #EXCLUDED = /#{EXCLUDED_FILES}/i 24 | PER_DAY = false # true = show 1-commit-per-day, false = show all commits 25 | DATA_POINTS = 25 26 | 27 | attr :matcher 28 | 29 | attr :repo 30 | 31 | # 32 | def initialize(matcher) 33 | @matcher = matcher 34 | @repo = Repo.new(".") 35 | 36 | @data_points = DATA_POINTS 37 | @output_file = OUTPUT_FILE 38 | #@file_extension = FILE_EXTENSION 39 | #@exclude = EXCLUDED 40 | @per_day = PER_DAY 41 | end 42 | 43 | # 44 | def title 45 | "'#{repo.head.name}' branch LOC #{@per_day ? 'per day' : 'per commit'}" 46 | end 47 | 48 | # Count lines by groups. 49 | def recursive_loc_count(dir, tree_or_blob, total_count=nil) 50 | total_count ||= Hash.new{|h,k| h[k]=0 } 51 | if tree_or_blob.is_a?(Grit::Tree) # directory 52 | tree_or_blob.contents.each do |tob| 53 | dname = dir ? File.join(dir, tree_or_blob.name) : tree_or_blob.name 54 | recursive_loc_count(dname, tob, total_count) 55 | end 56 | elsif tree_or_blob.is_a?(Grit::Blob) # file 57 | file = dir ? File.join(dir, tree_or_blob.name) : tree_or_blob.name 58 | matcher.each do |glob, block| 59 | if File.fnmatch?(glob, file) 60 | tree_or_blob.data.lines.each do |line| 61 | group = block.call(file, line) 62 | if group 63 | total_count[group] += 1 64 | end 65 | #total_count['Total'] += 1 66 | end 67 | end 68 | end 69 | else 70 | # what is it then? 71 | end 72 | total_count 73 | end 74 | 75 | # 76 | def commits_with_loc 77 | @commits_with_loc ||= ( 78 | table = [] 79 | total_count = Hash.new{|h,k|h[k]=0} # gets reset every commit 80 | current_date = nil # gets reset every commit 81 | 82 | size = repo.commits('master', MAX_COMMITS).size 83 | mod = size < @data_points ? 1 : (size / @data_points).round 84 | 85 | # puts repo.commits[0].methods.sort.join(', ') 86 | repo.commits('master', MAX_COMMITS).each_with_index do |commit, index| 87 | next unless index % mod == 0 88 | 89 | total_count = Hash.new{|h,k|h[k]=0} 90 | this_date = commit.committed_date.to_date 91 | if !@per_day || (@per_day && this_date != current_date) 92 | # Record this commit as end-of-day commit 93 | current_date = this_date 94 | commit.tree.contents.each do |tob| 95 | recursive_loc_count(nil, tob, total_count) 96 | end 97 | table << { 98 | :date => commit.committed_date.to_datetime, 99 | :id => commit.id, 100 | :loc => total_count 101 | } 102 | else 103 | # The day this commits falls on has already been recorded 104 | end 105 | end 106 | 107 | table.reverse 108 | ) 109 | end 110 | 111 | # 112 | def timeline_table 113 | #mod = 7 114 | th = [nil] 115 | commits_with_loc.each_with_index do |commit, index| 116 | #if index % mod == 0 || index == commits_with_loc.size - 1 117 | # th << commit[:date].strftime("%-y %-m %-d") 118 | #else 119 | th << commit[:date].strftime("%y %m %d") 120 | #end 121 | end 122 | tg = [] 123 | groups = [] 124 | commits_with_loc.each do |commit| 125 | groups = groups | commit[:loc].keys 126 | end 127 | groups.each_with_index do |g, i| 128 | tg[i] = [g] 129 | end 130 | tt = ['Total'] 131 | commits_with_loc.each do |commit| 132 | sum = 0 133 | groups.each_with_index do |g, i| 134 | tg[i] << commit[:loc][g] 135 | sum += commit[:loc][g] 136 | end 137 | tt << sum 138 | end 139 | [th] + tg + [tt] 140 | end 141 | 142 | end 143 | 144 | end 145 | -------------------------------------------------------------------------------- /.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'yaml' 4 | require 'pathname' 5 | 6 | module Indexer 7 | 8 | # Convert index data into a gemspec. 9 | # 10 | # Notes: 11 | # * Assumes all executables are in bin/. 12 | # * Does not yet handle default_executable setting. 13 | # * Does not yet handle platform setting. 14 | # * Does not yet handle required_ruby_version. 15 | # * Support for rdoc entries is weak. 16 | # 17 | class GemspecExporter 18 | 19 | # File globs to include in package --unless a manifest file exists. 20 | FILES = ".index .yardopts alt bin data demo ext features lib man spec test try* [A-Z]*.*" unless defined?(FILES) 21 | 22 | # File globs to omit from FILES. 23 | OMIT = "Config.rb" unless defined?(OMIT) 24 | 25 | # Standard file patterns. 26 | PATTERNS = { 27 | :root => '{.index,Gemfile}', 28 | :bin => 'bin/*', 29 | :lib => 'lib/{**/}*', #.rb', 30 | :ext => 'ext/{**/}extconf.rb', 31 | :doc => '*.{txt,rdoc,md,markdown,tt,textile}', 32 | :test => '{test,spec}/{**/}*.rb' 33 | } unless defined?(PATTERNS) 34 | 35 | # For which revision of indexer spec is this converter intended? 36 | REVISION = 2013 unless defined?(REVISION) 37 | 38 | # 39 | def self.gemspec 40 | new.to_gemspec 41 | end 42 | 43 | # 44 | attr :metadata 45 | 46 | # 47 | def initialize(metadata=nil) 48 | @root_check = false 49 | 50 | if metadata 51 | root_dir = metadata.delete(:root) 52 | if root_dir 53 | @root = root_dir 54 | @root_check = true 55 | end 56 | metadata = nil if metadata.empty? 57 | end 58 | 59 | @metadata = metadata || YAML.load_file(root + '.index') 60 | 61 | if @metadata['revision'].to_i != REVISION 62 | warn "This gemspec exporter was not designed for this revision of index metadata." 63 | end 64 | end 65 | 66 | # 67 | def has_root? 68 | root ? true : false 69 | end 70 | 71 | # 72 | def root 73 | return @root if @root || @root_check 74 | @root_check = true 75 | @root = find_root 76 | end 77 | 78 | # 79 | def manifest 80 | return nil unless root 81 | @manifest ||= Dir.glob(root + 'manifest{,.txt}', File::FNM_CASEFOLD).first 82 | end 83 | 84 | # 85 | def scm 86 | return nil unless root 87 | @scm ||= %w{git hg}.find{ |m| (root + ".#{m}").directory? }.to_sym 88 | end 89 | 90 | # 91 | def files 92 | return [] unless root 93 | @files ||= \ 94 | if manifest 95 | File.readlines(manifest). 96 | map{ |line| line.strip }. 97 | reject{ |line| line.empty? || line[0,1] == '#' } 98 | else 99 | list = [] 100 | Dir.chdir(root) do 101 | FILES.split(/\s+/).each do |pattern| 102 | list.concat(glob(pattern)) 103 | end 104 | OMIT.split(/\s+/).each do |pattern| 105 | list = list - glob(pattern) 106 | end 107 | end 108 | list 109 | end.select{ |path| File.file?(path) }.uniq 110 | end 111 | 112 | # 113 | def glob_files(pattern) 114 | return [] unless root 115 | Dir.chdir(root) do 116 | Dir.glob(pattern).select do |path| 117 | File.file?(path) && files.include?(path) 118 | end 119 | end 120 | end 121 | 122 | def patterns 123 | PATTERNS 124 | end 125 | 126 | def executables 127 | @executables ||= \ 128 | glob_files(patterns[:bin]).map do |path| 129 | File.basename(path) 130 | end 131 | end 132 | 133 | def extensions 134 | @extensions ||= \ 135 | glob_files(patterns[:ext]).map do |path| 136 | File.basename(path) 137 | end 138 | end 139 | 140 | def name 141 | metadata['name'] || metadata['title'].downcase.gsub(/\W+/,'_') 142 | end 143 | 144 | def homepage 145 | page = ( 146 | metadata['resources'].find{ |r| r['type'] =~ /^home/i } || 147 | metadata['resources'].find{ |r| r['name'] =~ /^home/i } || 148 | metadata['resources'].find{ |r| r['name'] =~ /^web/i } 149 | ) 150 | page ? page['uri'] : false 151 | end 152 | 153 | def licenses 154 | metadata['copyrights'].map{ |c| c['license'] }.compact 155 | end 156 | 157 | def require_paths 158 | paths = metadata['paths'] || {} 159 | paths['load'] || ['lib'] 160 | end 161 | 162 | # 163 | # Convert to gemnspec. 164 | # 165 | def to_gemspec 166 | if has_root? 167 | Gem::Specification.new do |gemspec| 168 | to_gemspec_data(gemspec) 169 | to_gemspec_paths(gemspec) 170 | end 171 | else 172 | Gem::Specification.new do |gemspec| 173 | to_gemspec_data(gemspec) 174 | to_gemspec_paths(gemspec) 175 | end 176 | end 177 | end 178 | 179 | # 180 | # Convert pure data settings. 181 | # 182 | def to_gemspec_data(gemspec) 183 | gemspec.name = name 184 | gemspec.version = metadata['version'] 185 | gemspec.summary = metadata['summary'] 186 | gemspec.description = metadata['description'] 187 | 188 | metadata['authors'].each do |author| 189 | gemspec.authors << author['name'] 190 | 191 | if author.has_key?('email') 192 | if gemspec.email 193 | gemspec.email << author['email'] 194 | else 195 | gemspec.email = [author['email']] 196 | end 197 | end 198 | end 199 | 200 | gemspec.licenses = licenses 201 | 202 | requirements = metadata['requirements'] || [] 203 | requirements.each do |req| 204 | next if req['optional'] 205 | next if req['external'] 206 | 207 | name = req['name'] 208 | groups = req['groups'] || [] 209 | 210 | version = gemify_version(req['version']) 211 | 212 | if groups.empty? or groups.include?('runtime') 213 | # populate runtime dependencies 214 | if gemspec.respond_to?(:add_runtime_dependency) 215 | gemspec.add_runtime_dependency(name,*version) 216 | else 217 | gemspec.add_dependency(name,*version) 218 | end 219 | else 220 | # populate development dependencies 221 | if gemspec.respond_to?(:add_development_dependency) 222 | gemspec.add_development_dependency(name,*version) 223 | else 224 | gemspec.add_dependency(name,*version) 225 | end 226 | end 227 | end 228 | 229 | # convert external dependencies into gemspec requirements 230 | requirements.each do |req| 231 | next unless req['external'] 232 | gemspec.requirements << ("%s-%s" % req.values_at('name', 'version')) 233 | end 234 | 235 | gemspec.homepage = homepage 236 | gemspec.require_paths = require_paths 237 | gemspec.post_install_message = metadata['install_message'] 238 | end 239 | 240 | # 241 | # Set gemspec settings that require a root directory path. 242 | # 243 | def to_gemspec_paths(gemspec) 244 | gemspec.files = files 245 | gemspec.extensions = extensions 246 | gemspec.executables = executables 247 | 248 | if Gem::VERSION < '1.7.' 249 | gemspec.default_executable = gemspec.executables.first 250 | end 251 | 252 | gemspec.test_files = glob_files(patterns[:test]) 253 | 254 | unless gemspec.files.include?('.document') 255 | gemspec.extra_rdoc_files = glob_files(patterns[:doc]) 256 | end 257 | end 258 | 259 | # 260 | # Return a copy of this file. This is used to generate a local 261 | # .gemspec file that can automatically read the index file. 262 | # 263 | def self.source_code 264 | File.read(__FILE__) 265 | end 266 | 267 | private 268 | 269 | def find_root 270 | root_files = patterns[:root] 271 | if Dir.glob(root_files).first 272 | Pathname.new(Dir.pwd) 273 | elsif Dir.glob("../#{root_files}").first 274 | Pathname.new(Dir.pwd).parent 275 | else 276 | #raise "Can't find root of project containing `#{root_files}'." 277 | warn "Can't find root of project containing `#{root_files}'." 278 | nil 279 | end 280 | end 281 | 282 | def glob(pattern) 283 | if File.directory?(pattern) 284 | Dir.glob(File.join(pattern, '**', '*')) 285 | else 286 | Dir.glob(pattern) 287 | end 288 | end 289 | 290 | def gemify_version(version) 291 | case version 292 | when /^(.*?)\+$/ 293 | ">= #{$1}" 294 | when /^(.*?)\-$/ 295 | "< #{$1}" 296 | when /^(.*?)\~$/ 297 | "~> #{$1}" 298 | else 299 | version 300 | end 301 | end 302 | 303 | end 304 | 305 | end 306 | 307 | Indexer::GemspecExporter.gemspec -------------------------------------------------------------------------------- /lib/locat/template/highchart.rhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LOCat - <%= title %> 5 | 6 | 7 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 36 | 37 | 47 | 48 | 60 | 61 | 62 | 63 |

<%= title %>

64 | 65 | 73 | 74 | 75 | 76 |
77 | 78 |

LINE COUNTS

79 | 80 |
81 | 82 | 83 | 84 | <% counts.keys.each do |h| %> 85 | 86 | <% end %> 87 | 88 | 89 | 90 | 91 | 92 | <% counts.values.each do |v| %> 93 | 94 | <% end %> 95 | 96 | 97 | 98 |
<%= h %>Total
<%= v %><%= total %>
99 |
100 | 101 |
102 | 103 |
104 | Column 105 | Bar 106 | Scatter 107 | 113 | Pie Chart 114 |
115 | 116 |
117 | 118 | 119 | 120 | 165 | 166 | 167 | 168 | 213 | 214 | <% if scm? %> 215 | 216 | 217 | 218 | 237 | 238 | <% end %> 239 | 240 | 241 | 242 | 246 | 247 | 248 | 249 | 250 | -------------------------------------------------------------------------------- /lib/locat/template/graphael.rhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LOCat - <%= config.title %> 5 | 6 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 47 | 48 | 98 | 99 | 100 | 101 |

<%= config.title %>

102 | 103 | 108 | 109 | 110 | 111 |
112 | 113 |

LOC

114 | 115 |
116 | 117 | 118 | 119 | <% counts.keys.each do |h| %> 120 | 121 | <% end %> 122 | 123 | 124 | 125 | 126 | 127 | <% counts.values.each do |v| %> 128 | 129 | <% end %> 130 | 131 | 132 | 133 |
<%= h %>Total
<%= v %><%= total %>
134 |
135 | 136 |
137 |
138 | 139 |
140 | Column 141 | Bar 142 | Scatter 143 | 149 | Pie Chart 150 |
151 | 152 |
153 | 154 | 155 | 156 | 201 | 202 | 203 | 204 | 249 | 250 | 254 | 255 | 256 | 257 | 258 | --------------------------------------------------------------------------------