hotspots 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7f43a6b0f28f054b6f4b3799ccabb18ebe2327f6709c325c4c7a2c07f16d0c8d
4
+ data.tar.gz: c165186a7d68e49e3bd6a3507d30f22ade3513d7f1182ba0f7b9eabcdc6739b0
5
+ SHA512:
6
+ metadata.gz: 589499e787b881d179b267e3d2ff468db0af16b5d7aef558cd4799807288474d8cef5e1c3c4a2998cbd568d27681ace4cb8c409f3ca607d290a80136737b7d6f
7
+ data.tar.gz: 742faf60cca8d31eeda8e42e66f13d7a6d771bd75fd61cbc8820834a5423f1d12b15367da64ccc00084314d50c6c27a59f4d54a9b1f987fb45bc0c23b911eb68
data/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ v1.2.0
2
+ -------
3
+
4
+ * Use ruby's built-in logger instead of a custom one.
5
+ * Introduce the concept of Configuration. Configurations have defaults and are overridden by command-line options.
6
+ * Remove compatibility layers.
7
+ * Always enable colour for verbose output.
8
+ * Minimum supported version of ruby is 3.0.0.
9
+
10
+ v1.1.0
11
+ ------
12
+
13
+ * Cleanup some of the internals. All compatibility layers exist in a separate file. Remove dependency on deprecated class.
14
+ * Defer to ruby for interpreting line endings
15
+
1
16
  v1.0.0
2
17
  ------
3
18
 
@@ -17,7 +32,7 @@ v0.2.0
17
32
  v0.1.1
18
33
  ------
19
34
 
20
- * Sort for an array of array via spaceship operator returns different result on each run on ruby 1.8.7. Store has a string representation breaks intermittently on 1.8.7. Tests fail intermittently on 1.8.7. So support for ruby 1.9.x only
35
+ * Sort for an array of array via spaceship operator returns different result on each run on ruby 1.8.7. Store has a string representation that breaks intermittently on 1.8.7. Tests fail intermittently on 1.8.7. So support for ruby 1.9.x only
21
36
  * Simplify Rakefile
22
37
  * Pull out a minitest_helper
23
38
  * Allow gem to be used as a library
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) <2011> <Chirantan Mitra>
1
+ Copyright (c) 2011-2024 Chirantan Mitra
2
2
 
3
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
4
 
data/README.md CHANGED
@@ -1,9 +1,10 @@
1
- [![Build Status](https://secure.travis-ci.org/chiku/hotspots.png?branch=master)](https://travis-ci.org/chiku/hotspots)
1
+ ![Build Status](https://github.com/chiku/r0man/actions/workflows/build.yml/badge.svg)
2
+ [![Gem Version](https://badge.fury.io/rb/hotspots.svg)](http://badge.fury.io/rb/hotspots)
2
3
 
3
4
  Overview
4
5
  --------
5
6
 
6
- 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.
7
+ 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 bugs. If the same file is modified many times for bug fixes, it indicates that the file needs some refactoring or redesign love.
7
8
 
8
9
 
9
10
  Dependencies
@@ -29,11 +30,12 @@ Specific options:
29
30
  -r, --repository [PATH] Path to the repository to scan. Defaults to current path
30
31
  -f, --file-filter [REGEX] Regular expression to filtering file names. All files are allowed when not specified
31
32
  -m, --message-filter [PIPE SEPARATED] Pipe separated values to filter files names against each commit message.
32
- All files are allowed when not specified
33
+ All commit messages are allowed when not specified
33
34
  -c, --cutoff [CUTOFF] The minimum occurrence to consider for a file to appear in the list. Defaults to zero
35
+ --log [LOG LEVEL] Log level (debug, info, warn, error, fatal)
34
36
  -v, --verbose Show verbose output
35
- -C, --colour, --color Show verbose output in colours
36
- --version Show version information
37
+ -C, --colour, --color Show output in colours. The log level should be info or debug for colours
38
+ --version Show version
37
39
  -h, --help Show this message
38
40
  ```
39
41
 
@@ -64,15 +66,6 @@ gem install simplecov
64
66
  rake
65
67
  ```
66
68
 
67
- Contributing
68
- ------------
69
-
70
- * Fork the project.
71
- * Make your feature addition or bug fix.
72
- * Add tests for it. This is important so I don't break it in a future version unintentionally.
73
- * Commit, but do not mess with the VERSION. If you want to have your own version, that is fine but bump the version in a commit by itself in another branch so I can ignore it when I pull.
74
- * Send me a pull request.
75
-
76
69
  License
77
70
  -------
78
71
 
data/TODO.md CHANGED
@@ -1,2 +1,3 @@
1
1
  * Allow better use as a library - add documentation
2
2
  * Reduce conditionals
3
+ * Group files based on their extensions
data/bin/hotspots CHANGED
@@ -1,7 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'hotspots'
3
+ lib = File.expand_path(File.join("..", "..", "lib"), __FILE__)
4
+ $:.unshift lib unless $:.include?(lib)
4
5
 
5
- options = Hotspots::OptionsParser.new.parse(*ARGV)
6
+ require "logger"
7
+ require "hotspots"
6
8
 
7
- Hotspots::Main.new(options).output
9
+ default_configuration = Hotspots::Configuration.new
10
+ user_options = Hotspots::OptionsParser.new(:configuration => default_configuration)
11
+ overridden_configuration = user_options.parse(*ARGV)
12
+
13
+ Hotspots.new(overridden_configuration).output
@@ -0,0 +1,36 @@
1
+ class Hotspots
2
+ class Configuration #:nodoc: all
3
+ attr_accessor :repository, :time, :message_filters, :file_filter, :cutoff
4
+ attr_accessor :logger
5
+ attr_accessor :exit_strategy
6
+
7
+ def initialize(options = {})
8
+ @repository = "."
9
+ @time = 15
10
+ @message_filters = [""]
11
+ @file_filter = ""
12
+ @cutoff = 0
13
+ @info_log_level = options[:info_log_level] || ::Logger::INFO
14
+ @error_log_level = options[:error_log_level] || ::Logger::ERROR
15
+ @logger = options[:logger] || default_logger
16
+ @exit_strategy = Exit::Noop.new
17
+ @logger.level = @error_log_level
18
+ end
19
+
20
+ def log_level
21
+ @logger.level
22
+ end
23
+
24
+ def enable_verbosity
25
+ @logger.level = @info_log_level
26
+ end
27
+
28
+ private
29
+
30
+ def default_logger
31
+ ::Logger.new(STDOUT).tap do |logger|
32
+ logger.formatter = proc { |severity, datetime, progname, msg| "#{datetime}: #{msg}\n" }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,5 +1,5 @@
1
- module Hotspots
2
- module OptionBasedExit #:nodoc: all
1
+ class Hotspots
2
+ module Exit #:nodoc: all
3
3
  class Error
4
4
  attr_reader :code, :message
5
5
 
@@ -9,7 +9,7 @@ module Hotspots
9
9
  end
10
10
 
11
11
  def perform
12
- puts @message
12
+ $stderr.puts @message
13
13
  exit @code
14
14
  end
15
15
  end
@@ -23,7 +23,7 @@ module Hotspots
23
23
  end
24
24
 
25
25
  def perform
26
- puts @message
26
+ $stdout.puts @message
27
27
  exit @code
28
28
  end
29
29
  end
@@ -31,7 +31,7 @@ module Hotspots
31
31
  class Noop
32
32
  attr_reader :code, :message
33
33
 
34
- def initialize(options = {})
34
+ def initialize
35
35
  @message = ""
36
36
  @code = nil
37
37
  end
@@ -1,37 +1,19 @@
1
- require 'optparse'
1
+ require "optparse"
2
2
 
3
- require 'hotspots/version'
4
- require 'hotspots/option_based_exit'
5
-
6
- module Hotspots
3
+ class Hotspots
7
4
  class OptionsParser #:nodoc: all
8
- class << self
9
- def default_options
10
- {
11
- :time => 15,
12
- :repository => ".",
13
- :file_filter => "",
14
- :message_filters => [""],
15
- :cutoff => 0,
16
- :verbose => false,
17
- :exit_strategy => OptionBasedExit::Noop.new,
18
- :colour => false,
19
- }
20
- end
21
- end
22
-
23
- def initialize
24
- @options = self.class.default_options
5
+ def initialize(opts)
6
+ @configuration = opts[:configuration].dup
25
7
  end
26
8
 
27
9
  def parse(*args)
28
10
  parser = new_option_parser
29
11
  begin
30
- parser.parse args
12
+ parser.parse(args)
31
13
  rescue ::OptionParser::InvalidOption, ::OptionParser::InvalidArgument => ex
32
- @options[:exit_strategy] = OptionBasedExit::Error.new(:code => 1, :message => (ex.to_s << "\nUse -h for help\n"))
14
+ @configuration.exit_strategy = Exit::Error.new(:code => 1, :message => (ex.to_s + "\nUse -h for help\n"))
33
15
  end
34
- @options
16
+ @configuration
35
17
  end
36
18
 
37
19
  private
@@ -47,7 +29,7 @@ module Hotspots
47
29
  handle_message_filter_on(opts)
48
30
  handle_cutoff_on(opts)
49
31
  handle_verbosity_on(opts)
50
- handle_colours_on(opts)
32
+ handle_version_on(opts)
51
33
  handle_help_on(opts)
52
34
  end
53
35
  end
@@ -60,9 +42,9 @@ module Hotspots
60
42
  opts.banner = "Tool to find most modified files over the past few days in a git repository."
61
43
 
62
44
  opts.separator "Version #{::Hotspots::VERSION}"
63
- opts.separator "Copyright (C) 2011-2012 Chirantan Mitra"
45
+ opts.separator "Copyright (C) 2011-2013 Chirantan Mitra"
64
46
  opts.separator ""
65
- opts.separator "Usage: ruby hotspots [options]"
47
+ opts.separator "Usage: hotspots [options]"
66
48
  opts.separator ""
67
49
  opts.separator "Specific options:"
68
50
  end
@@ -70,14 +52,14 @@ module Hotspots
70
52
  def handle_time_on(opts)
71
53
  opts.on("-t", "--time [TIME]", OptionParser::DecimalInteger,
72
54
  "Time in days to scan the repository for. Defaults to fifteen") do |o|
73
- @options[:time] = o.to_i
55
+ @configuration.time = o.to_i
74
56
  end
75
57
  end
76
58
 
77
59
  def handle_path_on(opts)
78
60
  opts.on("-r", "--repository [PATH]", String,
79
61
  "Path to the repository to scan. Defaults to current path") do |o|
80
- @options[:repository] = o.to_s
62
+ @configuration.repository = o.to_s
81
63
  end
82
64
  end
83
65
 
@@ -85,43 +67,43 @@ module Hotspots
85
67
  opts.on("-f", "--file-filter [REGEX]", String,
86
68
  "Regular expression to filtering file names.",
87
69
  "All files are allowed when not specified") do |o|
88
- @options[:file_filter] = o.to_s
70
+ @configuration.file_filter = o.to_s
89
71
  end
90
72
  end
91
73
 
92
74
  def handle_message_filter_on(opts)
93
75
  opts.on("-m", "--message-filter [PIPE SEPARATED]", String,
94
76
  "Pipe separated values to filter files names against each commit message.",
95
- "All files are allowed when not specified") do |o|
96
- @options[:message_filters] = o.to_s.split("|")
77
+ "All commit messages are allowed when not specified") do |o|
78
+ @configuration.message_filters = o.to_s.split("|")
97
79
  end
98
80
  end
99
81
 
100
82
  def handle_cutoff_on(opts)
101
83
  opts.on("-c", "--cutoff [CUTOFF]", OptionParser::DecimalInteger,
102
- "The minimum occurance to consider for a file to appear in the list. Defaults to zero") do |o|
103
- @options[:cutoff] = o.to_i
84
+ "The minimum occurrence to consider for a file to appear in the list. Defaults to zero") do |o|
85
+ @configuration.cutoff = o.to_i
104
86
  end
105
87
  end
106
88
 
107
89
  def handle_verbosity_on(opts)
108
90
  opts.on("-v", "--verbose",
109
91
  "Show verbose output") do
110
- @options[:verbose] = true
92
+ @configuration.enable_verbosity
111
93
  end
112
94
  end
113
95
 
114
- def handle_colours_on(opts)
115
- opts.on("-C", "--colour", "--color",
116
- "Show verbose output in colours") do
117
- @options[:colour] = true
96
+ def handle_version_on(opts)
97
+ opts.on_tail("--version",
98
+ "Show version") do
99
+ @configuration.exit_strategy = Exit::Safe.new(:message => "hotspots #{::Hotspots::VERSION}\n")
118
100
  end
119
101
  end
120
102
 
121
103
  def handle_help_on(opts)
122
104
  opts.on_tail("-h", "--help",
123
105
  "Show this message") do
124
- @options[:exit_strategy] = OptionBasedExit::Safe.new(:message => opts.to_s)
106
+ @configuration.exit_strategy = Exit::Safe.new(:message => opts.to_s)
125
107
  end
126
108
  end
127
109
  end
@@ -0,0 +1,17 @@
1
+ class Hotspots
2
+ module Repository #:nodoc: all
3
+ module Git
4
+ class << self
5
+ def installed?
6
+ `git --help 2>&1`
7
+ $? == 0
8
+ end
9
+
10
+ def inside_valid_repository?
11
+ `git status 2>&1`
12
+ $? == 0
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ class Hotspots
2
+ module Repository #:nodoc: all
3
+ module GitCommand
4
+ class Log
5
+ def initialize(options)
6
+ @since_days = options[:since_days]
7
+ @message_filter = options[:message_filter].to_s
8
+ end
9
+
10
+ def to_s
11
+ "git log --pretty=\"%H\" #{since_clause}#{grep_clause}"
12
+ end
13
+
14
+ private
15
+
16
+ def since_clause
17
+ "--since=\"#{@since_days} days ago\""
18
+ end
19
+
20
+ def grep_clause
21
+ @message_filter.empty? ? "" : " --grep \"#{@message_filter}\""
22
+ end
23
+ end
24
+
25
+ class Show
26
+ attr_reader :commit_hash
27
+
28
+ def initialize(options)
29
+ @commit_hash = options[:commit_hash]
30
+ end
31
+
32
+ def to_s
33
+ "git show --oneline --name-only #{commit_hash}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,35 @@
1
+ require "ansi/code"
2
+ require "open3"
3
+
4
+ class Hotspots
5
+ module Repository #:nodoc: all
6
+ class GitDriver
7
+ def initialize(options)
8
+ @logger = options[:logger]
9
+ end
10
+
11
+ def pretty_log(options)
12
+ execute GitCommand::Log.new(:since_days => options[:since_days], :message_filter => options[:message_filter]).to_s
13
+ end
14
+
15
+ def show_one_line_names(options)
16
+ execute GitCommand::Show.new(:commit_hash => options[:commit_hash]).to_s
17
+ end
18
+
19
+ private
20
+
21
+ def execute(command)
22
+ @logger.info { ::ANSI::Code.blue("[input]\n#{command}") }
23
+ output = ""
24
+ Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
25
+ error = stderr.gets(nil)
26
+ output = stdout.gets(nil)
27
+ @logger.error { ::ANSI::Code.red("[error]\n#{error}") } if error
28
+ @logger.info { ::ANSI::Code.green("[output]\n#{output}") } if output
29
+ Exit::Error.new(:message => "Error encountered while running git", :code => 1).perform unless wait_thr.value.success?
30
+ end
31
+ output || ""
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ class Hotspots
2
+ module Repository #:nodoc: all
3
+ class GitParser
4
+ attr_reader :driver, :time, :message_filters
5
+
6
+ def initialize(driver, options)
7
+ @driver = driver
8
+ @time = options[:time]
9
+ @message_filters = options[:message_filters]
10
+ end
11
+
12
+ def files
13
+ filtered_commit_hashes.reduce([]) do |acc, commit_hash|
14
+ acc + driver.show_one_line_names({:commit_hash => commit_hash}).lines.map(&:strip)[1..-1]
15
+ end
16
+ end
17
+
18
+ def filtered_commit_hashes
19
+ message_filters.reduce([]) do |acc, filter|
20
+ acc + driver.pretty_log({:since_days => time, :message_filter => filter}).lines.map(&:strip)
21
+ end.uniq
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,3 +1,5 @@
1
- require 'hotspots/repository/command'
2
- require 'hotspots/repository/driver'
3
- require 'hotspots/repository/parser'
1
+ require "hotspots/repository/git"
2
+ require "hotspots/repository/git_command"
3
+ require "hotspots/repository/git_command"
4
+ require "hotspots/repository/git_driver"
5
+ require "hotspots/repository/git_parser"
@@ -1,4 +1,4 @@
1
- module Hotspots
1
+ class Hotspots
2
2
  class Store #:nodoc: all
3
3
  def initialize(lines, options = {})
4
4
  @lines = lines
@@ -7,7 +7,7 @@ module Hotspots
7
7
  @filter = options[:file_filter] || ""
8
8
 
9
9
  @lines.map { |line| line.strip }.
10
- select{ |line| not line.empty? and line =~ Regexp.new(@filter) }.
10
+ select{ |line| !line.empty? && line =~ Regexp.new(@filter) }.
11
11
  each { |line| @store[line] += 1 }
12
12
  end
13
13
 
@@ -23,7 +23,7 @@ module Hotspots
23
23
  private
24
24
 
25
25
  def sorted_array
26
- @sorted_array ||= @store.sort do |(key1, value1), (key2, value2)|
26
+ @sorted_array ||= @store.sort do |(_, value1), (_, value2)|
27
27
  value2 <=> value1
28
28
  end
29
29
  end
@@ -1,3 +1,3 @@
1
- module Hotspots
2
- VERSION = "1.0.0"
1
+ class Hotspots
2
+ VERSION = "1.2.0"
3
3
  end
data/lib/hotspots.rb CHANGED
@@ -1,83 +1,61 @@
1
- require 'hotspots/version'
2
- require 'hotspots/logger'
3
- require 'hotspots/store'
4
- require 'hotspots/options_parser'
5
- require 'hotspots/repository'
6
-
7
- module Hotspots
8
- class Main
9
- attr_reader :logger, :repository, :verbose, :colour,
10
- :exit_strategy, :driver, :parser, :store,
11
- :time, :message_filters, :file_filter, :cutoff
12
-
13
- def initialize(opts)
14
- options = Hotspots::OptionsParser.default_options.merge(opts)
15
-
16
- @logger = Hotspots::Logger.new
17
- @repository = options[:repository]
18
- @verbose = options[:verbose]
19
- @colour = options[:colour]
20
- @exit_strategy = options[:exit_strategy]
21
-
22
- @time = options[:time]
23
- @message_filters = options[:message_filters]
24
- @file_filter = options[:file_filter]
25
- @cutoff = options[:cutoff]
26
- end
1
+ require "hotspots/version"
2
+ require "hotspots/exit"
3
+ require "hotspots/configuration"
4
+ require "hotspots/store"
5
+ require "hotspots/options_parser"
6
+ require "hotspots/repository"
7
+
8
+ class Hotspots
9
+ attr_reader :configuration
10
+
11
+ def initialize(configuration)
12
+ @configuration = configuration
13
+ end
27
14
 
28
- def output
29
- validate
30
- set
15
+ def output
16
+ validate
17
+ inside_repository do
31
18
  run
32
19
  end
20
+ end
33
21
 
34
- private
22
+ private
35
23
 
36
- def validate #:nodoc:
37
- exit_if_options_are_for_help
38
- exit_if_not_git_repository
39
- end
24
+ def validate
25
+ configuration.exit_strategy.perform
26
+ ensure_git_installed
27
+ ensure_git_repository
28
+ end
40
29
 
41
- def set #:nodoc:
42
- configure_logger
43
- set_path
44
- assign
45
- end
30
+ def inside_repository
31
+ yield Dir.chdir(repository)
32
+ end
46
33
 
47
- def run #:nodoc:
48
- puts store.to_s
49
- end
34
+ def run
35
+ puts store.to_s
36
+ end
50
37
 
51
- def exit_if_options_are_for_help
52
- exit_strategy.perform
53
- end
38
+ def ensure_git_installed
39
+ Exit::Error.new(:message => "git not installed or not present in PATH!", :code => 10).perform unless Repository::Git.installed?
40
+ end
54
41
 
55
- def exit_if_not_git_repository
56
- output = `git status 2>&1`
57
- unless $? == 0
58
- puts "'#{repository}' doesn't seem to be a git repository!"
59
- exit 10
60
- end
61
- end
42
+ def ensure_git_repository
43
+ Exit::Error.new(:message => "'#{@repository}' doesn't seem to be a git repository!", :code => 10).perform unless Repository::Git.inside_valid_repository?
44
+ end
62
45
 
63
- def configure_logger
64
- if verbose
65
- logger.as_console
66
- end
46
+ def repository
47
+ configuration.repository
48
+ end
67
49
 
68
- if colour
69
- logger.colourize
70
- end
71
- end
50
+ def store
51
+ Store.new(parser.files, :cutoff => configuration.cutoff, :file_filter => configuration.file_filter)
52
+ end
72
53
 
73
- def set_path
74
- Dir.chdir(repository)
75
- end
54
+ def parser
55
+ Repository::GitParser.new(driver, :time => configuration.time, :message_filters => configuration.message_filters)
56
+ end
76
57
 
77
- def assign
78
- @driver = Hotspots::Repository::Driver::Git.new logger
79
- @parser = Hotspots::Repository::Parser::Git.new driver, :time => time, :message_filters => message_filters
80
- @store = Hotspots::Store.new parser.files, :cutoff => cutoff, :file_filter => file_filter
81
- end
58
+ def driver
59
+ Repository::GitDriver.new(:logger => configuration.logger)
82
60
  end
83
61
  end