hotspots 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/LICENSE +7 -0
- data/README.markdown +56 -0
- data/bin/hotspots +8 -0
- data/hotspots.gemspec +24 -0
- data/lib/hotspots.rb +75 -0
- data/lib/hotspots/logger.rb +31 -0
- data/lib/hotspots/options_parser.rb +109 -0
- data/lib/hotspots/repository.rb +2 -0
- data/lib/hotspots/repository/driver.rb +1 -0
- data/lib/hotspots/repository/driver/git.rb +29 -0
- data/lib/hotspots/repository/parser.rb +1 -0
- data/lib/hotspots/repository/parser/git.rb +25 -0
- data/lib/hotspots/store.rb +31 -0
- data/lib/hotspots/version.rb +3 -0
- data/test/hotspots/options_parser_test.rb +152 -0
- data/test/hotspots/repository/parser/git_test.rb +74 -0
- data/test/hotspots/store_test.rb +115 -0
- data/test/hotspots_test.rb +11 -0
- metadata +73 -0
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) <2011> <Chirantan Mitra>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
Overview
|
2
|
+
--------
|
3
|
+
|
4
|
+
This program helps in identifying files with maximum churn in a git repository. The more the number of changes made to a file, the more likelyhood of the file being a source of bug. If the same file is modified many times for bug fixes, it indicates that the file needs some refactoring or redesign love.
|
5
|
+
|
6
|
+
|
7
|
+
Dependencies
|
8
|
+
------------
|
9
|
+
|
10
|
+
This script depends on ruby. Obviously it also depends on git. Ruby and git executables should be in your execution path for the script to run.
|
11
|
+
|
12
|
+
Syntax
|
13
|
+
------
|
14
|
+
|
15
|
+
``` script
|
16
|
+
Usage: ruby hotspots [options]
|
17
|
+
|
18
|
+
Specific options:
|
19
|
+
-t, --time [TIME] Time in days to scan the repository for. Defaults to fifteen
|
20
|
+
-r, --repository [PATH] Path to the repository to scan. Defaults to current path
|
21
|
+
-f, --file-filter [REGEX] Regular expression to filtering file names. All files are allowed when not specified
|
22
|
+
-m, --message-filter [PIPE SEPARATED] Pipe separated values to filter files names against each commit message separated by pipe. All files are allowed when not specified
|
23
|
+
-c, --cutoff [CUTOFF] The minimum occurance to consider for a file to appear in the list. Defaults to zero
|
24
|
+
-v, --verbose Show verbose output
|
25
|
+
--version Show version information
|
26
|
+
-h, --help Show this message
|
27
|
+
```
|
28
|
+
|
29
|
+
Examples
|
30
|
+
--------
|
31
|
+
|
32
|
+
This will give you all file names that contain '.c' and have been modified at least once in the past 15 days in the git repository pointed to by the current path.
|
33
|
+
|
34
|
+
``` script
|
35
|
+
ruby hotspots --file-filter "/.c"
|
36
|
+
```
|
37
|
+
|
38
|
+
*Note that the dot "." is escaped as it is a regular expression matcher.*
|
39
|
+
|
40
|
+
This will give you all file names that contain '.rb' and have been modified at least thrice in the past 5 days in git repository present in 'rails' directory.
|
41
|
+
|
42
|
+
``` script
|
43
|
+
ruby hotspots --file-filter "/.rb" --path rails --cutoff 3 --time 5
|
44
|
+
```
|
45
|
+
|
46
|
+
Running tests
|
47
|
+
-------------
|
48
|
+
|
49
|
+
``` script
|
50
|
+
ruby test/hotspots_test.rb
|
51
|
+
```
|
52
|
+
|
53
|
+
License
|
54
|
+
-------
|
55
|
+
|
56
|
+
This tool is released under the MIT license. Please refer to LICENSE for more details.
|
data/bin/hotspots
ADDED
data/hotspots.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
$:.push File.expand_path("../lib", __FILE__)
|
4
|
+
|
5
|
+
require "hotspots/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "hotspots"
|
9
|
+
s.version = Hotspots::VERSION
|
10
|
+
s.authors = ["Chirantan Mitra"]
|
11
|
+
s.email = ["chirantan.mitra@gmail.com"]
|
12
|
+
s.homepage = "https://github.com/chiku/Hotspot"
|
13
|
+
s.summary = "Find all files that changed over the past in a git repository based on conditions"
|
14
|
+
s.description = <<-EOS
|
15
|
+
Find all files that changed over the past days for a git repository. If the same file is modified over
|
16
|
+
and over again, it may require re-design. Watch out for file changes that don't have a corresponding
|
17
|
+
test change.
|
18
|
+
EOS
|
19
|
+
s.rubyforge_project = "hotspots"
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test}/*`.split("\n")
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
end
|
data/lib/hotspots.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'hotspots/version'
|
2
|
+
require 'hotspots/logger'
|
3
|
+
require 'hotspots/store'
|
4
|
+
require 'hotspots/options_parser'
|
5
|
+
require 'hotspots/repository'
|
6
|
+
|
7
|
+
module Repository
|
8
|
+
class Main
|
9
|
+
attr_reader :logger, :options, :repository, :verbose,
|
10
|
+
:exit_code, :exit_message,
|
11
|
+
:driver, :parser, :store
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@logger = Hotspots::Logger.new
|
15
|
+
@options = Hotspots::OptionsParser.new.parse(*ARGV)
|
16
|
+
@repository = options[:repository]
|
17
|
+
@verbose = options[:verbose]
|
18
|
+
@exit_code = options[:exit][:code]
|
19
|
+
@exit_message = options[:exit][:message]
|
20
|
+
end
|
21
|
+
|
22
|
+
def execute!
|
23
|
+
validate!
|
24
|
+
set
|
25
|
+
run
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate!
|
29
|
+
validate_exit_code!
|
30
|
+
validate_git_repository!
|
31
|
+
end
|
32
|
+
|
33
|
+
def set
|
34
|
+
set_logger
|
35
|
+
set_path
|
36
|
+
assign
|
37
|
+
end
|
38
|
+
|
39
|
+
def run
|
40
|
+
puts store.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def validate_exit_code!
|
46
|
+
if exit_code
|
47
|
+
puts exit_message
|
48
|
+
exit exit_code
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_git_repository!
|
53
|
+
if not File.directory?(repository) or not File.directory?(File.join(repository, '.git'))
|
54
|
+
puts "'#{repository}' doesn't seem to be a git repository!"
|
55
|
+
exit 10
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def set_logger
|
60
|
+
if verbose
|
61
|
+
logger.set Hotspots::Logger::Console
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def set_path
|
66
|
+
Dir.chdir(repository)
|
67
|
+
end
|
68
|
+
|
69
|
+
def assign
|
70
|
+
@driver = Hotspots::Repository::Driver::Git.new logger
|
71
|
+
@parser = Hotspots::Repository::Parser::Git.new driver, options.clone
|
72
|
+
@store = Hotspots::Store.new parser.files, options.clone
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Hotspots
|
2
|
+
class Logger
|
3
|
+
class Console
|
4
|
+
def self.<<(message)
|
5
|
+
$stdout << message
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Null
|
10
|
+
def self.<<(message)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :drain
|
15
|
+
def initialize
|
16
|
+
@drain = Null
|
17
|
+
end
|
18
|
+
|
19
|
+
def set(logger)
|
20
|
+
@drain = logger
|
21
|
+
end
|
22
|
+
|
23
|
+
def log(message)
|
24
|
+
@drain << format(message)
|
25
|
+
end
|
26
|
+
|
27
|
+
def format(message)
|
28
|
+
"\n<#{Time.now}> #{message}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
require 'hotspots/version'
|
4
|
+
|
5
|
+
module Hotspots
|
6
|
+
class OptionsParser
|
7
|
+
def initialize
|
8
|
+
@options = {
|
9
|
+
:time => 15,
|
10
|
+
:repository => ".",
|
11
|
+
:file_filter => "",
|
12
|
+
:message_filters => [""],
|
13
|
+
:cutoff => 0,
|
14
|
+
:verbose => false,
|
15
|
+
:exit => { :code => nil, :message => "" }
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse(*args)
|
20
|
+
parser = new_option_parser
|
21
|
+
begin
|
22
|
+
parser.parse args
|
23
|
+
rescue ::OptionParser::InvalidOption, ::OptionParser::InvalidArgument => ex
|
24
|
+
@options[:exit] = { :code => 1, :message => (ex.to_s << "\nUse -h for help\n") }
|
25
|
+
end
|
26
|
+
@options
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def new_option_parser
|
32
|
+
::OptionParser.new do |opts|
|
33
|
+
set_banner_on(opts)
|
34
|
+
set_version_on(opts)
|
35
|
+
|
36
|
+
handle_time_on(opts)
|
37
|
+
handle_path_on(opts)
|
38
|
+
handle_file_filter_on(opts)
|
39
|
+
handle_message_filter_on(opts)
|
40
|
+
handle_cutoff_on(opts)
|
41
|
+
handle_verbosity_on(opts)
|
42
|
+
handle_help_on(opts)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_version_on(opts)
|
47
|
+
opts.version = ::Hotspots::VERSION
|
48
|
+
end
|
49
|
+
|
50
|
+
def set_banner_on(opts)
|
51
|
+
opts.banner = "Tool to find most modified files over the past few days in a git repository. version #{::Hotspots::VERSION}"
|
52
|
+
|
53
|
+
opts.separator "Copyright (C) 2011 Chirantan Mitra"
|
54
|
+
opts.separator ""
|
55
|
+
opts.separator "Usage: ruby hotspots [options]"
|
56
|
+
opts.separator ""
|
57
|
+
opts.separator "Specific options:"
|
58
|
+
end
|
59
|
+
|
60
|
+
def handle_time_on(opts)
|
61
|
+
opts.on("-t", "--time [TIME]", OptionParser::DecimalInteger,
|
62
|
+
"Time in days to scan the repository for. Defaults to fifteen") do |o|
|
63
|
+
@options[:time] = o.to_i
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def handle_path_on(opts)
|
68
|
+
opts.on("-r", "--repository [PATH]", String,
|
69
|
+
"Path to the repository to scan. Defaults to current path") do |o|
|
70
|
+
@options[:repository] = o.to_s
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def handle_file_filter_on(opts)
|
75
|
+
opts.on("-f", "--file-filter [REGEX]", String,
|
76
|
+
"Regular expression to filtering file names. All files are allowed when not specified") do |o|
|
77
|
+
@options[:file_filter] = o.to_s
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def handle_message_filter_on(opts)
|
82
|
+
opts.on("-m", "--message-filter [PIPE SEPARATED]", String,
|
83
|
+
"Pipe separated values to filter files names against each commit message separated by pipe. All files are allowed when not specified") do |o|
|
84
|
+
@options[:message_filters] = o.to_s.split("|")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def handle_cutoff_on(opts)
|
89
|
+
opts.on("-c", "--cutoff [CUTOFF]", OptionParser::DecimalInteger,
|
90
|
+
"The minimum occurance to consider for a file to appear in the list. Defaults to zero") do |o|
|
91
|
+
@options[:cutoff] = o.to_i
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def handle_verbosity_on(opts)
|
96
|
+
opts.on("-v", "--verbose",
|
97
|
+
"Show verbose output") do
|
98
|
+
@options[:verbose] = true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def handle_help_on(opts)
|
103
|
+
opts.on_tail("-h", "--help",
|
104
|
+
"Show this message") do
|
105
|
+
@options[:exit] = { :code => 0, :message => opts.to_s }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'hotspots/repository/driver/git'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Hotspots
|
2
|
+
module Repository
|
3
|
+
module Driver
|
4
|
+
class Git
|
5
|
+
attr_reader :logger
|
6
|
+
|
7
|
+
def initialize(logger)
|
8
|
+
@logger = logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def pretty_log(options)
|
12
|
+
command = %Q(git log --pretty="%H" --since #{options[:since_days]}.days.ago --grep "#{options[:message_filter]}")
|
13
|
+
.tap {|raw| logger.log "<Input> #{raw}"}
|
14
|
+
%x(#{command})
|
15
|
+
.tap {|raw| logger.log raw}
|
16
|
+
.gsub("\r", "")
|
17
|
+
end
|
18
|
+
|
19
|
+
def show_one_line_names(options)
|
20
|
+
command = %Q(git show --oneline --name-only #{options[:commit_hash]})
|
21
|
+
.tap {|raw| logger.log "<Input> #{raw}"}
|
22
|
+
%x(#{command})
|
23
|
+
.tap {|raw| logger.log "<Output> #{raw}"}
|
24
|
+
.gsub("\r", "")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'hotspots/repository/parser/git'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Hotspots
|
2
|
+
module Repository
|
3
|
+
module Parser
|
4
|
+
class Git
|
5
|
+
def initialize(driver, options)
|
6
|
+
@driver = driver
|
7
|
+
@time = options[:time]
|
8
|
+
@message_filters = options[:message_filters]
|
9
|
+
end
|
10
|
+
|
11
|
+
def files
|
12
|
+
filtered_commit_hashes.map do |commit_hash|
|
13
|
+
@driver.show_one_line_names(:commit_hash => commit_hash).split("\n")[1..-1]
|
14
|
+
end.flatten
|
15
|
+
end
|
16
|
+
|
17
|
+
def filtered_commit_hashes
|
18
|
+
@message_filters.map do |filter|
|
19
|
+
@driver.pretty_log(:since_days => @time, :message_filter => filter).split("\n")
|
20
|
+
end.flatten.uniq
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Hotspots
|
2
|
+
class Store
|
3
|
+
def initialize(lines, options = {})
|
4
|
+
@lines = lines
|
5
|
+
@store = Hash.new(0)
|
6
|
+
@cutoff = options[:cutoff] || 0
|
7
|
+
@filter = options[:file_filter] || ""
|
8
|
+
|
9
|
+
@lines.map { |line| line.strip.downcase }
|
10
|
+
.select{ |line| not line.empty? and line =~ Regexp.new(@filter) }
|
11
|
+
.each { |line| @store[line] += 1 }
|
12
|
+
end
|
13
|
+
|
14
|
+
def on(line)
|
15
|
+
@store[line]
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
sorted_array.select { |key, value| value >= @cutoff }
|
20
|
+
.reduce("") { |acc, (key, value)| acc << "#{key},#{value}\n" }
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def sorted_array
|
26
|
+
@store.sort do |(key1, value1), (key2, value2)|
|
27
|
+
value2 <=> value1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'hotspots/logger'
|
2
|
+
require 'hotspots/options_parser'
|
3
|
+
|
4
|
+
module Hotspots
|
5
|
+
describe "OptionsParser" do
|
6
|
+
before do
|
7
|
+
@parser = OptionsParser.new
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "defaults" do
|
11
|
+
it "repository to current path" do
|
12
|
+
@parser.parse[:repository].must_equal "."
|
13
|
+
end
|
14
|
+
|
15
|
+
it "time to 15" do
|
16
|
+
@parser.parse[:time].must_equal 15
|
17
|
+
end
|
18
|
+
|
19
|
+
it "file filter to empty string" do
|
20
|
+
@parser.parse[:file_filter].must_equal ""
|
21
|
+
end
|
22
|
+
|
23
|
+
it "message filters to array with an empty string" do
|
24
|
+
@parser.parse[:message_filters].must_equal [""]
|
25
|
+
end
|
26
|
+
|
27
|
+
it "cutoff to 0" do
|
28
|
+
@parser.parse[:cutoff].must_equal 0
|
29
|
+
end
|
30
|
+
|
31
|
+
it "verbose to nil" do
|
32
|
+
@parser.parse[:verbose].must_equal false
|
33
|
+
end
|
34
|
+
|
35
|
+
it "exit code to nil" do
|
36
|
+
@parser.parse[:exit][:code].must_equal nil
|
37
|
+
end
|
38
|
+
|
39
|
+
it "exit message to empty string" do
|
40
|
+
@parser.parse[:exit][:message].must_equal ""
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
["--repository", "--repo", "-r"].each do |option|
|
45
|
+
describe option do
|
46
|
+
it "sets the specified value repository" do
|
47
|
+
@parser.parse(option, "rails")[:repository].must_equal "rails"
|
48
|
+
end
|
49
|
+
|
50
|
+
it "sets empty repository when missing" do
|
51
|
+
@parser.parse(option)[:repository].must_equal ""
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
["--time", "--ti", "-t"].each do |option|
|
57
|
+
describe option do
|
58
|
+
it "sets the specified time to consider" do
|
59
|
+
@parser.parse(option, "8")[:time].must_equal 8
|
60
|
+
end
|
61
|
+
|
62
|
+
it "sets zero time when missing" do
|
63
|
+
@parser.parse(option)[:time].must_equal 0
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
["--cutoff", "--cut", "-c"].each do |option|
|
69
|
+
describe option do
|
70
|
+
it "sets the specified cutoff" do
|
71
|
+
@parser.parse(option, "5")[:cutoff].must_equal 5
|
72
|
+
end
|
73
|
+
|
74
|
+
it "sets zero cutoff when missing" do
|
75
|
+
@parser.parse(option)[:cutoff].must_equal 0
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
["--file-filter", "--file", "-f"].each do |option|
|
81
|
+
describe option do
|
82
|
+
it "sets the specified file-filter" do
|
83
|
+
@parser.parse(option, "rb")[:file_filter].must_equal "rb"
|
84
|
+
end
|
85
|
+
|
86
|
+
it "sets empty file-filter when missing" do
|
87
|
+
@parser.parse(option)[:file_filter].must_equal ""
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
["--message-filter", "--message", "-m"].each do |option|
|
93
|
+
describe option do
|
94
|
+
it "sets the specified message filters" do
|
95
|
+
@parser.parse(option, "cleanup|defect")[:message_filters].must_equal ["cleanup", "defect"]
|
96
|
+
end
|
97
|
+
|
98
|
+
it "sets empty message-filter when missing" do
|
99
|
+
@parser.parse(option)[:message_filters].must_equal []
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
["--verbose", "-v"].each do |option|
|
105
|
+
describe option do
|
106
|
+
it "sets the console logger" do
|
107
|
+
@parser.parse(option)[:verbose].must_equal true
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
["--help", "-h"].each do |option|
|
113
|
+
describe option do
|
114
|
+
it "sets exit code to zero" do
|
115
|
+
@parser.parse(option)[:exit][:code].must_equal 0
|
116
|
+
end
|
117
|
+
|
118
|
+
it "sets an exit message" do
|
119
|
+
@parser.parse(option)[:exit][:message].wont_be_empty
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "on a invalid option" do
|
125
|
+
before do
|
126
|
+
@options = @parser.parse("--invalid-option")
|
127
|
+
end
|
128
|
+
|
129
|
+
it "sets an exit code" do
|
130
|
+
@options[:exit][:code].must_equal 1
|
131
|
+
end
|
132
|
+
|
133
|
+
it "sets an exit message" do
|
134
|
+
@options[:exit][:message].wont_be_empty
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "on a invalid argument" do
|
139
|
+
before do
|
140
|
+
@options = @parser.parse("--repo", "")
|
141
|
+
end
|
142
|
+
|
143
|
+
it "sets an exit code" do
|
144
|
+
@options[:exit][:code].must_equal 1
|
145
|
+
end
|
146
|
+
|
147
|
+
it "sets an exit message" do
|
148
|
+
@options[:exit][:message].wont_be_empty
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'hotspots/repository/parser/git'
|
2
|
+
|
3
|
+
module Hotspots::Repository
|
4
|
+
describe "Parser::Git test" do
|
5
|
+
it "fetches a commit hash based on filter and time" do
|
6
|
+
mock_git_driver = MiniTest::Mock.new
|
7
|
+
options = {:time => 10, :message_filters => ["Foo"]}
|
8
|
+
git_parser = Parser::Git.new mock_git_driver, options
|
9
|
+
|
10
|
+
mock_git_driver.expect(:pretty_log, "SHA1\nSHA2", [:since_days => options[:time], :message_filter => "Foo"])
|
11
|
+
|
12
|
+
git_parser.filtered_commit_hashes.must_equal(["SHA1", "SHA2"])
|
13
|
+
|
14
|
+
assert mock_git_driver.verify
|
15
|
+
end
|
16
|
+
|
17
|
+
it "fetches multiple commit hashes" do
|
18
|
+
options = {:time => 10, :message_filters => ["Foo", "Bar"]}
|
19
|
+
git_parser = Parser::Git.new StubGitDriver, options
|
20
|
+
|
21
|
+
git_parser.filtered_commit_hashes.must_equal(["SHA1", "SHA2", "SHA3"])
|
22
|
+
end
|
23
|
+
|
24
|
+
it "finds all affected files for a commit message" do
|
25
|
+
mock_git_driver = MiniTest::Mock.new
|
26
|
+
options = {:time => 10, :message_filters => ["Foo"]}
|
27
|
+
git_parser = Parser::Git.new mock_git_driver, options
|
28
|
+
|
29
|
+
mock_git_driver.expect(:pretty_log, "SHA1", [:since_days => options[:time], :message_filter => "Foo"])
|
30
|
+
mock_git_driver.expect(:show_one_line_names, "SHA1 CommitMessage\nfile1\nfile2", [:commit_hash => "SHA1"])
|
31
|
+
|
32
|
+
git_parser.files.must_equal(["file1", "file2"])
|
33
|
+
|
34
|
+
assert mock_git_driver.verify
|
35
|
+
end
|
36
|
+
|
37
|
+
it "finds all affected files for multiple commit messages" do
|
38
|
+
options = {:time => 10, :message_filters => ["Foo", "Bar"]}
|
39
|
+
git_parser = Parser::Git.new StubGitDriver, options
|
40
|
+
|
41
|
+
git_parser.files.must_equal(["file1", "file2", "file2", "file3", "file5", "file4"])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "git driver stub" do
|
46
|
+
it "pretty log is sane" do
|
47
|
+
StubGitDriver.pretty_log(:time => 10, :message_filter => "Foo").must_equal "SHA1\nSHA2"
|
48
|
+
StubGitDriver.pretty_log(:time => 10, :message_filter => "Bar").must_equal "SHA2\nSHA3"
|
49
|
+
end
|
50
|
+
|
51
|
+
it "show one line names is sane" do
|
52
|
+
StubGitDriver.show_one_line_names(:commit_hash => "SHA2").must_equal "SHA1 commit message\nfile2\nfile3\nfile5"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class StubGitDriver
|
57
|
+
@pretty_log_enum = ["SHA1\nSHA2", "SHA2\nSHA3"].cycle
|
58
|
+
@commits = {
|
59
|
+
"SHA1" => "SHA1 commit message\nfile1\nfile2",
|
60
|
+
"SHA2" => "SHA1 commit message\nfile2\nfile3\nfile5",
|
61
|
+
"SHA3" => "SHA1 commit message\nfile4",
|
62
|
+
}
|
63
|
+
|
64
|
+
class << self
|
65
|
+
def pretty_log(options)
|
66
|
+
@pretty_log_enum.next
|
67
|
+
end
|
68
|
+
|
69
|
+
def show_one_line_names(options)
|
70
|
+
@commits[options[:commit_hash]]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'hotspots/store'
|
2
|
+
|
3
|
+
module Hotspots
|
4
|
+
describe "Store test" do
|
5
|
+
it "counts occurances of a line present once" do
|
6
|
+
lines = [
|
7
|
+
"abc.txt",
|
8
|
+
"efg.txt"
|
9
|
+
]
|
10
|
+
store = Store.new(lines)
|
11
|
+
store.on("abc.txt").must_equal 1
|
12
|
+
store.on("efg.txt").must_equal 1
|
13
|
+
end
|
14
|
+
|
15
|
+
it "counts occurances of a line present multiple times" do
|
16
|
+
lines = [
|
17
|
+
"abc.txt",
|
18
|
+
"abc.txt",
|
19
|
+
"efg.txt"
|
20
|
+
]
|
21
|
+
store = Store.new(lines)
|
22
|
+
store.on("abc.txt").must_equal 2
|
23
|
+
end
|
24
|
+
|
25
|
+
it "identifies zero occurances" do
|
26
|
+
lines = [
|
27
|
+
"abc.txt",
|
28
|
+
"efg.txt"
|
29
|
+
]
|
30
|
+
store = Store.new(lines)
|
31
|
+
store.on("absent.txt").must_equal 0
|
32
|
+
end
|
33
|
+
|
34
|
+
it "disregards file case" do
|
35
|
+
lines = [
|
36
|
+
"ABC.TXT"
|
37
|
+
]
|
38
|
+
store = Store.new(lines)
|
39
|
+
store.on("abc.txt").must_equal 1
|
40
|
+
end
|
41
|
+
|
42
|
+
it "neglects empty lines" do
|
43
|
+
lines = [
|
44
|
+
" "
|
45
|
+
]
|
46
|
+
store = Store.new(lines)
|
47
|
+
store.on(" ").must_equal 0
|
48
|
+
end
|
49
|
+
|
50
|
+
it "neglects spaces at the extremes of the line" do
|
51
|
+
lines = [
|
52
|
+
" abc.txt "
|
53
|
+
]
|
54
|
+
store = Store.new(lines)
|
55
|
+
store.on("abc.txt").must_equal 1
|
56
|
+
end
|
57
|
+
|
58
|
+
it "neglects linefeeds at the extremes of the line" do
|
59
|
+
lines = [
|
60
|
+
"\n\n abc.txt \n\r",
|
61
|
+
"\n\n abc.txt \r\n"
|
62
|
+
]
|
63
|
+
store = Store.new(lines)
|
64
|
+
store.on("abc.txt").must_equal 2
|
65
|
+
end
|
66
|
+
|
67
|
+
it "has a string representation" do
|
68
|
+
lines = [
|
69
|
+
"abc.txt",
|
70
|
+
"efg.txt"
|
71
|
+
]
|
72
|
+
store = Store.new(lines)
|
73
|
+
store.to_s.must_equal "abc.txt,1\nefg.txt,1\n"
|
74
|
+
end
|
75
|
+
|
76
|
+
it "string representation has maximum occuring string at the top" do
|
77
|
+
lines = [
|
78
|
+
"abc.txt",
|
79
|
+
"abc.txt",
|
80
|
+
"efg.txt",
|
81
|
+
"efg.txt",
|
82
|
+
"efg.txt"
|
83
|
+
]
|
84
|
+
store = Store.new(lines)
|
85
|
+
store.to_s.must_equal "efg.txt,3\nabc.txt,2\n"
|
86
|
+
end
|
87
|
+
|
88
|
+
it "doesn't display setting below cut-off" do
|
89
|
+
lines = [
|
90
|
+
"abc.txt",
|
91
|
+
"abc.txt",
|
92
|
+
"efg.txt",
|
93
|
+
"efg.txt",
|
94
|
+
"efg.txt"
|
95
|
+
]
|
96
|
+
store = Store.new(lines, :cutoff => 3)
|
97
|
+
store.to_s.must_equal "efg.txt,3\n"
|
98
|
+
end
|
99
|
+
|
100
|
+
it "doesn't display lines that don't match criteria" do
|
101
|
+
lines = [
|
102
|
+
"abc.txt",
|
103
|
+
"abc.txt",
|
104
|
+
"abc.txt",
|
105
|
+
"dont.exist",
|
106
|
+
"abc.log",
|
107
|
+
"efg.txt",
|
108
|
+
"efg.txt",
|
109
|
+
"missing.txt"
|
110
|
+
]
|
111
|
+
store = Store.new(lines, :file_filter => "abc|efg")
|
112
|
+
store.to_s.must_equal "abc.txt,3\nefg.txt,2\nabc.log,1\n"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'minitest/spec'
|
5
|
+
|
6
|
+
lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
|
7
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
8
|
+
|
9
|
+
Dir[File.join(File.expand_path(File.dirname(__FILE__)), "hotspots", "**", "*_test.rb")].each do |file|
|
10
|
+
require file
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hotspots
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.8
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Chirantan Mitra
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-12-25 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: ! 'Find all files that changed over the past days for a git repository.
|
15
|
+
If the same file is modified over
|
16
|
+
|
17
|
+
and over again, it may require re-design. Watch out for file changes that don''t
|
18
|
+
have a corresponding
|
19
|
+
|
20
|
+
test change.
|
21
|
+
|
22
|
+
'
|
23
|
+
email:
|
24
|
+
- chirantan.mitra@gmail.com
|
25
|
+
executables:
|
26
|
+
- hotspots
|
27
|
+
extensions: []
|
28
|
+
extra_rdoc_files: []
|
29
|
+
files:
|
30
|
+
- Gemfile
|
31
|
+
- LICENSE
|
32
|
+
- README.markdown
|
33
|
+
- bin/hotspots
|
34
|
+
- hotspots.gemspec
|
35
|
+
- lib/hotspots.rb
|
36
|
+
- lib/hotspots/logger.rb
|
37
|
+
- lib/hotspots/options_parser.rb
|
38
|
+
- lib/hotspots/repository.rb
|
39
|
+
- lib/hotspots/repository/driver.rb
|
40
|
+
- lib/hotspots/repository/driver/git.rb
|
41
|
+
- lib/hotspots/repository/parser.rb
|
42
|
+
- lib/hotspots/repository/parser/git.rb
|
43
|
+
- lib/hotspots/store.rb
|
44
|
+
- lib/hotspots/version.rb
|
45
|
+
- test/hotspots/options_parser_test.rb
|
46
|
+
- test/hotspots/repository/parser/git_test.rb
|
47
|
+
- test/hotspots/store_test.rb
|
48
|
+
- test/hotspots_test.rb
|
49
|
+
homepage: https://github.com/chiku/Hotspot
|
50
|
+
licenses: []
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubyforge_project: hotspots
|
69
|
+
rubygems_version: 1.8.10
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: Find all files that changed over the past in a git repository based on conditions
|
73
|
+
test_files: []
|